mirror of
https://github.com/home-assistant/frontend.git
synced 2025-08-03 22:47:47 +00:00
Merge dev to bump action versions
This commit is contained in:
commit
2fdb6f1241
8
.github/workflows/cast_deployment.yaml
vendored
8
.github/workflows/cast_deployment.yaml
vendored
@ -22,12 +22,12 @@ jobs:
|
|||||||
url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }}
|
url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }}
|
||||||
steps:
|
steps:
|
||||||
- name: Check out files from GitHub
|
- name: Check out files from GitHub
|
||||||
uses: actions/checkout@v3.2.0
|
uses: actions/checkout@v3.3.0
|
||||||
with:
|
with:
|
||||||
ref: dev
|
ref: dev
|
||||||
|
|
||||||
- name: Set up Node ${{ env.NODE_VERSION }}
|
- name: Set up Node ${{ env.NODE_VERSION }}
|
||||||
uses: actions/setup-node@v3.5.1
|
uses: actions/setup-node@v3.6.0
|
||||||
with:
|
with:
|
||||||
node-version: ${{ env.NODE_VERSION }}
|
node-version: ${{ env.NODE_VERSION }}
|
||||||
cache: yarn
|
cache: yarn
|
||||||
@ -60,12 +60,12 @@ jobs:
|
|||||||
url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }}
|
url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }}
|
||||||
steps:
|
steps:
|
||||||
- name: Check out files from GitHub
|
- name: Check out files from GitHub
|
||||||
uses: actions/checkout@v3.2.0
|
uses: actions/checkout@v3.3.0
|
||||||
with:
|
with:
|
||||||
ref: master
|
ref: master
|
||||||
|
|
||||||
- name: Set up Node ${{ env.NODE_VERSION }}
|
- name: Set up Node ${{ env.NODE_VERSION }}
|
||||||
uses: actions/setup-node@v3.5.1
|
uses: actions/setup-node@v3.6.0
|
||||||
with:
|
with:
|
||||||
node-version: ${{ env.NODE_VERSION }}
|
node-version: ${{ env.NODE_VERSION }}
|
||||||
cache: yarn
|
cache: yarn
|
||||||
|
20
.github/workflows/ci.yaml
vendored
20
.github/workflows/ci.yaml
vendored
@ -32,13 +32,13 @@ jobs:
|
|||||||
sha: ${{ steps.get-sha.outputs.sha }}
|
sha: ${{ steps.get-sha.outputs.sha }}
|
||||||
steps:
|
steps:
|
||||||
- name: Check out files from GitHub
|
- name: Check out files from GitHub
|
||||||
uses: actions/checkout@v3.2.0
|
uses: actions/checkout@v3.3.0
|
||||||
with:
|
with:
|
||||||
# Checkout PR head instead of merge commit
|
# Checkout PR head instead of merge commit
|
||||||
# Use ref, not SHA, so reruns get the dedupe commit
|
# Use ref, not SHA, so reruns get the dedupe commit
|
||||||
ref: ${{ github.event.pull_request.head.ref }}
|
ref: ${{ github.event.pull_request.head.ref }}
|
||||||
- name: Set up Node ${{ env.NODE_VERSION }}
|
- name: Set up Node ${{ env.NODE_VERSION }}
|
||||||
uses: actions/setup-node@v3.5.1
|
uses: actions/setup-node@v3.6.0
|
||||||
with:
|
with:
|
||||||
node-version: ${{ env.NODE_VERSION }}
|
node-version: ${{ env.NODE_VERSION }}
|
||||||
cache: yarn
|
cache: yarn
|
||||||
@ -81,11 +81,11 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Check out files from GitHub
|
- name: Check out files from GitHub
|
||||||
uses: actions/checkout@v3.2.0
|
uses: actions/checkout@v3.3.0
|
||||||
with:
|
with:
|
||||||
ref: ${{ needs.dedupe.outputs.sha }}
|
ref: ${{ needs.dedupe.outputs.sha }}
|
||||||
- name: Set up Node ${{ env.NODE_VERSION }}
|
- name: Set up Node ${{ env.NODE_VERSION }}
|
||||||
uses: actions/setup-node@v3.5.1
|
uses: actions/setup-node@v3.6.0
|
||||||
with:
|
with:
|
||||||
node-version: ${{ env.NODE_VERSION }}
|
node-version: ${{ env.NODE_VERSION }}
|
||||||
cache: yarn
|
cache: yarn
|
||||||
@ -109,11 +109,11 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Check out files from GitHub
|
- name: Check out files from GitHub
|
||||||
uses: actions/checkout@v3.2.0
|
uses: actions/checkout@v3.3.0
|
||||||
with:
|
with:
|
||||||
ref: ${{ needs.dedupe.outputs.sha }}
|
ref: ${{ needs.dedupe.outputs.sha }}
|
||||||
- name: Set up Node ${{ env.NODE_VERSION }}
|
- name: Set up Node ${{ env.NODE_VERSION }}
|
||||||
uses: actions/setup-node@v3.5.1
|
uses: actions/setup-node@v3.6.0
|
||||||
with:
|
with:
|
||||||
node-version: ${{ env.NODE_VERSION }}
|
node-version: ${{ env.NODE_VERSION }}
|
||||||
cache: yarn
|
cache: yarn
|
||||||
@ -131,11 +131,11 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Check out files from GitHub
|
- name: Check out files from GitHub
|
||||||
uses: actions/checkout@v3.2.0
|
uses: actions/checkout@v3.3.0
|
||||||
with:
|
with:
|
||||||
ref: ${{ needs.dedupe.outputs.sha }}
|
ref: ${{ needs.dedupe.outputs.sha }}
|
||||||
- name: Set up Node ${{ env.NODE_VERSION }}
|
- name: Set up Node ${{ env.NODE_VERSION }}
|
||||||
uses: actions/setup-node@v3.5.1
|
uses: actions/setup-node@v3.6.0
|
||||||
with:
|
with:
|
||||||
node-version: ${{ env.NODE_VERSION }}
|
node-version: ${{ env.NODE_VERSION }}
|
||||||
cache: yarn
|
cache: yarn
|
||||||
@ -153,11 +153,11 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Check out files from GitHub
|
- name: Check out files from GitHub
|
||||||
uses: actions/checkout@v3.2.0
|
uses: actions/checkout@v3.3.0
|
||||||
with:
|
with:
|
||||||
ref: ${{ needs.dedupe.outputs.sha }}
|
ref: ${{ needs.dedupe.outputs.sha }}
|
||||||
- name: Set up Node ${{ env.NODE_VERSION }}
|
- name: Set up Node ${{ env.NODE_VERSION }}
|
||||||
uses: actions/setup-node@v3.5.1
|
uses: actions/setup-node@v3.6.0
|
||||||
with:
|
with:
|
||||||
node-version: ${{ env.NODE_VERSION }}
|
node-version: ${{ env.NODE_VERSION }}
|
||||||
cache: yarn
|
cache: yarn
|
||||||
|
2
.github/workflows/codeql-analysis.yml
vendored
2
.github/workflows/codeql-analysis.yml
vendored
@ -23,7 +23,7 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v3.2.0
|
uses: actions/checkout@v3.3.0
|
||||||
with:
|
with:
|
||||||
# We must fetch at least the immediate parents so that if this is
|
# We must fetch at least the immediate parents so that if this is
|
||||||
# a pull request then we can checkout the head.
|
# a pull request then we can checkout the head.
|
||||||
|
8
.github/workflows/demo_deployment.yaml
vendored
8
.github/workflows/demo_deployment.yaml
vendored
@ -23,12 +23,12 @@ jobs:
|
|||||||
url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }}
|
url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }}
|
||||||
steps:
|
steps:
|
||||||
- name: Check out files from GitHub
|
- name: Check out files from GitHub
|
||||||
uses: actions/checkout@v3.2.0
|
uses: actions/checkout@v3.3.0
|
||||||
with:
|
with:
|
||||||
ref: dev
|
ref: dev
|
||||||
|
|
||||||
- name: Set up Node ${{ env.NODE_VERSION }}
|
- name: Set up Node ${{ env.NODE_VERSION }}
|
||||||
uses: actions/setup-node@v3.5.1
|
uses: actions/setup-node@v3.6.0
|
||||||
with:
|
with:
|
||||||
node-version: ${{ env.NODE_VERSION }}
|
node-version: ${{ env.NODE_VERSION }}
|
||||||
cache: yarn
|
cache: yarn
|
||||||
@ -61,12 +61,12 @@ jobs:
|
|||||||
url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }}
|
url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }}
|
||||||
steps:
|
steps:
|
||||||
- name: Check out files from GitHub
|
- name: Check out files from GitHub
|
||||||
uses: actions/checkout@v3.2.0
|
uses: actions/checkout@v3.3.0
|
||||||
with:
|
with:
|
||||||
ref: master
|
ref: master
|
||||||
|
|
||||||
- name: Set up Node ${{ env.NODE_VERSION }}
|
- name: Set up Node ${{ env.NODE_VERSION }}
|
||||||
uses: actions/setup-node@v3.5.1
|
uses: actions/setup-node@v3.6.0
|
||||||
with:
|
with:
|
||||||
node-version: ${{ env.NODE_VERSION }}
|
node-version: ${{ env.NODE_VERSION }}
|
||||||
cache: yarn
|
cache: yarn
|
||||||
|
4
.github/workflows/design_deployment.yaml
vendored
4
.github/workflows/design_deployment.yaml
vendored
@ -17,10 +17,10 @@ jobs:
|
|||||||
url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }}
|
url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }}
|
||||||
steps:
|
steps:
|
||||||
- name: Check out files from GitHub
|
- name: Check out files from GitHub
|
||||||
uses: actions/checkout@v3.2.0
|
uses: actions/checkout@v3.3.0
|
||||||
|
|
||||||
- name: Set up Node ${{ env.NODE_VERSION }}
|
- name: Set up Node ${{ env.NODE_VERSION }}
|
||||||
uses: actions/setup-node@v3.5.1
|
uses: actions/setup-node@v3.6.0
|
||||||
with:
|
with:
|
||||||
node-version: ${{ env.NODE_VERSION }}
|
node-version: ${{ env.NODE_VERSION }}
|
||||||
cache: yarn
|
cache: yarn
|
||||||
|
4
.github/workflows/design_preview.yaml
vendored
4
.github/workflows/design_preview.yaml
vendored
@ -22,10 +22,10 @@ jobs:
|
|||||||
if: github.repository == 'home-assistant/frontend' && contains(github.event.pull_request.labels.*.name, 'needs design preview')
|
if: github.repository == 'home-assistant/frontend' && contains(github.event.pull_request.labels.*.name, 'needs design preview')
|
||||||
steps:
|
steps:
|
||||||
- name: Check out files from GitHub
|
- name: Check out files from GitHub
|
||||||
uses: actions/checkout@v3.2.0
|
uses: actions/checkout@v3.3.0
|
||||||
|
|
||||||
- name: Set up Node ${{ env.NODE_VERSION }}
|
- name: Set up Node ${{ env.NODE_VERSION }}
|
||||||
uses: actions/setup-node@v3.5.1
|
uses: actions/setup-node@v3.6.0
|
||||||
with:
|
with:
|
||||||
node-version: ${{ env.NODE_VERSION }}
|
node-version: ${{ env.NODE_VERSION }}
|
||||||
cache: yarn
|
cache: yarn
|
||||||
|
4
.github/workflows/nightly.yaml
vendored
4
.github/workflows/nightly.yaml
vendored
@ -21,7 +21,7 @@ jobs:
|
|||||||
contents: write
|
contents: write
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout the repository
|
- name: Checkout the repository
|
||||||
uses: actions/checkout@v3.2.0
|
uses: actions/checkout@v3.3.0
|
||||||
|
|
||||||
- name: Set up Python ${{ env.PYTHON_VERSION }}
|
- name: Set up Python ${{ env.PYTHON_VERSION }}
|
||||||
uses: actions/setup-python@v4
|
uses: actions/setup-python@v4
|
||||||
@ -29,7 +29,7 @@ jobs:
|
|||||||
python-version: ${{ env.PYTHON_VERSION }}
|
python-version: ${{ env.PYTHON_VERSION }}
|
||||||
|
|
||||||
- name: Set up Node ${{ env.NODE_VERSION }}
|
- name: Set up Node ${{ env.NODE_VERSION }}
|
||||||
uses: actions/setup-node@v3.5.1
|
uses: actions/setup-node@v3.6.0
|
||||||
with:
|
with:
|
||||||
node-version: ${{ env.NODE_VERSION }}
|
node-version: ${{ env.NODE_VERSION }}
|
||||||
cache: yarn
|
cache: yarn
|
||||||
|
4
.github/workflows/release.yaml
vendored
4
.github/workflows/release.yaml
vendored
@ -24,7 +24,7 @@ jobs:
|
|||||||
contents: write # Required to upload release assets
|
contents: write # Required to upload release assets
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout the repository
|
- name: Checkout the repository
|
||||||
uses: actions/checkout@v3.2.0
|
uses: actions/checkout@v3.3.0
|
||||||
|
|
||||||
- name: Verify version
|
- name: Verify version
|
||||||
uses: home-assistant/actions/helpers/verify-version@master
|
uses: home-assistant/actions/helpers/verify-version@master
|
||||||
@ -35,7 +35,7 @@ jobs:
|
|||||||
python-version: ${{ env.PYTHON_VERSION }}
|
python-version: ${{ env.PYTHON_VERSION }}
|
||||||
|
|
||||||
- name: Set up Node ${{ env.NODE_VERSION }}
|
- name: Set up Node ${{ env.NODE_VERSION }}
|
||||||
uses: actions/setup-node@v3.5.1
|
uses: actions/setup-node@v3.6.0
|
||||||
with:
|
with:
|
||||||
node-version: ${{ env.NODE_VERSION }}
|
node-version: ${{ env.NODE_VERSION }}
|
||||||
cache: yarn
|
cache: yarn
|
||||||
|
2
.github/workflows/translations.yaml
vendored
2
.github/workflows/translations.yaml
vendored
@ -16,7 +16,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout the repository
|
- name: Checkout the repository
|
||||||
uses: actions/checkout@v3.2.0
|
uses: actions/checkout@v3.3.0
|
||||||
|
|
||||||
- name: Upload Translations
|
- name: Upload Translations
|
||||||
run: |
|
run: |
|
||||||
|
@ -98,7 +98,9 @@ const alerts: {
|
|||||||
description: "Alert with slotted image",
|
description: "Alert with slotted image",
|
||||||
type: "warning",
|
type: "warning",
|
||||||
iconSlot: html`<span slot="icon" class="image"
|
iconSlot: html`<span slot="icon" class="image"
|
||||||
><img src="https://www.home-assistant.io/images/home-assistant-logo.svg"
|
><img
|
||||||
|
alt="Home Assistant logo"
|
||||||
|
src="https://www.home-assistant.io/images/home-assistant-logo.svg"
|
||||||
/></span>`,
|
/></span>`,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -29,7 +29,9 @@ class HassioAddonRepositoryEl extends LitElement {
|
|||||||
if (filter) {
|
if (filter) {
|
||||||
return filterAndSort(addons, filter);
|
return filterAndSort(addons, filter);
|
||||||
}
|
}
|
||||||
return addons.sort((a, b) => caseInsensitiveStringCompare(a.name, b.name));
|
return addons.sort((a, b) =>
|
||||||
|
caseInsensitiveStringCompare(a.name, b.name, this.hass.locale.language)
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
protected render(): TemplateResult {
|
protected render(): TemplateResult {
|
||||||
|
@ -404,6 +404,7 @@ class HassioAddonInfo extends LitElement {
|
|||||||
? html`
|
? html`
|
||||||
<img
|
<img
|
||||||
class="logo"
|
class="logo"
|
||||||
|
alt=""
|
||||||
src="/api/hassio/addons/${this.addon.slug}/logo"
|
src="/api/hassio/addons/${this.addon.slug}/logo"
|
||||||
/>
|
/>
|
||||||
`
|
`
|
||||||
|
@ -35,7 +35,13 @@ class HassioAddons extends LitElement {
|
|||||||
</ha-card>
|
</ha-card>
|
||||||
`
|
`
|
||||||
: this.supervisor.addon.addons
|
: this.supervisor.addon.addons
|
||||||
.sort((a, b) => caseInsensitiveStringCompare(a.name, b.name))
|
.sort((a, b) =>
|
||||||
|
caseInsensitiveStringCompare(
|
||||||
|
a.name,
|
||||||
|
b.name,
|
||||||
|
this.hass.locale.language
|
||||||
|
)
|
||||||
|
)
|
||||||
.map(
|
.map(
|
||||||
(addon) => html`
|
(addon) => html`
|
||||||
<ha-card
|
<ha-card
|
||||||
|
@ -15,7 +15,12 @@ import { HomeAssistant } from "../../../../src/types";
|
|||||||
import { HassioHardwareDialogParams } from "./show-dialog-hassio-hardware";
|
import { HassioHardwareDialogParams } from "./show-dialog-hassio-hardware";
|
||||||
|
|
||||||
const _filterDevices = memoizeOne(
|
const _filterDevices = memoizeOne(
|
||||||
(showAdvanced: boolean, hardware: HassioHardwareInfo, filter: string) =>
|
(
|
||||||
|
showAdvanced: boolean,
|
||||||
|
hardware: HassioHardwareInfo,
|
||||||
|
filter: string,
|
||||||
|
language: string
|
||||||
|
) =>
|
||||||
hardware.devices
|
hardware.devices
|
||||||
.filter(
|
.filter(
|
||||||
(device) =>
|
(device) =>
|
||||||
@ -28,7 +33,7 @@ const _filterDevices = memoizeOne(
|
|||||||
.toLocaleLowerCase()
|
.toLocaleLowerCase()
|
||||||
.includes(filter))
|
.includes(filter))
|
||||||
)
|
)
|
||||||
.sort((a, b) => stringCompare(a.name, b.name))
|
.sort((a, b) => stringCompare(a.name, b.name, language))
|
||||||
);
|
);
|
||||||
|
|
||||||
@customElement("dialog-hassio-hardware")
|
@customElement("dialog-hassio-hardware")
|
||||||
@ -56,7 +61,8 @@ class HassioHardwareDialog extends LitElement {
|
|||||||
const devices = _filterDevices(
|
const devices = _filterDevices(
|
||||||
this.hass.userData?.showAdvanced || false,
|
this.hass.userData?.showAdvanced || false,
|
||||||
this._dialogParams.hardware,
|
this._dialogParams.hardware,
|
||||||
(this._filter || "").toLowerCase()
|
(this._filter || "").toLowerCase(),
|
||||||
|
this.hass.locale.language
|
||||||
);
|
);
|
||||||
|
|
||||||
return html`
|
return html`
|
||||||
|
@ -68,7 +68,9 @@ class HassioRepositoriesDialog extends LitElement {
|
|||||||
repo.slug !== "a0d7b954" && // Home Assistant Community Add-ons
|
repo.slug !== "a0d7b954" && // Home Assistant Community Add-ons
|
||||||
repo.slug !== "5c53de3b" // The ESPHome repository
|
repo.slug !== "5c53de3b" // The ESPHome repository
|
||||||
)
|
)
|
||||||
.sort((a, b) => caseInsensitiveStringCompare(a.name, b.name))
|
.sort((a, b) =>
|
||||||
|
caseInsensitiveStringCompare(a.name, b.name, this.hass.locale.language)
|
||||||
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
private _filteredUsedRepositories = memoizeOne(
|
private _filteredUsedRepositories = memoizeOne(
|
||||||
|
46
package.json
46
package.json
@ -25,18 +25,13 @@
|
|||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@braintree/sanitize-url": "^6.0.0",
|
"@braintree/sanitize-url": "^6.0.0",
|
||||||
"@codemirror/autocomplete": "^0.19.12",
|
"@codemirror/autocomplete": "^6.4.0",
|
||||||
"@codemirror/commands": "^0.19.8",
|
"@codemirror/commands": "^6.1.3",
|
||||||
"@codemirror/gutter": "^0.19.9",
|
"@codemirror/language": "^6.3.2",
|
||||||
"@codemirror/highlight": "^0.19.7",
|
"@codemirror/legacy-modes": "^6.3.1",
|
||||||
"@codemirror/history": "^0.19.2",
|
"@codemirror/search": "^6.2.3",
|
||||||
"@codemirror/legacy-modes": "^0.19.0",
|
"@codemirror/state": "^6.2.0",
|
||||||
"@codemirror/rectangular-selection": "^0.19.1",
|
"@codemirror/view": "^6.7.1",
|
||||||
"@codemirror/search": "^0.19.6",
|
|
||||||
"@codemirror/state": "^0.19.6",
|
|
||||||
"@codemirror/stream-parser": "^0.19.5",
|
|
||||||
"@codemirror/text": "^0.19.6",
|
|
||||||
"@codemirror/view": "^0.19.40",
|
|
||||||
"@formatjs/intl-datetimeformat": "^4.2.5",
|
"@formatjs/intl-datetimeformat": "^4.2.5",
|
||||||
"@formatjs/intl-getcanonicallocales": "^1.8.0",
|
"@formatjs/intl-getcanonicallocales": "^1.8.0",
|
||||||
"@formatjs/intl-locale": "^2.4.40",
|
"@formatjs/intl-locale": "^2.4.40",
|
||||||
@ -49,6 +44,7 @@
|
|||||||
"@fullcalendar/interaction": "5.9.0",
|
"@fullcalendar/interaction": "5.9.0",
|
||||||
"@fullcalendar/list": "5.9.0",
|
"@fullcalendar/list": "5.9.0",
|
||||||
"@fullcalendar/timegrid": "5.9.0",
|
"@fullcalendar/timegrid": "5.9.0",
|
||||||
|
"@lezer/highlight": "^1.1.3",
|
||||||
"@lit-labs/motion": "^1.0.2",
|
"@lit-labs/motion": "^1.0.2",
|
||||||
"@lit-labs/virtualizer": "patch:@lit-labs/virtualizer@0.7.0-pre.2#./.yarn/patches/@lit-labs/virtualizer/event-target-shim.patch",
|
"@lit-labs/virtualizer": "patch:@lit-labs/virtualizer@0.7.0-pre.2#./.yarn/patches/@lit-labs/virtualizer/event-target-shim.patch",
|
||||||
"@material/chips": "14.0.0-canary.261f2db59.0",
|
"@material/chips": "14.0.0-canary.261f2db59.0",
|
||||||
@ -75,8 +71,8 @@
|
|||||||
"@material/mwc-textfield": "0.25.3",
|
"@material/mwc-textfield": "0.25.3",
|
||||||
"@material/mwc-top-app-bar-fixed": "^0.25.3",
|
"@material/mwc-top-app-bar-fixed": "^0.25.3",
|
||||||
"@material/top-app-bar": "14.0.0-canary.261f2db59.0",
|
"@material/top-app-bar": "14.0.0-canary.261f2db59.0",
|
||||||
"@mdi/js": "7.0.96",
|
"@mdi/js": "7.1.96",
|
||||||
"@mdi/svg": "7.0.96",
|
"@mdi/svg": "7.1.96",
|
||||||
"@polymer/app-layout": "^3.1.0",
|
"@polymer/app-layout": "^3.1.0",
|
||||||
"@polymer/iron-flex-layout": "^3.0.1",
|
"@polymer/iron-flex-layout": "^3.0.1",
|
||||||
"@polymer/iron-icon": "^3.0.1",
|
"@polymer/iron-icon": "^3.0.1",
|
||||||
@ -97,7 +93,7 @@
|
|||||||
"@vibrant/color": "^3.2.1-alpha.1",
|
"@vibrant/color": "^3.2.1-alpha.1",
|
||||||
"@vibrant/core": "^3.2.1-alpha.1",
|
"@vibrant/core": "^3.2.1-alpha.1",
|
||||||
"@vibrant/quantizer-mmcq": "^3.2.1-alpha.1",
|
"@vibrant/quantizer-mmcq": "^3.2.1-alpha.1",
|
||||||
"@vue/web-component-wrapper": "^1.2.0",
|
"@vue/web-component-wrapper": "^1.3.0",
|
||||||
"@webcomponents/scoped-custom-element-registry": "^0.0.5",
|
"@webcomponents/scoped-custom-element-registry": "^0.0.5",
|
||||||
"@webcomponents/webcomponentsjs": "^2.2.10",
|
"@webcomponents/webcomponentsjs": "^2.2.10",
|
||||||
"app-datepicker": "^5.1.0",
|
"app-datepicker": "^5.1.0",
|
||||||
@ -141,12 +137,12 @@
|
|||||||
"vue": "^2.6.12",
|
"vue": "^2.6.12",
|
||||||
"vue2-daterange-picker": "^0.5.1",
|
"vue2-daterange-picker": "^0.5.1",
|
||||||
"weekstart": "^1.1.0",
|
"weekstart": "^1.1.0",
|
||||||
"workbox-cacheable-response": "^6.4.2",
|
"workbox-cacheable-response": "^6.5.4",
|
||||||
"workbox-core": "^6.4.2",
|
"workbox-core": "^6.5.4",
|
||||||
"workbox-expiration": "^6.4.2",
|
"workbox-expiration": "^6.5.4",
|
||||||
"workbox-precaching": "^6.4.2",
|
"workbox-precaching": "^6.5.4",
|
||||||
"workbox-routing": "^6.4.2",
|
"workbox-routing": "^6.5.4",
|
||||||
"workbox-strategies": "^6.4.2",
|
"workbox-strategies": "^6.5.4",
|
||||||
"xss": "^1.0.9"
|
"xss": "^1.0.9"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
@ -202,7 +198,7 @@
|
|||||||
"eslint-plugin-unused-imports": "^1.1.5",
|
"eslint-plugin-unused-imports": "^1.1.5",
|
||||||
"eslint-plugin-wc": "^1.3.2",
|
"eslint-plugin-wc": "^1.3.2",
|
||||||
"fancy-log": "^2.0.0",
|
"fancy-log": "^2.0.0",
|
||||||
"fs-extra": "^7.0.1",
|
"fs-extra": "^11.1.0",
|
||||||
"glob": "^7.2.0",
|
"glob": "^7.2.0",
|
||||||
"gulp": "^4.0.2",
|
"gulp": "^4.0.2",
|
||||||
"gulp-flatmap": "^1.0.2",
|
"gulp-flatmap": "^1.0.2",
|
||||||
@ -222,7 +218,7 @@
|
|||||||
"merge-stream": "^1.0.1",
|
"merge-stream": "^1.0.1",
|
||||||
"mocha": "^8.4.0",
|
"mocha": "^8.4.0",
|
||||||
"object-hash": "^2.0.3",
|
"object-hash": "^2.0.3",
|
||||||
"open": "^7.0.4",
|
"open": "^8.4.0",
|
||||||
"pinst": "^3.0.0",
|
"pinst": "^3.0.0",
|
||||||
"prettier": "^2.8.1",
|
"prettier": "^2.8.1",
|
||||||
"require-dir": "^1.2.0",
|
"require-dir": "^1.2.0",
|
||||||
@ -237,7 +233,7 @@
|
|||||||
"tar": "^6.1.11",
|
"tar": "^6.1.11",
|
||||||
"terser-webpack-plugin": "^5.2.4",
|
"terser-webpack-plugin": "^5.2.4",
|
||||||
"ts-lit-plugin": "^1.2.1",
|
"ts-lit-plugin": "^1.2.1",
|
||||||
"typescript": "^4.9.3",
|
"typescript": "^4.9.4",
|
||||||
"vinyl-buffer": "^1.0.1",
|
"vinyl-buffer": "^1.0.1",
|
||||||
"vinyl-source-stream": "^2.0.0",
|
"vinyl-source-stream": "^2.0.0",
|
||||||
"webpack": "^5.55.1",
|
"webpack": "^5.55.1",
|
||||||
@ -245,7 +241,7 @@
|
|||||||
"webpack-dev-server": "^4.3.0",
|
"webpack-dev-server": "^4.3.0",
|
||||||
"webpack-manifest-plugin": "^4.0.2",
|
"webpack-manifest-plugin": "^4.0.2",
|
||||||
"webpackbar": "^5.0.0-3",
|
"webpackbar": "^5.0.0-3",
|
||||||
"workbox-build": "^6.4.2"
|
"workbox-build": "^6.5.4"
|
||||||
},
|
},
|
||||||
"_comment": "Polymer 3.2 contained a bug, fixed in https://github.com/Polymer/polymer/pull/5569, add as patch",
|
"_comment": "Polymer 3.2 contained a bug, fixed in https://github.com/Polymer/polymer/pull/5569, add as patch",
|
||||||
"resolutions": {
|
"resolutions": {
|
||||||
|
@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|||||||
|
|
||||||
[project]
|
[project]
|
||||||
name = "home-assistant-frontend"
|
name = "home-assistant-frontend"
|
||||||
version = "20221213.0"
|
version = "20230104.0"
|
||||||
license = {text = "Apache-2.0"}
|
license = {text = "Apache-2.0"}
|
||||||
description = "The Home Assistant frontend"
|
description = "The Home Assistant frontend"
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
|
@ -201,6 +201,7 @@ export const DOMAINS_WITH_CARD = [
|
|||||||
export const SENSOR_ENTITIES = [
|
export const SENSOR_ENTITIES = [
|
||||||
"sensor",
|
"sensor",
|
||||||
"binary_sensor",
|
"binary_sensor",
|
||||||
|
"calendar",
|
||||||
"camera",
|
"camera",
|
||||||
"device_tracker",
|
"device_tracker",
|
||||||
"weather",
|
"weather",
|
||||||
|
@ -1,4 +1,15 @@
|
|||||||
export const stringCompare = (a: string, b: string) => {
|
import memoizeOne from "memoize-one";
|
||||||
|
|
||||||
|
const collator = memoizeOne(
|
||||||
|
(language: string | undefined) => new Intl.Collator(language)
|
||||||
|
);
|
||||||
|
|
||||||
|
const caseInsensitiveCollator = memoizeOne(
|
||||||
|
(language: string | undefined) =>
|
||||||
|
new Intl.Collator(language, { sensitivity: "accent" })
|
||||||
|
);
|
||||||
|
|
||||||
|
const fallbackStringCompare = (a: string, b: string) => {
|
||||||
if (a < b) {
|
if (a < b) {
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
@ -9,5 +20,28 @@ export const stringCompare = (a: string, b: string) => {
|
|||||||
return 0;
|
return 0;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const caseInsensitiveStringCompare = (a: string, b: string) =>
|
export const stringCompare = (
|
||||||
stringCompare(a.toLowerCase(), b.toLowerCase());
|
a: string,
|
||||||
|
b: string,
|
||||||
|
language: string | undefined = undefined
|
||||||
|
) => {
|
||||||
|
// @ts-ignore
|
||||||
|
if (Intl?.Collator) {
|
||||||
|
return collator(language).compare(a, b);
|
||||||
|
}
|
||||||
|
|
||||||
|
return fallbackStringCompare(a, b);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const caseInsensitiveStringCompare = (
|
||||||
|
a: string,
|
||||||
|
b: string,
|
||||||
|
language: string | undefined = undefined
|
||||||
|
) => {
|
||||||
|
// @ts-ignore
|
||||||
|
if (Intl?.Collator) {
|
||||||
|
return caseInsensitiveCollator(language).compare(a, b);
|
||||||
|
}
|
||||||
|
|
||||||
|
return fallbackStringCompare(a.toLowerCase(), b.toLowerCase());
|
||||||
|
};
|
||||||
|
@ -266,14 +266,16 @@ export const getCountryOptions = memoizeOne((language?: string) => {
|
|||||||
value: country,
|
value: country,
|
||||||
label: countryDisplayNames ? countryDisplayNames.of(country)! : country,
|
label: countryDisplayNames ? countryDisplayNames.of(country)! : country,
|
||||||
}));
|
}));
|
||||||
options.sort((a, b) => caseInsensitiveStringCompare(a.label, b.label));
|
options.sort((a, b) =>
|
||||||
|
caseInsensitiveStringCompare(a.label, b.label, language)
|
||||||
|
);
|
||||||
return options;
|
return options;
|
||||||
});
|
});
|
||||||
|
|
||||||
export const createCountryListEl = () => {
|
export const createCountryListEl = (language?: string) => {
|
||||||
const list = document.createElement("datalist");
|
const list = document.createElement("datalist");
|
||||||
list.id = "countries";
|
list.id = "countries";
|
||||||
const options = getCountryOptions();
|
const options = getCountryOptions(language);
|
||||||
for (const country of options) {
|
for (const country of options) {
|
||||||
const option = document.createElement("option");
|
const option = document.createElement("option");
|
||||||
option.value = country.value;
|
option.value = country.value;
|
||||||
|
@ -157,7 +157,7 @@ export const CURRENCIES = [
|
|||||||
"XPF",
|
"XPF",
|
||||||
"YER",
|
"YER",
|
||||||
"ZAR",
|
"ZAR",
|
||||||
"ZMK",
|
"ZMW",
|
||||||
"ZWL",
|
"ZWL",
|
||||||
];
|
];
|
||||||
|
|
||||||
@ -173,14 +173,16 @@ export const getCurrencyOptions = memoizeOne((language?: string) => {
|
|||||||
value: currency,
|
value: currency,
|
||||||
label: currencyDisplayNames ? currencyDisplayNames.of(currency)! : currency,
|
label: currencyDisplayNames ? currencyDisplayNames.of(currency)! : currency,
|
||||||
}));
|
}));
|
||||||
options.sort((a, b) => caseInsensitiveStringCompare(a.label, b.label));
|
options.sort((a, b) =>
|
||||||
|
caseInsensitiveStringCompare(a.label, b.label, language)
|
||||||
|
);
|
||||||
return options;
|
return options;
|
||||||
});
|
});
|
||||||
|
|
||||||
export const createCurrencyListEl = () => {
|
export const createCurrencyListEl = (language: string) => {
|
||||||
const list = document.createElement("datalist");
|
const list = document.createElement("datalist");
|
||||||
list.id = "currencies";
|
list.id = "currencies";
|
||||||
for (const currency of getCurrencyOptions()) {
|
for (const currency of getCurrencyOptions(language)) {
|
||||||
const option = document.createElement("option");
|
const option = document.createElement("option");
|
||||||
option.value = currency.value;
|
option.value = currency.value;
|
||||||
option.innerText = currency.label;
|
option.innerText = currency.label;
|
||||||
|
@ -5,7 +5,6 @@ import DateRangePicker from "vue2-daterange-picker";
|
|||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
import dateRangePickerStyles from "vue2-daterange-picker/dist/vue2-daterange-picker.css";
|
import dateRangePickerStyles from "vue2-daterange-picker/dist/vue2-daterange-picker.css";
|
||||||
import { fireEvent } from "../common/dom/fire_event";
|
import { fireEvent } from "../common/dom/fire_event";
|
||||||
import { Constructor } from "../types";
|
|
||||||
|
|
||||||
const Component = Vue.extend({
|
const Component = Vue.extend({
|
||||||
props: {
|
props: {
|
||||||
@ -47,35 +46,26 @@ const Component = Vue.extend({
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
render(createElement) {
|
render(createElement) {
|
||||||
// @ts-ignore
|
// @ts-expect-error
|
||||||
return createElement(DateRangePicker, {
|
return createElement(DateRangePicker, {
|
||||||
props: {
|
props: {
|
||||||
// @ts-ignore
|
|
||||||
"time-picker": this.timePicker,
|
"time-picker": this.timePicker,
|
||||||
// @ts-ignore
|
|
||||||
"auto-apply": this.autoApply,
|
"auto-apply": this.autoApply,
|
||||||
opens: "right",
|
opens: "right",
|
||||||
"show-dropdowns": false,
|
"show-dropdowns": false,
|
||||||
// @ts-ignore
|
|
||||||
"time-picker24-hour": this.twentyfourHours,
|
"time-picker24-hour": this.twentyfourHours,
|
||||||
// @ts-ignore
|
|
||||||
disabled: this.disabled,
|
disabled: this.disabled,
|
||||||
// @ts-ignore
|
|
||||||
ranges: this.ranges ? {} : false,
|
ranges: this.ranges ? {} : false,
|
||||||
"locale-data": {
|
"locale-data": {
|
||||||
// @ts-ignore
|
|
||||||
firstDay: this.firstDay,
|
firstDay: this.firstDay,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
model: {
|
model: {
|
||||||
value: {
|
value: {
|
||||||
// @ts-ignore
|
|
||||||
startDate: this.startDate,
|
startDate: this.startDate,
|
||||||
// @ts-ignore
|
|
||||||
endDate: this.endDate,
|
endDate: this.endDate,
|
||||||
},
|
},
|
||||||
callback: (value) => {
|
callback: (value) => {
|
||||||
// @ts-ignore
|
|
||||||
fireEvent(this.$el as HTMLElement, "change", value);
|
fireEvent(this.$el as HTMLElement, "change", value);
|
||||||
},
|
},
|
||||||
expression: "dateRange",
|
expression: "dateRange",
|
||||||
@ -106,7 +96,11 @@ const Component = Vue.extend({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const WrappedElement: Constructor<HTMLElement> = wrap(Vue, Component);
|
// Assertion corrects HTMLElement type from package
|
||||||
|
const WrappedElement = wrap(
|
||||||
|
Vue,
|
||||||
|
Component
|
||||||
|
) as unknown as CustomElementConstructor;
|
||||||
|
|
||||||
@customElement("date-range-picker")
|
@customElement("date-range-picker")
|
||||||
class DateRangePickerElement extends WrappedElement {
|
class DateRangePickerElement extends WrappedElement {
|
||||||
|
@ -189,7 +189,8 @@ export class HaAreaDevicesPicker extends SubscribeMixin(LitElement) {
|
|||||||
.sort((a, b) =>
|
.sort((a, b) =>
|
||||||
stringCompare(
|
stringCompare(
|
||||||
devicesByArea[a].name || "",
|
devicesByArea[a].name || "",
|
||||||
devicesByArea[b].name || ""
|
devicesByArea[b].name || "",
|
||||||
|
this.hass.locale.language
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
.map((key) => devicesByArea[key]);
|
.map((key) => devicesByArea[key]);
|
||||||
|
@ -84,6 +84,14 @@ export class HaDevicePicker extends SubscribeMixin(LitElement) {
|
|||||||
@property({ type: Array, attribute: "include-device-classes" })
|
@property({ type: Array, attribute: "include-device-classes" })
|
||||||
public includeDeviceClasses?: string[];
|
public includeDeviceClasses?: string[];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* List of devices to be excluded.
|
||||||
|
* @type {Array}
|
||||||
|
* @attr exclude-devices
|
||||||
|
*/
|
||||||
|
@property({ type: Array, attribute: "exclude-devices" })
|
||||||
|
public excludeDevices?: string[];
|
||||||
|
|
||||||
@property() public deviceFilter?: HaDevicePickerDeviceFilterFunc;
|
@property() public deviceFilter?: HaDevicePickerDeviceFilterFunc;
|
||||||
|
|
||||||
@property({ type: Boolean }) public disabled?: boolean;
|
@property({ type: Boolean }) public disabled?: boolean;
|
||||||
@ -104,7 +112,8 @@ export class HaDevicePicker extends SubscribeMixin(LitElement) {
|
|||||||
includeDomains: this["includeDomains"],
|
includeDomains: this["includeDomains"],
|
||||||
excludeDomains: this["excludeDomains"],
|
excludeDomains: this["excludeDomains"],
|
||||||
includeDeviceClasses: this["includeDeviceClasses"],
|
includeDeviceClasses: this["includeDeviceClasses"],
|
||||||
deviceFilter: this["deviceFilter"]
|
deviceFilter: this["deviceFilter"],
|
||||||
|
excludeDevices: this["excludeDevices"]
|
||||||
): Device[] => {
|
): Device[] => {
|
||||||
if (!devices.length) {
|
if (!devices.length) {
|
||||||
return [
|
return [
|
||||||
@ -164,6 +173,12 @@ export class HaDevicePicker extends SubscribeMixin(LitElement) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (excludeDevices) {
|
||||||
|
inputDevices = inputDevices.filter(
|
||||||
|
(device) => !excludeDevices!.includes(device.id)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if (includeDeviceClasses) {
|
if (includeDeviceClasses) {
|
||||||
inputDevices = inputDevices.filter((device) => {
|
inputDevices = inputDevices.filter((device) => {
|
||||||
const devEntities = deviceEntityLookup[device.id];
|
const devEntities = deviceEntityLookup[device.id];
|
||||||
@ -216,7 +231,7 @@ export class HaDevicePicker extends SubscribeMixin(LitElement) {
|
|||||||
return outputDevices;
|
return outputDevices;
|
||||||
}
|
}
|
||||||
return outputDevices.sort((a, b) =>
|
return outputDevices.sort((a, b) =>
|
||||||
stringCompare(a.name || "", b.name || "")
|
stringCompare(a.name || "", b.name || "", this.hass.locale.language)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
@ -258,7 +273,8 @@ export class HaDevicePicker extends SubscribeMixin(LitElement) {
|
|||||||
this.includeDomains,
|
this.includeDomains,
|
||||||
this.excludeDomains,
|
this.excludeDomains,
|
||||||
this.includeDeviceClasses,
|
this.includeDeviceClasses,
|
||||||
this.deviceFilter
|
this.deviceFilter,
|
||||||
|
this.excludeDevices
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -174,7 +174,8 @@ export class HaEntityPicker extends LitElement {
|
|||||||
.sort((entityA, entityB) =>
|
.sort((entityA, entityB) =>
|
||||||
caseInsensitiveStringCompare(
|
caseInsensitiveStringCompare(
|
||||||
entityA.friendly_name,
|
entityA.friendly_name,
|
||||||
entityB.friendly_name
|
entityB.friendly_name,
|
||||||
|
this.hass.locale.language
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -205,7 +206,8 @@ export class HaEntityPicker extends LitElement {
|
|||||||
.sort((entityA, entityB) =>
|
.sort((entityA, entityB) =>
|
||||||
caseInsensitiveStringCompare(
|
caseInsensitiveStringCompare(
|
||||||
entityA.friendly_name,
|
entityA.friendly_name,
|
||||||
entityB.friendly_name
|
entityB.friendly_name,
|
||||||
|
this.hass.locale.language
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -223,6 +223,10 @@ export class HaStateLabelBadge extends LitElement {
|
|||||||
if (domainStateKey) {
|
if (domainStateKey) {
|
||||||
return this.hass!.localize(`state_badge.${domainStateKey}`);
|
return this.hass!.localize(`state_badge.${domainStateKey}`);
|
||||||
}
|
}
|
||||||
|
// Person and device tracker state can be zone name
|
||||||
|
if (domain === "person" || domain === "device_tracker") {
|
||||||
|
return entityState.state;
|
||||||
|
}
|
||||||
if (domain === "timer") {
|
if (domain === "timer") {
|
||||||
return secondsToDuration(_timerTimeRemaining);
|
return secondsToDuration(_timerTimeRemaining);
|
||||||
}
|
}
|
||||||
|
@ -177,7 +177,9 @@ export class HaStatisticPicker extends LitElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (output.length > 1) {
|
if (output.length > 1) {
|
||||||
output.sort((a, b) => stringCompare(a.name || "", b.name || ""));
|
output.sort((a, b) =>
|
||||||
|
stringCompare(a.name || "", b.name || "", this.hass.locale.language)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
output.push({
|
output.push({
|
||||||
|
@ -28,7 +28,7 @@ class StateInfo extends LitElement {
|
|||||||
|
|
||||||
const name = computeStateName(this.stateObj);
|
const name = computeStateName(this.stateObj);
|
||||||
|
|
||||||
return html` <state-badge
|
return html`<state-badge
|
||||||
.stateObj=${this.stateObj}
|
.stateObj=${this.stateObj}
|
||||||
.stateColor=${true}
|
.stateColor=${true}
|
||||||
.color=${this.color}
|
.color=${this.color}
|
||||||
|
@ -16,7 +16,11 @@ const rowRenderer: ComboBoxLitRenderer<HassioAddonInfo> = (
|
|||||||
<span>${item.name}</span>
|
<span>${item.name}</span>
|
||||||
<span slot="secondary">${item.slug}</span>
|
<span slot="secondary">${item.slug}</span>
|
||||||
${item.icon
|
${item.icon
|
||||||
? html`<img slot="graphic" .src="/api/hassio/addons/${item.slug}/icon" />`
|
? html`<img
|
||||||
|
alt=""
|
||||||
|
slot="graphic"
|
||||||
|
.src="/api/hassio/addons/${item.slug}/icon"
|
||||||
|
/>`
|
||||||
: ""}
|
: ""}
|
||||||
</mwc-list-item>`;
|
</mwc-list-item>`;
|
||||||
|
|
||||||
@ -80,7 +84,9 @@ class HaAddonPicker extends LitElement {
|
|||||||
const addonsInfo = await fetchHassioAddonsInfo(this.hass);
|
const addonsInfo = await fetchHassioAddonsInfo(this.hass);
|
||||||
this._addons = addonsInfo.addons
|
this._addons = addonsInfo.addons
|
||||||
.filter((addon) => addon.version)
|
.filter((addon) => addon.version)
|
||||||
.sort((a, b) => stringCompare(a.name, b.name));
|
.sort((a, b) =>
|
||||||
|
stringCompare(a.name, b.name, this.hass.locale.language)
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
showAlertDialog(this, {
|
showAlertDialog(this, {
|
||||||
title: this.hass.localize(
|
title: this.hass.localize(
|
||||||
|
@ -73,6 +73,14 @@ export class HaAreaPicker extends LitElement {
|
|||||||
@property({ type: Array, attribute: "include-device-classes" })
|
@property({ type: Array, attribute: "include-device-classes" })
|
||||||
public includeDeviceClasses?: string[];
|
public includeDeviceClasses?: string[];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* List of areas to be excluded.
|
||||||
|
* @type {Array}
|
||||||
|
* @attr exclude-areas
|
||||||
|
*/
|
||||||
|
@property({ type: Array, attribute: "exclude-areas" })
|
||||||
|
public excludeAreas?: string[];
|
||||||
|
|
||||||
@property() public deviceFilter?: HaDevicePickerDeviceFilterFunc;
|
@property() public deviceFilter?: HaDevicePickerDeviceFilterFunc;
|
||||||
|
|
||||||
@property() public entityFilter?: (entity: EntityRegistryEntry) => boolean;
|
@property() public entityFilter?: (entity: EntityRegistryEntry) => boolean;
|
||||||
@ -109,7 +117,8 @@ export class HaAreaPicker extends LitElement {
|
|||||||
includeDeviceClasses: this["includeDeviceClasses"],
|
includeDeviceClasses: this["includeDeviceClasses"],
|
||||||
deviceFilter: this["deviceFilter"],
|
deviceFilter: this["deviceFilter"],
|
||||||
entityFilter: this["entityFilter"],
|
entityFilter: this["entityFilter"],
|
||||||
noAdd: this["noAdd"]
|
noAdd: this["noAdd"],
|
||||||
|
excludeAreas: this["excludeAreas"]
|
||||||
): AreaRegistryEntry[] => {
|
): AreaRegistryEntry[] => {
|
||||||
if (!areas.length) {
|
if (!areas.length) {
|
||||||
return [
|
return [
|
||||||
@ -235,6 +244,12 @@ export class HaAreaPicker extends LitElement {
|
|||||||
outputAreas = areas.filter((area) => areaIds!.includes(area.area_id));
|
outputAreas = areas.filter((area) => areaIds!.includes(area.area_id));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (excludeAreas) {
|
||||||
|
outputAreas = outputAreas.filter(
|
||||||
|
(area) => !excludeAreas!.includes(area.area_id)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if (!outputAreas.length) {
|
if (!outputAreas.length) {
|
||||||
outputAreas = [
|
outputAreas = [
|
||||||
{
|
{
|
||||||
@ -264,7 +279,7 @@ export class HaAreaPicker extends LitElement {
|
|||||||
(this._init && changedProps.has("_opened") && this._opened)
|
(this._init && changedProps.has("_opened") && this._opened)
|
||||||
) {
|
) {
|
||||||
this._init = true;
|
this._init = true;
|
||||||
(this.comboBox as any).items = this._getAreas(
|
const areas = this._getAreas(
|
||||||
Object.values(this.hass.areas),
|
Object.values(this.hass.areas),
|
||||||
Object.values(this.hass.devices),
|
Object.values(this.hass.devices),
|
||||||
Object.values(this.hass.entities),
|
Object.values(this.hass.entities),
|
||||||
@ -273,8 +288,11 @@ export class HaAreaPicker extends LitElement {
|
|||||||
this.includeDeviceClasses,
|
this.includeDeviceClasses,
|
||||||
this.deviceFilter,
|
this.deviceFilter,
|
||||||
this.entityFilter,
|
this.entityFilter,
|
||||||
this.noAdd
|
this.noAdd,
|
||||||
|
this.excludeAreas
|
||||||
);
|
);
|
||||||
|
(this.comboBox as any).items = areas;
|
||||||
|
(this.comboBox as any).filteredItems = areas;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -384,7 +402,8 @@ export class HaAreaPicker extends LitElement {
|
|||||||
this.includeDeviceClasses,
|
this.includeDeviceClasses,
|
||||||
this.deviceFilter,
|
this.deviceFilter,
|
||||||
this.entityFilter,
|
this.entityFilter,
|
||||||
this.noAdd
|
this.noAdd,
|
||||||
|
this.excludeAreas
|
||||||
);
|
);
|
||||||
await this.updateComplete;
|
await this.updateComplete;
|
||||||
await this.comboBox.updateComplete;
|
await this.comboBox.updateComplete;
|
||||||
|
@ -266,6 +266,9 @@ export class HaBaseTimeInput extends LitElement {
|
|||||||
seconds: this.seconds,
|
seconds: this.seconds,
|
||||||
milliseconds: this.milliseconds,
|
milliseconds: this.milliseconds,
|
||||||
};
|
};
|
||||||
|
if (this.enableDay) {
|
||||||
|
value.days = this.days;
|
||||||
|
}
|
||||||
if (this.format === 12) {
|
if (this.format === 12) {
|
||||||
value.amPm = this.amPm;
|
value.amPm = this.amPm;
|
||||||
}
|
}
|
||||||
|
@ -46,7 +46,9 @@ class HaBluePrintPicker extends LitElement {
|
|||||||
...(blueprint as Blueprint).metadata,
|
...(blueprint as Blueprint).metadata,
|
||||||
path,
|
path,
|
||||||
}));
|
}));
|
||||||
return result.sort((a, b) => stringCompare(a.name, b.name));
|
return result.sort((a, b) =>
|
||||||
|
stringCompare(a.name, b.name, this.hass!.locale.language)
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
protected render(): TemplateResult {
|
protected render(): TemplateResult {
|
||||||
|
@ -4,6 +4,7 @@ import type {
|
|||||||
CompletionResult,
|
CompletionResult,
|
||||||
CompletionSource,
|
CompletionSource,
|
||||||
} from "@codemirror/autocomplete";
|
} from "@codemirror/autocomplete";
|
||||||
|
import type { Extension } from "@codemirror/state";
|
||||||
import type { EditorView, KeyBinding, ViewUpdate } from "@codemirror/view";
|
import type { EditorView, KeyBinding, ViewUpdate } from "@codemirror/view";
|
||||||
import { HassEntities } from "home-assistant-js-websocket";
|
import { HassEntities } from "home-assistant-js-websocket";
|
||||||
import { css, CSSResultGroup, PropertyValues, ReactiveElement } from "lit";
|
import { css, CSSResultGroup, PropertyValues, ReactiveElement } from "lit";
|
||||||
@ -72,9 +73,9 @@ export class HaCodeEditor extends ReactiveElement {
|
|||||||
if (!this.codemirror || !this._loadedCodeMirror) {
|
if (!this.codemirror || !this._loadedCodeMirror) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
const className = this._loadedCodeMirror.HighlightStyle.get(
|
const className = this._loadedCodeMirror.highlightingFor(
|
||||||
this.codemirror.state,
|
this.codemirror.state,
|
||||||
this._loadedCodeMirror.tags.comment
|
[this._loadedCodeMirror.tags.comment]
|
||||||
);
|
);
|
||||||
return !!this.shadowRoot!.querySelector(`span.${className}`);
|
return !!this.shadowRoot!.querySelector(`span.${className}`);
|
||||||
}
|
}
|
||||||
@ -136,7 +137,7 @@ export class HaCodeEditor extends ReactiveElement {
|
|||||||
|
|
||||||
private async _load(): Promise<void> {
|
private async _load(): Promise<void> {
|
||||||
this._loadedCodeMirror = await loadCodeMirror();
|
this._loadedCodeMirror = await loadCodeMirror();
|
||||||
const extensions = [
|
const extensions: Extension[] = [
|
||||||
this._loadedCodeMirror.lineNumbers(),
|
this._loadedCodeMirror.lineNumbers(),
|
||||||
this._loadedCodeMirror.EditorState.allowMultipleSelections.of(true),
|
this._loadedCodeMirror.EditorState.allowMultipleSelections.of(true),
|
||||||
this._loadedCodeMirror.history(),
|
this._loadedCodeMirror.history(),
|
||||||
@ -152,10 +153,8 @@ export class HaCodeEditor extends ReactiveElement {
|
|||||||
saveKeyBinding,
|
saveKeyBinding,
|
||||||
] as KeyBinding[]),
|
] as KeyBinding[]),
|
||||||
this._loadedCodeMirror.langCompartment.of(this._mode),
|
this._loadedCodeMirror.langCompartment.of(this._mode),
|
||||||
this._loadedCodeMirror.theme,
|
this._loadedCodeMirror.haTheme,
|
||||||
this._loadedCodeMirror.Prec.fallback(
|
this._loadedCodeMirror.haSyntaxHighlighting,
|
||||||
this._loadedCodeMirror.highlightStyle
|
|
||||||
),
|
|
||||||
this._loadedCodeMirror.readonlyCompartment.of(
|
this._loadedCodeMirror.readonlyCompartment.of(
|
||||||
this._loadedCodeMirror.EditorView.editable.of(!this.readOnly)
|
this._loadedCodeMirror.EditorView.editable.of(!this.readOnly)
|
||||||
),
|
),
|
||||||
@ -227,7 +226,7 @@ export class HaCodeEditor extends ReactiveElement {
|
|||||||
return {
|
return {
|
||||||
from: Number(entityWord.from),
|
from: Number(entityWord.from),
|
||||||
options: states,
|
options: states,
|
||||||
span: /^[a-z_]{3,}\.\w*$/,
|
validFor: /^[a-z_]{3,}\.\w*$/,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -268,7 +267,7 @@ export class HaCodeEditor extends ReactiveElement {
|
|||||||
return {
|
return {
|
||||||
from: Number(match.from),
|
from: Number(match.from),
|
||||||
options: iconItems,
|
options: iconItems,
|
||||||
span: /^mdi:\S*$/,
|
validFor: /^mdi:\S*$/,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -59,6 +59,7 @@ class HaConfigEntryPicker extends LitElement {
|
|||||||
>
|
>
|
||||||
<span slot="secondary">${item.localized_domain_name}</span>
|
<span slot="secondary">${item.localized_domain_name}</span>
|
||||||
<img
|
<img
|
||||||
|
alt=""
|
||||||
slot="graphic"
|
slot="graphic"
|
||||||
src=${brandsUrl({
|
src=${brandsUrl({
|
||||||
domain: item.domain,
|
domain: item.domain,
|
||||||
@ -121,7 +122,8 @@ class HaConfigEntryPicker extends LitElement {
|
|||||||
.sort((conf1, conf2) =>
|
.sort((conf1, conf2) =>
|
||||||
caseInsensitiveStringCompare(
|
caseInsensitiveStringCompare(
|
||||||
conf1.localized_domain_name + conf1.title,
|
conf1.localized_domain_name + conf1.title,
|
||||||
conf2.localized_domain_name + conf2.title
|
conf2.localized_domain_name + conf2.title,
|
||||||
|
this.hass.locale.language
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
@ -3,10 +3,12 @@ import { styles } from "@material/mwc-dialog/mwc-dialog.css";
|
|||||||
import { mdiClose } from "@mdi/js";
|
import { mdiClose } from "@mdi/js";
|
||||||
import { css, html, TemplateResult } from "lit";
|
import { css, html, TemplateResult } from "lit";
|
||||||
import { customElement } from "lit/decorators";
|
import { customElement } from "lit/decorators";
|
||||||
import type { HomeAssistant } from "../types";
|
|
||||||
import { FOCUS_TARGET } from "../dialogs/make-dialog-manager";
|
import { FOCUS_TARGET } from "../dialogs/make-dialog-manager";
|
||||||
|
import type { HomeAssistant } from "../types";
|
||||||
import "./ha-icon-button";
|
import "./ha-icon-button";
|
||||||
|
|
||||||
|
const SUPPRESS_DEFAULT_PRESS_SELECTOR = ["button"];
|
||||||
|
|
||||||
export const createCloseHeading = (
|
export const createCloseHeading = (
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
title: string | TemplateResult
|
title: string | TemplateResult
|
||||||
@ -32,6 +34,14 @@ export class HaDialog extends DialogBase {
|
|||||||
return html`<slot name="heading"> ${super.renderHeading()} </slot>`;
|
return html`<slot name="heading"> ${super.renderHeading()} </slot>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected firstUpdated(): void {
|
||||||
|
super.firstUpdated();
|
||||||
|
this.suppressDefaultPressSelector = [
|
||||||
|
this.suppressDefaultPressSelector,
|
||||||
|
SUPPRESS_DEFAULT_PRESS_SELECTOR,
|
||||||
|
].join(", ");
|
||||||
|
}
|
||||||
|
|
||||||
static override styles = [
|
static override styles = [
|
||||||
styles,
|
styles,
|
||||||
css`
|
css`
|
||||||
|
@ -67,6 +67,9 @@ export class HaFormInteger extends LitElement implements HaFormElement {
|
|||||||
@change=${this._valueChanged}
|
@change=${this._valueChanged}
|
||||||
></ha-slider>
|
></ha-slider>
|
||||||
</div>
|
</div>
|
||||||
|
${this.helper
|
||||||
|
? html`<ha-input-helper-text>${this.helper}</ha-input-helper-text>`
|
||||||
|
: ""}
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
@ -1,8 +1,4 @@
|
|||||||
import {
|
import { HassEntity, HassServiceTarget } from "home-assistant-js-websocket";
|
||||||
HassEntity,
|
|
||||||
HassServiceTarget,
|
|
||||||
UnsubscribeFunc,
|
|
||||||
} from "home-assistant-js-websocket";
|
|
||||||
import {
|
import {
|
||||||
css,
|
css,
|
||||||
CSSResultGroup,
|
CSSResultGroup,
|
||||||
@ -17,8 +13,7 @@ import {
|
|||||||
DeviceRegistryEntry,
|
DeviceRegistryEntry,
|
||||||
getDeviceIntegrationLookup,
|
getDeviceIntegrationLookup,
|
||||||
} from "../../data/device_registry";
|
} from "../../data/device_registry";
|
||||||
import type { EntityRegistryEntry } from "../../data/entity_registry";
|
import { EntityRegistryEntry } from "../../data/entity_registry";
|
||||||
import { subscribeEntityRegistry } from "../../data/entity_registry";
|
|
||||||
import {
|
import {
|
||||||
EntitySources,
|
EntitySources,
|
||||||
fetchEntitySourcesWithCache,
|
fetchEntitySourcesWithCache,
|
||||||
@ -28,12 +23,11 @@ import {
|
|||||||
filterSelectorEntities,
|
filterSelectorEntities,
|
||||||
TargetSelector,
|
TargetSelector,
|
||||||
} from "../../data/selector";
|
} from "../../data/selector";
|
||||||
import { SubscribeMixin } from "../../mixins/subscribe-mixin";
|
|
||||||
import type { HomeAssistant } from "../../types";
|
import type { HomeAssistant } from "../../types";
|
||||||
import "../ha-target-picker";
|
import "../ha-target-picker";
|
||||||
|
|
||||||
@customElement("ha-selector-target")
|
@customElement("ha-selector-target")
|
||||||
export class HaTargetSelector extends SubscribeMixin(LitElement) {
|
export class HaTargetSelector extends LitElement {
|
||||||
@property() public hass!: HomeAssistant;
|
@property() public hass!: HomeAssistant;
|
||||||
|
|
||||||
@property() public selector!: TargetSelector;
|
@property() public selector!: TargetSelector;
|
||||||
@ -48,18 +42,8 @@ export class HaTargetSelector extends SubscribeMixin(LitElement) {
|
|||||||
|
|
||||||
@state() private _entitySources?: EntitySources;
|
@state() private _entitySources?: EntitySources;
|
||||||
|
|
||||||
@state() private _entities?: EntityRegistryEntry[];
|
|
||||||
|
|
||||||
private _deviceIntegrationLookup = memoizeOne(getDeviceIntegrationLookup);
|
private _deviceIntegrationLookup = memoizeOne(getDeviceIntegrationLookup);
|
||||||
|
|
||||||
public hassSubscribe(): UnsubscribeFunc[] {
|
|
||||||
return [
|
|
||||||
subscribeEntityRegistry(this.hass.connection!, (entities) => {
|
|
||||||
this._entities = entities.filter((entity) => entity.device_id !== null);
|
|
||||||
}),
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
protected updated(changedProperties: PropertyValues): void {
|
protected updated(changedProperties: PropertyValues): void {
|
||||||
super.updated(changedProperties);
|
super.updated(changedProperties);
|
||||||
if (
|
if (
|
||||||
@ -88,12 +72,19 @@ export class HaTargetSelector extends SubscribeMixin(LitElement) {
|
|||||||
.value=${this.value}
|
.value=${this.value}
|
||||||
.helper=${this.helper}
|
.helper=${this.helper}
|
||||||
.deviceFilter=${this._filterDevices}
|
.deviceFilter=${this._filterDevices}
|
||||||
.entityFilter=${this._filterEntities}
|
.entityFilter=${this._filterStates}
|
||||||
|
.entityRegFilter=${this._filterRegEntities}
|
||||||
|
.includeDeviceClasses=${this.selector.target?.entity?.device_class
|
||||||
|
? [this.selector.target?.entity.device_class]
|
||||||
|
: undefined}
|
||||||
|
.includeDomains=${this.selector.target?.entity?.domain
|
||||||
|
? [this.selector.target?.entity.domain]
|
||||||
|
: undefined}
|
||||||
.disabled=${this.disabled}
|
.disabled=${this.disabled}
|
||||||
></ha-target-picker>`;
|
></ha-target-picker>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
private _filterEntities = (entity: HassEntity): boolean => {
|
private _filterStates = (entity: HassEntity): boolean => {
|
||||||
if (!this.selector.target?.entity) {
|
if (!this.selector.target?.entity) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@ -105,14 +96,25 @@ export class HaTargetSelector extends SubscribeMixin(LitElement) {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
private _filterRegEntities = (entity: EntityRegistryEntry): boolean => {
|
||||||
|
if (this.selector.target?.entity?.integration) {
|
||||||
|
if (entity.platform !== this.selector.target.entity.integration) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
private _filterDevices = (device: DeviceRegistryEntry): boolean => {
|
private _filterDevices = (device: DeviceRegistryEntry): boolean => {
|
||||||
if (!this.selector.target?.device) {
|
if (!this.selector.target?.device) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
const deviceIntegrations =
|
const deviceIntegrations = this._entitySources
|
||||||
this._entitySources && this._entities
|
? this._deviceIntegrationLookup(
|
||||||
? this._deviceIntegrationLookup(this._entitySources, this._entities)
|
this._entitySources,
|
||||||
|
Object.values(this.hass.entities)
|
||||||
|
)
|
||||||
: undefined;
|
: undefined;
|
||||||
|
|
||||||
return filterSelectorDevices(
|
return filterSelectorDevices(
|
||||||
|
@ -87,7 +87,8 @@ const panelSorter = (
|
|||||||
reverseSort: string[],
|
reverseSort: string[],
|
||||||
defaultPanel: string,
|
defaultPanel: string,
|
||||||
a: PanelInfo,
|
a: PanelInfo,
|
||||||
b: PanelInfo
|
b: PanelInfo,
|
||||||
|
language: string
|
||||||
) => {
|
) => {
|
||||||
const indexA = reverseSort.indexOf(a.url_path);
|
const indexA = reverseSort.indexOf(a.url_path);
|
||||||
const indexB = reverseSort.indexOf(b.url_path);
|
const indexB = reverseSort.indexOf(b.url_path);
|
||||||
@ -97,13 +98,14 @@ const panelSorter = (
|
|||||||
}
|
}
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
return defaultPanelSorter(defaultPanel, a, b);
|
return defaultPanelSorter(defaultPanel, a, b, language);
|
||||||
};
|
};
|
||||||
|
|
||||||
const defaultPanelSorter = (
|
const defaultPanelSorter = (
|
||||||
defaultPanel: string,
|
defaultPanel: string,
|
||||||
a: PanelInfo,
|
a: PanelInfo,
|
||||||
b: PanelInfo
|
b: PanelInfo,
|
||||||
|
language: string
|
||||||
) => {
|
) => {
|
||||||
// Put all the Lovelace at the top.
|
// Put all the Lovelace at the top.
|
||||||
const aLovelace = a.component_name === "lovelace";
|
const aLovelace = a.component_name === "lovelace";
|
||||||
@ -117,7 +119,7 @@ const defaultPanelSorter = (
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (aLovelace && bLovelace) {
|
if (aLovelace && bLovelace) {
|
||||||
return stringCompare(a.title!, b.title!);
|
return stringCompare(a.title!, b.title!, language);
|
||||||
}
|
}
|
||||||
if (aLovelace && !bLovelace) {
|
if (aLovelace && !bLovelace) {
|
||||||
return -1;
|
return -1;
|
||||||
@ -139,7 +141,7 @@ const defaultPanelSorter = (
|
|||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
// both not built in, sort by title
|
// both not built in, sort by title
|
||||||
return stringCompare(a.title!, b.title!);
|
return stringCompare(a.title!, b.title!, language);
|
||||||
};
|
};
|
||||||
|
|
||||||
const computePanels = memoizeOne(
|
const computePanels = memoizeOne(
|
||||||
@ -147,7 +149,8 @@ const computePanels = memoizeOne(
|
|||||||
panels: HomeAssistant["panels"],
|
panels: HomeAssistant["panels"],
|
||||||
defaultPanel: HomeAssistant["defaultPanel"],
|
defaultPanel: HomeAssistant["defaultPanel"],
|
||||||
panelsOrder: string[],
|
panelsOrder: string[],
|
||||||
hiddenPanels: string[]
|
hiddenPanels: string[],
|
||||||
|
locale: HomeAssistant["locale"]
|
||||||
): [PanelInfo[], PanelInfo[]] => {
|
): [PanelInfo[], PanelInfo[]] => {
|
||||||
if (!panels) {
|
if (!panels) {
|
||||||
return [[], []];
|
return [[], []];
|
||||||
@ -171,8 +174,12 @@ const computePanels = memoizeOne(
|
|||||||
|
|
||||||
const reverseSort = [...panelsOrder].reverse();
|
const reverseSort = [...panelsOrder].reverse();
|
||||||
|
|
||||||
beforeSpacer.sort((a, b) => panelSorter(reverseSort, defaultPanel, a, b));
|
beforeSpacer.sort((a, b) =>
|
||||||
afterSpacer.sort((a, b) => panelSorter(reverseSort, defaultPanel, a, b));
|
panelSorter(reverseSort, defaultPanel, a, b, locale.language)
|
||||||
|
);
|
||||||
|
afterSpacer.sort((a, b) =>
|
||||||
|
panelSorter(reverseSort, defaultPanel, a, b, locale.language)
|
||||||
|
);
|
||||||
|
|
||||||
return [beforeSpacer, afterSpacer];
|
return [beforeSpacer, afterSpacer];
|
||||||
}
|
}
|
||||||
@ -374,7 +381,8 @@ class HaSidebar extends SubscribeMixin(LitElement) {
|
|||||||
this.hass.panels,
|
this.hass.panels,
|
||||||
this.hass.defaultPanel,
|
this.hass.defaultPanel,
|
||||||
this._panelOrder,
|
this._panelOrder,
|
||||||
this._hiddenPanels
|
this._hiddenPanels,
|
||||||
|
this.hass.locale
|
||||||
);
|
);
|
||||||
|
|
||||||
// Show the supervisor as beeing part of configuration
|
// Show the supervisor as beeing part of configuration
|
||||||
|
@ -345,6 +345,7 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
|
|||||||
.entityFilter=${this.entityRegFilter}
|
.entityFilter=${this.entityRegFilter}
|
||||||
.includeDeviceClasses=${this.includeDeviceClasses}
|
.includeDeviceClasses=${this.includeDeviceClasses}
|
||||||
.includeDomains=${this.includeDomains}
|
.includeDomains=${this.includeDomains}
|
||||||
|
.excludeAreas=${ensureArray(this.value?.area_id)}
|
||||||
@value-changed=${this._targetPicked}
|
@value-changed=${this._targetPicked}
|
||||||
></ha-area-picker>
|
></ha-area-picker>
|
||||||
`;
|
`;
|
||||||
@ -358,9 +359,9 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
|
|||||||
"ui.components.target-picker.add_device_id"
|
"ui.components.target-picker.add_device_id"
|
||||||
)}
|
)}
|
||||||
.deviceFilter=${this.deviceFilter}
|
.deviceFilter=${this.deviceFilter}
|
||||||
.entityFilter=${this.entityRegFilter}
|
|
||||||
.includeDeviceClasses=${this.includeDeviceClasses}
|
.includeDeviceClasses=${this.includeDeviceClasses}
|
||||||
.includeDomains=${this.includeDomains}
|
.includeDomains=${this.includeDomains}
|
||||||
|
.excludeDevices=${ensureArray(this.value?.device_id)}
|
||||||
@value-changed=${this._targetPicked}
|
@value-changed=${this._targetPicked}
|
||||||
></ha-device-picker>
|
></ha-device-picker>
|
||||||
`;
|
`;
|
||||||
@ -376,6 +377,7 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
|
|||||||
.entityFilter=${this.entityFilter}
|
.entityFilter=${this.entityFilter}
|
||||||
.includeDeviceClasses=${this.includeDeviceClasses}
|
.includeDeviceClasses=${this.includeDeviceClasses}
|
||||||
.includeDomains=${this.includeDomains}
|
.includeDomains=${this.includeDomains}
|
||||||
|
.excludeEntities=${ensureArray(this.value?.entity_id)}
|
||||||
@value-changed=${this._targetPicked}
|
@value-changed=${this._targetPicked}
|
||||||
allow-custom-entity
|
allow-custom-entity
|
||||||
></ha-entity-picker>
|
></ha-entity-picker>
|
||||||
@ -393,6 +395,13 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
|
|||||||
const target = ev.currentTarget;
|
const target = ev.currentTarget;
|
||||||
target.value = "";
|
target.value = "";
|
||||||
this._addMode = undefined;
|
this._addMode = undefined;
|
||||||
|
if (
|
||||||
|
this.value &&
|
||||||
|
this.value[target.type] &&
|
||||||
|
ensureArray(this.value[target.type]).includes(value)
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
fireEvent(this, "value-changed", {
|
fireEvent(this, "value-changed", {
|
||||||
value: this.value
|
value: this.value
|
||||||
? {
|
? {
|
||||||
|
@ -1,14 +1,19 @@
|
|||||||
import { CSSResultGroup, html, css, LitElement, TemplateResult } from "lit";
|
import { CSSResultGroup, html, css, LitElement, TemplateResult } from "lit";
|
||||||
import { customElement, property } from "lit/decorators";
|
import { customElement, property } from "lit/decorators";
|
||||||
|
import { ifDefined } from "lit/directives/if-defined";
|
||||||
|
|
||||||
@customElement("ha-tile-image")
|
@customElement("ha-tile-image")
|
||||||
export class HaTileImage extends LitElement {
|
export class HaTileImage extends LitElement {
|
||||||
@property() public imageUrl?: string;
|
@property() public imageUrl?: string;
|
||||||
|
|
||||||
|
@property() public imageAlt?: string;
|
||||||
|
|
||||||
protected render(): TemplateResult {
|
protected render(): TemplateResult {
|
||||||
return html`
|
return html`
|
||||||
<div class="image">
|
<div class="image">
|
||||||
${this.imageUrl ? html`<img src=${this.imageUrl} />` : null}
|
${this.imageUrl
|
||||||
|
? html`<img alt=${ifDefined(this.imageAlt)} src=${this.imageUrl} />`
|
||||||
|
: null}
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
@ -30,7 +30,9 @@ class HaUserPicker extends LitElement {
|
|||||||
|
|
||||||
return users
|
return users
|
||||||
.filter((user) => !user.system_generated)
|
.filter((user) => !user.system_generated)
|
||||||
.sort((a, b) => stringCompare(a.name, b.name));
|
.sort((a, b) =>
|
||||||
|
stringCompare(a.name, b.name, this.hass!.locale.language)
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
protected render(): TemplateResult {
|
protected render(): TemplateResult {
|
||||||
|
@ -50,6 +50,7 @@ export enum RecurrenceRange {
|
|||||||
export const enum CalendarEntityFeature {
|
export const enum CalendarEntityFeature {
|
||||||
CREATE_EVENT = 1,
|
CREATE_EVENT = 1,
|
||||||
DELETE_EVENT = 2,
|
DELETE_EVENT = 2,
|
||||||
|
UPDATE_EVENT = 4,
|
||||||
}
|
}
|
||||||
|
|
||||||
export const fetchCalendarEvents = async (
|
export const fetchCalendarEvents = async (
|
||||||
@ -161,12 +162,18 @@ export const createCalendarEvent = (
|
|||||||
export const updateCalendarEvent = (
|
export const updateCalendarEvent = (
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
entityId: string,
|
entityId: string,
|
||||||
event: CalendarEventMutableParams
|
uid: string,
|
||||||
|
event: CalendarEventMutableParams,
|
||||||
|
recurrence_id?: string,
|
||||||
|
recurrence_range?: RecurrenceRange
|
||||||
) =>
|
) =>
|
||||||
hass.callWS<void>({
|
hass.callWS<void>({
|
||||||
type: "calendar/event/update",
|
type: "calendar/event/update",
|
||||||
entity_id: entityId,
|
entity_id: entityId,
|
||||||
event: event,
|
uid,
|
||||||
|
recurrence_id,
|
||||||
|
recurrence_range,
|
||||||
|
event,
|
||||||
});
|
});
|
||||||
|
|
||||||
export const deleteCalendarEvent = (
|
export const deleteCalendarEvent = (
|
||||||
|
@ -61,12 +61,14 @@ export const processConversationInput = (
|
|||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
text: string,
|
text: string,
|
||||||
// eslint-disable-next-line: variable-name
|
// eslint-disable-next-line: variable-name
|
||||||
conversation_id: string
|
conversation_id: string | null,
|
||||||
|
language: string
|
||||||
): Promise<ConversationResult> =>
|
): Promise<ConversationResult> =>
|
||||||
hass.callWS({
|
hass.callWS({
|
||||||
type: "conversation/process",
|
type: "conversation/process",
|
||||||
text,
|
text,
|
||||||
conversation_id,
|
conversation_id,
|
||||||
|
language,
|
||||||
});
|
});
|
||||||
|
|
||||||
export const getAgentInfo = (hass: HomeAssistant): Promise<AgentInfo> =>
|
export const getAgentInfo = (hass: HomeAssistant): Promise<AgentInfo> =>
|
||||||
|
@ -123,9 +123,12 @@ export const subscribeDeviceRegistry = (
|
|||||||
onChange
|
onChange
|
||||||
);
|
);
|
||||||
|
|
||||||
export const sortDeviceRegistryByName = (entries: DeviceRegistryEntry[]) =>
|
export const sortDeviceRegistryByName = (
|
||||||
|
entries: DeviceRegistryEntry[],
|
||||||
|
language: string
|
||||||
|
) =>
|
||||||
entries.sort((entry1, entry2) =>
|
entries.sort((entry1, entry2) =>
|
||||||
caseInsensitiveStringCompare(entry1.name || "", entry2.name || "")
|
caseInsensitiveStringCompare(entry1.name || "", entry2.name || "", language)
|
||||||
);
|
);
|
||||||
|
|
||||||
export const getDeviceEntityLookup = (
|
export const getDeviceEntityLookup = (
|
||||||
|
@ -29,6 +29,7 @@ export interface ExtEntityRegistryEntry extends EntityRegistryEntry {
|
|||||||
original_icon?: string;
|
original_icon?: string;
|
||||||
device_class?: string;
|
device_class?: string;
|
||||||
original_device_class?: string;
|
original_device_class?: string;
|
||||||
|
aliases: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface UpdateEntityRegistryEntryResult {
|
export interface UpdateEntityRegistryEntryResult {
|
||||||
@ -63,6 +64,7 @@ export interface EntityRegistryEntryUpdateParams {
|
|||||||
new_entity_id?: string;
|
new_entity_id?: string;
|
||||||
options_domain?: string;
|
options_domain?: string;
|
||||||
options?: SensorEntityOptions | NumberEntityOptions | WeatherEntityOptions;
|
options?: SensorEntityOptions | NumberEntityOptions | WeatherEntityOptions;
|
||||||
|
aliases?: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export const findBatteryEntity = (
|
export const findBatteryEntity = (
|
||||||
@ -109,6 +111,15 @@ export const getExtendedEntityRegistryEntry = (
|
|||||||
entity_id: entityId,
|
entity_id: entityId,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const getExtendedEntityRegistryEntries = (
|
||||||
|
hass: HomeAssistant,
|
||||||
|
entityIds: string[]
|
||||||
|
): Promise<Record<string, ExtEntityRegistryEntry>> =>
|
||||||
|
hass.callWS({
|
||||||
|
type: "config/entity_registry/get_entries",
|
||||||
|
entity_ids: entityIds,
|
||||||
|
});
|
||||||
|
|
||||||
export const updateEntityRegistryEntry = (
|
export const updateEntityRegistryEntry = (
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
entityId: string,
|
entityId: string,
|
||||||
@ -162,9 +173,12 @@ export const subscribeEntityRegistry = (
|
|||||||
onChange
|
onChange
|
||||||
);
|
);
|
||||||
|
|
||||||
export const sortEntityRegistryByName = (entries: EntityRegistryEntry[]) =>
|
export const sortEntityRegistryByName = (
|
||||||
|
entries: EntityRegistryEntry[],
|
||||||
|
language: string
|
||||||
|
) =>
|
||||||
entries.sort((entry1, entry2) =>
|
entries.sort((entry1, entry2) =>
|
||||||
caseInsensitiveStringCompare(entry1.name || "", entry2.name || "")
|
caseInsensitiveStringCompare(entry1.name || "", entry2.name || "", language)
|
||||||
);
|
);
|
||||||
|
|
||||||
export const entityRegistryById = memoizeOne(
|
export const entityRegistryById = memoizeOne(
|
||||||
|
@ -68,7 +68,10 @@ export const updateReleaseNotes = (hass: HomeAssistant, entityId: string) =>
|
|||||||
entity_id: entityId,
|
entity_id: entityId,
|
||||||
});
|
});
|
||||||
|
|
||||||
export const filterUpdateEntities = (entities: HassEntities) =>
|
export const filterUpdateEntities = (
|
||||||
|
entities: HassEntities,
|
||||||
|
language?: string
|
||||||
|
) =>
|
||||||
(
|
(
|
||||||
Object.values(entities).filter(
|
Object.values(entities).filter(
|
||||||
(entity) => computeStateDomain(entity) === "update"
|
(entity) => computeStateDomain(entity) === "update"
|
||||||
@ -94,7 +97,8 @@ export const filterUpdateEntities = (entities: HassEntities) =>
|
|||||||
}
|
}
|
||||||
return caseInsensitiveStringCompare(
|
return caseInsensitiveStringCompare(
|
||||||
a.attributes.title || a.attributes.friendly_name || "",
|
a.attributes.title || a.attributes.friendly_name || "",
|
||||||
b.attributes.title || b.attributes.friendly_name || ""
|
b.attributes.title || b.attributes.friendly_name || "",
|
||||||
|
language
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -110,7 +114,7 @@ export const checkForEntityUpdates = async (
|
|||||||
element: HTMLElement,
|
element: HTMLElement,
|
||||||
hass: HomeAssistant
|
hass: HomeAssistant
|
||||||
) => {
|
) => {
|
||||||
const entities = filterUpdateEntities(hass.states).map(
|
const entities = filterUpdateEntities(hass.states, hass.locale.language).map(
|
||||||
(entity) => entity.entity_id
|
(entity) => entity.entity_id
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -74,7 +74,7 @@ export class HaImagecropperDialog extends LitElement {
|
|||||||
round: Boolean(this._params?.options.round),
|
round: Boolean(this._params?.options.round),
|
||||||
})}"
|
})}"
|
||||||
>
|
>
|
||||||
<img />
|
<img alt=${this.hass.localize("ui.dialogs.image_cropper.crop_image")} />
|
||||||
</div>
|
</div>
|
||||||
<mwc-button slot="secondaryAction" @click=${this.closeDialog}>
|
<mwc-button slot="secondaryAction" @click=${this.closeDialog}>
|
||||||
${this.hass.localize("ui.common.cancel")}
|
${this.hass.localize("ui.common.cancel")}
|
||||||
|
@ -484,7 +484,11 @@ export class QuickBar extends LitElement {
|
|||||||
};
|
};
|
||||||
})
|
})
|
||||||
.sort((a, b) =>
|
.sort((a, b) =>
|
||||||
caseInsensitiveStringCompare(a.primaryText, b.primaryText)
|
caseInsensitiveStringCompare(
|
||||||
|
a.primaryText,
|
||||||
|
b.primaryText,
|
||||||
|
this.hass.locale.language
|
||||||
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -494,7 +498,11 @@ export class QuickBar extends LitElement {
|
|||||||
...this._generateServerControlCommands(),
|
...this._generateServerControlCommands(),
|
||||||
...(await this._generateNavigationCommands()),
|
...(await this._generateNavigationCommands()),
|
||||||
].sort((a, b) =>
|
].sort((a, b) =>
|
||||||
caseInsensitiveStringCompare(a.strings.join(" "), b.strings.join(" "))
|
caseInsensitiveStringCompare(
|
||||||
|
a.strings.join(" "),
|
||||||
|
b.strings.join(" "),
|
||||||
|
this.hass.locale.language
|
||||||
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -13,7 +13,6 @@ import { customElement, property, query, state } from "lit/decorators";
|
|||||||
import { classMap } from "lit/directives/class-map";
|
import { classMap } from "lit/directives/class-map";
|
||||||
import { fireEvent } from "../../common/dom/fire_event";
|
import { fireEvent } from "../../common/dom/fire_event";
|
||||||
import { SpeechRecognition } from "../../common/dom/speech-recognition";
|
import { SpeechRecognition } from "../../common/dom/speech-recognition";
|
||||||
import { uid } from "../../common/util/uid";
|
|
||||||
import "../../components/ha-dialog";
|
import "../../components/ha-dialog";
|
||||||
import type { HaDialog } from "../../components/ha-dialog";
|
import type { HaDialog } from "../../components/ha-dialog";
|
||||||
import "../../components/ha-icon-button";
|
import "../../components/ha-icon-button";
|
||||||
@ -60,7 +59,7 @@ export class HaVoiceCommandDialog extends LitElement {
|
|||||||
|
|
||||||
private recognition!: SpeechRecognition;
|
private recognition!: SpeechRecognition;
|
||||||
|
|
||||||
private _conversationId?: string;
|
private _conversationId: string | null = null;
|
||||||
|
|
||||||
public async showDialog(): Promise<void> {
|
public async showDialog(): Promise<void> {
|
||||||
this._opened = true;
|
this._opened = true;
|
||||||
@ -175,7 +174,6 @@ export class HaVoiceCommandDialog extends LitElement {
|
|||||||
|
|
||||||
protected firstUpdated(changedProps: PropertyValues) {
|
protected firstUpdated(changedProps: PropertyValues) {
|
||||||
super.updated(changedProps);
|
super.updated(changedProps);
|
||||||
this._conversationId = uid();
|
|
||||||
this._conversation = [
|
this._conversation = [
|
||||||
{
|
{
|
||||||
who: "hass",
|
who: "hass",
|
||||||
@ -211,18 +209,29 @@ export class HaVoiceCommandDialog extends LitElement {
|
|||||||
private _initRecognition() {
|
private _initRecognition() {
|
||||||
this.recognition = new SpeechRecognition();
|
this.recognition = new SpeechRecognition();
|
||||||
this.recognition.interimResults = true;
|
this.recognition.interimResults = true;
|
||||||
this.recognition.lang = "en-US";
|
this.recognition.lang = this.hass.language;
|
||||||
|
|
||||||
this.recognition.onstart = () => {
|
this.recognition.addEventListener("start", () => {
|
||||||
this.results = {
|
this.results = {
|
||||||
final: false,
|
final: false,
|
||||||
transcript: "",
|
transcript: "",
|
||||||
};
|
};
|
||||||
};
|
});
|
||||||
this.recognition.onerror = (event) => {
|
this.recognition.addEventListener("nomatch", () => {
|
||||||
|
this._addMessage({
|
||||||
|
who: "user",
|
||||||
|
text: `<${this.hass.localize(
|
||||||
|
"ui.dialogs.voice_command.did_not_understand"
|
||||||
|
)}>`,
|
||||||
|
error: true,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
this.recognition.addEventListener("error", (event) => {
|
||||||
|
// eslint-disable-next-line
|
||||||
|
console.error("Error recognizing text", event);
|
||||||
this.recognition!.abort();
|
this.recognition!.abort();
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
if (event.error !== "aborted") {
|
if (event.error !== "aborted" && event.error !== "no-speech") {
|
||||||
const text =
|
const text =
|
||||||
this.results && this.results.transcript
|
this.results && this.results.transcript
|
||||||
? this.results.transcript
|
? this.results.transcript
|
||||||
@ -232,8 +241,8 @@ export class HaVoiceCommandDialog extends LitElement {
|
|||||||
this._addMessage({ who: "user", text, error: true });
|
this._addMessage({ who: "user", text, error: true });
|
||||||
}
|
}
|
||||||
this.results = null;
|
this.results = null;
|
||||||
};
|
});
|
||||||
this.recognition.onend = () => {
|
this.recognition.addEventListener("end", () => {
|
||||||
// Already handled by onerror
|
// Already handled by onerror
|
||||||
if (this.results == null) {
|
if (this.results == null) {
|
||||||
return;
|
return;
|
||||||
@ -251,15 +260,14 @@ export class HaVoiceCommandDialog extends LitElement {
|
|||||||
error: true,
|
error: true,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
});
|
||||||
|
this.recognition.addEventListener("result", (event) => {
|
||||||
this.recognition.onresult = (event) => {
|
|
||||||
const result = event.results[0];
|
const result = event.results[0];
|
||||||
this.results = {
|
this.results = {
|
||||||
transcript: result[0].transcript,
|
transcript: result[0].transcript,
|
||||||
final: result.isFinal,
|
final: result.isFinal,
|
||||||
};
|
};
|
||||||
};
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private async _processText(text: string) {
|
private async _processText(text: string) {
|
||||||
@ -277,8 +285,10 @@ export class HaVoiceCommandDialog extends LitElement {
|
|||||||
const response = await processConversationInput(
|
const response = await processConversationInput(
|
||||||
this.hass,
|
this.hass,
|
||||||
text,
|
text,
|
||||||
this._conversationId!
|
this._conversationId,
|
||||||
|
this.hass.language
|
||||||
);
|
);
|
||||||
|
this._conversationId = response.conversation_id;
|
||||||
const plain = response.response.speech?.plain;
|
const plain = response.response.speech?.plain;
|
||||||
if (plain) {
|
if (plain) {
|
||||||
message.text = plain.speech;
|
message.text = plain.speech;
|
||||||
|
@ -19,6 +19,7 @@ class IntegrationBadge extends LitElement {
|
|||||||
return html`
|
return html`
|
||||||
<div class="icon">
|
<div class="icon">
|
||||||
<img
|
<img
|
||||||
|
alt=""
|
||||||
src=${brandsUrl({
|
src=${brandsUrl({
|
||||||
domain: this.domain,
|
domain: this.domain,
|
||||||
type: "icon",
|
type: "icon",
|
||||||
|
@ -271,7 +271,9 @@ class OnboardingCoreConfig extends LitElement {
|
|||||||
"[name=currency]"
|
"[name=currency]"
|
||||||
) as HaTextField;
|
) as HaTextField;
|
||||||
curInput.updateComplete.then(() => {
|
curInput.updateComplete.then(() => {
|
||||||
curInput.shadowRoot!.appendChild(createCurrencyListEl());
|
curInput.shadowRoot!.appendChild(
|
||||||
|
createCurrencyListEl(this.hass.locale.language)
|
||||||
|
);
|
||||||
curInput.formElement.setAttribute("list", "currencies");
|
curInput.formElement.setAttribute("list", "currencies");
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -279,7 +281,9 @@ class OnboardingCoreConfig extends LitElement {
|
|||||||
"[name=country]"
|
"[name=country]"
|
||||||
) as HaTextField;
|
) as HaTextField;
|
||||||
countryInput.updateComplete.then(() => {
|
countryInput.updateComplete.then(() => {
|
||||||
countryInput.shadowRoot!.appendChild(createCountryListEl());
|
countryInput.shadowRoot!.appendChild(
|
||||||
|
createCountryListEl(this.hass.locale.language)
|
||||||
|
);
|
||||||
countryInput.formElement.setAttribute("list", "countries");
|
countryInput.formElement.setAttribute("list", "countries");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -117,7 +117,7 @@ class OnboardingIntegrations extends LitElement {
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
const content = [...entries, ...discovered]
|
const content = [...entries, ...discovered]
|
||||||
.sort((a, b) => stringCompare(a[0], b[0]))
|
.sort((a, b) => stringCompare(a[0], b[0], this.hass.locale.language))
|
||||||
.map((item) => item[1]);
|
.map((item) => item[1]);
|
||||||
|
|
||||||
return html`
|
return html`
|
||||||
|
@ -4,15 +4,11 @@ import { addDays, isSameDay } from "date-fns/esm";
|
|||||||
import { toDate } from "date-fns-tz";
|
import { toDate } from "date-fns-tz";
|
||||||
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
|
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
|
||||||
import { property, state } from "lit/decorators";
|
import { property, state } from "lit/decorators";
|
||||||
import { RRule, Weekday } from "rrule";
|
|
||||||
import { formatDate } from "../../common/datetime/format_date";
|
import { formatDate } from "../../common/datetime/format_date";
|
||||||
import { formatDateTime } from "../../common/datetime/format_date_time";
|
import { formatDateTime } from "../../common/datetime/format_date_time";
|
||||||
import { formatTime } from "../../common/datetime/format_time";
|
import { formatTime } from "../../common/datetime/format_time";
|
||||||
import { fireEvent } from "../../common/dom/fire_event";
|
import { fireEvent } from "../../common/dom/fire_event";
|
||||||
import { capitalizeFirstLetter } from "../../common/string/capitalize-first-letter";
|
|
||||||
import { isDate } from "../../common/string/is_date";
|
import { isDate } from "../../common/string/is_date";
|
||||||
import { dayNames } from "../../common/translations/day_names";
|
|
||||||
import { monthNames } from "../../common/translations/month_names";
|
|
||||||
import "../../components/entity/state-info";
|
import "../../components/entity/state-info";
|
||||||
import "../../components/ha-date-input";
|
import "../../components/ha-date-input";
|
||||||
import "../../components/ha-time-input";
|
import "../../components/ha-time-input";
|
||||||
@ -23,10 +19,10 @@ import {
|
|||||||
import { haStyleDialog } from "../../resources/styles";
|
import { haStyleDialog } from "../../resources/styles";
|
||||||
import { HomeAssistant } from "../../types";
|
import { HomeAssistant } from "../../types";
|
||||||
import "../lovelace/components/hui-generic-entity-row";
|
import "../lovelace/components/hui-generic-entity-row";
|
||||||
import "./ha-recurrence-rule-editor";
|
|
||||||
import { showConfirmEventDialog } from "./show-confirm-event-dialog-box";
|
import { showConfirmEventDialog } from "./show-confirm-event-dialog-box";
|
||||||
import { CalendarEventDetailDialogParams } from "./show-dialog-calendar-event-detail";
|
import { CalendarEventDetailDialogParams } from "./show-dialog-calendar-event-detail";
|
||||||
import { showCalendarEventEditDialog } from "./show-dialog-calendar-event-editor";
|
import { showCalendarEventEditDialog } from "./show-dialog-calendar-event-editor";
|
||||||
|
import { renderRRuleAsText } from "./recurrence";
|
||||||
|
|
||||||
class DialogCalendarEventDetail extends LitElement {
|
class DialogCalendarEventDetail extends LitElement {
|
||||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||||
@ -137,54 +133,16 @@ class DialogCalendarEventDetail extends LitElement {
|
|||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
const rule = RRule.fromString(`RRULE:${value}`);
|
const ruleText = renderRRuleAsText(this.hass, value);
|
||||||
if (rule.isFullyConvertibleToText()) {
|
if (ruleText !== undefined) {
|
||||||
return html`<div id="text">
|
return html`<div id="text">${ruleText}</div>`;
|
||||||
${capitalizeFirstLetter(
|
|
||||||
rule.toText(
|
|
||||||
this._translateRRuleElement,
|
|
||||||
{
|
|
||||||
dayNames: dayNames(this.hass.locale),
|
|
||||||
monthNames: monthNames(this.hass.locale),
|
|
||||||
tokens: {},
|
|
||||||
},
|
|
||||||
this._formatDate
|
|
||||||
)
|
|
||||||
)}
|
|
||||||
</div>`;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return html`<div id="text">Cannot convert recurrence rule</div>`;
|
return html`<div id="text">Cannot convert recurrence rule</div>`;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
return "Error while processing the rule";
|
return "Error while processing the rule";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private _translateRRuleElement = (id: string | number | Weekday): string => {
|
|
||||||
if (typeof id === "string") {
|
|
||||||
return this.hass.localize(`ui.components.calendar.event.rrule.${id}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
return "";
|
|
||||||
};
|
|
||||||
|
|
||||||
private _formatDate = (year: number, month: string, day: number): string => {
|
|
||||||
if (!year || !month || !day) {
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
|
|
||||||
// Build date so we can then format it
|
|
||||||
const date = new Date();
|
|
||||||
date.setFullYear(year);
|
|
||||||
// As input we already get the localized month name, so we now unfortunately
|
|
||||||
// need to convert it back to something Date can work with. The already localized
|
|
||||||
// months names are a must in the RRule.Language structure (an empty string[] would
|
|
||||||
// mean we get undefined months input in this method here).
|
|
||||||
date.setMonth(monthNames(this.hass.locale).indexOf(month));
|
|
||||||
date.setDate(day);
|
|
||||||
return formatDate(date, this.hass.locale);
|
|
||||||
};
|
|
||||||
|
|
||||||
private _formatDateRange() {
|
private _formatDateRange() {
|
||||||
// Parse a dates in the browser timezone
|
// Parse a dates in the browser timezone
|
||||||
const timeZone = Intl.DateTimeFormat().resolvedOptions().timeZone;
|
const timeZone = Intl.DateTimeFormat().resolvedOptions().timeZone;
|
||||||
|
@ -25,6 +25,8 @@ import {
|
|||||||
CalendarEventMutableParams,
|
CalendarEventMutableParams,
|
||||||
createCalendarEvent,
|
createCalendarEvent,
|
||||||
deleteCalendarEvent,
|
deleteCalendarEvent,
|
||||||
|
updateCalendarEvent,
|
||||||
|
RecurrenceRange,
|
||||||
} from "../../data/calendar";
|
} from "../../data/calendar";
|
||||||
import { haStyleDialog } from "../../resources/styles";
|
import { haStyleDialog } from "../../resources/styles";
|
||||||
import { HomeAssistant } from "../../types";
|
import { HomeAssistant } from "../../types";
|
||||||
@ -49,7 +51,7 @@ class DialogCalendarEventEditor extends LitElement {
|
|||||||
|
|
||||||
@state() private _summary = "";
|
@state() private _summary = "";
|
||||||
|
|
||||||
@state() private _description = "";
|
@state() private _description? = "";
|
||||||
|
|
||||||
@state() private _rrule?: string;
|
@state() private _rrule?: string;
|
||||||
|
|
||||||
@ -85,12 +87,13 @@ class DialogCalendarEventEditor extends LitElement {
|
|||||||
const entry = params.entry!;
|
const entry = params.entry!;
|
||||||
this._allDay = isDate(entry.dtstart);
|
this._allDay = isDate(entry.dtstart);
|
||||||
this._summary = entry.summary;
|
this._summary = entry.summary;
|
||||||
|
this._description = entry.description;
|
||||||
this._rrule = entry.rrule;
|
this._rrule = entry.rrule;
|
||||||
if (this._allDay) {
|
if (this._allDay) {
|
||||||
this._dtstart = new Date(entry.dtstart);
|
this._dtstart = new Date(entry.dtstart + "T00:00:00");
|
||||||
// Calendar event end dates are exclusive, but not shown that way in the UI. The
|
// Calendar event end dates are exclusive, but not shown that way in the UI. The
|
||||||
// reverse happens when persisting the event.
|
// reverse happens when persisting the event.
|
||||||
this._dtend = addDays(new Date(entry.dtend), -1);
|
this._dtend = addDays(new Date(entry.dtend + "T00:00:00"), -1);
|
||||||
} else {
|
} else {
|
||||||
this._dtstart = new Date(entry.dtstart);
|
this._dtstart = new Date(entry.dtstart);
|
||||||
this._dtend = new Date(entry.dtend);
|
this._dtend = new Date(entry.dtend);
|
||||||
@ -168,6 +171,7 @@ class DialogCalendarEventEditor extends LitElement {
|
|||||||
class="summary"
|
class="summary"
|
||||||
name="summary"
|
name="summary"
|
||||||
.label=${this.hass.localize("ui.components.calendar.event.summary")}
|
.label=${this.hass.localize("ui.components.calendar.event.summary")}
|
||||||
|
.value=${this._summary}
|
||||||
required
|
required
|
||||||
@change=${this._handleSummaryChanged}
|
@change=${this._handleSummaryChanged}
|
||||||
error-message=${this.hass.localize("ui.common.error_required")}
|
error-message=${this.hass.localize("ui.common.error_required")}
|
||||||
@ -179,6 +183,7 @@ class DialogCalendarEventEditor extends LitElement {
|
|||||||
.label=${this.hass.localize(
|
.label=${this.hass.localize(
|
||||||
"ui.components.calendar.event.description"
|
"ui.components.calendar.event.description"
|
||||||
)}
|
)}
|
||||||
|
.value=${this._description}
|
||||||
@change=${this._handleDescriptionChanged}
|
@change=${this._handleDescriptionChanged}
|
||||||
autogrow
|
autogrow
|
||||||
></ha-textarea>
|
></ha-textarea>
|
||||||
@ -244,6 +249,9 @@ class DialogCalendarEventEditor extends LitElement {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<ha-recurrence-rule-editor
|
<ha-recurrence-rule-editor
|
||||||
|
.hass=${this.hass}
|
||||||
|
.dtstart=${this._dtstart}
|
||||||
|
.allDay=${this._allDay}
|
||||||
.locale=${this.hass.locale}
|
.locale=${this.hass.locale}
|
||||||
.timezone=${this.hass.config.time_zone}
|
.timezone=${this.hass.config.time_zone}
|
||||||
.value=${this._rrule || ""}
|
.value=${this._rrule || ""}
|
||||||
@ -412,6 +420,13 @@ class DialogCalendarEventEditor extends LitElement {
|
|||||||
this._calendarId = ev.detail.value;
|
this._calendarId = ev.detail.value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private _isValidStartEnd(): boolean {
|
||||||
|
if (this._allDay) {
|
||||||
|
return this._dtend! >= this._dtstart!;
|
||||||
|
}
|
||||||
|
return this._dtend! > this._dtstart!;
|
||||||
|
}
|
||||||
|
|
||||||
private async _createEvent() {
|
private async _createEvent() {
|
||||||
if (!this._summary || !this._calendarId) {
|
if (!this._summary || !this._calendarId) {
|
||||||
this._error = this.hass.localize(
|
this._error = this.hass.localize(
|
||||||
@ -420,7 +435,7 @@ class DialogCalendarEventEditor extends LitElement {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this._dtend! <= this._dtstart!) {
|
if (!this._isValidStartEnd()) {
|
||||||
this._error = this.hass.localize(
|
this._error = this.hass.localize(
|
||||||
"ui.components.calendar.event.invalid_duration"
|
"ui.components.calendar.event.invalid_duration"
|
||||||
);
|
);
|
||||||
@ -445,7 +460,61 @@ class DialogCalendarEventEditor extends LitElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private async _saveEvent() {
|
private async _saveEvent() {
|
||||||
// to be implemented
|
if (!this._summary || !this._calendarId) {
|
||||||
|
this._error = this.hass.localize(
|
||||||
|
"ui.components.calendar.event.not_all_required_fields"
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this._isValidStartEnd()) {
|
||||||
|
this._error = this.hass.localize(
|
||||||
|
"ui.components.calendar.event.invalid_duration"
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this._submitting = true;
|
||||||
|
const entry = this._params!.entry!;
|
||||||
|
let range: RecurrenceRange | undefined = RecurrenceRange.THISEVENT;
|
||||||
|
if (entry.recurrence_id) {
|
||||||
|
range = await showConfirmEventDialog(this, {
|
||||||
|
title: this.hass.localize(
|
||||||
|
"ui.components.calendar.event.confirm_update.update"
|
||||||
|
),
|
||||||
|
text: this.hass.localize(
|
||||||
|
"ui.components.calendar.event.confirm_update.recurring_prompt"
|
||||||
|
),
|
||||||
|
confirmText: this.hass.localize(
|
||||||
|
"ui.components.calendar.event.confirm_update.update_this"
|
||||||
|
),
|
||||||
|
confirmFutureText: this.hass.localize(
|
||||||
|
"ui.components.calendar.event.confirm_update.update_future"
|
||||||
|
),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (range === undefined) {
|
||||||
|
// Cancel
|
||||||
|
this._submitting = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
await updateCalendarEvent(
|
||||||
|
this.hass!,
|
||||||
|
this._calendarId!,
|
||||||
|
entry.uid!,
|
||||||
|
this._calculateData(),
|
||||||
|
entry.recurrence_id || "",
|
||||||
|
range!
|
||||||
|
);
|
||||||
|
} catch (err: any) {
|
||||||
|
this._error = err ? err.message : "Unknown error";
|
||||||
|
return;
|
||||||
|
} finally {
|
||||||
|
this._submitting = false;
|
||||||
|
}
|
||||||
|
await this._params!.updated();
|
||||||
|
this.closeDialog();
|
||||||
}
|
}
|
||||||
|
|
||||||
private async _deleteEvent() {
|
private async _deleteEvent() {
|
||||||
|
@ -302,6 +302,9 @@ export class HAFullCalendar extends LitElement {
|
|||||||
|
|
||||||
private _handleEventClick(info): void {
|
private _handleEventClick(info): void {
|
||||||
const entityStateObj = this.hass.states[info.event.extendedProps.calendar];
|
const entityStateObj = this.hass.states[info.event.extendedProps.calendar];
|
||||||
|
const canEdit =
|
||||||
|
entityStateObj &&
|
||||||
|
supportsFeature(entityStateObj, CalendarEntityFeature.UPDATE_EVENT);
|
||||||
const canDelete =
|
const canDelete =
|
||||||
entityStateObj &&
|
entityStateObj &&
|
||||||
supportsFeature(entityStateObj, CalendarEntityFeature.DELETE_EVENT);
|
supportsFeature(entityStateObj, CalendarEntityFeature.DELETE_EVENT);
|
||||||
@ -312,6 +315,7 @@ export class HAFullCalendar extends LitElement {
|
|||||||
updated: () => {
|
updated: () => {
|
||||||
this._fireViewChanged();
|
this._fireViewChanged();
|
||||||
},
|
},
|
||||||
|
canEdit: canEdit,
|
||||||
canDelete: canDelete,
|
canDelete: canDelete,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -1,11 +1,13 @@
|
|||||||
import type { SelectedDetail } from "@material/mwc-list";
|
import type { SelectedDetail } from "@material/mwc-list";
|
||||||
|
import { formatInTimeZone, toDate } from "date-fns-tz";
|
||||||
import { css, html, LitElement, PropertyValues } from "lit";
|
import { css, html, LitElement, PropertyValues } from "lit";
|
||||||
import { customElement, property, state } from "lit/decorators";
|
import { customElement, property, query, state } from "lit/decorators";
|
||||||
import { classMap } from "lit/directives/class-map";
|
import { classMap } from "lit/directives/class-map";
|
||||||
import type { Options, WeekdayStr } from "rrule";
|
import type { Options, WeekdayStr } from "rrule";
|
||||||
import { ByWeekday, RRule, Weekday } from "rrule";
|
import { ByWeekday, RRule, Weekday } from "rrule";
|
||||||
import { firstWeekdayIndex } from "../../common/datetime/first_weekday";
|
import { firstWeekdayIndex } from "../../common/datetime/first_weekday";
|
||||||
import { stopPropagation } from "../../common/dom/stop_propagation";
|
import { stopPropagation } from "../../common/dom/stop_propagation";
|
||||||
|
import { LocalizeKeys } from "../../common/translations/localize";
|
||||||
import "../../components/ha-chip";
|
import "../../components/ha-chip";
|
||||||
import "../../components/ha-list-item";
|
import "../../components/ha-list-item";
|
||||||
import "../../components/ha-select";
|
import "../../components/ha-select";
|
||||||
@ -16,22 +18,31 @@ import {
|
|||||||
convertFrequency,
|
convertFrequency,
|
||||||
convertRepeatFrequency,
|
convertRepeatFrequency,
|
||||||
DEFAULT_COUNT,
|
DEFAULT_COUNT,
|
||||||
|
getWeekday,
|
||||||
getWeekdays,
|
getWeekdays,
|
||||||
intervalSuffix,
|
getMonthlyRepeatItems,
|
||||||
RepeatEnd,
|
RepeatEnd,
|
||||||
RepeatFrequency,
|
RepeatFrequency,
|
||||||
ruleByWeekDay,
|
ruleByWeekDay,
|
||||||
untilValue,
|
untilValue,
|
||||||
WEEKDAY_NAME,
|
MonthlyRepeatItem,
|
||||||
|
getMonthlyRepeatWeekdayFromRule,
|
||||||
|
getMonthdayRepeatFromRule,
|
||||||
} from "./recurrence";
|
} from "./recurrence";
|
||||||
import "../../components/ha-date-input";
|
import "../../components/ha-date-input";
|
||||||
|
|
||||||
@customElement("ha-recurrence-rule-editor")
|
@customElement("ha-recurrence-rule-editor")
|
||||||
export class RecurrenceRuleEditor extends LitElement {
|
export class RecurrenceRuleEditor extends LitElement {
|
||||||
|
@property() public hass!: HomeAssistant;
|
||||||
|
|
||||||
@property() public disabled = false;
|
@property() public disabled = false;
|
||||||
|
|
||||||
@property() public value = "";
|
@property() public value = "";
|
||||||
|
|
||||||
|
@property() public dtstart?: Date;
|
||||||
|
|
||||||
|
@property() public allDay?: boolean;
|
||||||
|
|
||||||
@property({ attribute: false }) public locale!: HomeAssistant["locale"];
|
@property({ attribute: false }) public locale!: HomeAssistant["locale"];
|
||||||
|
|
||||||
@property() public timezone?: string;
|
@property() public timezone?: string;
|
||||||
@ -44,14 +55,24 @@ export class RecurrenceRuleEditor extends LitElement {
|
|||||||
|
|
||||||
@state() private _weekday: Set<WeekdayStr> = new Set<WeekdayStr>();
|
@state() private _weekday: Set<WeekdayStr> = new Set<WeekdayStr>();
|
||||||
|
|
||||||
|
@state() private _monthlyRepeat?: string;
|
||||||
|
|
||||||
|
@state() private _monthlyRepeatWeekday?: Weekday;
|
||||||
|
|
||||||
|
@state() private _monthday?: number;
|
||||||
|
|
||||||
@state() private _end: RepeatEnd = "never";
|
@state() private _end: RepeatEnd = "never";
|
||||||
|
|
||||||
@state() private _count?: number;
|
@state() private _count?: number;
|
||||||
|
|
||||||
@state() private _until?: Date;
|
@state() private _untilDay?: Date;
|
||||||
|
|
||||||
|
@query("#monthly") private _monthlyRepeatSelect!: HaSelect;
|
||||||
|
|
||||||
private _allWeekdays?: WeekdayStr[];
|
private _allWeekdays?: WeekdayStr[];
|
||||||
|
|
||||||
|
private _monthlyRepeatItems: MonthlyRepeatItem[] = [];
|
||||||
|
|
||||||
protected willUpdate(changedProps: PropertyValues) {
|
protected willUpdate(changedProps: PropertyValues) {
|
||||||
super.willUpdate(changedProps);
|
super.willUpdate(changedProps);
|
||||||
|
|
||||||
@ -61,15 +82,50 @@ export class RecurrenceRuleEditor extends LitElement {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!changedProps.has("value") || this._computedRRule === this.value) {
|
if (changedProps.has("dtstart") || changedProps.has("_interval")) {
|
||||||
|
this._monthlyRepeatItems = this.dtstart
|
||||||
|
? getMonthlyRepeatItems(this.hass, this._interval, this.dtstart)
|
||||||
|
: [];
|
||||||
|
this._computeWeekday();
|
||||||
|
const selectElement = this._monthlyRepeatSelect;
|
||||||
|
if (selectElement) {
|
||||||
|
const oldSelected = selectElement.index;
|
||||||
|
selectElement.select(-1);
|
||||||
|
this.updateComplete.then(() => {
|
||||||
|
selectElement.select(changedProps.has("dtstart") ? 0 : oldSelected);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
!changedProps.has("value") &&
|
||||||
|
(changedProps.has("dtstart") ||
|
||||||
|
changedProps.has("timezone") ||
|
||||||
|
changedProps.has("_freq") ||
|
||||||
|
changedProps.has("_interval") ||
|
||||||
|
changedProps.has("_weekday") ||
|
||||||
|
changedProps.has("_monthlyRepeatWeekday") ||
|
||||||
|
changedProps.has("_monthday") ||
|
||||||
|
changedProps.has("_end") ||
|
||||||
|
changedProps.has("_count") ||
|
||||||
|
changedProps.has("_untilDay"))
|
||||||
|
) {
|
||||||
|
this._updateRule();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this._computedRRule === this.value) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this._interval = 1;
|
this._interval = 1;
|
||||||
this._weekday.clear();
|
this._weekday.clear();
|
||||||
|
this._monthlyRepeat = undefined;
|
||||||
|
this._monthday = undefined;
|
||||||
|
this._monthlyRepeatWeekday = undefined;
|
||||||
this._end = "never";
|
this._end = "never";
|
||||||
this._count = undefined;
|
this._count = undefined;
|
||||||
this._until = undefined;
|
this._untilDay = undefined;
|
||||||
|
|
||||||
this._computedRRule = this.value;
|
this._computedRRule = this.value;
|
||||||
if (this.value === "") {
|
if (this.value === "") {
|
||||||
@ -88,6 +144,14 @@ export class RecurrenceRuleEditor extends LitElement {
|
|||||||
if (rrule.interval) {
|
if (rrule.interval) {
|
||||||
this._interval = rrule.interval;
|
this._interval = rrule.interval;
|
||||||
}
|
}
|
||||||
|
this._monthlyRepeatWeekday = getMonthlyRepeatWeekdayFromRule(rrule);
|
||||||
|
if (this._monthlyRepeatWeekday) {
|
||||||
|
this._monthlyRepeat = `BYDAY=${this._monthlyRepeatWeekday.toString()}`;
|
||||||
|
}
|
||||||
|
this._monthday = getMonthdayRepeatFromRule(rrule);
|
||||||
|
if (this._monthday) {
|
||||||
|
this._monthlyRepeat = `BYMONTHDAY=${this._monthday}`;
|
||||||
|
}
|
||||||
if (
|
if (
|
||||||
this._freq === "weekly" &&
|
this._freq === "weekly" &&
|
||||||
rrule.byweekday &&
|
rrule.byweekday &&
|
||||||
@ -101,7 +165,7 @@ export class RecurrenceRuleEditor extends LitElement {
|
|||||||
}
|
}
|
||||||
if (rrule.until) {
|
if (rrule.until) {
|
||||||
this._end = "on";
|
this._end = "on";
|
||||||
this._until = rrule.until;
|
this._untilDay = toDate(rrule.until, { timeZone: this.timezone });
|
||||||
} else if (rrule.count) {
|
} else if (rrule.count) {
|
||||||
this._end = "after";
|
this._end = "after";
|
||||||
this._count = rrule.count;
|
this._count = rrule.count;
|
||||||
@ -112,24 +176,65 @@ export class RecurrenceRuleEditor extends LitElement {
|
|||||||
return html`
|
return html`
|
||||||
<ha-select
|
<ha-select
|
||||||
id="freq"
|
id="freq"
|
||||||
label="Repeat"
|
label=${this.hass.localize("ui.components.calendar.event.repeat.label")}
|
||||||
@selected=${this._onRepeatSelected}
|
@selected=${this._onRepeatSelected}
|
||||||
@closed=${stopPropagation}
|
@closed=${stopPropagation}
|
||||||
fixedMenuPosition
|
fixedMenuPosition
|
||||||
naturalMenuWidth
|
naturalMenuWidth
|
||||||
.value=${this._freq}
|
.value=${this._freq}
|
||||||
>
|
>
|
||||||
<ha-list-item value="none">None</ha-list-item>
|
<ha-list-item value="none">
|
||||||
<ha-list-item value="yearly">Yearly</ha-list-item>
|
${this.hass.localize("ui.components.calendar.event.repeat.freq.none")}
|
||||||
<ha-list-item value="monthly">Monthly</ha-list-item>
|
</ha-list-item>
|
||||||
<ha-list-item value="weekly">Weekly</ha-list-item>
|
<ha-list-item value="yearly">
|
||||||
<ha-list-item value="daily">Daily</ha-list-item>
|
${this.hass.localize(
|
||||||
|
"ui.components.calendar.event.repeat.freq.yearly"
|
||||||
|
)}
|
||||||
|
</ha-list-item>
|
||||||
|
<ha-list-item value="monthly">
|
||||||
|
${this.hass.localize(
|
||||||
|
"ui.components.calendar.event.repeat.freq.monthly"
|
||||||
|
)}
|
||||||
|
</ha-list-item>
|
||||||
|
<ha-list-item value="weekly">
|
||||||
|
${this.hass.localize(
|
||||||
|
"ui.components.calendar.event.repeat.freq.weekly"
|
||||||
|
)}
|
||||||
|
</ha-list-item>
|
||||||
|
<ha-list-item value="daily">
|
||||||
|
${this.hass.localize(
|
||||||
|
"ui.components.calendar.event.repeat.freq.daily"
|
||||||
|
)}
|
||||||
|
</ha-list-item>
|
||||||
</ha-select>
|
</ha-select>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
renderMonthly() {
|
renderMonthly() {
|
||||||
return this.renderInterval();
|
return html`
|
||||||
|
${this.renderInterval()}
|
||||||
|
${this._monthlyRepeatItems.length > 0
|
||||||
|
? html`<ha-select
|
||||||
|
id="monthly"
|
||||||
|
label=${this.hass.localize(
|
||||||
|
"ui.components.calendar.event.repeat.monthly.label"
|
||||||
|
)}
|
||||||
|
@selected=${this._onMonthlyDetailSelected}
|
||||||
|
.value=${this._monthlyRepeat || this._monthlyRepeatItems[0]?.value}
|
||||||
|
@closed=${stopPropagation}
|
||||||
|
fixedMenuPosition
|
||||||
|
naturalMenuWidth
|
||||||
|
>
|
||||||
|
${this._monthlyRepeatItems!.map(
|
||||||
|
(item) => html`
|
||||||
|
<ha-list-item .value=${item.value} .item=${item}>
|
||||||
|
${item.label}
|
||||||
|
</ha-list-item>
|
||||||
|
`
|
||||||
|
)}
|
||||||
|
</ha-select>`
|
||||||
|
: html``}
|
||||||
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
renderWeekly() {
|
renderWeekly() {
|
||||||
@ -142,7 +247,11 @@ export class RecurrenceRuleEditor extends LitElement {
|
|||||||
.value=${item}
|
.value=${item}
|
||||||
class=${classMap({ active: this._weekday.has(item) })}
|
class=${classMap({ active: this._weekday.has(item) })}
|
||||||
@click=${this._onWeekdayToggle}
|
@click=${this._onWeekdayToggle}
|
||||||
>${WEEKDAY_NAME[item]}</ha-chip
|
>${this.hass.localize(
|
||||||
|
`ui.components.calendar.event.repeat.weekly.weekday.${
|
||||||
|
item.toLowerCase() as Lowercase<WeekdayStr>
|
||||||
|
}`
|
||||||
|
)}</ha-chip
|
||||||
>
|
>
|
||||||
`
|
`
|
||||||
)}
|
)}
|
||||||
@ -158,11 +267,16 @@ export class RecurrenceRuleEditor extends LitElement {
|
|||||||
return html`
|
return html`
|
||||||
<ha-textfield
|
<ha-textfield
|
||||||
id="interval"
|
id="interval"
|
||||||
label="Repeat interval"
|
label=${this.hass.localize(
|
||||||
|
"ui.components.calendar.event.repeat.interval.label"
|
||||||
|
)}
|
||||||
type="number"
|
type="number"
|
||||||
min="1"
|
min="1"
|
||||||
.value=${this._interval}
|
.value=${this._interval}
|
||||||
.suffix=${intervalSuffix(this._freq!)}
|
.suffix=${this.hass.localize(
|
||||||
|
`ui.components.calendar.event.repeat.interval.${this
|
||||||
|
._freq!}` as LocalizeKeys
|
||||||
|
)}
|
||||||
@change=${this._onIntervalChange}
|
@change=${this._onIntervalChange}
|
||||||
></ha-textfield>
|
></ha-textfield>
|
||||||
`;
|
`;
|
||||||
@ -172,26 +286,38 @@ export class RecurrenceRuleEditor extends LitElement {
|
|||||||
return html`
|
return html`
|
||||||
<ha-select
|
<ha-select
|
||||||
id="end"
|
id="end"
|
||||||
label="Ends"
|
label=${this.hass.localize(
|
||||||
|
"ui.components.calendar.event.repeat.end.label"
|
||||||
|
)}
|
||||||
.value=${this._end}
|
.value=${this._end}
|
||||||
@selected=${this._onEndSelected}
|
@selected=${this._onEndSelected}
|
||||||
@closed=${stopPropagation}
|
@closed=${stopPropagation}
|
||||||
fixedMenuPosition
|
fixedMenuPosition
|
||||||
naturalMenuWidth
|
naturalMenuWidth
|
||||||
>
|
>
|
||||||
<ha-list-item value="never">Never</ha-list-item>
|
<ha-list-item value="never">
|
||||||
<ha-list-item value="after">After</ha-list-item>
|
${this.hass.localize("ui.components.calendar.event.repeat.end.never")}
|
||||||
<ha-list-item value="on">On</ha-list-item>
|
</ha-list-item>
|
||||||
|
<ha-list-item value="after">
|
||||||
|
${this.hass.localize("ui.components.calendar.event.repeat.end.after")}
|
||||||
|
</ha-list-item>
|
||||||
|
<ha-list-item value="on">
|
||||||
|
${this.hass.localize("ui.components.calendar.event.repeat.end.on")}
|
||||||
|
</ha-list-item>
|
||||||
</ha-select>
|
</ha-select>
|
||||||
${this._end === "after"
|
${this._end === "after"
|
||||||
? html`
|
? html`
|
||||||
<ha-textfield
|
<ha-textfield
|
||||||
id="after"
|
id="after"
|
||||||
label="End after"
|
label=${this.hass.localize(
|
||||||
|
"ui.components.calendar.event.repeat.end_after.label"
|
||||||
|
)}
|
||||||
type="number"
|
type="number"
|
||||||
min="1"
|
min="1"
|
||||||
.value=${this._count!}
|
.value=${this._count!}
|
||||||
suffix="ocurrences"
|
suffix=${this.hass.localize(
|
||||||
|
"ui.components.calendar.event.repeat.end_after.ocurrences"
|
||||||
|
)}
|
||||||
@change=${this._onCountChange}
|
@change=${this._onCountChange}
|
||||||
></ha-textfield>
|
></ha-textfield>
|
||||||
`
|
`
|
||||||
@ -200,9 +326,11 @@ export class RecurrenceRuleEditor extends LitElement {
|
|||||||
? html`
|
? html`
|
||||||
<ha-date-input
|
<ha-date-input
|
||||||
id="on"
|
id="on"
|
||||||
label="End on"
|
label=${this.hass.localize(
|
||||||
|
"ui.components.calendar.event.repeat.end_on.label"
|
||||||
|
)}
|
||||||
.locale=${this.locale}
|
.locale=${this.locale}
|
||||||
.value=${this._until!.toISOString()}
|
.value=${this._formatDate(this._untilDay!)}
|
||||||
@value-changed=${this._onUntilChange}
|
@value-changed=${this._onUntilChange}
|
||||||
></ha-date-input>
|
></ha-date-input>
|
||||||
`
|
`
|
||||||
@ -222,7 +350,6 @@ export class RecurrenceRuleEditor extends LitElement {
|
|||||||
|
|
||||||
private _onIntervalChange(e: Event) {
|
private _onIntervalChange(e: Event) {
|
||||||
this._interval = (e.target! as any).value;
|
this._interval = (e.target! as any).value;
|
||||||
this._updateRule();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private _onRepeatSelected(e: CustomEvent<SelectedDetail<number>>) {
|
private _onRepeatSelected(e: CustomEvent<SelectedDetail<number>>) {
|
||||||
@ -233,9 +360,20 @@ export class RecurrenceRuleEditor extends LitElement {
|
|||||||
}
|
}
|
||||||
if (this._freq !== "weekly") {
|
if (this._freq !== "weekly") {
|
||||||
this._weekday.clear();
|
this._weekday.clear();
|
||||||
|
this._computeWeekday();
|
||||||
}
|
}
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
this._updateRule();
|
}
|
||||||
|
|
||||||
|
private _onMonthlyDetailSelected(e: CustomEvent<SelectedDetail<number>>) {
|
||||||
|
e.stopPropagation();
|
||||||
|
const selectedItem = this._monthlyRepeatItems[e.detail.index];
|
||||||
|
if (!selectedItem) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this._monthlyRepeat = selectedItem.value;
|
||||||
|
this._monthlyRepeatWeekday = selectedItem.byday;
|
||||||
|
this._monthday = selectedItem.bymonthday;
|
||||||
}
|
}
|
||||||
|
|
||||||
private _onWeekdayToggle(e: MouseEvent) {
|
private _onWeekdayToggle(e: MouseEvent) {
|
||||||
@ -246,7 +384,7 @@ export class RecurrenceRuleEditor extends LitElement {
|
|||||||
} else {
|
} else {
|
||||||
this._weekday.delete(value);
|
this._weekday.delete(value);
|
||||||
}
|
}
|
||||||
this._updateRule();
|
this.requestUpdate("_weekday");
|
||||||
}
|
}
|
||||||
|
|
||||||
private _onEndSelected(e: CustomEvent<SelectedDetail<number>>) {
|
private _onEndSelected(e: CustomEvent<SelectedDetail<number>>) {
|
||||||
@ -259,44 +397,78 @@ export class RecurrenceRuleEditor extends LitElement {
|
|||||||
switch (this._end) {
|
switch (this._end) {
|
||||||
case "after":
|
case "after":
|
||||||
this._count = DEFAULT_COUNT[this._freq!];
|
this._count = DEFAULT_COUNT[this._freq!];
|
||||||
this._until = undefined;
|
this._untilDay = undefined;
|
||||||
break;
|
break;
|
||||||
case "on":
|
case "on":
|
||||||
this._count = undefined;
|
this._count = undefined;
|
||||||
this._until = untilValue(this._freq!);
|
this._untilDay = untilValue(this._freq!);
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
this._count = undefined;
|
this._count = undefined;
|
||||||
this._until = undefined;
|
this._untilDay = undefined;
|
||||||
}
|
}
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
this._updateRule();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private _onCountChange(e: Event) {
|
private _onCountChange(e: Event) {
|
||||||
this._count = (e.target! as any).value;
|
this._count = (e.target! as any).value;
|
||||||
this._updateRule();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private _onUntilChange(e: CustomEvent) {
|
private _onUntilChange(e: CustomEvent) {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
this._until = new Date(e.detail.value);
|
this._untilDay = toDate(e.detail.value + "T00:00:00", {
|
||||||
this._updateRule();
|
timeZone: this.timezone,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reset the weekday selected when there is only a single value
|
||||||
|
private _computeWeekday() {
|
||||||
|
if (this.dtstart && this._weekday.size <= 1) {
|
||||||
|
const weekdayNum = getWeekday(this.dtstart);
|
||||||
|
this._weekday.clear();
|
||||||
|
this._weekday.add(new Weekday(weekdayNum).toString() as WeekdayStr);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private _computeRRule() {
|
private _computeRRule() {
|
||||||
if (this._freq === undefined || this._freq === "none") {
|
if (this._freq === undefined || this._freq === "none") {
|
||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
const options = {
|
let byweekday: Weekday[] | undefined;
|
||||||
|
let bymonthday: number | undefined;
|
||||||
|
if (this._freq === "monthly" && this._monthlyRepeatWeekday !== undefined) {
|
||||||
|
byweekday = [this._monthlyRepeatWeekday];
|
||||||
|
} else if (this._freq === "monthly" && this._monthday !== undefined) {
|
||||||
|
bymonthday = this._monthday;
|
||||||
|
} else if (this._freq === "weekly") {
|
||||||
|
byweekday = ruleByWeekDay(this._weekday);
|
||||||
|
}
|
||||||
|
const options: Partial<Options> = {
|
||||||
freq: convertRepeatFrequency(this._freq!)!,
|
freq: convertRepeatFrequency(this._freq!)!,
|
||||||
interval: this._interval > 1 ? this._interval : undefined,
|
interval: this._interval > 1 ? this._interval : undefined,
|
||||||
byweekday: ruleByWeekDay(this._weekday),
|
|
||||||
count: this._count,
|
count: this._count,
|
||||||
until: this._until,
|
byweekday: byweekday,
|
||||||
tzid: this.timezone,
|
bymonthday: bymonthday,
|
||||||
};
|
};
|
||||||
const contentline = RRule.optionsToString(options);
|
let contentline = RRule.optionsToString(options);
|
||||||
|
if (this._untilDay) {
|
||||||
|
// The UNTIL value should be inclusive of the last event instance
|
||||||
|
const until = toDate(
|
||||||
|
this._formatDate(this._untilDay!) +
|
||||||
|
"T" +
|
||||||
|
this._formatTime(this.dtstart!),
|
||||||
|
{ timeZone: this.timezone }
|
||||||
|
);
|
||||||
|
// rrule.js can't compute some UNTIL variations so we compute that ourself. Must be
|
||||||
|
// in the same format as dtstart.
|
||||||
|
const format = this.allDay ? "yyyyMMdd" : "yyyyMMdd'T'HHmmss";
|
||||||
|
const newUntilValue = formatInTimeZone(
|
||||||
|
until,
|
||||||
|
this.hass.config.time_zone,
|
||||||
|
format
|
||||||
|
);
|
||||||
|
contentline += `;UNTIL=${newUntilValue}`;
|
||||||
|
}
|
||||||
return contentline.slice(6); // Strip "RRULE:" prefix
|
return contentline.slice(6); // Strip "RRULE:" prefix
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -315,6 +487,16 @@ export class RecurrenceRuleEditor extends LitElement {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Formats a date in browser display timezone
|
||||||
|
private _formatDate(date: Date): string {
|
||||||
|
return formatInTimeZone(date, this.timezone!, "yyyy-MM-dd");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Formats a time in browser display timezone
|
||||||
|
private _formatTime(date: Date): string {
|
||||||
|
return formatInTimeZone(date, this.timezone!, "HH:mm:ss");
|
||||||
|
}
|
||||||
|
|
||||||
static styles = css`
|
static styles = css`
|
||||||
ha-textfield,
|
ha-textfield,
|
||||||
ha-select {
|
ha-select {
|
||||||
|
@ -1,8 +1,22 @@
|
|||||||
// Library for converting back and forth from values use by this webcomponent
|
// Library for converting back and forth from values use by this webcomponent
|
||||||
// and the values defined by rrule.js.
|
// and the values defined by rrule.js.
|
||||||
import { RRule, Frequency, Weekday } from "rrule";
|
import {
|
||||||
import type { WeekdayStr } from "rrule";
|
addDays,
|
||||||
import { addDays, addMonths, addWeeks, addYears } from "date-fns";
|
addMonths,
|
||||||
|
addWeeks,
|
||||||
|
addYears,
|
||||||
|
getDate,
|
||||||
|
getDay,
|
||||||
|
isLastDayOfMonth,
|
||||||
|
isSameMonth,
|
||||||
|
} from "date-fns";
|
||||||
|
import type { Options, WeekdayStr } from "rrule";
|
||||||
|
import { Frequency, RRule, Weekday } from "rrule";
|
||||||
|
import { formatDate } from "../../common/datetime/format_date";
|
||||||
|
import { capitalizeFirstLetter } from "../../common/string/capitalize-first-letter";
|
||||||
|
import { dayNames } from "../../common/translations/day_names";
|
||||||
|
import { monthNames } from "../../common/translations/month_names";
|
||||||
|
import { HomeAssistant } from "../../types";
|
||||||
|
|
||||||
export type RepeatFrequency =
|
export type RepeatFrequency =
|
||||||
| "none"
|
| "none"
|
||||||
@ -21,14 +35,11 @@ export const DEFAULT_COUNT = {
|
|||||||
daily: 30,
|
daily: 30,
|
||||||
};
|
};
|
||||||
|
|
||||||
export function intervalSuffix(freq: RepeatFrequency) {
|
export interface MonthlyRepeatItem {
|
||||||
if (freq === "monthly") {
|
value: string;
|
||||||
return "months";
|
byday?: Weekday;
|
||||||
}
|
bymonthday?: number;
|
||||||
if (freq === "weekly") {
|
label: string;
|
||||||
return "weeks";
|
|
||||||
}
|
|
||||||
return "days";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function untilValue(freq: RepeatFrequency): Date {
|
export function untilValue(freq: RepeatFrequency): Date {
|
||||||
@ -81,16 +92,6 @@ export const convertRepeatFrequency = (
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const WEEKDAY_NAME = {
|
|
||||||
SU: "Sun",
|
|
||||||
MO: "Mon",
|
|
||||||
TU: "Tue",
|
|
||||||
WE: "Wed",
|
|
||||||
TH: "Thu",
|
|
||||||
FR: "Fri",
|
|
||||||
SA: "Sat",
|
|
||||||
};
|
|
||||||
|
|
||||||
export const WEEKDAYS = [
|
export const WEEKDAYS = [
|
||||||
RRule.SU,
|
RRule.SU,
|
||||||
RRule.MO,
|
RRule.MO,
|
||||||
@ -101,7 +102,16 @@ export const WEEKDAYS = [
|
|||||||
RRule.SA,
|
RRule.SA,
|
||||||
];
|
];
|
||||||
|
|
||||||
export function getWeekdays(firstDay?: number) {
|
/** Return a weekday number compatible with rrule.js weekdays */
|
||||||
|
export function getWeekday(dtstart: Date): number {
|
||||||
|
let weekDay = getDay(dtstart) - 1;
|
||||||
|
if (weekDay < 0) {
|
||||||
|
weekDay += 7;
|
||||||
|
}
|
||||||
|
return weekDay;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getWeekdays(firstDay?: number): Weekday[] {
|
||||||
if (firstDay === undefined || firstDay === 0) {
|
if (firstDay === undefined || firstDay === 0) {
|
||||||
return WEEKDAYS;
|
return WEEKDAYS;
|
||||||
}
|
}
|
||||||
@ -114,9 +124,7 @@ export function getWeekdays(firstDay?: number) {
|
|||||||
return weekDays;
|
return weekDays;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ruleByWeekDay(
|
export function ruleByWeekDay(weekdays: Set<WeekdayStr>): Weekday[] {
|
||||||
weekdays: Set<WeekdayStr>
|
|
||||||
): Weekday[] | undefined {
|
|
||||||
return Array.from(weekdays).map((value: string) => {
|
return Array.from(weekdays).map((value: string) => {
|
||||||
switch (value) {
|
switch (value) {
|
||||||
case "MO":
|
case "MO":
|
||||||
@ -138,3 +146,127 @@ export function ruleByWeekDay(
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determine the recurrence options based on the day of the month. The
|
||||||
|
* return values are a Weekday object that represent a BYDAY for a
|
||||||
|
* particular week of the month like "first Saturday" or "last Friday".
|
||||||
|
*/
|
||||||
|
function getWeekydaysForMonth(dtstart: Date): Weekday[] {
|
||||||
|
const weekDay = getWeekday(dtstart);
|
||||||
|
const dayOfMonth = getDate(dtstart);
|
||||||
|
const nthWeekdayOfMonth = Math.floor((dayOfMonth - 1) / 7) + 1;
|
||||||
|
const isLastWeekday = !isSameMonth(dtstart, addDays(dtstart, 7));
|
||||||
|
const byweekdays: Weekday[] = [];
|
||||||
|
if (!isLastWeekday || dayOfMonth <= 28) {
|
||||||
|
byweekdays.push(new Weekday(weekDay, nthWeekdayOfMonth));
|
||||||
|
}
|
||||||
|
if (isLastWeekday) {
|
||||||
|
byweekdays.push(new Weekday(weekDay, -1));
|
||||||
|
}
|
||||||
|
return byweekdays;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the list of repeat values available for the specified date.
|
||||||
|
*/
|
||||||
|
export function getMonthlyRepeatItems(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
interval: number,
|
||||||
|
dtstart: Date
|
||||||
|
): MonthlyRepeatItem[] {
|
||||||
|
const getLabel = (repeatValue: string) =>
|
||||||
|
renderRRuleAsText(hass, `FREQ=MONTHLY;INTERVAL=${interval};${repeatValue}`);
|
||||||
|
|
||||||
|
const result: MonthlyRepeatItem[] = [
|
||||||
|
// The default repeat rule is on day of month e.g. 3rd day of month
|
||||||
|
{
|
||||||
|
value: `BYMONTHDAY=${getDate(dtstart)}`,
|
||||||
|
label: getLabel(`BYMONTHDAY=${getDate(dtstart)}`)!,
|
||||||
|
},
|
||||||
|
// Additional optional rules based on the week of month e.g. 2nd sunday of month
|
||||||
|
...getWeekydaysForMonth(dtstart).map((item) => ({
|
||||||
|
value: `BYDAY=${item.toString()}`,
|
||||||
|
byday: item,
|
||||||
|
label: getLabel(`BYDAY=${item.toString()}`)!,
|
||||||
|
})),
|
||||||
|
];
|
||||||
|
if (isLastDayOfMonth(dtstart)) {
|
||||||
|
result.push({
|
||||||
|
value: "BYMONTHDAY=-1",
|
||||||
|
bymonthday: -1,
|
||||||
|
label: getLabel(`BYMONTHDAY=-1`)!,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getMonthlyRepeatWeekdayFromRule(
|
||||||
|
rrule: Partial<Options>
|
||||||
|
): Weekday | undefined {
|
||||||
|
if (rrule.freq !== Frequency.MONTHLY) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
rrule.byweekday &&
|
||||||
|
Array.isArray(rrule.byweekday) &&
|
||||||
|
rrule.byweekday.length === 1 &&
|
||||||
|
rrule.byweekday[0] instanceof Weekday
|
||||||
|
) {
|
||||||
|
return rrule.byweekday[0];
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getMonthdayRepeatFromRule(
|
||||||
|
rrule: Partial<Options>
|
||||||
|
): number | undefined {
|
||||||
|
if (rrule.freq !== Frequency.MONTHLY || !rrule.bymonthday) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
if (Array.isArray(rrule.bymonthday)) {
|
||||||
|
return rrule.bymonthday[0];
|
||||||
|
}
|
||||||
|
return rrule.bymonthday;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A wrapper around RRule.toText that assists with translation.
|
||||||
|
*/
|
||||||
|
export function renderRRuleAsText(hass: HomeAssistant, value: string) {
|
||||||
|
const rule = RRule.fromString(`RRULE:${value}`);
|
||||||
|
if (!rule.isFullyConvertibleToText()) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
return capitalizeFirstLetter(
|
||||||
|
rule.toText(
|
||||||
|
(id: string | number | Weekday): string => {
|
||||||
|
if (typeof id === "string") {
|
||||||
|
return hass.localize(`ui.components.calendar.event.rrule.${id}`);
|
||||||
|
}
|
||||||
|
return "";
|
||||||
|
},
|
||||||
|
{
|
||||||
|
dayNames: dayNames(hass.locale),
|
||||||
|
monthNames: monthNames(hass.locale),
|
||||||
|
tokens: {},
|
||||||
|
},
|
||||||
|
// Format the date
|
||||||
|
(year: number, month: string, day: number): string => {
|
||||||
|
if (!year || !month || !day) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
// Build date so we can then format it
|
||||||
|
const date = new Date();
|
||||||
|
date.setFullYear(year);
|
||||||
|
// As input we already get the localized month name, so we now unfortunately
|
||||||
|
// need to convert it back to something Date can work with. The already localized
|
||||||
|
// months names are a must in the RRule.Language structure (an empty string[] would
|
||||||
|
// mean we get undefined months input in this method here).
|
||||||
|
date.setMonth(monthNames(hass.locale).indexOf(month));
|
||||||
|
date.setDate(day);
|
||||||
|
return formatDate(date, hass.locale);
|
||||||
|
}
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
@ -192,13 +192,13 @@ class HaConfigAreaPage extends SubscribeMixin(LitElement) {
|
|||||||
devices.forEach((entry) => {
|
devices.forEach((entry) => {
|
||||||
entry.name = computeDeviceName(entry, this.hass);
|
entry.name = computeDeviceName(entry, this.hass);
|
||||||
});
|
});
|
||||||
sortDeviceRegistryByName(devices);
|
sortDeviceRegistryByName(devices, this.hass.locale.language);
|
||||||
}
|
}
|
||||||
if (entities) {
|
if (entities) {
|
||||||
entities.forEach((entry) => {
|
entities.forEach((entry) => {
|
||||||
entry.name = computeEntityRegistryName(this.hass, entry);
|
entry.name = computeEntityRegistryName(this.hass, entry);
|
||||||
});
|
});
|
||||||
sortEntityRegistryByName(entities);
|
sortEntityRegistryByName(entities, this.hass.locale.language);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Group entities by domain
|
// Group entities by domain
|
||||||
@ -258,7 +258,8 @@ class HaConfigAreaPage extends SubscribeMixin(LitElement) {
|
|||||||
<div class="column">
|
<div class="column">
|
||||||
${area.picture
|
${area.picture
|
||||||
? html`<div class="img-container">
|
? html`<div class="img-container">
|
||||||
<img src=${area.picture} /><ha-icon-button
|
<img alt=${area.name} src=${area.picture} />
|
||||||
|
<ha-icon-button
|
||||||
.path=${mdiPencil}
|
.path=${mdiPencil}
|
||||||
.entry=${area}
|
.entry=${area}
|
||||||
@click=${this._showSettings}
|
@click=${this._showSettings}
|
||||||
@ -507,7 +508,11 @@ class HaConfigAreaPage extends SubscribeMixin(LitElement) {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
groupedEntities.sort((entry1, entry2) =>
|
groupedEntities.sort((entry1, entry2) =>
|
||||||
caseInsensitiveStringCompare(entry1.name!, entry2.name!)
|
caseInsensitiveStringCompare(
|
||||||
|
entry1.name!,
|
||||||
|
entry2.name!,
|
||||||
|
this.hass.locale.language
|
||||||
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (relatedEntityIds?.length) {
|
if (relatedEntityIds?.length) {
|
||||||
@ -521,7 +526,11 @@ class HaConfigAreaPage extends SubscribeMixin(LitElement) {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
relatedEntities.sort((entry1, entry2) =>
|
relatedEntities.sort((entry1, entry2) =>
|
||||||
caseInsensitiveStringCompare(entry1.name!, entry2.name!)
|
caseInsensitiveStringCompare(
|
||||||
|
entry1.name!,
|
||||||
|
entry2.name!,
|
||||||
|
this.hass.locale.language
|
||||||
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -299,7 +299,7 @@ export default class HaAutomationAction extends LitElement {
|
|||||||
icon,
|
icon,
|
||||||
] as [string, string, string]
|
] as [string, string, string]
|
||||||
)
|
)
|
||||||
.sort((a, b) => stringCompare(a[1], b[1]))
|
.sort((a, b) => stringCompare(a[1], b[1], this.hass.locale.language))
|
||||||
);
|
);
|
||||||
|
|
||||||
static get styles(): CSSResultGroup {
|
static get styles(): CSSResultGroup {
|
||||||
|
@ -66,7 +66,7 @@ export class HaConditionAction extends LitElement implements ActionElement {
|
|||||||
icon,
|
icon,
|
||||||
] as [string, string, string]
|
] as [string, string, string]
|
||||||
)
|
)
|
||||||
.sort((a, b) => stringCompare(a[1], b[1]))
|
.sort((a, b) => stringCompare(a[1], b[1], this.hass.locale.language))
|
||||||
);
|
);
|
||||||
|
|
||||||
private _conditionChanged(ev: CustomEvent) {
|
private _conditionChanged(ev: CustomEvent) {
|
||||||
|
@ -158,6 +158,7 @@ export class HaDeviceAction extends LitElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
ha-form {
|
ha-form {
|
||||||
|
display: block;
|
||||||
margin-top: 24px;
|
margin-top: 24px;
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
@ -328,7 +328,7 @@ export default class HaAutomationCondition extends LitElement {
|
|||||||
icon,
|
icon,
|
||||||
] as [string, string, string]
|
] as [string, string, string]
|
||||||
)
|
)
|
||||||
.sort((a, b) => stringCompare(a[1], b[1]))
|
.sort((a, b) => stringCompare(a[1], b[1], this.hass.locale.language))
|
||||||
);
|
);
|
||||||
|
|
||||||
static get styles(): CSSResultGroup {
|
static get styles(): CSSResultGroup {
|
||||||
|
@ -541,15 +541,10 @@ export class HaAutomationTrace extends LitElement {
|
|||||||
justify-content: center;
|
justify-content: center;
|
||||||
display: flex;
|
display: flex;
|
||||||
}
|
}
|
||||||
|
|
||||||
.info {
|
.info {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
background-color: var(--card-background-color);
|
background-color: var(--card-background-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
.linkButton {
|
|
||||||
color: var(--primary-text-color);
|
|
||||||
}
|
|
||||||
.trace-link {
|
.trace-link {
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
}
|
}
|
||||||
|
@ -302,7 +302,7 @@ export default class HaAutomationTrigger extends LitElement {
|
|||||||
icon,
|
icon,
|
||||||
] as [string, string, string]
|
] as [string, string, string]
|
||||||
)
|
)
|
||||||
.sort((a, b) => stringCompare(a[1], b[1]))
|
.sort((a, b) => stringCompare(a[1], b[1], this.hass.locale.language))
|
||||||
);
|
);
|
||||||
|
|
||||||
static get styles(): CSSResultGroup {
|
static get styles(): CSSResultGroup {
|
||||||
|
@ -46,7 +46,7 @@ export class HaCalendarTrigger extends LitElement implements TriggerElement {
|
|||||||
],
|
],
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{ name: "offset", selector: { duration: { enable_day: true } } },
|
{ name: "offset", selector: { duration: {} } },
|
||||||
{
|
{
|
||||||
name: "offset_type",
|
name: "offset_type",
|
||||||
type: "select",
|
type: "select",
|
||||||
|
@ -54,7 +54,11 @@ export class HaTagTrigger extends LitElement implements TriggerElement {
|
|||||||
|
|
||||||
private async _fetchTags() {
|
private async _fetchTags() {
|
||||||
this._tags = (await fetchTags(this.hass)).sort((a, b) =>
|
this._tags = (await fetchTags(this.hass)).sort((a, b) =>
|
||||||
caseInsensitiveStringCompare(a.name || a.id, b.name || b.id)
|
caseInsensitiveStringCompare(
|
||||||
|
a.name || a.id,
|
||||||
|
b.name || b.id,
|
||||||
|
this.hass.locale.language
|
||||||
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -9,7 +9,6 @@ import {
|
|||||||
mdiFormatListChecks,
|
mdiFormatListChecks,
|
||||||
mdiSync,
|
mdiSync,
|
||||||
} from "@mdi/js";
|
} from "@mdi/js";
|
||||||
import type { UnsubscribeFunc } from "home-assistant-js-websocket";
|
|
||||||
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
|
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
|
||||||
import { customElement, property, state } from "lit/decorators";
|
import { customElement, property, state } from "lit/decorators";
|
||||||
import { classMap } from "lit/directives/class-map";
|
import { classMap } from "lit/directives/class-map";
|
||||||
@ -41,22 +40,17 @@ import {
|
|||||||
updateCloudAlexaEntityConfig,
|
updateCloudAlexaEntityConfig,
|
||||||
updateCloudPref,
|
updateCloudPref,
|
||||||
} from "../../../../data/cloud";
|
} from "../../../../data/cloud";
|
||||||
import {
|
import { EntityRegistryEntry } from "../../../../data/entity_registry";
|
||||||
EntityRegistryEntry,
|
|
||||||
subscribeEntityRegistry,
|
|
||||||
} from "../../../../data/entity_registry";
|
|
||||||
import { showDomainTogglerDialog } from "../../../../dialogs/domain-toggler/show-dialog-domain-toggler";
|
import { showDomainTogglerDialog } from "../../../../dialogs/domain-toggler/show-dialog-domain-toggler";
|
||||||
import "../../../../layouts/hass-loading-screen";
|
import "../../../../layouts/hass-loading-screen";
|
||||||
import "../../../../layouts/hass-subpage";
|
import "../../../../layouts/hass-subpage";
|
||||||
import { SubscribeMixin } from "../../../../mixins/subscribe-mixin";
|
|
||||||
import { haStyle } from "../../../../resources/styles";
|
import { haStyle } from "../../../../resources/styles";
|
||||||
import type { HomeAssistant } from "../../../../types";
|
import type { HomeAssistant } from "../../../../types";
|
||||||
|
|
||||||
const DEFAULT_CONFIG_EXPOSE = true;
|
const DEFAULT_CONFIG_EXPOSE = true;
|
||||||
const IGNORE_INTERFACES = ["Alexa.EndpointHealth"];
|
|
||||||
|
|
||||||
@customElement("cloud-alexa")
|
@customElement("cloud-alexa")
|
||||||
class CloudAlexa extends SubscribeMixin(LitElement) {
|
class CloudAlexa extends LitElement {
|
||||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||||
|
|
||||||
@property()
|
@property()
|
||||||
@ -168,13 +162,8 @@ class CloudAlexa extends SubscribeMixin(LitElement) {
|
|||||||
<state-info
|
<state-info
|
||||||
.hass=${this.hass}
|
.hass=${this.hass}
|
||||||
.stateObj=${stateObj}
|
.stateObj=${stateObj}
|
||||||
secondary-line
|
|
||||||
@click=${this._showMoreInfo}
|
@click=${this._showMoreInfo}
|
||||||
>
|
>
|
||||||
${entity.interfaces
|
|
||||||
.filter((ifc) => !IGNORE_INTERFACES.includes(ifc))
|
|
||||||
.map((ifc) => ifc.replace(/(Alexa.|Controller)/g, ""))
|
|
||||||
.join(", ")}
|
|
||||||
</state-info>
|
</state-info>
|
||||||
${!emptyFilter
|
${!emptyFilter
|
||||||
? html`${iconButton}`
|
? html`${iconButton}`
|
||||||
@ -323,23 +312,18 @@ class CloudAlexa extends SubscribeMixin(LitElement) {
|
|||||||
if (changedProps.has("cloudStatus")) {
|
if (changedProps.has("cloudStatus")) {
|
||||||
this._entityConfigs = this.cloudStatus.prefs.alexa_entity_configs;
|
this._entityConfigs = this.cloudStatus.prefs.alexa_entity_configs;
|
||||||
}
|
}
|
||||||
}
|
if (
|
||||||
|
changedProps.has("hass") &&
|
||||||
protected override hassSubscribe(): (
|
changedProps.get("hass")?.entities !== this.hass.entities
|
||||||
| UnsubscribeFunc
|
) {
|
||||||
| Promise<UnsubscribeFunc>
|
|
||||||
)[] {
|
|
||||||
return [
|
|
||||||
subscribeEntityRegistry(this.hass.connection, (entries) => {
|
|
||||||
const categories = {};
|
const categories = {};
|
||||||
|
|
||||||
for (const entry of entries) {
|
for (const entry of Object.values(this.hass.entities)) {
|
||||||
categories[entry.entity_id] = entry.entity_category;
|
categories[entry.entity_id] = entry.entity_category;
|
||||||
}
|
}
|
||||||
|
|
||||||
this._entityCategories = categories;
|
this._entityCategories = categories;
|
||||||
}),
|
}
|
||||||
];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private async _fetchData() {
|
private async _fetchData() {
|
||||||
@ -349,7 +333,8 @@ class CloudAlexa extends SubscribeMixin(LitElement) {
|
|||||||
const stateB = this.hass.states[b.entity_id];
|
const stateB = this.hass.states[b.entity_id];
|
||||||
return stringCompare(
|
return stringCompare(
|
||||||
stateA ? computeStateName(stateA) : a.entity_id,
|
stateA ? computeStateName(stateA) : a.entity_id,
|
||||||
stateB ? computeStateName(stateB) : b.entity_id
|
stateB ? computeStateName(stateB) : b.entity_id,
|
||||||
|
this.hass.locale.language
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
this._entities = entities;
|
this._entities = entities;
|
||||||
@ -541,6 +526,7 @@ class CloudAlexa extends SubscribeMixin(LitElement) {
|
|||||||
}
|
}
|
||||||
state-info {
|
state-info {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
height: 40px;
|
||||||
}
|
}
|
||||||
ha-switch {
|
ha-switch {
|
||||||
padding: 8px 0;
|
padding: 8px 0;
|
||||||
|
@ -9,7 +9,6 @@ import {
|
|||||||
mdiFormatListChecks,
|
mdiFormatListChecks,
|
||||||
mdiSync,
|
mdiSync,
|
||||||
} from "@mdi/js";
|
} from "@mdi/js";
|
||||||
import type { UnsubscribeFunc } from "home-assistant-js-websocket";
|
|
||||||
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
|
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
|
||||||
import { customElement, property, state } from "lit/decorators";
|
import { customElement, property, state } from "lit/decorators";
|
||||||
import { classMap } from "lit/directives/class-map";
|
import { classMap } from "lit/directives/class-map";
|
||||||
@ -41,7 +40,9 @@ import {
|
|||||||
} from "../../../../data/cloud";
|
} from "../../../../data/cloud";
|
||||||
import {
|
import {
|
||||||
EntityRegistryEntry,
|
EntityRegistryEntry,
|
||||||
subscribeEntityRegistry,
|
ExtEntityRegistryEntry,
|
||||||
|
getExtendedEntityRegistryEntries,
|
||||||
|
updateEntityRegistryEntry,
|
||||||
} from "../../../../data/entity_registry";
|
} from "../../../../data/entity_registry";
|
||||||
import {
|
import {
|
||||||
fetchCloudGoogleEntities,
|
fetchCloudGoogleEntities,
|
||||||
@ -51,15 +52,15 @@ import { showDomainTogglerDialog } from "../../../../dialogs/domain-toggler/show
|
|||||||
import { showAlertDialog } from "../../../../dialogs/generic/show-dialog-box";
|
import { showAlertDialog } from "../../../../dialogs/generic/show-dialog-box";
|
||||||
import "../../../../layouts/hass-loading-screen";
|
import "../../../../layouts/hass-loading-screen";
|
||||||
import "../../../../layouts/hass-subpage";
|
import "../../../../layouts/hass-subpage";
|
||||||
import { SubscribeMixin } from "../../../../mixins/subscribe-mixin";
|
import { buttonLinkStyle, haStyle } from "../../../../resources/styles";
|
||||||
import { haStyle } from "../../../../resources/styles";
|
|
||||||
import type { HomeAssistant } from "../../../../types";
|
import type { HomeAssistant } from "../../../../types";
|
||||||
import { showToast } from "../../../../util/toast";
|
import { showToast } from "../../../../util/toast";
|
||||||
|
import { showEntityAliasesDialog } from "../../entities/entity-aliases/show-dialog-entity-aliases";
|
||||||
|
|
||||||
const DEFAULT_CONFIG_EXPOSE = true;
|
const DEFAULT_CONFIG_EXPOSE = true;
|
||||||
|
|
||||||
@customElement("cloud-google-assistant")
|
@customElement("cloud-google-assistant")
|
||||||
class CloudGoogleAssistant extends SubscribeMixin(LitElement) {
|
class CloudGoogleAssistant extends LitElement {
|
||||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||||
|
|
||||||
@property() public cloudStatus!: CloudStatusLoggedIn;
|
@property() public cloudStatus!: CloudStatusLoggedIn;
|
||||||
@ -68,6 +69,8 @@ class CloudGoogleAssistant extends SubscribeMixin(LitElement) {
|
|||||||
|
|
||||||
@state() private _entities?: GoogleEntity[];
|
@state() private _entities?: GoogleEntity[];
|
||||||
|
|
||||||
|
@state() private _entries?: { [id: string]: ExtEntityRegistryEntry };
|
||||||
|
|
||||||
@state() private _syncing = false;
|
@state() private _syncing = false;
|
||||||
|
|
||||||
@state()
|
@state()
|
||||||
@ -164,6 +167,8 @@ class CloudGoogleAssistant extends SubscribeMixin(LitElement) {
|
|||||||
: mdiCloseBoxMultiple}
|
: mdiCloseBoxMultiple}
|
||||||
></ha-icon-button>`;
|
></ha-icon-button>`;
|
||||||
|
|
||||||
|
const aliases = this._entries?.[entity.entity_id]?.aliases;
|
||||||
|
|
||||||
target.push(html`
|
target.push(html`
|
||||||
<ha-card outlined>
|
<ha-card outlined>
|
||||||
<div class="card-content">
|
<div class="card-content">
|
||||||
@ -174,15 +179,57 @@ class CloudGoogleAssistant extends SubscribeMixin(LitElement) {
|
|||||||
secondary-line
|
secondary-line
|
||||||
@click=${this._showMoreInfo}
|
@click=${this._showMoreInfo}
|
||||||
>
|
>
|
||||||
${entity.traits
|
${aliases
|
||||||
.map((trait) => trait.substr(trait.lastIndexOf(".") + 1))
|
? html`
|
||||||
.join(", ")}
|
<span>
|
||||||
|
${aliases.length > 0
|
||||||
|
? [...aliases]
|
||||||
|
.sort((a, b) =>
|
||||||
|
stringCompare(a, b, this.hass.locale.language)
|
||||||
|
)
|
||||||
|
.join(", ")
|
||||||
|
: this.hass.localize(
|
||||||
|
"ui.panel.config.cloud.google.no_aliases"
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
<br />
|
||||||
|
<button
|
||||||
|
class="link"
|
||||||
|
.entityId=${entity.entity_id}
|
||||||
|
@click=${this._openAliasesSettings}
|
||||||
|
>
|
||||||
|
${this.hass.localize(
|
||||||
|
`ui.panel.config.cloud.google.${
|
||||||
|
aliases.length > 0
|
||||||
|
? "manage_aliases"
|
||||||
|
: "add_aliases"
|
||||||
|
}`
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
`
|
||||||
|
: html`
|
||||||
|
<span>
|
||||||
|
${this.hass.localize(
|
||||||
|
"ui.panel.config.cloud.google.aliases_not_available"
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
<br />
|
||||||
|
<button
|
||||||
|
class="link"
|
||||||
|
.stateObj=${stateObj}
|
||||||
|
@click=${this._showMoreInfoSettings}
|
||||||
|
>
|
||||||
|
${this.hass.localize(
|
||||||
|
"ui.panel.config.cloud.google.aliases_not_available_learn_more"
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
`}
|
||||||
</state-info>
|
</state-info>
|
||||||
${!emptyFilter
|
${!emptyFilter
|
||||||
? html`${iconButton}`
|
? html`${iconButton}`
|
||||||
: html`<ha-button-menu
|
: html`<ha-button-menu
|
||||||
corner="BOTTOM_START"
|
corner="BOTTOM_START"
|
||||||
.entityId=${stateObj.entity_id}
|
.entityId=${entity.entity_id}
|
||||||
@action=${this._exposeChanged}
|
@action=${this._exposeChanged}
|
||||||
>
|
>
|
||||||
${iconButton}
|
${iconButton}
|
||||||
@ -308,7 +355,7 @@ class CloudGoogleAssistant extends SubscribeMixin(LitElement) {
|
|||||||
</h3>
|
</h3>
|
||||||
${!this.narrow
|
${!this.narrow
|
||||||
? this.hass!.localize(
|
? this.hass!.localize(
|
||||||
"ui.panel.config.cloud.alexa.exposed",
|
"ui.panel.config.cloud.google.exposed",
|
||||||
"selected",
|
"selected",
|
||||||
selected
|
selected
|
||||||
)
|
)
|
||||||
@ -329,7 +376,7 @@ class CloudGoogleAssistant extends SubscribeMixin(LitElement) {
|
|||||||
</h3>
|
</h3>
|
||||||
${!this.narrow
|
${!this.narrow
|
||||||
? this.hass!.localize(
|
? this.hass!.localize(
|
||||||
"ui.panel.config.cloud.alexa.not_exposed",
|
"ui.panel.config.cloud.google.not_exposed",
|
||||||
"selected",
|
"selected",
|
||||||
this._entities.length - selected
|
this._entities.length - selected
|
||||||
)
|
)
|
||||||
@ -354,23 +401,38 @@ class CloudGoogleAssistant extends SubscribeMixin(LitElement) {
|
|||||||
if (changedProps.has("cloudStatus")) {
|
if (changedProps.has("cloudStatus")) {
|
||||||
this._entityConfigs = this.cloudStatus.prefs.google_entity_configs;
|
this._entityConfigs = this.cloudStatus.prefs.google_entity_configs;
|
||||||
}
|
}
|
||||||
}
|
if (
|
||||||
|
changedProps.has("hass") &&
|
||||||
protected override hassSubscribe(): (
|
changedProps.get("hass")?.entities !== this.hass.entities
|
||||||
| UnsubscribeFunc
|
) {
|
||||||
| Promise<UnsubscribeFunc>
|
|
||||||
)[] {
|
|
||||||
return [
|
|
||||||
subscribeEntityRegistry(this.hass.connection, (entries) => {
|
|
||||||
const categories = {};
|
const categories = {};
|
||||||
|
|
||||||
for (const entry of entries) {
|
for (const entry of Object.values(this.hass.entities)) {
|
||||||
categories[entry.entity_id] = entry.entity_category;
|
categories[entry.entity_id] = entry.entity_category;
|
||||||
}
|
}
|
||||||
|
|
||||||
this._entityCategories = categories;
|
this._entityCategories = categories;
|
||||||
}),
|
}
|
||||||
];
|
}
|
||||||
|
|
||||||
|
private async _openAliasesSettings(ev) {
|
||||||
|
ev.stopPropagation();
|
||||||
|
const entityId = ev.target.entityId;
|
||||||
|
const entry = this._entries![entityId];
|
||||||
|
if (!entry) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
showEntityAliasesDialog(this, {
|
||||||
|
entity: entry,
|
||||||
|
updateEntry: async (updates) => {
|
||||||
|
const { entity_entry } = await updateEntityRegistryEntry(
|
||||||
|
this.hass,
|
||||||
|
entry.entity_id,
|
||||||
|
updates
|
||||||
|
);
|
||||||
|
this._entries![entity_entry.entity_id] = entity_entry;
|
||||||
|
},
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private _configIsDomainExposed(
|
private _configIsDomainExposed(
|
||||||
@ -397,12 +459,20 @@ class CloudGoogleAssistant extends SubscribeMixin(LitElement) {
|
|||||||
|
|
||||||
private async _fetchData() {
|
private async _fetchData() {
|
||||||
const entities = await fetchCloudGoogleEntities(this.hass);
|
const entities = await fetchCloudGoogleEntities(this.hass);
|
||||||
|
this._entries = await getExtendedEntityRegistryEntries(
|
||||||
|
this.hass,
|
||||||
|
entities
|
||||||
|
.filter((ent) => this.hass.entities[ent.entity_id])
|
||||||
|
.map((e) => e.entity_id)
|
||||||
|
);
|
||||||
|
|
||||||
entities.sort((a, b) => {
|
entities.sort((a, b) => {
|
||||||
const stateA = this.hass.states[a.entity_id];
|
const stateA = this.hass.states[a.entity_id];
|
||||||
const stateB = this.hass.states[b.entity_id];
|
const stateB = this.hass.states[b.entity_id];
|
||||||
return stringCompare(
|
return stringCompare(
|
||||||
stateA ? computeStateName(stateA) : a.entity_id,
|
stateA ? computeStateName(stateA) : a.entity_id,
|
||||||
stateB ? computeStateName(stateB) : b.entity_id
|
stateB ? computeStateName(stateB) : b.entity_id,
|
||||||
|
this.hass.locale.language
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
this._entities = entities;
|
this._entities = entities;
|
||||||
@ -410,7 +480,14 @@ class CloudGoogleAssistant extends SubscribeMixin(LitElement) {
|
|||||||
|
|
||||||
private _showMoreInfo(ev) {
|
private _showMoreInfo(ev) {
|
||||||
const entityId = ev.currentTarget.stateObj.entity_id;
|
const entityId = ev.currentTarget.stateObj.entity_id;
|
||||||
fireEvent(this, "hass-more-info", { entityId });
|
const moreInfoTab = ev.currentTarget.moreInfoTab;
|
||||||
|
fireEvent(this, "hass-more-info", { entityId, tab: moreInfoTab });
|
||||||
|
}
|
||||||
|
|
||||||
|
private _showMoreInfoSettings(ev) {
|
||||||
|
ev.stopPropagation();
|
||||||
|
const entityId = ev.currentTarget.stateObj.entity_id;
|
||||||
|
fireEvent(this, "hass-more-info", { entityId, tab: "settings" });
|
||||||
}
|
}
|
||||||
|
|
||||||
private async _exposeChanged(ev: CustomEvent<ActionDetail>) {
|
private async _exposeChanged(ev: CustomEvent<ActionDetail>) {
|
||||||
@ -582,6 +659,7 @@ class CloudGoogleAssistant extends SubscribeMixin(LitElement) {
|
|||||||
static get styles(): CSSResultGroup {
|
static get styles(): CSSResultGroup {
|
||||||
return [
|
return [
|
||||||
haStyle,
|
haStyle,
|
||||||
|
buttonLinkStyle,
|
||||||
css`
|
css`
|
||||||
mwc-list-item > [slot="meta"] {
|
mwc-list-item > [slot="meta"] {
|
||||||
margin-left: 4px;
|
margin-left: 4px;
|
||||||
|
@ -304,7 +304,11 @@ class HaConfigSectionGeneral extends LitElement {
|
|||||||
}
|
}
|
||||||
this._languages = Object.entries(this.hass.translationMetadata.translations)
|
this._languages = Object.entries(this.hass.translationMetadata.translations)
|
||||||
.sort((a, b) =>
|
.sort((a, b) =>
|
||||||
caseInsensitiveStringCompare(a[1].nativeName, b[1].nativeName)
|
caseInsensitiveStringCompare(
|
||||||
|
a[1].nativeName,
|
||||||
|
b[1].nativeName,
|
||||||
|
this.hass.locale.language
|
||||||
|
)
|
||||||
)
|
)
|
||||||
.map(([value, metaData]) => ({
|
.map(([value, metaData]) => ({
|
||||||
value,
|
value,
|
||||||
|
@ -31,7 +31,8 @@ export class HaDeviceViaDevicesCard extends LitElement {
|
|||||||
.sort((d1, d2) =>
|
.sort((d1, d2) =>
|
||||||
caseInsensitiveStringCompare(
|
caseInsensitiveStringCompare(
|
||||||
computeDeviceName(d1, this.hass),
|
computeDeviceName(d1, this.hass),
|
||||||
computeDeviceName(d2, this.hass)
|
computeDeviceName(d2, this.hass),
|
||||||
|
this.hass.locale.language
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
@ -157,7 +157,8 @@ export class HaConfigDevicePage extends LitElement {
|
|||||||
.sort((ent1, ent2) =>
|
.sort((ent1, ent2) =>
|
||||||
stringCompare(
|
stringCompare(
|
||||||
ent1.stateName || `zzz${ent1.entity_id}`,
|
ent1.stateName || `zzz${ent1.entity_id}`,
|
||||||
ent2.stateName || `zzz${ent2.entity_id}`
|
ent2.stateName || `zzz${ent2.entity_id}`,
|
||||||
|
this.hass.locale.language
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
@ -658,6 +659,10 @@ export class HaConfigDevicePage extends LitElement {
|
|||||||
integrations.length
|
integrations.length
|
||||||
? html`
|
? html`
|
||||||
<img
|
<img
|
||||||
|
alt=${domainToName(
|
||||||
|
this.hass.localize,
|
||||||
|
integrations[0].domain
|
||||||
|
)}
|
||||||
src=${brandsUrl({
|
src=${brandsUrl({
|
||||||
domain: integrations[0].domain,
|
domain: integrations[0].domain,
|
||||||
type: "logo",
|
type: "logo",
|
||||||
|
@ -220,6 +220,7 @@ export class EnergyGridSettings extends LitElement {
|
|||||||
${this._co2ConfigEntry
|
${this._co2ConfigEntry
|
||||||
? html`<div class="row" .entry=${this._co2ConfigEntry}>
|
? html`<div class="row" .entry=${this._co2ConfigEntry}>
|
||||||
<img
|
<img
|
||||||
|
alt=""
|
||||||
referrerpolicy="no-referrer"
|
referrerpolicy="no-referrer"
|
||||||
src=${brandsUrl({
|
src=${brandsUrl({
|
||||||
domain: "co2signal",
|
domain: "co2signal",
|
||||||
@ -244,6 +245,7 @@ export class EnergyGridSettings extends LitElement {
|
|||||||
: html`
|
: html`
|
||||||
<div class="row border-bottom">
|
<div class="row border-bottom">
|
||||||
<img
|
<img
|
||||||
|
alt=""
|
||||||
referrerpolicy="no-referrer"
|
referrerpolicy="no-referrer"
|
||||||
src=${brandsUrl({
|
src=${brandsUrl({
|
||||||
domain: "co2signal",
|
domain: "co2signal",
|
||||||
|
@ -130,6 +130,7 @@ export class DialogEnergySolarSettings
|
|||||||
style="display: flex; align-items: center;"
|
style="display: flex; align-items: center;"
|
||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
|
alt=""
|
||||||
referrerpolicy="no-referrer"
|
referrerpolicy="no-referrer"
|
||||||
style="height: 24px; margin-right: 16px;"
|
style="height: 24px; margin-right: 16px;"
|
||||||
src=${brandsUrl({
|
src=${brandsUrl({
|
||||||
|
@ -57,7 +57,13 @@ export class EntityRegistrySettingsHelper extends LitElement {
|
|||||||
super.updated(changedProperties);
|
super.updated(changedProperties);
|
||||||
if (changedProperties.has("entry")) {
|
if (changedProperties.has("entry")) {
|
||||||
this._error = undefined;
|
this._error = undefined;
|
||||||
|
if (
|
||||||
|
this.entry.unique_id !==
|
||||||
|
(changedProperties.get("entry") as ExtEntityRegistryEntry)?.unique_id
|
||||||
|
) {
|
||||||
this._item = undefined;
|
this._item = undefined;
|
||||||
|
}
|
||||||
|
|
||||||
this._getItem();
|
this._getItem();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,212 @@
|
|||||||
|
import "@material/mwc-button/mwc-button";
|
||||||
|
import { mdiDeleteOutline, mdiPlus } from "@mdi/js";
|
||||||
|
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
|
||||||
|
import { customElement, property, state } from "lit/decorators";
|
||||||
|
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||||
|
import { computeStateName } from "../../../../common/entity/compute_state_name";
|
||||||
|
import "../../../../components/ha-alert";
|
||||||
|
import "../../../../components/ha-area-picker";
|
||||||
|
import "../../../../components/ha-dialog";
|
||||||
|
import "../../../../components/ha-textfield";
|
||||||
|
import type { HaTextField } from "../../../../components/ha-textfield";
|
||||||
|
import { haStyle, haStyleDialog } from "../../../../resources/styles";
|
||||||
|
import { HomeAssistant } from "../../../../types";
|
||||||
|
import { EntityAliasesDialogParams } from "./show-dialog-entity-aliases";
|
||||||
|
|
||||||
|
@customElement("dialog-entity-aliases")
|
||||||
|
class DialogEntityAliases extends LitElement {
|
||||||
|
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||||
|
|
||||||
|
@state() private _error?: string;
|
||||||
|
|
||||||
|
@state() private _params?: EntityAliasesDialogParams;
|
||||||
|
|
||||||
|
@state() private _aliases!: string[];
|
||||||
|
|
||||||
|
@state() private _submitting = false;
|
||||||
|
|
||||||
|
public async showDialog(params: EntityAliasesDialogParams): Promise<void> {
|
||||||
|
this._params = params;
|
||||||
|
this._error = undefined;
|
||||||
|
this._aliases =
|
||||||
|
this._params.entity.aliases?.length > 0
|
||||||
|
? [...this._params.entity.aliases].sort()
|
||||||
|
: [""];
|
||||||
|
await this.updateComplete;
|
||||||
|
}
|
||||||
|
|
||||||
|
public closeDialog(): void {
|
||||||
|
this._error = "";
|
||||||
|
this._params = undefined;
|
||||||
|
fireEvent(this, "dialog-closed", { dialog: this.localName });
|
||||||
|
}
|
||||||
|
|
||||||
|
protected render(): TemplateResult {
|
||||||
|
if (!this._params) {
|
||||||
|
return html``;
|
||||||
|
}
|
||||||
|
|
||||||
|
const entityId = this._params.entity.entity_id;
|
||||||
|
const stateObj = entityId ? this.hass.states[entityId] : undefined;
|
||||||
|
|
||||||
|
const name = (stateObj && computeStateName(stateObj)) || entityId;
|
||||||
|
|
||||||
|
return html`
|
||||||
|
<ha-dialog
|
||||||
|
open
|
||||||
|
@closed=${this.closeDialog}
|
||||||
|
.heading=${this.hass.localize(
|
||||||
|
"ui.dialogs.entity_registry.editor.aliases.heading",
|
||||||
|
{ name }
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
${this._error
|
||||||
|
? html`<ha-alert alert-type="error">${this._error}</ha-alert> `
|
||||||
|
: ""}
|
||||||
|
<div class="form">
|
||||||
|
${this._aliases.map(
|
||||||
|
(alias, index) => html`
|
||||||
|
<div class="layout horizontal center-center row">
|
||||||
|
<ha-textfield
|
||||||
|
dialogInitialFocus=${index}
|
||||||
|
.index=${index}
|
||||||
|
class="flex-auto"
|
||||||
|
.label=${this.hass!.localize(
|
||||||
|
"ui.dialogs.entity_registry.editor.aliases.input_label",
|
||||||
|
{ number: index + 1 }
|
||||||
|
)}
|
||||||
|
.value=${alias}
|
||||||
|
?data-last=${index === this._aliases.length - 1}
|
||||||
|
@input=${this._editAlias}
|
||||||
|
@keydown=${this._keyDownAlias}
|
||||||
|
></ha-textfield>
|
||||||
|
<ha-icon-button
|
||||||
|
.index=${index}
|
||||||
|
slot="navigationIcon"
|
||||||
|
label=${this.hass!.localize(
|
||||||
|
"ui.dialogs.entity_registry.editor.aliases.remove_alias",
|
||||||
|
{ number: index + 1 }
|
||||||
|
)}
|
||||||
|
@click=${this._removeAlias}
|
||||||
|
.path=${mdiDeleteOutline}
|
||||||
|
></ha-icon-button>
|
||||||
|
</div>
|
||||||
|
`
|
||||||
|
)}
|
||||||
|
<div class="layout horizontal center-center">
|
||||||
|
<mwc-button @click=${this._addAlias}>
|
||||||
|
${this.hass!.localize(
|
||||||
|
"ui.dialogs.entity_registry.editor.aliases.add_alias"
|
||||||
|
)}
|
||||||
|
<ha-svg-icon slot="icon" .path=${mdiPlus}></ha-svg-icon>
|
||||||
|
</mwc-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<mwc-button
|
||||||
|
slot="secondaryAction"
|
||||||
|
@click=${this.closeDialog}
|
||||||
|
.disabled=${this._submitting}
|
||||||
|
>
|
||||||
|
${this.hass.localize("ui.common.cancel")}
|
||||||
|
</mwc-button>
|
||||||
|
<mwc-button
|
||||||
|
slot="primaryAction"
|
||||||
|
@click=${this._updateEntry}
|
||||||
|
.disabled=${this._submitting}
|
||||||
|
>
|
||||||
|
${this.hass.localize(
|
||||||
|
"ui.dialogs.entity_registry.editor.aliases.save"
|
||||||
|
)}
|
||||||
|
</mwc-button>
|
||||||
|
</ha-dialog>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async _addAlias() {
|
||||||
|
this._aliases = [...this._aliases, ""];
|
||||||
|
await this.updateComplete;
|
||||||
|
const field = this.shadowRoot?.querySelector(`ha-textfield[data-last]`) as
|
||||||
|
| HaTextField
|
||||||
|
| undefined;
|
||||||
|
field?.focus();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async _editAlias(ev: Event) {
|
||||||
|
const index = (ev.target as any).index;
|
||||||
|
this._aliases[index] = (ev.target as any).value;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async _keyDownAlias(ev: KeyboardEvent) {
|
||||||
|
if (ev.key === "Enter") {
|
||||||
|
ev.stopPropagation();
|
||||||
|
this._addAlias();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async _removeAlias(ev: Event) {
|
||||||
|
const index = (ev.target as any).index;
|
||||||
|
const aliases = [...this._aliases];
|
||||||
|
aliases.splice(index, 1);
|
||||||
|
this._aliases = aliases;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async _updateEntry(): Promise<void> {
|
||||||
|
this._submitting = true;
|
||||||
|
const noEmptyAliases = this._aliases
|
||||||
|
.map((alias) => alias.trim())
|
||||||
|
.filter((alias) => alias);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await this._params!.updateEntry({
|
||||||
|
aliases: noEmptyAliases,
|
||||||
|
});
|
||||||
|
this.closeDialog();
|
||||||
|
} catch (err: any) {
|
||||||
|
this._error =
|
||||||
|
err.message ||
|
||||||
|
this.hass.localize(
|
||||||
|
"ui.dialogs.entity_registry.editor.aliases.unknown_error"
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
this._submitting = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static get styles(): CSSResultGroup {
|
||||||
|
return [
|
||||||
|
haStyle,
|
||||||
|
haStyleDialog,
|
||||||
|
css`
|
||||||
|
.row {
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
ha-textfield {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
ha-icon-button {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
mwc-button {
|
||||||
|
margin-left: 8px;
|
||||||
|
}
|
||||||
|
#alias_input {
|
||||||
|
margin-top: 8px;
|
||||||
|
}
|
||||||
|
.alias {
|
||||||
|
border: 1px solid var(--divider-color);
|
||||||
|
border-radius: 4px;
|
||||||
|
margin-top: 4px;
|
||||||
|
--mdc-icon-button-size: 24px;
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface HTMLElementTagNameMap {
|
||||||
|
"dialog-entity-aliases": DialogEntityAliases;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,25 @@
|
|||||||
|
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||||
|
import {
|
||||||
|
EntityRegistryEntryUpdateParams,
|
||||||
|
ExtEntityRegistryEntry,
|
||||||
|
} from "../../../../data/entity_registry";
|
||||||
|
|
||||||
|
export interface EntityAliasesDialogParams {
|
||||||
|
entity: ExtEntityRegistryEntry;
|
||||||
|
updateEntry: (
|
||||||
|
updates: Partial<EntityRegistryEntryUpdateParams>
|
||||||
|
) => Promise<unknown>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const loadEntityAliasesDialog = () => import("./dialog-entity-aliases");
|
||||||
|
|
||||||
|
export const showEntityAliasesDialog = (
|
||||||
|
element: HTMLElement,
|
||||||
|
entityAliasesParams: EntityAliasesDialogParams
|
||||||
|
): void => {
|
||||||
|
fireEvent(element, "show-dialog", {
|
||||||
|
dialogTag: "dialog-entity-aliases",
|
||||||
|
dialogImport: loadEntityAliasesDialog,
|
||||||
|
dialogParams: entityAliasesParams,
|
||||||
|
});
|
||||||
|
};
|
@ -1,8 +1,13 @@
|
|||||||
import "@material/mwc-formfield/mwc-formfield";
|
import "@material/mwc-formfield/mwc-formfield";
|
||||||
|
import "@material/mwc-list/mwc-list";
|
||||||
|
import "@material/mwc-list/mwc-list-item";
|
||||||
|
import { mdiPencil } from "@mdi/js";
|
||||||
import { UnsubscribeFunc } from "home-assistant-js-websocket";
|
import { UnsubscribeFunc } from "home-assistant-js-websocket";
|
||||||
import { css, html, LitElement, PropertyValues, TemplateResult } from "lit";
|
import { css, html, LitElement, PropertyValues, TemplateResult } from "lit";
|
||||||
import { customElement, property, state } from "lit/decorators";
|
import { customElement, property, state } from "lit/decorators";
|
||||||
|
import { fireEvent } from "../../../common/dom/fire_event";
|
||||||
import { computeDomain } from "../../../common/entity/compute_domain";
|
import { computeDomain } from "../../../common/entity/compute_domain";
|
||||||
|
import { stringCompare } from "../../../common/string/compare";
|
||||||
import "../../../components/ha-area-picker";
|
import "../../../components/ha-area-picker";
|
||||||
import "../../../components/ha-expansion-panel";
|
import "../../../components/ha-expansion-panel";
|
||||||
import "../../../components/ha-radio";
|
import "../../../components/ha-radio";
|
||||||
@ -21,6 +26,7 @@ import {
|
|||||||
import { showAlertDialog } from "../../../dialogs/generic/show-dialog-box";
|
import { showAlertDialog } from "../../../dialogs/generic/show-dialog-box";
|
||||||
import { SubscribeMixin } from "../../../mixins/subscribe-mixin";
|
import { SubscribeMixin } from "../../../mixins/subscribe-mixin";
|
||||||
import type { HomeAssistant } from "../../../types";
|
import type { HomeAssistant } from "../../../types";
|
||||||
|
import { showEntityAliasesDialog } from "./entity-aliases/show-dialog-entity-aliases";
|
||||||
|
|
||||||
@customElement("ha-registry-basic-editor")
|
@customElement("ha-registry-basic-editor")
|
||||||
export class HaEntityRegistryBasicEditor extends SubscribeMixin(LitElement) {
|
export class HaEntityRegistryBasicEditor extends SubscribeMixin(LitElement) {
|
||||||
@ -44,6 +50,21 @@ export class HaEntityRegistryBasicEditor extends SubscribeMixin(LitElement) {
|
|||||||
|
|
||||||
@state() private _submitting = false;
|
@state() private _submitting = false;
|
||||||
|
|
||||||
|
private _handleAliasesClicked(ev: CustomEvent) {
|
||||||
|
if (ev.detail.index !== 0) return;
|
||||||
|
showEntityAliasesDialog(this, {
|
||||||
|
entity: this.entry!,
|
||||||
|
updateEntry: async (updates) => {
|
||||||
|
const result = await updateEntityRegistryEntry(
|
||||||
|
this.hass,
|
||||||
|
this.entry.entity_id,
|
||||||
|
updates
|
||||||
|
);
|
||||||
|
fireEvent(this, "entity-entry-updated", result.entity_entry);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
public async updateEntry(): Promise<void> {
|
public async updateEntry(): Promise<void> {
|
||||||
this._submitting = true;
|
this._submitting = true;
|
||||||
const params: Partial<EntityRegistryEntryUpdateParams> = {
|
const params: Partial<EntityRegistryEntryUpdateParams> = {
|
||||||
@ -247,6 +268,37 @@ export class HaEntityRegistryBasicEditor extends SubscribeMixin(LitElement) {
|
|||||||
</div>
|
</div>
|
||||||
`
|
`
|
||||||
: ""}
|
: ""}
|
||||||
|
|
||||||
|
<div class="label">
|
||||||
|
${this.hass.localize(
|
||||||
|
"ui.dialogs.entity_registry.editor.aliases_section"
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<mwc-list class="aliases" @action=${this._handleAliasesClicked}>
|
||||||
|
<mwc-list-item .twoline=${this.entry.aliases.length > 0} hasMeta>
|
||||||
|
<span>
|
||||||
|
${this.entry.aliases.length > 0
|
||||||
|
? this.hass.localize(
|
||||||
|
"ui.dialogs.entity_registry.editor.configured_aliases",
|
||||||
|
{ count: this.entry.aliases.length }
|
||||||
|
)
|
||||||
|
: this.hass.localize(
|
||||||
|
"ui.dialogs.entity_registry.editor.no_aliases"
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
<span slot="secondary">
|
||||||
|
${[...this.entry.aliases]
|
||||||
|
.sort((a, b) => stringCompare(a, b, this.hass.locale.language))
|
||||||
|
.join(", ")}
|
||||||
|
</span>
|
||||||
|
<ha-svg-icon slot="meta" .path=${mdiPencil}></ha-svg-icon>
|
||||||
|
</mwc-list-item>
|
||||||
|
</mwc-list>
|
||||||
|
<div class="secondary">
|
||||||
|
${this.hass.localize(
|
||||||
|
"ui.dialogs.entity_registry.editor.aliases.description"
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</ha-expansion-panel>
|
</ha-expansion-panel>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
@ -300,6 +352,13 @@ export class HaEntityRegistryBasicEditor extends SubscribeMixin(LitElement) {
|
|||||||
.label {
|
.label {
|
||||||
margin-top: 16px;
|
margin-top: 16px;
|
||||||
}
|
}
|
||||||
|
.aliases {
|
||||||
|
border-radius: 4px;
|
||||||
|
margin-top: 4px;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
--mdc-icon-button-size: 24px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import "@material/mwc-button/mwc-button";
|
import "@material/mwc-button/mwc-button";
|
||||||
import "@material/mwc-formfield/mwc-formfield";
|
import "@material/mwc-formfield/mwc-formfield";
|
||||||
import "@material/mwc-list/mwc-list-item";
|
import "@material/mwc-list/mwc-list-item";
|
||||||
|
import { mdiPencil } from "@mdi/js";
|
||||||
import { HassEntity, UnsubscribeFunc } from "home-assistant-js-websocket";
|
import { HassEntity, UnsubscribeFunc } from "home-assistant-js-websocket";
|
||||||
import {
|
import {
|
||||||
css,
|
css,
|
||||||
@ -26,6 +27,7 @@ import {
|
|||||||
import "../../../components/ha-alert";
|
import "../../../components/ha-alert";
|
||||||
import "../../../components/ha-area-picker";
|
import "../../../components/ha-area-picker";
|
||||||
import "../../../components/ha-expansion-panel";
|
import "../../../components/ha-expansion-panel";
|
||||||
|
import "../../../components/ha-icon";
|
||||||
import "../../../components/ha-icon-picker";
|
import "../../../components/ha-icon-picker";
|
||||||
import "../../../components/ha-radio";
|
import "../../../components/ha-radio";
|
||||||
import "../../../components/ha-select";
|
import "../../../components/ha-select";
|
||||||
@ -75,6 +77,7 @@ import { SubscribeMixin } from "../../../mixins/subscribe-mixin";
|
|||||||
import { haStyle } from "../../../resources/styles";
|
import { haStyle } from "../../../resources/styles";
|
||||||
import type { HomeAssistant } from "../../../types";
|
import type { HomeAssistant } from "../../../types";
|
||||||
import { showDeviceRegistryDetailDialog } from "../devices/device-registry-detail/show-dialog-device-registry-detail";
|
import { showDeviceRegistryDetailDialog } from "../devices/device-registry-detail/show-dialog-device-registry-detail";
|
||||||
|
import { showEntityAliasesDialog } from "./entity-aliases/show-dialog-entity-aliases";
|
||||||
|
|
||||||
const OVERRIDE_DEVICE_CLASSES = {
|
const OVERRIDE_DEVICE_CLASSES = {
|
||||||
cover: [
|
cover: [
|
||||||
@ -115,6 +118,43 @@ const OVERRIDE_NUMBER_UNITS = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const OVERRIDE_SENSOR_UNITS = {
|
const OVERRIDE_SENSOR_UNITS = {
|
||||||
|
current: ["A", "mA"],
|
||||||
|
data_rate: [
|
||||||
|
"bit/s",
|
||||||
|
"kbit/s",
|
||||||
|
"Mbit/s",
|
||||||
|
"Gbit/s",
|
||||||
|
"B/s",
|
||||||
|
"kB/s",
|
||||||
|
"MB/s",
|
||||||
|
"GB/s",
|
||||||
|
"KiB/s",
|
||||||
|
"MiB/s",
|
||||||
|
"GiB/s",
|
||||||
|
],
|
||||||
|
data_size: [
|
||||||
|
"bit",
|
||||||
|
"kbit",
|
||||||
|
"Mbit",
|
||||||
|
"Gbit",
|
||||||
|
"B",
|
||||||
|
"kB",
|
||||||
|
"MB",
|
||||||
|
"GB",
|
||||||
|
"TB",
|
||||||
|
"PB",
|
||||||
|
"EB",
|
||||||
|
"ZB",
|
||||||
|
"YB",
|
||||||
|
"KiB",
|
||||||
|
"MiB",
|
||||||
|
"GiB",
|
||||||
|
"TiB",
|
||||||
|
"PiB",
|
||||||
|
"EiB",
|
||||||
|
"ZiB",
|
||||||
|
"YiB",
|
||||||
|
],
|
||||||
distance: ["cm", "ft", "in", "km", "m", "mi", "mm", "yd"],
|
distance: ["cm", "ft", "in", "km", "m", "mi", "mm", "yd"],
|
||||||
gas: ["CCF", "ft³", "m³"],
|
gas: ["CCF", "ft³", "m³"],
|
||||||
precipitation: ["cm", "in", "mm"],
|
precipitation: ["cm", "in", "mm"],
|
||||||
@ -122,6 +162,7 @@ const OVERRIDE_SENSOR_UNITS = {
|
|||||||
pressure: ["hPa", "Pa", "kPa", "bar", "cbar", "mbar", "mmHg", "inHg", "psi"],
|
pressure: ["hPa", "Pa", "kPa", "bar", "cbar", "mbar", "mmHg", "inHg", "psi"],
|
||||||
speed: ["ft/s", "in/d", "in/h", "km/h", "kn", "m/s", "mm/d", "mm/h", "mph"],
|
speed: ["ft/s", "in/d", "in/h", "km/h", "kn", "m/s", "mm/d", "mm/h", "mph"],
|
||||||
temperature: ["°C", "°F", "K"],
|
temperature: ["°C", "°F", "K"],
|
||||||
|
voltage: ["V", "mV"],
|
||||||
volume: ["CCF", "fl. oz.", "ft³", "gal", "L", "mL", "m³"],
|
volume: ["CCF", "fl. oz.", "ft³", "gal", "L", "mL", "m³"],
|
||||||
water: ["CCF", "ft³", "gal", "L", "m³"],
|
water: ["CCF", "ft³", "gal", "L", "m³"],
|
||||||
weight: ["g", "kg", "lb", "mg", "oz", "st", "µg"],
|
weight: ["g", "kg", "lb", "mg", "oz", "st", "µg"],
|
||||||
@ -673,7 +714,7 @@ export class EntityRegistrySettings extends SubscribeMixin(LitElement) {
|
|||||||
<div class="label">
|
<div class="label">
|
||||||
${this.hass.localize(
|
${this.hass.localize(
|
||||||
"ui.dialogs.entity_registry.editor.entity_status"
|
"ui.dialogs.entity_registry.editor.entity_status"
|
||||||
)}:
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div class="secondary">
|
<div class="secondary">
|
||||||
${this._disabledBy &&
|
${this._disabledBy &&
|
||||||
@ -760,12 +801,45 @@ export class EntityRegistrySettings extends SubscribeMixin(LitElement) {
|
|||||||
</div>
|
</div>
|
||||||
`
|
`
|
||||||
: ""}
|
: ""}
|
||||||
|
|
||||||
|
<div class="label">
|
||||||
|
${this.hass.localize(
|
||||||
|
"ui.dialogs.entity_registry.editor.aliases_section"
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<mwc-list class="aliases" @action=${this._handleAliasesClicked}>
|
||||||
|
<mwc-list-item .twoline=${this.entry.aliases.length > 0} hasMeta>
|
||||||
|
<span>
|
||||||
|
${this.entry.aliases.length > 0
|
||||||
|
? this.hass.localize(
|
||||||
|
"ui.dialogs.entity_registry.editor.configured_aliases",
|
||||||
|
{ count: this.entry.aliases.length }
|
||||||
|
)
|
||||||
|
: this.hass.localize(
|
||||||
|
"ui.dialogs.entity_registry.editor.no_aliases"
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
<span slot="secondary">
|
||||||
|
${[...this.entry.aliases]
|
||||||
|
.sort((a, b) =>
|
||||||
|
stringCompare(a, b, this.hass.locale.language)
|
||||||
|
)
|
||||||
|
.join(", ")}
|
||||||
|
</span>
|
||||||
|
<ha-svg-icon slot="meta" .path=${mdiPencil}></ha-svg-icon>
|
||||||
|
</mwc-list-item>
|
||||||
|
</mwc-list>
|
||||||
|
<div class="secondary">
|
||||||
|
${this.hass.localize(
|
||||||
|
"ui.dialogs.entity_registry.editor.aliases.description"
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
${this.entry.device_id
|
${this.entry.device_id
|
||||||
? html`
|
? html`
|
||||||
<div class="label">
|
<div class="label">
|
||||||
${this.hass.localize(
|
${this.hass.localize(
|
||||||
"ui.dialogs.entity_registry.editor.change_area"
|
"ui.dialogs.entity_registry.editor.change_area"
|
||||||
)}:
|
)}
|
||||||
</div>
|
</div>
|
||||||
<ha-area-picker
|
<ha-area-picker
|
||||||
.hass=${this.hass}
|
.hass=${this.hass}
|
||||||
@ -943,6 +1017,21 @@ export class EntityRegistrySettings extends SubscribeMixin(LitElement) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private _handleAliasesClicked(ev: CustomEvent) {
|
||||||
|
if (ev.detail.index !== 0) return;
|
||||||
|
showEntityAliasesDialog(this, {
|
||||||
|
entity: this.entry!,
|
||||||
|
updateEntry: async (updates) => {
|
||||||
|
const result = await updateEntityRegistryEntry(
|
||||||
|
this.hass,
|
||||||
|
this.entry.entity_id,
|
||||||
|
updates
|
||||||
|
);
|
||||||
|
fireEvent(this, "entity-entry-updated", result.entity_entry);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
private async _enableEntry() {
|
private async _enableEntry() {
|
||||||
this._error = undefined;
|
this._error = undefined;
|
||||||
this._submitting = true;
|
this._submitting = true;
|
||||||
@ -1143,7 +1232,9 @@ export class EntityRegistrySettings extends SubscribeMixin(LitElement) {
|
|||||||
domain: entry,
|
domain: entry,
|
||||||
label: domainToName(localize, entry),
|
label: domainToName(localize, entry),
|
||||||
}))
|
}))
|
||||||
.sort((a, b) => stringCompare(a.label, b.label))
|
.sort((a, b) =>
|
||||||
|
stringCompare(a.label, b.label, this.hass.locale.language)
|
||||||
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
private _deviceClassesSorted = memoizeOne(
|
private _deviceClassesSorted = memoizeOne(
|
||||||
@ -1155,7 +1246,9 @@ export class EntityRegistrySettings extends SubscribeMixin(LitElement) {
|
|||||||
`ui.dialogs.entity_registry.editor.device_classes.${domain}.${entry}`
|
`ui.dialogs.entity_registry.editor.device_classes.${domain}.${entry}`
|
||||||
),
|
),
|
||||||
}))
|
}))
|
||||||
.sort((a, b) => stringCompare(a.label, b.label))
|
.sort((a, b) =>
|
||||||
|
stringCompare(a.label, b.label, this.hass.locale.language)
|
||||||
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
static get styles(): CSSResultGroup {
|
static get styles(): CSSResultGroup {
|
||||||
@ -1212,7 +1305,6 @@ export class EntityRegistrySettings extends SubscribeMixin(LitElement) {
|
|||||||
}
|
}
|
||||||
.secondary {
|
.secondary {
|
||||||
margin: 8px 0;
|
margin: 8px 0;
|
||||||
width: 340px;
|
|
||||||
}
|
}
|
||||||
li[divider] {
|
li[divider] {
|
||||||
border-bottom-color: var(--divider-color);
|
border-bottom-color: var(--divider-color);
|
||||||
@ -1220,6 +1312,13 @@ export class EntityRegistrySettings extends SubscribeMixin(LitElement) {
|
|||||||
ha-alert mwc-button {
|
ha-alert mwc-button {
|
||||||
width: max-content;
|
width: max-content;
|
||||||
}
|
}
|
||||||
|
.aliases {
|
||||||
|
border-radius: 4px;
|
||||||
|
margin-top: 4px;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
--mdc-icon-button-size: 24px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
`,
|
`,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
@ -20,7 +20,12 @@ import { haStyle, haStyleDialog } from "../../../resources/styles";
|
|||||||
import type { HomeAssistant } from "../../../types";
|
import type { HomeAssistant } from "../../../types";
|
||||||
|
|
||||||
const _filterDevices = memoizeOne(
|
const _filterDevices = memoizeOne(
|
||||||
(showAdvanced: boolean, hardware: HassioHardwareInfo, filter: string) =>
|
(
|
||||||
|
showAdvanced: boolean,
|
||||||
|
hardware: HassioHardwareInfo,
|
||||||
|
filter: string,
|
||||||
|
language: string
|
||||||
|
) =>
|
||||||
hardware.devices
|
hardware.devices
|
||||||
.filter(
|
.filter(
|
||||||
(device) =>
|
(device) =>
|
||||||
@ -33,7 +38,7 @@ const _filterDevices = memoizeOne(
|
|||||||
.toLocaleLowerCase()
|
.toLocaleLowerCase()
|
||||||
.includes(filter))
|
.includes(filter))
|
||||||
)
|
)
|
||||||
.sort((a, b) => stringCompare(a.name, b.name))
|
.sort((a, b) => stringCompare(a.name, b.name, language))
|
||||||
);
|
);
|
||||||
|
|
||||||
@customElement("ha-dialog-hardware-available")
|
@customElement("ha-dialog-hardware-available")
|
||||||
@ -70,7 +75,8 @@ class DialogHardwareAvailable extends LitElement implements HassDialog {
|
|||||||
const devices = _filterDevices(
|
const devices = _filterDevices(
|
||||||
this.hass.userData?.showAdvanced || false,
|
this.hass.userData?.showAdvanced || false,
|
||||||
this._hardware,
|
this._hardware,
|
||||||
(this._filter || "").toLowerCase()
|
(this._filter || "").toLowerCase(),
|
||||||
|
this.hass.locale.language
|
||||||
);
|
);
|
||||||
|
|
||||||
return html`
|
return html`
|
||||||
|
@ -305,7 +305,7 @@ class HaConfigHardware extends SubscribeMixin(LitElement) {
|
|||||||
.twoline=${Boolean(boardId)}
|
.twoline=${Boolean(boardId)}
|
||||||
>
|
>
|
||||||
${imageURL
|
${imageURL
|
||||||
? html`<img slot="graphic" src=${imageURL} />`
|
? html`<img alt="" slot="graphic" src=${imageURL} />`
|
||||||
: ""}
|
: ""}
|
||||||
<span class="primary-text">
|
<span class="primary-text">
|
||||||
${boardName ||
|
${boardName ||
|
||||||
|
@ -147,7 +147,13 @@ class AddIntegrationDialog extends LitElement {
|
|||||||
is_built_in: true,
|
is_built_in: true,
|
||||||
is_add: true,
|
is_add: true,
|
||||||
}))
|
}))
|
||||||
.sort((a, b) => caseInsensitiveStringCompare(a.name, b.name));
|
.sort((a, b) =>
|
||||||
|
caseInsensitiveStringCompare(
|
||||||
|
a.name,
|
||||||
|
b.name,
|
||||||
|
this.hass.locale.language
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
const integrations: IntegrationListItem[] = [];
|
const integrations: IntegrationListItem[] = [];
|
||||||
const yamlIntegrations: IntegrationListItem[] = [];
|
const yamlIntegrations: IntegrationListItem[] = [];
|
||||||
@ -242,7 +248,11 @@ class AddIntegrationDialog extends LitElement {
|
|||||||
return [
|
return [
|
||||||
...addDeviceRows,
|
...addDeviceRows,
|
||||||
...integrations.sort((a, b) =>
|
...integrations.sort((a, b) =>
|
||||||
caseInsensitiveStringCompare(a.name || "", b.name || "")
|
caseInsensitiveStringCompare(
|
||||||
|
a.name || "",
|
||||||
|
b.name || "",
|
||||||
|
this.hass.locale.language
|
||||||
|
)
|
||||||
),
|
),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
@ -230,7 +230,8 @@ class HaConfigIntegrations extends SubscribeMixin(LitElement) {
|
|||||||
(conf1, conf2) =>
|
(conf1, conf2) =>
|
||||||
caseInsensitiveStringCompare(
|
caseInsensitiveStringCompare(
|
||||||
conf1.localized_domain_name + conf1.title,
|
conf1.localized_domain_name + conf1.title,
|
||||||
conf2.localized_domain_name + conf2.title
|
conf2.localized_domain_name + conf2.title,
|
||||||
|
this.hass.locale.language
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
@ -48,6 +48,7 @@ class HaDomainIntegrations extends LitElement {
|
|||||||
hasMeta
|
hasMeta
|
||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
|
alt=""
|
||||||
slot="graphic"
|
slot="graphic"
|
||||||
loading="lazy"
|
loading="lazy"
|
||||||
src=${brandsUrl({
|
src=${brandsUrl({
|
||||||
@ -121,7 +122,8 @@ class HaDomainIntegrations extends LitElement {
|
|||||||
}
|
}
|
||||||
return caseInsensitiveStringCompare(
|
return caseInsensitiveStringCompare(
|
||||||
a[1].name || domainToName(this.hass.localize, a[0]),
|
a[1].name || domainToName(this.hass.localize, a[0]),
|
||||||
b[1].name || domainToName(this.hass.localize, b[0])
|
b[1].name || domainToName(this.hass.localize, b[0]),
|
||||||
|
this.hass.locale.language
|
||||||
);
|
);
|
||||||
})
|
})
|
||||||
.map(
|
.map(
|
||||||
|
@ -92,6 +92,7 @@ export class HaIntegrationHeader extends LitElement {
|
|||||||
<slot name="above-header"></slot>
|
<slot name="above-header"></slot>
|
||||||
<div class="header">
|
<div class="header">
|
||||||
<img
|
<img
|
||||||
|
alt=""
|
||||||
src=${brandsUrl({
|
src=${brandsUrl({
|
||||||
domain: this.domain,
|
domain: this.domain,
|
||||||
type: "icon",
|
type: "icon",
|
||||||
|
@ -47,6 +47,7 @@ export class HaIntegrationListItem extends ListItemBase {
|
|||||||
)}"
|
)}"
|
||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
|
alt=""
|
||||||
loading="lazy"
|
loading="lazy"
|
||||||
src=${brandsUrl({
|
src=${brandsUrl({
|
||||||
domain: this.integration.domain,
|
domain: this.integration.domain,
|
||||||
|
@ -22,25 +22,32 @@ class HaPanelDevMqtt extends LitElement {
|
|||||||
@property({ type: Boolean }) public narrow!: boolean;
|
@property({ type: Boolean }) public narrow!: boolean;
|
||||||
|
|
||||||
@LocalStorage("panel-dev-mqtt-topic-ls", true, false)
|
@LocalStorage("panel-dev-mqtt-topic-ls", true, false)
|
||||||
private topic = "";
|
private _topic = "";
|
||||||
|
|
||||||
@LocalStorage("panel-dev-mqtt-payload-ls", true, false)
|
@LocalStorage("panel-dev-mqtt-payload-ls", true, false)
|
||||||
private payload = "";
|
private _payload = "";
|
||||||
|
|
||||||
@LocalStorage("panel-dev-mqtt-qos-ls", true, false)
|
@LocalStorage("panel-dev-mqtt-qos-ls", true, false)
|
||||||
private qos = "0";
|
private _qos = "0";
|
||||||
|
|
||||||
@LocalStorage("panel-dev-mqtt-retain-ls", true, false)
|
@LocalStorage("panel-dev-mqtt-retain-ls", true, false)
|
||||||
private retain = false;
|
private _retain = false;
|
||||||
|
|
||||||
|
@LocalStorage("panel-dev-mqtt-allow-template-ls", true, false)
|
||||||
|
private _allowTemplate = false;
|
||||||
|
|
||||||
protected render(): TemplateResult {
|
protected render(): TemplateResult {
|
||||||
return html`
|
return html`
|
||||||
<hass-subpage .narrow=${this.narrow} .hass=${this.hass}>
|
<hass-subpage .narrow=${this.narrow} .hass=${this.hass}>
|
||||||
<div class="content">
|
<div class="content">
|
||||||
<ha-card header="MQTT settings">
|
<ha-card
|
||||||
|
.header=${this.hass.localize("ui.panel.config.mqtt.settings_title")}
|
||||||
|
>
|
||||||
<div class="card-actions">
|
<div class="card-actions">
|
||||||
<mwc-button @click=${this._openOptionFlow}
|
<mwc-button @click=${this._openOptionFlow}
|
||||||
>Re-configure MQTT</mwc-button
|
>${this.hass.localize(
|
||||||
|
"ui.panel.config.mqtt.reconfigure"
|
||||||
|
)}</mwc-button
|
||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
</ha-card>
|
</ha-card>
|
||||||
@ -53,12 +60,12 @@ class HaPanelDevMqtt extends LitElement {
|
|||||||
<div class="panel-dev-mqtt-fields">
|
<div class="panel-dev-mqtt-fields">
|
||||||
<ha-textfield
|
<ha-textfield
|
||||||
.label=${this.hass.localize("ui.panel.config.mqtt.topic")}
|
.label=${this.hass.localize("ui.panel.config.mqtt.topic")}
|
||||||
.value=${this.topic}
|
.value=${this._topic}
|
||||||
@change=${this._handleTopic}
|
@change=${this._handleTopic}
|
||||||
></ha-textfield>
|
></ha-textfield>
|
||||||
<ha-select
|
<ha-select
|
||||||
.label=${this.hass.localize("ui.panel.config.mqtt.qos")}
|
.label=${this.hass.localize("ui.panel.config.mqtt.qos")}
|
||||||
.value=${this.qos}
|
.value=${this._qos}
|
||||||
@selected=${this._handleQos}
|
@selected=${this._handleQos}
|
||||||
>${qosLevel.map(
|
>${qosLevel.map(
|
||||||
(qos) =>
|
(qos) =>
|
||||||
@ -70,17 +77,35 @@ class HaPanelDevMqtt extends LitElement {
|
|||||||
>
|
>
|
||||||
<ha-switch
|
<ha-switch
|
||||||
@change=${this._handleRetain}
|
@change=${this._handleRetain}
|
||||||
.checked=${this.retain}
|
.checked=${this._retain}
|
||||||
></ha-switch>
|
></ha-switch>
|
||||||
</ha-formfield>
|
</ha-formfield>
|
||||||
</div>
|
</div>
|
||||||
<p>${this.hass.localize("ui.panel.config.mqtt.payload")}</p>
|
<p>
|
||||||
|
<ha-formfield
|
||||||
|
.label=${this.hass!.localize(
|
||||||
|
"ui.panel.config.mqtt.allow_template"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<ha-switch
|
||||||
|
@change=${this._handleAllowTemplate}
|
||||||
|
.checked=${this._allowTemplate}
|
||||||
|
></ha-switch>
|
||||||
|
</ha-formfield>
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
${this._allowTemplate
|
||||||
|
? this.hass.localize("ui.panel.config.mqtt.payload")
|
||||||
|
: this.hass.localize(
|
||||||
|
"ui.panel.config.mqtt.payload_no_template"
|
||||||
|
)}
|
||||||
|
</p>
|
||||||
<ha-code-editor
|
<ha-code-editor
|
||||||
mode="jinja2"
|
mode="jinja2"
|
||||||
autocomplete-entities
|
autocomplete-entities
|
||||||
autocomplete-icons
|
autocomplete-icons
|
||||||
.hass=${this.hass}
|
.hass=${this.hass}
|
||||||
.value=${this.payload}
|
.value=${this._payload}
|
||||||
@value-changed=${this._handlePayload}
|
@value-changed=${this._handlePayload}
|
||||||
dir="ltr"
|
dir="ltr"
|
||||||
></ha-code-editor>
|
></ha-code-editor>
|
||||||
@ -101,22 +126,26 @@ class HaPanelDevMqtt extends LitElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private _handleTopic(ev: CustomEvent) {
|
private _handleTopic(ev: CustomEvent) {
|
||||||
this.topic = (ev.target! as any).value;
|
this._topic = (ev.target! as any).value;
|
||||||
}
|
}
|
||||||
|
|
||||||
private _handlePayload(ev: CustomEvent) {
|
private _handlePayload(ev: CustomEvent) {
|
||||||
this.payload = ev.detail.value;
|
this._payload = ev.detail.value;
|
||||||
}
|
}
|
||||||
|
|
||||||
private _handleQos(ev: CustomEvent) {
|
private _handleQos(ev: CustomEvent) {
|
||||||
const newValue = (ev.target! as any).value;
|
const newValue = (ev.target! as any).value;
|
||||||
if (newValue >= 0 && newValue !== this.qos) {
|
if (newValue >= 0 && newValue !== this._qos) {
|
||||||
this.qos = newValue;
|
this._qos = newValue;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private _handleRetain(ev: CustomEvent) {
|
private _handleRetain(ev: CustomEvent) {
|
||||||
this.retain = (ev.target! as any).checked;
|
this._retain = (ev.target! as any).checked;
|
||||||
|
}
|
||||||
|
|
||||||
|
private _handleAllowTemplate(ev: CustomEvent) {
|
||||||
|
this._allowTemplate = (ev.target! as any).checked;
|
||||||
}
|
}
|
||||||
|
|
||||||
private _publish(): void {
|
private _publish(): void {
|
||||||
@ -124,10 +153,11 @@ class HaPanelDevMqtt extends LitElement {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.hass.callService("mqtt", "publish", {
|
this.hass.callService("mqtt", "publish", {
|
||||||
topic: this.topic,
|
topic: this._topic,
|
||||||
payload_template: this.payload,
|
payload: !this._allowTemplate ? this._payload : undefined,
|
||||||
qos: parseInt(this.qos),
|
payload_template: this._allowTemplate ? this._payload : undefined,
|
||||||
retain: this.retain,
|
qos: parseInt(this._qos),
|
||||||
|
retain: this._retain,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -9,6 +9,8 @@ import { MQTTMessage, subscribeMQTTTopic } from "../../../../../data/mqtt";
|
|||||||
import { HomeAssistant } from "../../../../../types";
|
import { HomeAssistant } from "../../../../../types";
|
||||||
import "@material/mwc-list/mwc-list-item";
|
import "@material/mwc-list/mwc-list-item";
|
||||||
import { LocalStorage } from "../../../../../common/decorators/local-storage";
|
import { LocalStorage } from "../../../../../common/decorators/local-storage";
|
||||||
|
import "../../../../../components/ha-formfield";
|
||||||
|
import "../../../../../components/ha-switch";
|
||||||
|
|
||||||
const qosLevel = ["0", "1", "2"];
|
const qosLevel = ["0", "1", "2"];
|
||||||
|
|
||||||
@ -22,6 +24,9 @@ class MqttSubscribeCard extends LitElement {
|
|||||||
@LocalStorage("panel-dev-mqtt-qos-subscribe", true, false)
|
@LocalStorage("panel-dev-mqtt-qos-subscribe", true, false)
|
||||||
private _qos = "0";
|
private _qos = "0";
|
||||||
|
|
||||||
|
@LocalStorage("panel-dev-mqtt-json-format", true, false)
|
||||||
|
private _json_format = false;
|
||||||
|
|
||||||
@state() private _subscribed?: () => void;
|
@state() private _subscribed?: () => void;
|
||||||
|
|
||||||
@state() private _messages: Array<{
|
@state() private _messages: Array<{
|
||||||
@ -47,6 +52,18 @@ class MqttSubscribeCard extends LitElement {
|
|||||||
header=${this.hass.localize("ui.panel.config.mqtt.description_listen")}
|
header=${this.hass.localize("ui.panel.config.mqtt.description_listen")}
|
||||||
>
|
>
|
||||||
<form>
|
<form>
|
||||||
|
<p>
|
||||||
|
<ha-formfield
|
||||||
|
label=${this.hass!.localize(
|
||||||
|
"ui.panel.config.mqtt.json_formatting"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<ha-switch
|
||||||
|
@change=${this._handleJSONFormat}
|
||||||
|
.checked=${this._json_format}
|
||||||
|
></ha-switch>
|
||||||
|
</ha-formfield>
|
||||||
|
</p>
|
||||||
<div class="panel-dev-mqtt-subscribe-fields">
|
<div class="panel-dev-mqtt-subscribe-fields">
|
||||||
<ha-textfield
|
<ha-textfield
|
||||||
.label=${this._subscribed
|
.label=${this._subscribed
|
||||||
@ -114,6 +131,10 @@ class MqttSubscribeCard extends LitElement {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private _handleJSONFormat(ev: CustomEvent) {
|
||||||
|
this._json_format = (ev.target! as any).checked;
|
||||||
|
}
|
||||||
|
|
||||||
private async _handleSubmit(): Promise<void> {
|
private async _handleSubmit(): Promise<void> {
|
||||||
if (this._subscribed) {
|
if (this._subscribed) {
|
||||||
this._subscribed();
|
this._subscribed();
|
||||||
@ -132,11 +153,15 @@ class MqttSubscribeCard extends LitElement {
|
|||||||
const tail =
|
const tail =
|
||||||
this._messages.length > 30 ? this._messages.slice(0, 29) : this._messages;
|
this._messages.length > 30 ? this._messages.slice(0, 29) : this._messages;
|
||||||
let payload: string;
|
let payload: string;
|
||||||
|
if (this._json_format) {
|
||||||
try {
|
try {
|
||||||
payload = JSON.stringify(JSON.parse(message.payload), null, 4);
|
payload = JSON.stringify(JSON.parse(message.payload), null, 4);
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
payload = message.payload;
|
payload = message.payload;
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
payload = message.payload;
|
||||||
|
}
|
||||||
this._messages = [
|
this._messages = [
|
||||||
{
|
{
|
||||||
payload,
|
payload,
|
||||||
|
@ -50,7 +50,8 @@ class ZHADeviceCard extends SubscribeMixin(LitElement) {
|
|||||||
.sort((ent1, ent2) =>
|
.sort((ent1, ent2) =>
|
||||||
stringCompare(
|
stringCompare(
|
||||||
ent1.stateName || `zzz${ent1.entity_id}`,
|
ent1.stateName || `zzz${ent1.entity_id}`,
|
||||||
ent2.stateName || `zzz${ent2.entity_id}`
|
ent2.stateName || `zzz${ent2.entity_id}`,
|
||||||
|
this.hass.locale.language
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
@ -230,7 +230,9 @@ export class HaConfigLovelaceDashboards extends LitElement {
|
|||||||
|
|
||||||
result.push(
|
result.push(
|
||||||
...dashboards
|
...dashboards
|
||||||
.sort((a, b) => stringCompare(a.title, b.title))
|
.sort((a, b) =>
|
||||||
|
stringCompare(a.title, b.title, this.hass.locale.language)
|
||||||
|
)
|
||||||
.map((dashboard) => ({
|
.map((dashboard) => ({
|
||||||
filename: "",
|
filename: "",
|
||||||
...dashboard,
|
...dashboard,
|
||||||
@ -342,7 +344,12 @@ export class HaConfigLovelaceDashboards extends LitElement {
|
|||||||
createDashboard: async (values: LovelaceDashboardCreateParams) => {
|
createDashboard: async (values: LovelaceDashboardCreateParams) => {
|
||||||
const created = await createDashboard(this.hass!, values);
|
const created = await createDashboard(this.hass!, values);
|
||||||
this._dashboards = this._dashboards!.concat(created).sort(
|
this._dashboards = this._dashboards!.concat(created).sort(
|
||||||
(res1, res2) => stringCompare(res1.url_path, res2.url_path)
|
(res1, res2) =>
|
||||||
|
stringCompare(
|
||||||
|
res1.url_path,
|
||||||
|
res2.url_path,
|
||||||
|
this.hass.locale.language
|
||||||
|
)
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
updateDashboard: async (values) => {
|
updateDashboard: async (values) => {
|
||||||
|
@ -143,7 +143,7 @@ export class HaConfigLovelaceRescources extends LitElement {
|
|||||||
createResource: async (values) => {
|
createResource: async (values) => {
|
||||||
const created = await createResource(this.hass!, values);
|
const created = await createResource(this.hass!, values);
|
||||||
this._resources = this._resources!.concat(created).sort((res1, res2) =>
|
this._resources = this._resources!.concat(created).sort((res1, res2) =>
|
||||||
stringCompare(res1.url, res2.url)
|
stringCompare(res1.url, res2.url, this.hass!.locale.language)
|
||||||
);
|
);
|
||||||
loadLovelaceResources([created], this.hass!.auth.data.hassUrl);
|
loadLovelaceResources([created], this.hass!.auth.data.hassUrl);
|
||||||
},
|
},
|
||||||
|
@ -156,10 +156,10 @@ class HaConfigPerson extends LitElement {
|
|||||||
const personData = await fetchPersons(this.hass!);
|
const personData = await fetchPersons(this.hass!);
|
||||||
|
|
||||||
this._storageItems = personData.storage.sort((ent1, ent2) =>
|
this._storageItems = personData.storage.sort((ent1, ent2) =>
|
||||||
stringCompare(ent1.name, ent2.name)
|
stringCompare(ent1.name, ent2.name, this.hass!.locale.language)
|
||||||
);
|
);
|
||||||
this._configItems = personData.config.sort((ent1, ent2) =>
|
this._configItems = personData.config.sort((ent1, ent2) =>
|
||||||
stringCompare(ent1.name, ent2.name)
|
stringCompare(ent1.name, ent2.name, this.hass!.locale.language)
|
||||||
);
|
);
|
||||||
this._openDialogIfPersonSpecifiedInRoute();
|
this._openDialogIfPersonSpecifiedInRoute();
|
||||||
}
|
}
|
||||||
@ -221,7 +221,8 @@ class HaConfigPerson extends LitElement {
|
|||||||
createEntry: async (values) => {
|
createEntry: async (values) => {
|
||||||
const created = await createPerson(this.hass!, values);
|
const created = await createPerson(this.hass!, values);
|
||||||
this._storageItems = this._storageItems!.concat(created).sort(
|
this._storageItems = this._storageItems!.concat(created).sort(
|
||||||
(ent1, ent2) => stringCompare(ent1.name, ent2.name)
|
(ent1, ent2) =>
|
||||||
|
stringCompare(ent1.name, ent2.name, this.hass!.locale.language)
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
updateEntry: async (values) => {
|
updateEntry: async (values) => {
|
||||||
|
@ -52,6 +52,7 @@ class HaConfigRepairs extends LitElement {
|
|||||||
@click=${this._openShowMoreDialog}
|
@click=${this._openShowMoreDialog}
|
||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
|
alt=${domainToName(this.hass.localize, issue.domain)}
|
||||||
loading="lazy"
|
loading="lazy"
|
||||||
src=${brandsUrl({
|
src=${brandsUrl({
|
||||||
domain: issue.issue_domain || issue.domain,
|
domain: issue.issue_domain || issue.domain,
|
||||||
|
@ -65,6 +65,7 @@ class IntegrationsStartupTime extends LitElement {
|
|||||||
href=${docLink}
|
href=${docLink}
|
||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
|
alt=""
|
||||||
loading="lazy"
|
loading="lazy"
|
||||||
src=${brandsUrl({
|
src=${brandsUrl({
|
||||||
domain: setup.domain,
|
domain: setup.domain,
|
||||||
|
@ -527,15 +527,10 @@ export class HaScriptTrace extends LitElement {
|
|||||||
:host([narrow]) .graph {
|
:host([narrow]) .graph {
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.info {
|
.info {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
background-color: var(--card-background-color);
|
background-color: var(--card-background-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
.linkButton {
|
|
||||||
color: var(--primary-text-color);
|
|
||||||
}
|
|
||||||
.trace-link {
|
.trace-link {
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
}
|
}
|
||||||
|
@ -244,7 +244,14 @@ class DialogTagDetail
|
|||||||
canvas.height / 3
|
canvas.height / 3
|
||||||
);
|
);
|
||||||
|
|
||||||
this._qrCode = html`<img src=${canvas.toDataURL()}></img>`;
|
this._qrCode = html`<img
|
||||||
|
alt=${this.hass.localize(
|
||||||
|
"ui.panel.config.tag.qr_code_image",
|
||||||
|
"name",
|
||||||
|
this._name
|
||||||
|
)}
|
||||||
|
src=${canvas.toDataURL()}
|
||||||
|
></img>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
static get styles(): CSSResultGroup {
|
static get styles(): CSSResultGroup {
|
||||||
|
@ -296,7 +296,7 @@ export class HaConfigZone extends SubscribeMixin(LitElement) {
|
|||||||
|
|
||||||
private async _fetchData() {
|
private async _fetchData() {
|
||||||
this._storageItems = (await fetchZones(this.hass!)).sort((ent1, ent2) =>
|
this._storageItems = (await fetchZones(this.hass!)).sort((ent1, ent2) =>
|
||||||
stringCompare(ent1.name, ent2.name)
|
stringCompare(ent1.name, ent2.name, this.hass!.locale.language)
|
||||||
);
|
);
|
||||||
this._getStates();
|
this._getStates();
|
||||||
}
|
}
|
||||||
@ -411,7 +411,8 @@ export class HaConfigZone extends SubscribeMixin(LitElement) {
|
|||||||
private async _createEntry(values: ZoneMutableParams) {
|
private async _createEntry(values: ZoneMutableParams) {
|
||||||
const created = await createZone(this.hass!, values);
|
const created = await createZone(this.hass!, values);
|
||||||
this._storageItems = this._storageItems!.concat(created).sort(
|
this._storageItems = this._storageItems!.concat(created).sort(
|
||||||
(ent1, ent2) => stringCompare(ent1.name, ent2.name)
|
(ent1, ent2) =>
|
||||||
|
stringCompare(ent1.name, ent2.name, this.hass!.locale.language)
|
||||||
);
|
);
|
||||||
if (this.narrow) {
|
if (this.narrow) {
|
||||||
return;
|
return;
|
||||||
|
@ -58,7 +58,9 @@ class EventsList extends EventsMixin(LocalizeMixin(PolymerElement)) {
|
|||||||
connectedCallback() {
|
connectedCallback() {
|
||||||
super.connectedCallback();
|
super.connectedCallback();
|
||||||
this.hass.callApi("GET", "events").then((events) => {
|
this.hass.callApi("GET", "events").then((events) => {
|
||||||
this.events = events.sort((e1, e2) => stringCompare(e1.event, e2.event));
|
this.events = events.sort((e1, e2) =>
|
||||||
|
stringCompare(e1.event, e2.event, this.hass.locale.language)
|
||||||
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -79,7 +79,7 @@ class HuiMapCard extends LitElement implements LovelaceCard {
|
|||||||
config.geo_location_sources &&
|
config.geo_location_sources &&
|
||||||
!Array.isArray(config.geo_location_sources)
|
!Array.isArray(config.geo_location_sources)
|
||||||
) {
|
) {
|
||||||
throw new Error("Geo_location_sources needs to be an array");
|
throw new Error("Parameter geo_location_sources needs to be an array");
|
||||||
}
|
}
|
||||||
|
|
||||||
this._config = config;
|
this._config = config;
|
||||||
@ -102,6 +102,7 @@ class HuiMapCard extends LitElement implements LovelaceCard {
|
|||||||
ratio && ratio.w > 0 && ratio.h > 0
|
ratio && ratio.w > 0 && ratio.h > 0
|
||||||
? `${((100 * ratio.h) / ratio.w).toFixed(2)}`
|
? `${((100 * ratio.h) / ratio.w).toFixed(2)}`
|
||||||
: "100";
|
: "100";
|
||||||
|
|
||||||
return 1 + Math.floor(Number(ar) / 25) || 3;
|
return 1 + Math.floor(Number(ar) / 25) || 3;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -185,10 +186,21 @@ class HuiMapCard extends LitElement implements LovelaceCard {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected firstUpdated(changedProps: PropertyValues): void {
|
protected updated(changedProps: PropertyValues): void {
|
||||||
super.firstUpdated(changedProps);
|
if (this._config?.hours_to_show && this._configEntities?.length) {
|
||||||
const root = this.shadowRoot!.getElementById("root");
|
if (changedProps.has("_config")) {
|
||||||
|
this._getHistory();
|
||||||
|
} else if (Date.now() - this._date!.getTime() >= MINUTE) {
|
||||||
|
this._getHistory();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (changedProps.has("_config")) {
|
||||||
|
this._computePadding();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private _computePadding(): void {
|
||||||
|
const root = this.shadowRoot!.getElementById("root");
|
||||||
if (!this._config || this.isPanel || !root) {
|
if (!this._config || this.isPanel || !root) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -206,16 +218,6 @@ class HuiMapCard extends LitElement implements LovelaceCard {
|
|||||||
: (root.style.paddingBottom = "100%");
|
: (root.style.paddingBottom = "100%");
|
||||||
}
|
}
|
||||||
|
|
||||||
protected updated(changedProps: PropertyValues): void {
|
|
||||||
if (this._config?.hours_to_show && this._configEntities?.length) {
|
|
||||||
if (changedProps.has("_config")) {
|
|
||||||
this._getHistory();
|
|
||||||
} else if (Date.now() - this._date!.getTime() >= MINUTE) {
|
|
||||||
this._getHistory();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private _fitMap() {
|
private _fitMap() {
|
||||||
this._map?.fitMap();
|
this._map?.fitMap();
|
||||||
}
|
}
|
||||||
|
@ -101,7 +101,10 @@ export class HuiPictureCard extends LitElement implements LovelaceCard {
|
|||||||
),
|
),
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
<img src=${this.hass.hassUrl(this._config.image)} />
|
<img
|
||||||
|
alt=${this._config.alt_text}
|
||||||
|
src=${this.hass.hassUrl(this._config.image)}
|
||||||
|
/>
|
||||||
</ha-card>
|
</ha-card>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
@ -332,6 +332,7 @@ export interface PictureCardConfig extends LovelaceCardConfig {
|
|||||||
hold_action?: ActionConfig;
|
hold_action?: ActionConfig;
|
||||||
double_tap_action?: ActionConfig;
|
double_tap_action?: ActionConfig;
|
||||||
theme?: string;
|
theme?: string;
|
||||||
|
alt_text?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface PictureElementsCardConfig extends LovelaceCardConfig {
|
export interface PictureElementsCardConfig extends LovelaceCardConfig {
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user