mirror of
https://github.com/home-assistant/frontend.git
synced 2025-07-25 18:26:35 +00:00
20221228.0 (#14901)
This commit is contained in:
commit
2b8f7c46ff
6
.github/dependabot.yml
vendored
6
.github/dependabot.yml
vendored
@ -6,3 +6,9 @@ updates:
|
|||||||
interval: weekly
|
interval: weekly
|
||||||
time: "06:00"
|
time: "06:00"
|
||||||
open-pull-requests-limit: 10
|
open-pull-requests-limit: 10
|
||||||
|
- package-ecosystem: "npm"
|
||||||
|
directory: "/"
|
||||||
|
schedule:
|
||||||
|
interval: "daily"
|
||||||
|
time: "06:00"
|
||||||
|
open-pull-requests-limit: 5
|
||||||
|
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
|
uses: actions/checkout@v3.2.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
|
uses: actions/setup-node@v3.5.1
|
||||||
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
|
uses: actions/checkout@v3.2.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
|
uses: actions/setup-node@v3.5.1
|
||||||
with:
|
with:
|
||||||
node-version: ${{ env.NODE_VERSION }}
|
node-version: ${{ env.NODE_VERSION }}
|
||||||
cache: yarn
|
cache: yarn
|
||||||
|
16
.github/workflows/ci.yaml
vendored
16
.github/workflows/ci.yaml
vendored
@ -20,9 +20,9 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Check out files from GitHub
|
- name: Check out files from GitHub
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v3.2.0
|
||||||
- name: Set up Node ${{ env.NODE_VERSION }}
|
- name: Set up Node ${{ env.NODE_VERSION }}
|
||||||
uses: actions/setup-node@v3
|
uses: actions/setup-node@v3.5.1
|
||||||
with:
|
with:
|
||||||
node-version: ${{ env.NODE_VERSION }}
|
node-version: ${{ env.NODE_VERSION }}
|
||||||
cache: yarn
|
cache: yarn
|
||||||
@ -44,9 +44,9 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Check out files from GitHub
|
- name: Check out files from GitHub
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v3.2.0
|
||||||
- name: Set up Node ${{ env.NODE_VERSION }}
|
- name: Set up Node ${{ env.NODE_VERSION }}
|
||||||
uses: actions/setup-node@v3
|
uses: actions/setup-node@v3.5.1
|
||||||
with:
|
with:
|
||||||
node-version: ${{ env.NODE_VERSION }}
|
node-version: ${{ env.NODE_VERSION }}
|
||||||
cache: yarn
|
cache: yarn
|
||||||
@ -63,9 +63,9 @@ jobs:
|
|||||||
needs: [lint, test]
|
needs: [lint, test]
|
||||||
steps:
|
steps:
|
||||||
- name: Check out files from GitHub
|
- name: Check out files from GitHub
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v3.2.0
|
||||||
- name: Set up Node ${{ env.NODE_VERSION }}
|
- name: Set up Node ${{ env.NODE_VERSION }}
|
||||||
uses: actions/setup-node@v3
|
uses: actions/setup-node@v3.5.1
|
||||||
with:
|
with:
|
||||||
node-version: ${{ env.NODE_VERSION }}
|
node-version: ${{ env.NODE_VERSION }}
|
||||||
cache: yarn
|
cache: yarn
|
||||||
@ -82,9 +82,9 @@ jobs:
|
|||||||
needs: [lint, test]
|
needs: [lint, test]
|
||||||
steps:
|
steps:
|
||||||
- name: Check out files from GitHub
|
- name: Check out files from GitHub
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v3.2.0
|
||||||
- name: Set up Node ${{ env.NODE_VERSION }}
|
- name: Set up Node ${{ env.NODE_VERSION }}
|
||||||
uses: actions/setup-node@v3
|
uses: actions/setup-node@v3.5.1
|
||||||
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
|
uses: actions/checkout@v3.2.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.
|
||||||
|
51
.github/workflows/demo_deployment.yaml
vendored
51
.github/workflows/demo_deployment.yaml
vendored
@ -7,23 +7,28 @@ on:
|
|||||||
push:
|
push:
|
||||||
branches:
|
branches:
|
||||||
- dev
|
- dev
|
||||||
|
- master
|
||||||
|
|
||||||
env:
|
env:
|
||||||
NODE_VERSION: 16
|
NODE_VERSION: 16
|
||||||
NODE_OPTIONS: --max_old_space_size=6144
|
NODE_OPTIONS: --max_old_space_size=6144
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
deploy:
|
deploy_dev:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
name: Demo Development
|
||||||
|
if: github.event_name != 'push' || github.ref != 'master'
|
||||||
environment:
|
environment:
|
||||||
name: Demo
|
name: Demo Development
|
||||||
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
|
uses: actions/checkout@v3.2.0
|
||||||
|
with:
|
||||||
|
ref: dev
|
||||||
|
|
||||||
- name: Set up Node ${{ env.NODE_VERSION }}
|
- name: Set up Node ${{ env.NODE_VERSION }}
|
||||||
uses: actions/setup-node@v3
|
uses: actions/setup-node@v3.5.1
|
||||||
with:
|
with:
|
||||||
node-version: ${{ env.NODE_VERSION }}
|
node-version: ${{ env.NODE_VERSION }}
|
||||||
cache: yarn
|
cache: yarn
|
||||||
@ -46,3 +51,41 @@ jobs:
|
|||||||
env:
|
env:
|
||||||
NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }}
|
NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }}
|
||||||
NETLIFY_SITE_ID: ${{ secrets.NETLIFY_DEMO_DEV_SITE_ID }}
|
NETLIFY_SITE_ID: ${{ secrets.NETLIFY_DEMO_DEV_SITE_ID }}
|
||||||
|
|
||||||
|
deploy_master:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
name: Demo Production
|
||||||
|
if: github.event_name == 'push' && github.ref == 'master'
|
||||||
|
environment:
|
||||||
|
name: Demo Production
|
||||||
|
url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }}
|
||||||
|
steps:
|
||||||
|
- name: Check out files from GitHub
|
||||||
|
uses: actions/checkout@v3.2.0
|
||||||
|
with:
|
||||||
|
ref: master
|
||||||
|
|
||||||
|
- name: Set up Node ${{ env.NODE_VERSION }}
|
||||||
|
uses: actions/setup-node@v3.5.1
|
||||||
|
with:
|
||||||
|
node-version: ${{ env.NODE_VERSION }}
|
||||||
|
cache: yarn
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: yarn install
|
||||||
|
env:
|
||||||
|
CI: true
|
||||||
|
|
||||||
|
- name: Build Demo
|
||||||
|
run: ./node_modules/.bin/gulp build-demo
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
|
- name: Deploy to Netlify
|
||||||
|
id: deploy
|
||||||
|
uses: netlify/actions/cli@master
|
||||||
|
with:
|
||||||
|
args: deploy --dir=demo/dist --prod
|
||||||
|
env:
|
||||||
|
NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }}
|
||||||
|
NETLIFY_SITE_ID: ${{ secrets.NETLIFY_DEMO_SITE_ID }}
|
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
|
uses: actions/checkout@v3.2.0
|
||||||
|
|
||||||
- name: Set up Node ${{ env.NODE_VERSION }}
|
- name: Set up Node ${{ env.NODE_VERSION }}
|
||||||
uses: actions/setup-node@v3
|
uses: actions/setup-node@v3.5.1
|
||||||
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
|
uses: actions/checkout@v3.2.0
|
||||||
|
|
||||||
- name: Set up Node ${{ env.NODE_VERSION }}
|
- name: Set up Node ${{ env.NODE_VERSION }}
|
||||||
uses: actions/setup-node@v3
|
uses: actions/setup-node@v3.5.1
|
||||||
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
|
uses: actions/checkout@v3.2.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
|
uses: actions/setup-node@v3.5.1
|
||||||
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
|
uses: actions/checkout@v3.2.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
|
uses: actions/setup-node@v3.5.1
|
||||||
with:
|
with:
|
||||||
node-version: ${{ env.NODE_VERSION }}
|
node-version: ${{ env.NODE_VERSION }}
|
||||||
cache: yarn
|
cache: yarn
|
||||||
|
2
.github/workflows/stale.yml
vendored
2
.github/workflows/stale.yml
vendored
@ -10,7 +10,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: 90 days stale policy
|
- name: 90 days stale policy
|
||||||
uses: actions/stale@v6.0.1
|
uses: actions/stale@v7.0.0
|
||||||
with:
|
with:
|
||||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
days-before-stale: 90
|
days-before-stale: 90
|
||||||
|
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
|
uses: actions/checkout@v3.2.0
|
||||||
|
|
||||||
- name: Upload Translations
|
- name: Upload Translations
|
||||||
run: |
|
run: |
|
||||||
|
@ -71,6 +71,7 @@ class HaDemo extends HomeAssistantAppEl {
|
|||||||
entity_category: null,
|
entity_category: null,
|
||||||
has_entity_name: false,
|
has_entity_name: false,
|
||||||
unique_id: "co2_intensity",
|
unique_id: "co2_intensity",
|
||||||
|
aliases: [],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
config_entry_id: "co2signal",
|
config_entry_id: "co2signal",
|
||||||
@ -86,6 +87,7 @@ class HaDemo extends HomeAssistantAppEl {
|
|||||||
entity_category: null,
|
entity_category: null,
|
||||||
has_entity_name: false,
|
has_entity_name: false,
|
||||||
unique_id: "grid_fossil_fuel_percentage",
|
unique_id: "grid_fossil_fuel_percentage",
|
||||||
|
aliases: [],
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
@ -142,7 +142,8 @@ export class DemoHaBarSlider extends LitElement {
|
|||||||
}
|
}
|
||||||
.custom {
|
.custom {
|
||||||
--slider-bar-color: #ffcf4c;
|
--slider-bar-color: #ffcf4c;
|
||||||
--slider-bar-background: #ffcf4c64;
|
--slider-bar-background: #ffcf4c;
|
||||||
|
--slider-bar-background-opacity: 0.2;
|
||||||
--slider-bar-thickness: 100px;
|
--slider-bar-thickness: 100px;
|
||||||
--slider-bar-border-radius: 24px;
|
--slider-bar-border-radius: 24px;
|
||||||
}
|
}
|
||||||
|
@ -115,8 +115,8 @@ export class DemoHaBarSwitch extends LitElement {
|
|||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
.custom {
|
.custom {
|
||||||
--switch-bar-color-on: var(--rgb-green-color);
|
--switch-bar-on-color: rgb(var(--rgb-green-color));
|
||||||
--switch-bar-color-off: var(--rgb-red-color);
|
--switch-bar-off-color: rgb(var(--rgb-red-color));
|
||||||
--switch-bar-thickness: 100px;
|
--switch-bar-thickness: 100px;
|
||||||
--switch-bar-border-radius: 24px;
|
--switch-bar-border-radius: 24px;
|
||||||
--switch-bar-padding: 6px;
|
--switch-bar-padding: 6px;
|
||||||
|
@ -106,6 +106,7 @@ const ENTITIES: HassEntity[] = [
|
|||||||
// Alert
|
// Alert
|
||||||
createEntity("alert.off", "off"),
|
createEntity("alert.off", "off"),
|
||||||
createEntity("alert.on", "on"),
|
createEntity("alert.on", "on"),
|
||||||
|
createEntity("alert.idle", "idle"),
|
||||||
// Automation
|
// Automation
|
||||||
createEntity("automation.off", "off"),
|
createEntity("automation.off", "off"),
|
||||||
createEntity("automation.on", "on"),
|
createEntity("automation.on", "on"),
|
||||||
@ -219,6 +220,11 @@ const ENTITIES: HassEntity[] = [
|
|||||||
// Siren
|
// Siren
|
||||||
createEntity("siren.off", "off"),
|
createEntity("siren.off", "off"),
|
||||||
createEntity("siren.on", "on"),
|
createEntity("siren.on", "on"),
|
||||||
|
// Sun
|
||||||
|
createEntity("sun.below", "below_horizon"),
|
||||||
|
createEntity("sun.above", "above_horizon"),
|
||||||
|
createEntity("sun.unknown", "unknown"),
|
||||||
|
createEntity("sun.unavailable", "unavailable"),
|
||||||
// Switch
|
// Switch
|
||||||
createEntity("switch.off", "off"),
|
createEntity("switch.off", "off"),
|
||||||
createEntity("switch.on", "on"),
|
createEntity("switch.on", "on"),
|
||||||
@ -322,7 +328,7 @@ export class DemoEntityState extends LitElement {
|
|||||||
`,
|
`,
|
||||||
},
|
},
|
||||||
entity_id: {
|
entity_id: {
|
||||||
title: "Entity id",
|
title: "Entity ID",
|
||||||
width: "30%",
|
width: "30%",
|
||||||
filterable: true,
|
filterable: true,
|
||||||
sortable: true,
|
sortable: true,
|
||||||
|
@ -197,6 +197,7 @@ const createEntityRegistryEntries = (
|
|||||||
platform: "updater",
|
platform: "updater",
|
||||||
has_entity_name: false,
|
has_entity_name: false,
|
||||||
unique_id: "updater",
|
unique_id: "updater",
|
||||||
|
aliases: [],
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
@ -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 {
|
||||||
|
@ -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(
|
||||||
|
@ -59,7 +59,11 @@ class HassioIngressView extends LitElement {
|
|||||||
return html` <hass-loading-screen></hass-loading-screen> `;
|
return html` <hass-loading-screen></hass-loading-screen> `;
|
||||||
}
|
}
|
||||||
|
|
||||||
const iframe = html`<iframe src=${this._addon.ingress_url!}></iframe>`;
|
const iframe = html`<iframe
|
||||||
|
.title=${this._addon.name}
|
||||||
|
.src=${this._addon.ingress_url!}
|
||||||
|
>
|
||||||
|
</iframe>`;
|
||||||
|
|
||||||
if (!this.ingressPanel) {
|
if (!this.ingressPanel) {
|
||||||
return html`<hass-subpage
|
return html`<hass-subpage
|
||||||
|
@ -5,4 +5,5 @@ module.exports = {
|
|||||||
'printf "%s\n" "Translation files should not be added or modified here. Instead, make the necessary modifications in src/translations/en.json. Other languages are managed externally. Please see https://developers.home-assistant.io/docs/translations/ for details." ' +
|
'printf "%s\n" "Translation files should not be added or modified here. Instead, make the necessary modifications in src/translations/en.json. Other languages are managed externally. Please see https://developers.home-assistant.io/docs/translations/ for details." ' +
|
||||||
files.join(" ") +
|
files.join(" ") +
|
||||||
" >&2 && exit 1",
|
" >&2 && exit 1",
|
||||||
|
"/yarn.lock": () => "yarn dedupe",
|
||||||
};
|
};
|
||||||
|
@ -106,6 +106,7 @@
|
|||||||
"core-js": "^3.15.2",
|
"core-js": "^3.15.2",
|
||||||
"cropperjs": "^1.5.12",
|
"cropperjs": "^1.5.12",
|
||||||
"date-fns": "^2.23.0",
|
"date-fns": "^2.23.0",
|
||||||
|
"date-fns-tz": "^1.3.7",
|
||||||
"deep-clone-simple": "^1.1.1",
|
"deep-clone-simple": "^1.1.1",
|
||||||
"deep-freeze": "^0.0.1",
|
"deep-freeze": "^0.0.1",
|
||||||
"fuse.js": "^6.0.0",
|
"fuse.js": "^6.0.0",
|
||||||
@ -183,7 +184,7 @@
|
|||||||
"@types/sortablejs": "^1",
|
"@types/sortablejs": "^1",
|
||||||
"@types/tar": "^6",
|
"@types/tar": "^6",
|
||||||
"@types/webspeechapi": "^0.0.29",
|
"@types/webspeechapi": "^0.0.29",
|
||||||
"@typescript-eslint/eslint-plugin": "^5.44.0",
|
"@typescript-eslint/eslint-plugin": "^5.46.1",
|
||||||
"@typescript-eslint/parser": "^5.44.0",
|
"@typescript-eslint/parser": "^5.44.0",
|
||||||
"@web/dev-server": "^0.0.24",
|
"@web/dev-server": "^0.0.24",
|
||||||
"@web/dev-server-rollup": "^0.2.11",
|
"@web/dev-server-rollup": "^0.2.11",
|
||||||
@ -200,7 +201,7 @@
|
|||||||
"eslint-plugin-lit": "^1.6.1",
|
"eslint-plugin-lit": "^1.6.1",
|
||||||
"eslint-plugin-unused-imports": "^1.1.5",
|
"eslint-plugin-unused-imports": "^1.1.5",
|
||||||
"eslint-plugin-wc": "^1.3.2",
|
"eslint-plugin-wc": "^1.3.2",
|
||||||
"fancy-log": "^1.3.3",
|
"fancy-log": "^2.0.0",
|
||||||
"fs-extra": "^7.0.1",
|
"fs-extra": "^7.0.1",
|
||||||
"glob": "^7.2.0",
|
"glob": "^7.2.0",
|
||||||
"gulp": "^4.0.2",
|
"gulp": "^4.0.2",
|
||||||
|
@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|||||||
|
|
||||||
[project]
|
[project]
|
||||||
name = "home-assistant-frontend"
|
name = "home-assistant-frontend"
|
||||||
version = "20221213.1"
|
version = "20221228.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"
|
||||||
|
@ -1,5 +1,3 @@
|
|||||||
import { hex2rgb } from "./convert-color";
|
|
||||||
|
|
||||||
export const THEME_COLORS = new Set([
|
export const THEME_COLORS = new Set([
|
||||||
"primary",
|
"primary",
|
||||||
"accent",
|
"accent",
|
||||||
@ -27,16 +25,9 @@ export const THEME_COLORS = new Set([
|
|||||||
"white",
|
"white",
|
||||||
]);
|
]);
|
||||||
|
|
||||||
export function computeRgbColor(color: string): string {
|
export function computeCssColor(color: string): string {
|
||||||
if (THEME_COLORS.has(color)) {
|
if (THEME_COLORS.has(color)) {
|
||||||
return `var(--rgb-${color}-color)`;
|
return `rgb(var(--rgb-${color}-color))`;
|
||||||
}
|
|
||||||
if (color.startsWith("#")) {
|
|
||||||
try {
|
|
||||||
return hex2rgb(color).join(", ");
|
|
||||||
} catch (err) {
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return color;
|
return color;
|
||||||
}
|
}
|
||||||
|
@ -21,6 +21,8 @@ import {
|
|||||||
mdiCommentAlert,
|
mdiCommentAlert,
|
||||||
mdiCounter,
|
mdiCounter,
|
||||||
mdiCurrentAc,
|
mdiCurrentAc,
|
||||||
|
mdiDatabase,
|
||||||
|
mdiEarHearing,
|
||||||
mdiEye,
|
mdiEye,
|
||||||
mdiFan,
|
mdiFan,
|
||||||
mdiFlash,
|
mdiFlash,
|
||||||
@ -57,6 +59,7 @@ import {
|
|||||||
mdiThermometerLines,
|
mdiThermometerLines,
|
||||||
mdiThermostat,
|
mdiThermostat,
|
||||||
mdiTimerOutline,
|
mdiTimerOutline,
|
||||||
|
mdiTransmissionTower,
|
||||||
mdiVideo,
|
mdiVideo,
|
||||||
mdiWater,
|
mdiWater,
|
||||||
mdiWaterPercent,
|
mdiWaterPercent,
|
||||||
@ -133,6 +136,8 @@ export const FIXED_DEVICE_CLASS_ICONS = {
|
|||||||
carbon_dioxide: mdiMoleculeCo2,
|
carbon_dioxide: mdiMoleculeCo2,
|
||||||
carbon_monoxide: mdiMoleculeCo,
|
carbon_monoxide: mdiMoleculeCo,
|
||||||
current: mdiCurrentAc,
|
current: mdiCurrentAc,
|
||||||
|
data_rate: mdiTransmissionTower,
|
||||||
|
data_size: mdiDatabase,
|
||||||
date: mdiCalendar,
|
date: mdiCalendar,
|
||||||
distance: mdiArrowLeftRight,
|
distance: mdiArrowLeftRight,
|
||||||
duration: mdiProgressClock,
|
duration: mdiProgressClock,
|
||||||
@ -158,6 +163,7 @@ export const FIXED_DEVICE_CLASS_ICONS = {
|
|||||||
pressure: mdiGauge,
|
pressure: mdiGauge,
|
||||||
reactive_power: mdiFlash,
|
reactive_power: mdiFlash,
|
||||||
signal_strength: mdiWifi,
|
signal_strength: mdiWifi,
|
||||||
|
sound_pressure: mdiEarHearing,
|
||||||
speed: mdiSpeedometer,
|
speed: mdiSpeedometer,
|
||||||
sulphur_dioxide: mdiMolecule,
|
sulphur_dioxide: mdiMolecule,
|
||||||
temperature: mdiThermometer,
|
temperature: mdiThermometer,
|
||||||
|
10
src/common/entity/color/alert_color.ts
Normal file
10
src/common/entity/color/alert_color.ts
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
export const alertColor = (state?: string): string | undefined => {
|
||||||
|
switch (state) {
|
||||||
|
case "on":
|
||||||
|
return "alert";
|
||||||
|
case "off":
|
||||||
|
return "alert-off";
|
||||||
|
default:
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
};
|
@ -3,6 +3,7 @@ import { HvacAction } from "../../../data/climate";
|
|||||||
export const CLIMATE_HVAC_ACTION_COLORS: Record<HvacAction, string> = {
|
export const CLIMATE_HVAC_ACTION_COLORS: Record<HvacAction, string> = {
|
||||||
cooling: "var(--rgb-state-climate-cool-color)",
|
cooling: "var(--rgb-state-climate-cool-color)",
|
||||||
drying: "var(--rgb-state-climate-dry-color)",
|
drying: "var(--rgb-state-climate-dry-color)",
|
||||||
|
fan: "var(--rgb-state-climate-fan-only-color)",
|
||||||
heating: "var(--rgb-state-climate-heat-color)",
|
heating: "var(--rgb-state-climate-heat-color)",
|
||||||
idle: "var(--rgb-state-climate-idle-color)",
|
idle: "var(--rgb-state-climate-idle-color)",
|
||||||
off: "var(--rgb-state-climate-off-color)",
|
off: "var(--rgb-state-climate-off-color)",
|
||||||
|
52
src/common/entity/compute_attribute_display.ts
Normal file
52
src/common/entity/compute_attribute_display.ts
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
import { HassEntity } from "home-assistant-js-websocket";
|
||||||
|
import { EntityRegistryEntry } from "../../data/entity_registry";
|
||||||
|
import { HomeAssistant } from "../../types";
|
||||||
|
import { LocalizeFunc } from "../translations/localize";
|
||||||
|
import { computeDomain } from "./compute_domain";
|
||||||
|
|
||||||
|
export const computeAttributeValueDisplay = (
|
||||||
|
localize: LocalizeFunc,
|
||||||
|
stateObj: HassEntity,
|
||||||
|
entities: HomeAssistant["entities"],
|
||||||
|
attribute: string,
|
||||||
|
value?: any
|
||||||
|
): string => {
|
||||||
|
const entityId = stateObj.entity_id;
|
||||||
|
const attributeValue =
|
||||||
|
value !== undefined ? value : stateObj.attributes[attribute];
|
||||||
|
const domain = computeDomain(entityId);
|
||||||
|
const entity = entities[entityId] as EntityRegistryEntry | undefined;
|
||||||
|
const translationKey = entity?.translation_key;
|
||||||
|
|
||||||
|
return (
|
||||||
|
(translationKey &&
|
||||||
|
localize(
|
||||||
|
`component.${entity.platform}.entity.${domain}.${translationKey}.state_attributes.${attribute}.state.${attributeValue}`
|
||||||
|
)) ||
|
||||||
|
localize(
|
||||||
|
`component.${domain}.state_attributes._.${attribute}.state.${attributeValue}`
|
||||||
|
) ||
|
||||||
|
attributeValue
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const computeAttributeNameDisplay = (
|
||||||
|
localize: LocalizeFunc,
|
||||||
|
stateObj: HassEntity,
|
||||||
|
entities: HomeAssistant["entities"],
|
||||||
|
attribute: string
|
||||||
|
): string => {
|
||||||
|
const entityId = stateObj.entity_id;
|
||||||
|
const domain = computeDomain(entityId);
|
||||||
|
const entity = entities[entityId] as EntityRegistryEntry | undefined;
|
||||||
|
const translationKey = entity?.translation_key;
|
||||||
|
|
||||||
|
return (
|
||||||
|
(translationKey &&
|
||||||
|
localize(
|
||||||
|
`component.${entity.platform}.entity.${domain}.${translationKey}.state_attributes.${attribute}.name`
|
||||||
|
)) ||
|
||||||
|
localize(`component.${domain}.state_attributes._.${attribute}.name`) ||
|
||||||
|
attribute
|
||||||
|
);
|
||||||
|
};
|
@ -2,7 +2,7 @@ import { HassEntity } from "home-assistant-js-websocket";
|
|||||||
import { computeStateDomain } from "./compute_state_domain";
|
import { computeStateDomain } from "./compute_state_domain";
|
||||||
import { UNAVAILABLE_STATES } from "../../data/entity";
|
import { UNAVAILABLE_STATES } from "../../data/entity";
|
||||||
|
|
||||||
const FIXED_DOMAIN_STATES = {
|
export const FIXED_DOMAIN_STATES = {
|
||||||
alarm_control_panel: [
|
alarm_control_panel: [
|
||||||
"armed_away",
|
"armed_away",
|
||||||
"armed_custom_bypass",
|
"armed_custom_bypass",
|
||||||
@ -57,7 +57,7 @@ const FIXED_DOMAIN_STATES = {
|
|||||||
"windy-variant",
|
"windy-variant",
|
||||||
"windy",
|
"windy",
|
||||||
],
|
],
|
||||||
};
|
} as const;
|
||||||
|
|
||||||
const FIXED_DOMAIN_ATTRIBUTE_STATES = {
|
const FIXED_DOMAIN_ATTRIBUTE_STATES = {
|
||||||
alarm_control_panel: {
|
alarm_control_panel: {
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { HassEntity } from "home-assistant-js-websocket";
|
import { HassEntity } from "home-assistant-js-websocket";
|
||||||
import { OFF_STATES, UNAVAILABLE } from "../../data/entity";
|
import { isUnavailableState, OFF, UNAVAILABLE } from "../../data/entity";
|
||||||
import { computeDomain } from "./compute_domain";
|
import { computeDomain } from "./compute_domain";
|
||||||
|
|
||||||
export function stateActive(stateObj: HassEntity, state?: string): boolean {
|
export function stateActive(stateObj: HassEntity, state?: string): boolean {
|
||||||
@ -10,7 +10,15 @@ export function stateActive(stateObj: HassEntity, state?: string): boolean {
|
|||||||
return compareState !== UNAVAILABLE;
|
return compareState !== UNAVAILABLE;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (OFF_STATES.includes(compareState)) {
|
if (isUnavailableState(compareState)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The "off" check is relevant for most domains, but there are exceptions
|
||||||
|
// such as "alert" where "off" is still a somewhat active state and
|
||||||
|
// therefore gets a custom color and "idle" is instead the state that
|
||||||
|
// matches what most other domains consider inactive.
|
||||||
|
if (compareState === OFF && domain !== "alert") {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -18,8 +26,11 @@ export function stateActive(stateObj: HassEntity, state?: string): boolean {
|
|||||||
switch (domain) {
|
switch (domain) {
|
||||||
case "alarm_control_panel":
|
case "alarm_control_panel":
|
||||||
return compareState !== "disarmed";
|
return compareState !== "disarmed";
|
||||||
|
case "alert":
|
||||||
|
// "on" and "off" are active, as "off" just means alert was acknowledged but is still active
|
||||||
|
return compareState !== "idle";
|
||||||
case "cover":
|
case "cover":
|
||||||
return !["closed", "closing"].includes(compareState);
|
return compareState !== "closed";
|
||||||
case "device_tracker":
|
case "device_tracker":
|
||||||
case "person":
|
case "person":
|
||||||
return compareState !== "not_home";
|
return compareState !== "not_home";
|
||||||
@ -37,7 +48,7 @@ export function stateActive(stateObj: HassEntity, state?: string): boolean {
|
|||||||
return compareState === "active";
|
return compareState === "active";
|
||||||
case "camera":
|
case "camera":
|
||||||
return compareState === "streaming";
|
return compareState === "streaming";
|
||||||
default:
|
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
@ -2,6 +2,7 @@
|
|||||||
import { HassEntity } from "home-assistant-js-websocket";
|
import { HassEntity } from "home-assistant-js-websocket";
|
||||||
import { UNAVAILABLE } from "../../data/entity";
|
import { UNAVAILABLE } from "../../data/entity";
|
||||||
import { alarmControlPanelColor } from "./color/alarm_control_panel_color";
|
import { alarmControlPanelColor } from "./color/alarm_control_panel_color";
|
||||||
|
import { alertColor } from "./color/alert_color";
|
||||||
import { binarySensorColor } from "./color/binary_sensor_color";
|
import { binarySensorColor } from "./color/binary_sensor_color";
|
||||||
import { climateColor } from "./color/climate_color";
|
import { climateColor } from "./color/climate_color";
|
||||||
import { lockColor } from "./color/lock_color";
|
import { lockColor } from "./color/lock_color";
|
||||||
@ -12,7 +13,6 @@ import { computeDomain } from "./compute_domain";
|
|||||||
import { stateActive } from "./state_active";
|
import { stateActive } from "./state_active";
|
||||||
|
|
||||||
const STATIC_ACTIVE_COLORED_DOMAIN = new Set([
|
const STATIC_ACTIVE_COLORED_DOMAIN = new Set([
|
||||||
"alert",
|
|
||||||
"automation",
|
"automation",
|
||||||
"calendar",
|
"calendar",
|
||||||
"camera",
|
"camera",
|
||||||
@ -65,6 +65,9 @@ export const stateColor = (stateObj: HassEntity, state?: string) => {
|
|||||||
case "alarm_control_panel":
|
case "alarm_control_panel":
|
||||||
return alarmControlPanelColor(compareState);
|
return alarmControlPanelColor(compareState);
|
||||||
|
|
||||||
|
case "alert":
|
||||||
|
return alertColor(compareState);
|
||||||
|
|
||||||
case "binary_sensor":
|
case "binary_sensor":
|
||||||
return binarySensorColor(stateObj, compareState);
|
return binarySensorColor(stateObj, compareState);
|
||||||
|
|
||||||
|
@ -86,7 +86,7 @@ export const protocolIntegrationPicked = async (
|
|||||||
"ui.panel.config.integrations.config_flow.missing_zwave_zigbee",
|
"ui.panel.config.integrations.config_flow.missing_zwave_zigbee",
|
||||||
{
|
{
|
||||||
integration: "Zigbee",
|
integration: "Zigbee",
|
||||||
brand: options?.brand || options?.domain || "Z-Wave",
|
brand: options?.brand || options?.domain || "Zigbee",
|
||||||
supported_hardware_link: html`<a
|
supported_hardware_link: html`<a
|
||||||
href=${documentationUrl(
|
href=${documentationUrl(
|
||||||
hass,
|
hass,
|
||||||
|
@ -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());
|
||||||
|
};
|
||||||
|
@ -12,9 +12,6 @@ import { getLocalLanguage } from "../../util/common-translation";
|
|||||||
export type LocalizeKeys =
|
export type LocalizeKeys =
|
||||||
| FlattenObjectKeys<Omit<TranslationDict, "supervisor">>
|
| FlattenObjectKeys<Omit<TranslationDict, "supervisor">>
|
||||||
| `panel.${string}`
|
| `panel.${string}`
|
||||||
| `state.${string}`
|
|
||||||
| `state_attributes.${string}`
|
|
||||||
| `state_badge.${string}`
|
|
||||||
| `ui.card.alarm_control_panel.${string}`
|
| `ui.card.alarm_control_panel.${string}`
|
||||||
| `ui.card.weather.attributes.${string}`
|
| `ui.card.weather.attributes.${string}`
|
||||||
| `ui.card.weather.cardinal_direction.${string}`
|
| `ui.card.weather.cardinal_direction.${string}`
|
||||||
|
@ -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;
|
||||||
|
@ -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;
|
||||||
|
@ -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
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -12,7 +12,7 @@ import { property, state } from "lit/decorators";
|
|||||||
import { STATES_OFF } from "../../common/const";
|
import { STATES_OFF } from "../../common/const";
|
||||||
import { computeStateDomain } from "../../common/entity/compute_state_domain";
|
import { computeStateDomain } from "../../common/entity/compute_state_domain";
|
||||||
import { computeStateName } from "../../common/entity/compute_state_name";
|
import { computeStateName } from "../../common/entity/compute_state_name";
|
||||||
import { UNAVAILABLE, UNAVAILABLE_STATES, UNKNOWN } from "../../data/entity";
|
import { isUnavailableState, UNAVAILABLE, UNKNOWN } from "../../data/entity";
|
||||||
import { forwardHaptic } from "../../data/haptics";
|
import { forwardHaptic } from "../../data/haptics";
|
||||||
import { HomeAssistant } from "../../types";
|
import { HomeAssistant } from "../../types";
|
||||||
import "../ha-formfield";
|
import "../ha-formfield";
|
||||||
@ -22,7 +22,7 @@ import "../ha-switch";
|
|||||||
const isOn = (stateObj?: HassEntity) =>
|
const isOn = (stateObj?: HassEntity) =>
|
||||||
stateObj !== undefined &&
|
stateObj !== undefined &&
|
||||||
!STATES_OFF.includes(stateObj.state) &&
|
!STATES_OFF.includes(stateObj.state) &&
|
||||||
!UNAVAILABLE_STATES.includes(stateObj.state);
|
!isUnavailableState(stateObj.state);
|
||||||
|
|
||||||
export class HaEntityToggle extends LitElement {
|
export class HaEntityToggle extends LitElement {
|
||||||
// hass is not a property so that we only re-render on stateObj changes
|
// hass is not a property so that we only re-render on stateObj changes
|
||||||
|
@ -10,21 +10,45 @@ import {
|
|||||||
} from "lit";
|
} 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";
|
||||||
|
import { arrayLiteralIncludes } from "../../common/array/literal-includes";
|
||||||
import secondsToDuration from "../../common/datetime/seconds_to_duration";
|
import secondsToDuration from "../../common/datetime/seconds_to_duration";
|
||||||
import { computeStateDisplay } from "../../common/entity/compute_state_display";
|
import { computeStateDisplay } from "../../common/entity/compute_state_display";
|
||||||
import { computeStateDomain } from "../../common/entity/compute_state_domain";
|
import { computeStateDomain } from "../../common/entity/compute_state_domain";
|
||||||
import { computeStateName } from "../../common/entity/compute_state_name";
|
import { computeStateName } from "../../common/entity/compute_state_name";
|
||||||
|
import { FIXED_DOMAIN_STATES } from "../../common/entity/get_states";
|
||||||
import {
|
import {
|
||||||
formatNumber,
|
formatNumber,
|
||||||
getNumberFormatOptions,
|
getNumberFormatOptions,
|
||||||
isNumericState,
|
isNumericState,
|
||||||
} from "../../common/number/format_number";
|
} from "../../common/number/format_number";
|
||||||
import { UNAVAILABLE, UNKNOWN } from "../../data/entity";
|
import { isUnavailableState, UNAVAILABLE, UNKNOWN } from "../../data/entity";
|
||||||
import { timerTimeRemaining } from "../../data/timer";
|
import { timerTimeRemaining } from "../../data/timer";
|
||||||
import { HomeAssistant } from "../../types";
|
import { HomeAssistant } from "../../types";
|
||||||
import "../ha-label-badge";
|
import "../ha-label-badge";
|
||||||
import "../ha-state-icon";
|
import "../ha-state-icon";
|
||||||
|
|
||||||
|
// Define the domains whose states have special truncated strings
|
||||||
|
const TRUNCATED_DOMAINS = [
|
||||||
|
"alarm_control_panel",
|
||||||
|
"device_tracker",
|
||||||
|
"person",
|
||||||
|
] as const satisfies ReadonlyArray<keyof typeof FIXED_DOMAIN_STATES>;
|
||||||
|
|
||||||
|
type TruncatedDomain = typeof TRUNCATED_DOMAINS[number];
|
||||||
|
type TruncatedKey = {
|
||||||
|
[T in TruncatedDomain]: `${T}.${typeof FIXED_DOMAIN_STATES[T][number]}`;
|
||||||
|
}[TruncatedDomain];
|
||||||
|
|
||||||
|
const getTruncatedKey = (domainKey: string, stateKey: string) => {
|
||||||
|
if (
|
||||||
|
arrayLiteralIncludes(TRUNCATED_DOMAINS)(domainKey) &&
|
||||||
|
arrayLiteralIncludes(FIXED_DOMAIN_STATES[domainKey])(stateKey)
|
||||||
|
) {
|
||||||
|
return `${domainKey}.${stateKey}` as TruncatedKey;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
@customElement("ha-state-label-badge")
|
@customElement("ha-state-label-badge")
|
||||||
export class HaStateLabelBadge extends LitElement {
|
export class HaStateLabelBadge extends LitElement {
|
||||||
@property({ attribute: false }) public hass?: HomeAssistant;
|
@property({ attribute: false }) public hass?: HomeAssistant;
|
||||||
@ -186,19 +210,18 @@ export class HaStateLabelBadge extends LitElement {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private _computeLabel(domain, entityState, _timerTimeRemaining) {
|
private _computeLabel(
|
||||||
if (
|
domain: string,
|
||||||
entityState.state === UNAVAILABLE ||
|
entityState: HassEntity,
|
||||||
["device_tracker", "alarm_control_panel", "person"].includes(domain)
|
_timerTimeRemaining = 0
|
||||||
) {
|
) {
|
||||||
// Localize the state with a special state_badge namespace, which has variations of
|
// For unavailable states or certain domains, use a special translation that is truncated to fit within the badge label
|
||||||
// the state translations that are truncated to fit within the badge label. Translations
|
if (isUnavailableState(entityState.state)) {
|
||||||
// are only added for device_tracker, alarm_control_panel and person.
|
return this.hass!.localize(`state_badge.default.${entityState.state}`);
|
||||||
return (
|
}
|
||||||
this.hass!.localize(`state_badge.${domain}.${entityState.state}`) ||
|
const domainStateKey = getTruncatedKey(domain, entityState.state);
|
||||||
this.hass!.localize(`state_badge.default.${entityState.state}`) ||
|
if (domainStateKey) {
|
||||||
entityState.state
|
return this.hass!.localize(`state_badge.${domainStateKey}`);
|
||||||
);
|
|
||||||
}
|
}
|
||||||
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({
|
||||||
|
@ -32,6 +32,8 @@ export class StateBadge extends LitElement {
|
|||||||
|
|
||||||
@property({ type: Boolean }) public stateColor?: boolean;
|
@property({ type: Boolean }) public stateColor?: boolean;
|
||||||
|
|
||||||
|
@property() public color?: string;
|
||||||
|
|
||||||
@property({ type: Boolean, reflect: true, attribute: "icon" })
|
@property({ type: Boolean, reflect: true, attribute: "icon" })
|
||||||
private _showIcon = true;
|
private _showIcon = true;
|
||||||
|
|
||||||
@ -75,7 +77,8 @@ export class StateBadge extends LitElement {
|
|||||||
!changedProps.has("stateObj") &&
|
!changedProps.has("stateObj") &&
|
||||||
!changedProps.has("overrideImage") &&
|
!changedProps.has("overrideImage") &&
|
||||||
!changedProps.has("overrideIcon") &&
|
!changedProps.has("overrideIcon") &&
|
||||||
!changedProps.has("stateColor")
|
!changedProps.has("stateColor") &&
|
||||||
|
!changedProps.has("color")
|
||||||
) {
|
) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -106,6 +109,9 @@ export class StateBadge extends LitElement {
|
|||||||
}
|
}
|
||||||
hostStyle.backgroundImage = `url(${imageUrl})`;
|
hostStyle.backgroundImage = `url(${imageUrl})`;
|
||||||
this._showIcon = false;
|
this._showIcon = false;
|
||||||
|
} else if (this.color) {
|
||||||
|
// Externally provided overriding color wins over state color
|
||||||
|
iconStyle.color = this.color;
|
||||||
} else if (this._stateColor && stateActive(stateObj)) {
|
} else if (this._stateColor && stateActive(stateObj)) {
|
||||||
const color = stateColorCss(stateObj);
|
const color = stateColorCss(stateObj);
|
||||||
if (color) {
|
if (color) {
|
||||||
|
@ -19,6 +19,8 @@ class StateInfo extends LitElement {
|
|||||||
// property used only in CSS
|
// property used only in CSS
|
||||||
@property({ type: Boolean, reflect: true }) public rtl = false;
|
@property({ type: Boolean, reflect: true }) public rtl = false;
|
||||||
|
|
||||||
|
@property() public color?: string;
|
||||||
|
|
||||||
protected render(): TemplateResult {
|
protected render(): TemplateResult {
|
||||||
if (!this.hass || !this.stateObj) {
|
if (!this.hass || !this.stateObj) {
|
||||||
return html``;
|
return html``;
|
||||||
@ -26,9 +28,10 @@ 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}
|
||||||
></state-badge>
|
></state-badge>
|
||||||
<div class="info">
|
<div class="info">
|
||||||
<div class="name" .title=${name} .inDialog=${this.inDialog}>
|
<div class="name" .title=${name} .inDialog=${this.inDialog}>
|
||||||
|
@ -80,7 +80,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;
|
||||||
|
@ -272,7 +272,8 @@ export class HaBarSlider extends LitElement {
|
|||||||
:host {
|
:host {
|
||||||
display: block;
|
display: block;
|
||||||
--slider-bar-color: rgb(var(--rgb-primary-color));
|
--slider-bar-color: rgb(var(--rgb-primary-color));
|
||||||
--slider-bar-background: rgba(var(--rgb-disabled-color), 0.2);
|
--slider-bar-background: rgb(var(--rgb-disabled-color));
|
||||||
|
--slider-bar-background-opacity: 0.2;
|
||||||
--slider-bar-thickness: 40px;
|
--slider-bar-thickness: 40px;
|
||||||
--slider-bar-border-radius: 10px;
|
--slider-bar-border-radius: 10px;
|
||||||
height: var(--slider-bar-thickness);
|
height: var(--slider-bar-thickness);
|
||||||
@ -301,6 +302,7 @@ export class HaBarSlider extends LitElement {
|
|||||||
height: 100%;
|
height: 100%;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
background: var(--slider-bar-background);
|
background: var(--slider-bar-background);
|
||||||
|
opacity: var(--slider-bar-background-opacity);
|
||||||
}
|
}
|
||||||
.slider .slider-track-bar {
|
.slider .slider-track-bar {
|
||||||
--border-radius: var(--slider-bar-border-radius);
|
--border-radius: var(--slider-bar-border-radius);
|
||||||
|
@ -74,6 +74,7 @@ export class HaBarSwitch extends LitElement {
|
|||||||
protected render(): TemplateResult {
|
protected render(): TemplateResult {
|
||||||
return html`
|
return html`
|
||||||
<div class="switch">
|
<div class="switch">
|
||||||
|
<div class="background"></div>
|
||||||
<div class="button" aria-hidden="true">
|
<div class="button" aria-hidden="true">
|
||||||
${this.checked
|
${this.checked
|
||||||
? this.pathOn
|
? this.pathOn
|
||||||
@ -91,8 +92,9 @@ export class HaBarSwitch extends LitElement {
|
|||||||
return css`
|
return css`
|
||||||
:host {
|
:host {
|
||||||
display: block;
|
display: block;
|
||||||
--switch-bar-color-on: var(--rgb-primary-color);
|
--switch-bar-on-color: rgb(var(--rgb-primary-color));
|
||||||
--switch-bar-color-off: var(--rgb-disabled-color);
|
--switch-bar-off-color: rgb(var(--rgb-disabled-color));
|
||||||
|
--switch-bar-background-opacity: 0.2;
|
||||||
--switch-bar-thickness: 40px;
|
--switch-bar-thickness: 40px;
|
||||||
--switch-bar-border-radius: 12px;
|
--switch-bar-border-radius: 12px;
|
||||||
--switch-bar-padding: 4px;
|
--switch-bar-padding: 4px;
|
||||||
@ -109,11 +111,20 @@ export class HaBarSwitch extends LitElement {
|
|||||||
height: 100%;
|
height: 100%;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
border-radius: var(--switch-bar-border-radius);
|
border-radius: var(--switch-bar-border-radius);
|
||||||
background-color: rgba(var(--switch-bar-color-off), 0.3);
|
overflow: hidden;
|
||||||
padding: var(--switch-bar-padding);
|
padding: var(--switch-bar-padding);
|
||||||
transition: background-color 180ms ease-in-out;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
}
|
}
|
||||||
|
.switch .background {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
background-color: var(--switch-bar-off-color);
|
||||||
|
transition: background-color 180ms ease-in-out;
|
||||||
|
opacity: var(--switch-bar-background-opacity);
|
||||||
|
}
|
||||||
.switch .button {
|
.switch .button {
|
||||||
width: 50%;
|
width: 50%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
@ -123,18 +134,18 @@ export class HaBarSwitch extends LitElement {
|
|||||||
);
|
);
|
||||||
transition: transform 180ms ease-in-out,
|
transition: transform 180ms ease-in-out,
|
||||||
background-color 180ms ease-in-out;
|
background-color 180ms ease-in-out;
|
||||||
background-color: rgb(var(--switch-bar-color-off));
|
background-color: var(--switch-bar-off-color);
|
||||||
color: white;
|
color: white;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
}
|
}
|
||||||
:host([checked]) .switch {
|
:host([checked]) .switch .background {
|
||||||
background-color: rgba(var(--switch-bar-color-on), 0.3);
|
background-color: var(--switch-bar-on-color);
|
||||||
}
|
}
|
||||||
:host([checked]) .switch .button {
|
:host([checked]) .switch .button {
|
||||||
transform: translateX(100%);
|
transform: translateX(100%);
|
||||||
background-color: rgb(var(--switch-bar-color-on));
|
background-color: var(--switch-bar-on-color);
|
||||||
}
|
}
|
||||||
:host([reversed]) .switch {
|
:host([reversed]) .switch {
|
||||||
flex-direction: row-reverse;
|
flex-direction: row-reverse;
|
||||||
|
@ -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 {
|
||||||
|
@ -1,37 +1,41 @@
|
|||||||
import { HassEntity } from "home-assistant-js-websocket";
|
|
||||||
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
|
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
|
||||||
import { customElement, property } from "lit/decorators";
|
import { customElement, property } from "lit/decorators";
|
||||||
|
import { computeAttributeValueDisplay } from "../common/entity/compute_attribute_display";
|
||||||
|
import { computeStateDisplay } from "../common/entity/compute_state_display";
|
||||||
import { formatNumber } from "../common/number/format_number";
|
import { formatNumber } from "../common/number/format_number";
|
||||||
import { CLIMATE_PRESET_NONE } from "../data/climate";
|
import { ClimateEntity, CLIMATE_PRESET_NONE } from "../data/climate";
|
||||||
import { UNAVAILABLE_STATES } from "../data/entity";
|
import { isUnavailableState } from "../data/entity";
|
||||||
import type { HomeAssistant } from "../types";
|
import type { HomeAssistant } from "../types";
|
||||||
|
|
||||||
@customElement("ha-climate-state")
|
@customElement("ha-climate-state")
|
||||||
class HaClimateState extends LitElement {
|
class HaClimateState extends LitElement {
|
||||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||||
|
|
||||||
@property({ attribute: false }) public stateObj!: HassEntity;
|
@property({ attribute: false }) public stateObj!: ClimateEntity;
|
||||||
|
|
||||||
protected render(): TemplateResult {
|
protected render(): TemplateResult {
|
||||||
const currentStatus = this._computeCurrentStatus();
|
const currentStatus = this._computeCurrentStatus();
|
||||||
|
|
||||||
return html`<div class="target">
|
return html`<div class="target">
|
||||||
${!UNAVAILABLE_STATES.includes(this.stateObj.state)
|
${!isUnavailableState(this.stateObj.state)
|
||||||
? html`<span class="state-label">
|
? html`<span class="state-label">
|
||||||
${this._localizeState()}
|
${this._localizeState()}
|
||||||
${this.stateObj.attributes.preset_mode &&
|
${this.stateObj.attributes.preset_mode &&
|
||||||
this.stateObj.attributes.preset_mode !== CLIMATE_PRESET_NONE
|
this.stateObj.attributes.preset_mode !== CLIMATE_PRESET_NONE
|
||||||
? html`-
|
? html`-
|
||||||
${this.hass.localize(
|
${computeAttributeValueDisplay(
|
||||||
`state_attributes.climate.preset_mode.${this.stateObj.attributes.preset_mode}`
|
this.hass.localize,
|
||||||
) || this.stateObj.attributes.preset_mode}`
|
this.stateObj,
|
||||||
|
this.hass.entities,
|
||||||
|
"preset_mode"
|
||||||
|
)}`
|
||||||
: ""}
|
: ""}
|
||||||
</span>
|
</span>
|
||||||
<div class="unit">${this._computeTarget()}</div>`
|
<div class="unit">${this._computeTarget()}</div>`
|
||||||
: this._localizeState()}
|
: this._localizeState()}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
${currentStatus && !UNAVAILABLE_STATES.includes(this.stateObj.state)
|
${currentStatus && !isUnavailableState(this.stateObj.state)
|
||||||
? html`<div class="current">
|
? html`<div class="current">
|
||||||
${this.hass.localize("ui.card.climate.currently")}:
|
${this.hass.localize("ui.card.climate.currently")}:
|
||||||
<div class="unit">${currentStatus}</div>
|
<div class="unit">${currentStatus}</div>
|
||||||
@ -109,17 +113,23 @@ class HaClimateState extends LitElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private _localizeState(): string {
|
private _localizeState(): string {
|
||||||
if (UNAVAILABLE_STATES.includes(this.stateObj.state)) {
|
if (isUnavailableState(this.stateObj.state)) {
|
||||||
return this.hass.localize(`state.default.${this.stateObj.state}`);
|
return this.hass.localize(`state.default.${this.stateObj.state}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const stateString = this.hass.localize(
|
const stateString = computeStateDisplay(
|
||||||
`component.climate.state._.${this.stateObj.state}`
|
this.hass.localize,
|
||||||
|
this.stateObj,
|
||||||
|
this.hass.locale,
|
||||||
|
this.hass.entities
|
||||||
);
|
);
|
||||||
|
|
||||||
return this.stateObj.attributes.hvac_action
|
return this.stateObj.attributes.hvac_action
|
||||||
? `${this.hass.localize(
|
? `${computeAttributeValueDisplay(
|
||||||
`state_attributes.climate.hvac_action.${this.stateObj.attributes.hvac_action}`
|
this.hass.localize,
|
||||||
|
this.stateObj,
|
||||||
|
this.hass.entities,
|
||||||
|
"hvac_action"
|
||||||
)} (${stateString})`
|
)} (${stateString})`
|
||||||
: stateString;
|
: stateString;
|
||||||
}
|
}
|
||||||
|
@ -121,7 +121,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
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
@ -85,7 +85,7 @@ export class HaForm extends LitElement implements HaFormElement {
|
|||||||
.selector=${item.selector}
|
.selector=${item.selector}
|
||||||
.value=${getValue(this.data, item)}
|
.value=${getValue(this.data, item)}
|
||||||
.label=${this._computeLabel(item, this.data)}
|
.label=${this._computeLabel(item, this.data)}
|
||||||
.disabled=${item.disabled || this.disabled}
|
.disabled=${item.disabled || this.disabled || false}
|
||||||
.helper=${this._computeHelper(item)}
|
.helper=${this._computeHelper(item)}
|
||||||
.required=${item.required || false}
|
.required=${item.required || false}
|
||||||
.context=${this._generateContext(item)}
|
.context=${this._generateContext(item)}
|
||||||
@ -95,7 +95,7 @@ export class HaForm extends LitElement implements HaFormElement {
|
|||||||
data: getValue(this.data, item),
|
data: getValue(this.data, item),
|
||||||
label: this._computeLabel(item, this.data),
|
label: this._computeLabel(item, this.data),
|
||||||
helper: this._computeHelper(item),
|
helper: this._computeHelper(item),
|
||||||
disabled: this.disabled || item.disabled,
|
disabled: this.disabled || item.disabled || false,
|
||||||
hass: this.hass,
|
hass: this.hass,
|
||||||
computeLabel: this.computeLabel,
|
computeLabel: this.computeLabel,
|
||||||
computeHelper: this.computeHelper,
|
computeHelper: this.computeHelper,
|
||||||
|
@ -88,6 +88,10 @@ export class HaSelectSelector extends LitElement {
|
|||||||
const value =
|
const value =
|
||||||
!this.value || this.value === "" ? [] : (this.value as string[]);
|
!this.value || this.value === "" ? [] : (this.value as string[]);
|
||||||
|
|
||||||
|
const optionItems = options.filter(
|
||||||
|
(option) => !option.disabled && !value?.includes(option.value)
|
||||||
|
);
|
||||||
|
|
||||||
return html`
|
return html`
|
||||||
${value?.length
|
${value?.length
|
||||||
? html`<ha-chip-set>
|
? html`<ha-chip-set>
|
||||||
@ -118,11 +122,11 @@ export class HaSelectSelector extends LitElement {
|
|||||||
.disabled=${this.disabled}
|
.disabled=${this.disabled}
|
||||||
.required=${this.required && !value.length}
|
.required=${this.required && !value.length}
|
||||||
.value=${this._filter}
|
.value=${this._filter}
|
||||||
.filteredItems=${options.filter(
|
.items=${optionItems}
|
||||||
(option) => !option.disabled && !value?.includes(option.value)
|
.allowCustomValue=${this.selector.select.custom_value ?? false}
|
||||||
)}
|
|
||||||
@filter-changed=${this._filterChanged}
|
@filter-changed=${this._filterChanged}
|
||||||
@value-changed=${this._comboBoxValueChanged}
|
@value-changed=${this._comboBoxValueChanged}
|
||||||
|
@opened-changed=${this._openedChanged}
|
||||||
></ha-combo-box>
|
></ha-combo-box>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
@ -130,11 +134,14 @@ export class HaSelectSelector extends LitElement {
|
|||||||
if (this.selector.select?.custom_value) {
|
if (this.selector.select?.custom_value) {
|
||||||
if (
|
if (
|
||||||
this.value !== undefined &&
|
this.value !== undefined &&
|
||||||
|
!Array.isArray(this.value) &&
|
||||||
!options.find((option) => option.value === this.value)
|
!options.find((option) => option.value === this.value)
|
||||||
) {
|
) {
|
||||||
options.unshift({ value: this.value, label: this.value });
|
options.unshift({ value: this.value, label: this.value });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const optionItems = options.filter((option) => !option.disabled);
|
||||||
|
|
||||||
return html`
|
return html`
|
||||||
<ha-combo-box
|
<ha-combo-box
|
||||||
item-value-path="value"
|
item-value-path="value"
|
||||||
@ -144,10 +151,11 @@ export class HaSelectSelector extends LitElement {
|
|||||||
.helper=${this.helper}
|
.helper=${this.helper}
|
||||||
.disabled=${this.disabled}
|
.disabled=${this.disabled}
|
||||||
.required=${this.required}
|
.required=${this.required}
|
||||||
.items=${options.filter((item) => !item.disabled)}
|
.items=${optionItems}
|
||||||
.value=${this.value}
|
.value=${this.value}
|
||||||
@filter-changed=${this._filterChanged}
|
@filter-changed=${this._filterChanged}
|
||||||
@value-changed=${this._comboBoxValueChanged}
|
@value-changed=${this._comboBoxValueChanged}
|
||||||
|
@opened-changed=${this._openedChanged}
|
||||||
></ha-combo-box>
|
></ha-combo-box>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
@ -190,7 +198,7 @@ export class HaSelectSelector extends LitElement {
|
|||||||
private _valueChanged(ev) {
|
private _valueChanged(ev) {
|
||||||
ev.stopPropagation();
|
ev.stopPropagation();
|
||||||
const value = ev.detail?.value || ev.target.value;
|
const value = ev.detail?.value || ev.target.value;
|
||||||
if (this.disabled || !value) {
|
if (this.disabled || value === undefined) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
fireEvent(this, "value-changed", {
|
fireEvent(this, "value-changed", {
|
||||||
@ -271,13 +279,16 @@ export class HaSelectSelector extends LitElement {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private _openedChanged(ev?: CustomEvent): void {
|
||||||
|
if (ev?.detail.value) {
|
||||||
|
this._filterChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private _filterChanged(ev?: CustomEvent): void {
|
private _filterChanged(ev?: CustomEvent): void {
|
||||||
this._filter = ev?.detail.value || "";
|
this._filter = ev?.detail.value || "";
|
||||||
|
|
||||||
const filteredItems = this.comboBox.items?.filter((item) => {
|
const filteredItems = this.comboBox.items?.filter((item) => {
|
||||||
if (this.selector.select?.multiple && this.value?.includes(item.value)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
const label = item.label || item.value;
|
const label = item.label || item.value;
|
||||||
return label.toLowerCase().includes(this._filter?.toLowerCase());
|
return label.toLowerCase().includes(this._filter?.toLowerCase());
|
||||||
});
|
});
|
||||||
|
@ -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,15 +96,26 @@ 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,
|
||||||
: undefined;
|
Object.values(this.hass.entities)
|
||||||
|
)
|
||||||
|
: undefined;
|
||||||
|
|
||||||
return filterSelectorDevices(
|
return filterSelectorDevices(
|
||||||
this.selector.target.device,
|
this.selector.target.device,
|
||||||
|
@ -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
|
||||||
? {
|
? {
|
||||||
|
@ -67,23 +67,28 @@ export class HaLocationsEditor extends LitElement {
|
|||||||
|
|
||||||
private Leaflet?: LeafletModuleType;
|
private Leaflet?: LeafletModuleType;
|
||||||
|
|
||||||
|
private _loadPromise: Promise<boolean | void>;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
|
|
||||||
import("leaflet").then((module) => {
|
this._loadPromise = import("leaflet").then((module) =>
|
||||||
import("leaflet-draw").then(() => {
|
import("leaflet-draw").then(() => {
|
||||||
this.Leaflet = module.default as LeafletModuleType;
|
this.Leaflet = module.default as LeafletModuleType;
|
||||||
this._updateMarkers();
|
this._updateMarkers();
|
||||||
this.updateComplete.then(() => this.fitMap());
|
return this.updateComplete.then(() => this.fitMap());
|
||||||
});
|
})
|
||||||
});
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public fitMap(): void {
|
public fitMap(): void {
|
||||||
this.map.fitMap();
|
this.map.fitMap();
|
||||||
}
|
}
|
||||||
|
|
||||||
public fitMarker(id: string): void {
|
public async fitMarker(id: string): Promise<void> {
|
||||||
|
if (!this.Leaflet) {
|
||||||
|
await this._loadPromise;
|
||||||
|
}
|
||||||
if (!this.map.leafletMap || !this._locationMarkers) {
|
if (!this.map.leafletMap || !this._locationMarkers) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -23,8 +23,12 @@ import "./ha-entity-marker";
|
|||||||
const getEntityId = (entity: string | HaMapEntity): string =>
|
const getEntityId = (entity: string | HaMapEntity): string =>
|
||||||
typeof entity === "string" ? entity : entity.entity_id;
|
typeof entity === "string" ? entity : entity.entity_id;
|
||||||
|
|
||||||
|
export interface HaMapPathPoint {
|
||||||
|
point: LatLngTuple;
|
||||||
|
tooltip: string;
|
||||||
|
}
|
||||||
export interface HaMapPaths {
|
export interface HaMapPaths {
|
||||||
points: LatLngTuple[];
|
points: HaMapPathPoint[];
|
||||||
color?: string;
|
color?: string;
|
||||||
gradualOpacity?: number;
|
gradualOpacity?: number;
|
||||||
}
|
}
|
||||||
@ -247,19 +251,21 @@ export class HaMap extends ReactiveElement {
|
|||||||
|
|
||||||
// DRAW point
|
// DRAW point
|
||||||
this._mapPaths.push(
|
this._mapPaths.push(
|
||||||
Leaflet!.circleMarker(path.points[pointIndex], {
|
Leaflet!
|
||||||
radius: 3,
|
.circleMarker(path.points[pointIndex].point, {
|
||||||
color: path.color || darkPrimaryColor,
|
radius: 3,
|
||||||
opacity,
|
color: path.color || darkPrimaryColor,
|
||||||
fillOpacity: opacity,
|
opacity,
|
||||||
interactive: false,
|
fillOpacity: opacity,
|
||||||
})
|
interactive: true,
|
||||||
|
})
|
||||||
|
.bindTooltip(path.points[pointIndex].tooltip, { direction: "top" })
|
||||||
);
|
);
|
||||||
|
|
||||||
// DRAW line between this and next point
|
// DRAW line between this and next point
|
||||||
this._mapPaths.push(
|
this._mapPaths.push(
|
||||||
Leaflet!.polyline(
|
Leaflet!.polyline(
|
||||||
[path.points[pointIndex], path.points[pointIndex + 1]],
|
[path.points[pointIndex].point, path.points[pointIndex + 1].point],
|
||||||
{
|
{
|
||||||
color: path.color || darkPrimaryColor,
|
color: path.color || darkPrimaryColor,
|
||||||
opacity,
|
opacity,
|
||||||
@ -275,13 +281,15 @@ export class HaMap extends ReactiveElement {
|
|||||||
: undefined;
|
: undefined;
|
||||||
// DRAW end path point
|
// DRAW end path point
|
||||||
this._mapPaths.push(
|
this._mapPaths.push(
|
||||||
Leaflet!.circleMarker(path.points[pointIndex], {
|
Leaflet!
|
||||||
radius: 3,
|
.circleMarker(path.points[pointIndex].point, {
|
||||||
color: path.color || darkPrimaryColor,
|
radius: 3,
|
||||||
opacity,
|
color: path.color || darkPrimaryColor,
|
||||||
fillOpacity: opacity,
|
opacity,
|
||||||
interactive: false,
|
fillOpacity: opacity,
|
||||||
})
|
interactive: true,
|
||||||
|
})
|
||||||
|
.bindTooltip(path.points[pointIndex].tooltip, { direction: "top" })
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
this._mapPaths.forEach((marker) => map.addLayer(marker));
|
this._mapPaths.forEach((marker) => map.addLayer(marker));
|
||||||
@ -491,6 +499,14 @@ export class HaMap extends ReactiveElement {
|
|||||||
.leaflet-bottom {
|
.leaflet-bottom {
|
||||||
z-index: 1 !important;
|
z-index: 1 !important;
|
||||||
}
|
}
|
||||||
|
.leaflet-tooltip {
|
||||||
|
padding: 8px;
|
||||||
|
font-size: 90%;
|
||||||
|
background: rgba(80, 80, 80, 0.9) !important;
|
||||||
|
color: white !important;
|
||||||
|
border-radius: 4px;
|
||||||
|
box-shadow: none !important;
|
||||||
|
}
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -27,8 +27,7 @@ import { until } from "lit/directives/until";
|
|||||||
import { fireEvent } from "../../common/dom/fire_event";
|
import { fireEvent } from "../../common/dom/fire_event";
|
||||||
import { computeRTLDirection } from "../../common/util/compute_rtl";
|
import { computeRTLDirection } from "../../common/util/compute_rtl";
|
||||||
import { debounce } from "../../common/util/debounce";
|
import { debounce } from "../../common/util/debounce";
|
||||||
import { getSignedPath } from "../../data/auth";
|
import { isUnavailableState } from "../../data/entity";
|
||||||
import { UNAVAILABLE_STATES } from "../../data/entity";
|
|
||||||
import type { MediaPlayerItem } from "../../data/media-player";
|
import type { MediaPlayerItem } from "../../data/media-player";
|
||||||
import {
|
import {
|
||||||
browseMediaPlayer,
|
browseMediaPlayer,
|
||||||
@ -248,7 +247,7 @@ export class HaMediaPlayerBrowse extends LitElement {
|
|||||||
});
|
});
|
||||||
} else if (
|
} else if (
|
||||||
err.code === "entity_not_found" &&
|
err.code === "entity_not_found" &&
|
||||||
UNAVAILABLE_STATES.includes(this.hass.states[this.entityId]?.state)
|
isUnavailableState(this.hass.states[this.entityId]?.state)
|
||||||
) {
|
) {
|
||||||
this._setError({
|
this._setError({
|
||||||
message: this.hass.localize(
|
message: this.hass.localize(
|
||||||
@ -339,7 +338,7 @@ export class HaMediaPlayerBrowse extends LitElement {
|
|||||||
: MediaClassBrowserSettings.directory;
|
: MediaClassBrowserSettings.directory;
|
||||||
|
|
||||||
const backgroundImage = currentItem.thumbnail
|
const backgroundImage = currentItem.thumbnail
|
||||||
? this._getSignedThumbnail(currentItem.thumbnail).then(
|
? this._getThumbnailURLorBase64(currentItem.thumbnail).then(
|
||||||
(value) => `url(${value})`
|
(value) => `url(${value})`
|
||||||
)
|
)
|
||||||
: "none";
|
: "none";
|
||||||
@ -550,7 +549,7 @@ export class HaMediaPlayerBrowse extends LitElement {
|
|||||||
|
|
||||||
private _renderGridItem = (child: MediaPlayerItem): TemplateResult => {
|
private _renderGridItem = (child: MediaPlayerItem): TemplateResult => {
|
||||||
const backgroundImage = child.thumbnail
|
const backgroundImage = child.thumbnail
|
||||||
? this._getSignedThumbnail(child.thumbnail).then(
|
? this._getThumbnailURLorBase64(child.thumbnail).then(
|
||||||
(value) => `url(${value})`
|
(value) => `url(${value})`
|
||||||
)
|
)
|
||||||
: "none";
|
: "none";
|
||||||
@ -615,7 +614,7 @@ export class HaMediaPlayerBrowse extends LitElement {
|
|||||||
|
|
||||||
const backgroundImage =
|
const backgroundImage =
|
||||||
mediaClass.show_list_images && child.thumbnail
|
mediaClass.show_list_images && child.thumbnail
|
||||||
? this._getSignedThumbnail(child.thumbnail).then(
|
? this._getThumbnailURLorBase64(child.thumbnail).then(
|
||||||
(value) => `url(${value})`
|
(value) => `url(${value})`
|
||||||
)
|
)
|
||||||
: "none";
|
: "none";
|
||||||
@ -652,7 +651,7 @@ export class HaMediaPlayerBrowse extends LitElement {
|
|||||||
`;
|
`;
|
||||||
};
|
};
|
||||||
|
|
||||||
private async _getSignedThumbnail(
|
private async _getThumbnailURLorBase64(
|
||||||
thumbnailUrl: string | undefined
|
thumbnailUrl: string | undefined
|
||||||
): Promise<string> {
|
): Promise<string> {
|
||||||
if (!thumbnailUrl) {
|
if (!thumbnailUrl) {
|
||||||
@ -661,7 +660,24 @@ export class HaMediaPlayerBrowse extends LitElement {
|
|||||||
|
|
||||||
if (thumbnailUrl.startsWith("/")) {
|
if (thumbnailUrl.startsWith("/")) {
|
||||||
// Thumbnails served by local API require authentication
|
// Thumbnails served by local API require authentication
|
||||||
return (await getSignedPath(this.hass, thumbnailUrl)).path;
|
return new Promise((resolve, reject) => {
|
||||||
|
this.hass
|
||||||
|
.fetchWithAuth(thumbnailUrl!)
|
||||||
|
// Since we are fetching with an authorization header, we cannot just put the
|
||||||
|
// URL directly into the document; we need to embed the image. We could do this
|
||||||
|
// using blob URLs, but then we would need to keep track of them in order to
|
||||||
|
// release them properly. Instead, we embed the thumbnail using base64.
|
||||||
|
.then((response) => response.blob())
|
||||||
|
.then((blob) => {
|
||||||
|
const reader = new FileReader();
|
||||||
|
reader.onload = () => {
|
||||||
|
const result = reader.result;
|
||||||
|
resolve(typeof result === "string" ? result : "");
|
||||||
|
};
|
||||||
|
reader.onerror = (e) => reject(e);
|
||||||
|
reader.readAsDataURL(blob);
|
||||||
|
});
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isBrandUrl(thumbnailUrl)) {
|
if (isBrandUrl(thumbnailUrl)) {
|
||||||
|
@ -41,7 +41,9 @@ export class HaTileButton extends LitElement {
|
|||||||
@touchcancel=${this.handleRippleDeactivate}
|
@touchcancel=${this.handleRippleDeactivate}
|
||||||
>
|
>
|
||||||
<slot></slot>
|
<slot></slot>
|
||||||
${this._shouldRenderRipple ? html`<mwc-ripple></mwc-ripple>` : ""}
|
${this._shouldRenderRipple && !this.disabled
|
||||||
|
? html`<mwc-ripple></mwc-ripple>`
|
||||||
|
: ""}
|
||||||
</button>
|
</button>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
@ -79,9 +81,10 @@ export class HaTileButton extends LitElement {
|
|||||||
static get styles(): CSSResultGroup {
|
static get styles(): CSSResultGroup {
|
||||||
return css`
|
return css`
|
||||||
:host {
|
:host {
|
||||||
--icon-color: rgb(var(--color, var(--rgb-primary-text-color)));
|
--tile-button-icon-color: var(--primary-text-color);
|
||||||
--bg-color: rgba(var(--color, var(--rgb-disabled-color)), 0.2);
|
--tile-button-background-color: rgb(var(--rgb-disabled-color));
|
||||||
--mdc-ripple-color: rgba(var(--color, var(--rgb-disabled-color)));
|
--tile-button-background-opacity: 0.2;
|
||||||
|
--mdc-ripple-color: var(--tile-button-background-color);
|
||||||
width: 40px;
|
width: 40px;
|
||||||
height: 40px;
|
height: 40px;
|
||||||
-webkit-tap-highlight-color: transparent;
|
-webkit-tap-highlight-color: transparent;
|
||||||
@ -97,25 +100,37 @@ export class HaTileButton extends LitElement {
|
|||||||
height: 100%;
|
height: 100%;
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
border: none;
|
border: none;
|
||||||
background-color: var(--bg-color);
|
|
||||||
transition: background-color 280ms ease-in-out, transform 180ms ease-out;
|
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
line-height: 0;
|
line-height: 0;
|
||||||
outline: none;
|
outline: none;
|
||||||
|
overflow: hidden;
|
||||||
|
background: none;
|
||||||
|
}
|
||||||
|
.button::before {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
background-color: var(--tile-button-background-color);
|
||||||
|
transition: background-color 180ms ease-in-out,
|
||||||
|
opacity 180ms ease-in-out;
|
||||||
|
opacity: var(--tile-button-background-opacity);
|
||||||
}
|
}
|
||||||
.button ::slotted(*) {
|
.button ::slotted(*) {
|
||||||
--mdc-icon-size: 20px;
|
--mdc-icon-size: 20px;
|
||||||
color: var(--icon-color);
|
transition: color 180ms ease-in-out;
|
||||||
|
color: var(--tile-button-icon-color);
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
.button:disabled {
|
.button:disabled {
|
||||||
cursor: not-allowed;
|
cursor: not-allowed;
|
||||||
background-color: rgba(var(--rgb-disabled-color), 0.2);
|
--tile-button-background-color: rgb(var(--rgb-disabled-color));
|
||||||
}
|
--tile-button-icon-color: var(--disabled-text-color);
|
||||||
.button:disabled ::slotted(*) {
|
--tile-button-background-opacity: 0.2;
|
||||||
color: rgb(var(--rgb-disabled-color));
|
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
@ -22,10 +22,20 @@ export class HaTileIcon extends LitElement {
|
|||||||
static get styles(): CSSResultGroup {
|
static get styles(): CSSResultGroup {
|
||||||
return css`
|
return css`
|
||||||
:host {
|
:host {
|
||||||
--icon-color: rgb(var(--color));
|
--tile-icon-color: rgb(var(--rgb-disabled-color));
|
||||||
--shape-color: rgba(var(--color), 0.2);
|
|
||||||
--mdc-icon-size: 24px;
|
--mdc-icon-size: 24px;
|
||||||
}
|
}
|
||||||
|
.shape::before {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
background-color: var(--tile-icon-color);
|
||||||
|
transition: background-color 180ms ease-in-out;
|
||||||
|
opacity: 0.2;
|
||||||
|
}
|
||||||
.shape {
|
.shape {
|
||||||
position: relative;
|
position: relative;
|
||||||
width: 40px;
|
width: 40px;
|
||||||
@ -34,13 +44,13 @@ export class HaTileIcon extends LitElement {
|
|||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
background-color: var(--shape-color);
|
transition: color 180ms ease-in-out;
|
||||||
transition: background-color 180ms ease-in-out, color 180ms ease-in-out;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
.shape ha-icon,
|
.shape ha-icon,
|
||||||
.shape ha-svg-icon {
|
.shape ha-svg-icon {
|
||||||
display: flex;
|
display: flex;
|
||||||
color: var(--icon-color);
|
color: var(--tile-icon-color);
|
||||||
transition: color 180ms ease-in-out;
|
transition: color 180ms ease-in-out;
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
@ -48,12 +48,16 @@ export class HaTileSlider extends LitElement {
|
|||||||
return css`
|
return css`
|
||||||
ha-bar-slider {
|
ha-bar-slider {
|
||||||
--slider-bar-color: var(
|
--slider-bar-color: var(
|
||||||
--tile-slider-bar-color,
|
--tile-slider-color,
|
||||||
rgb(var(--rgb-primary-color))
|
rgb(var(--rgb-primary-color))
|
||||||
);
|
);
|
||||||
--slider-bar-background: var(
|
--slider-bar-background: var(
|
||||||
--tile-slider-bar-background,
|
--tile-slider-background,
|
||||||
rgba(var(--rgb-disabled-color), 0.2)
|
rgb(var(--rgb-disabled-color))
|
||||||
|
);
|
||||||
|
--slider-bar-background-opacity: var(
|
||||||
|
--tile-slider-background-opacity,
|
||||||
|
0.2
|
||||||
);
|
);
|
||||||
--slider-bar-thickness: 40px;
|
--slider-bar-thickness: 40px;
|
||||||
--slider-bar-border-radius: 10px;
|
--slider-bar-border-radius: 10px;
|
||||||
|
@ -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 {
|
||||||
|
@ -23,63 +23,8 @@ interface CachedResults {
|
|||||||
data: HistoryResult;
|
data: HistoryResult;
|
||||||
}
|
}
|
||||||
|
|
||||||
// This is a different interface, a different cache :(
|
|
||||||
interface RecentCacheResults {
|
|
||||||
created: number;
|
|
||||||
language: string;
|
|
||||||
data: Promise<HistoryResult>;
|
|
||||||
}
|
|
||||||
|
|
||||||
const RECENT_THRESHOLD = 60000; // 1 minute
|
|
||||||
const RECENT_CACHE: { [cacheKey: string]: RecentCacheResults } = {};
|
|
||||||
const stateHistoryCache: { [cacheKey: string]: CachedResults } = {};
|
const stateHistoryCache: { [cacheKey: string]: CachedResults } = {};
|
||||||
|
|
||||||
// Cached type 1 function. Without cache config.
|
|
||||||
export const getRecent = (
|
|
||||||
hass: HomeAssistant,
|
|
||||||
entityId: string,
|
|
||||||
startTime: Date,
|
|
||||||
endTime: Date,
|
|
||||||
localize: LocalizeFunc,
|
|
||||||
language: string
|
|
||||||
) => {
|
|
||||||
const cacheKey = entityId;
|
|
||||||
const cache = RECENT_CACHE[cacheKey];
|
|
||||||
|
|
||||||
if (
|
|
||||||
cache &&
|
|
||||||
Date.now() - cache.created < RECENT_THRESHOLD &&
|
|
||||||
cache.language === language
|
|
||||||
) {
|
|
||||||
return cache.data;
|
|
||||||
}
|
|
||||||
|
|
||||||
const noAttributes = !entityIdHistoryNeedsAttributes(hass, entityId);
|
|
||||||
const prom = fetchRecentWS(
|
|
||||||
hass,
|
|
||||||
entityId,
|
|
||||||
startTime,
|
|
||||||
endTime,
|
|
||||||
false,
|
|
||||||
undefined,
|
|
||||||
true,
|
|
||||||
noAttributes
|
|
||||||
).then(
|
|
||||||
(stateHistory) => computeHistory(hass, stateHistory, localize),
|
|
||||||
(err) => {
|
|
||||||
delete RECENT_CACHE[entityId];
|
|
||||||
throw err;
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
RECENT_CACHE[cacheKey] = {
|
|
||||||
created: Date.now(),
|
|
||||||
language,
|
|
||||||
data: prom,
|
|
||||||
};
|
|
||||||
return prom;
|
|
||||||
};
|
|
||||||
|
|
||||||
// Cache type 2 functionality
|
// Cache type 2 functionality
|
||||||
function getEmptyCache(
|
function getEmptyCache(
|
||||||
language: string,
|
language: string,
|
||||||
@ -97,7 +42,7 @@ function getEmptyCache(
|
|||||||
|
|
||||||
export const getRecentWithCache = (
|
export const getRecentWithCache = (
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
entityId: string,
|
entityIds: string[],
|
||||||
cacheConfig: CacheConfig,
|
cacheConfig: CacheConfig,
|
||||||
localize: LocalizeFunc,
|
localize: LocalizeFunc,
|
||||||
language: string
|
language: string
|
||||||
@ -132,7 +77,9 @@ export const getRecentWithCache = (
|
|||||||
}
|
}
|
||||||
|
|
||||||
const curCacheProm = cache.prom;
|
const curCacheProm = cache.prom;
|
||||||
const noAttributes = !entityIdHistoryNeedsAttributes(hass, entityId);
|
const noAttributes = !entityIds.some((entityId) =>
|
||||||
|
entityIdHistoryNeedsAttributes(hass, entityId)
|
||||||
|
);
|
||||||
|
|
||||||
const genProm = async () => {
|
const genProm = async () => {
|
||||||
let fetchedHistory: HistoryStates;
|
let fetchedHistory: HistoryStates;
|
||||||
@ -142,7 +89,7 @@ export const getRecentWithCache = (
|
|||||||
curCacheProm,
|
curCacheProm,
|
||||||
fetchRecentWS(
|
fetchRecentWS(
|
||||||
hass,
|
hass,
|
||||||
entityId,
|
entityIds,
|
||||||
toFetchStartTime,
|
toFetchStartTime,
|
||||||
endTime,
|
endTime,
|
||||||
appendingToCache,
|
appendingToCache,
|
||||||
|
@ -2,7 +2,7 @@ import { getColorByIndex } from "../common/color/colors";
|
|||||||
import { computeDomain } from "../common/entity/compute_domain";
|
import { computeDomain } from "../common/entity/compute_domain";
|
||||||
import { computeStateName } from "../common/entity/compute_state_name";
|
import { computeStateName } from "../common/entity/compute_state_name";
|
||||||
import type { HomeAssistant } from "../types";
|
import type { HomeAssistant } from "../types";
|
||||||
import { UNAVAILABLE_STATES } from "./entity";
|
import { isUnavailableState } from "./entity";
|
||||||
|
|
||||||
export interface Calendar {
|
export interface Calendar {
|
||||||
entity_id: string;
|
entity_id: string;
|
||||||
@ -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 (
|
||||||
@ -138,7 +139,7 @@ export const getCalendars = (hass: HomeAssistant): Calendar[] =>
|
|||||||
.filter(
|
.filter(
|
||||||
(eid) =>
|
(eid) =>
|
||||||
computeDomain(eid) === "calendar" &&
|
computeDomain(eid) === "calendar" &&
|
||||||
!UNAVAILABLE_STATES.includes(hass.states[eid].state)
|
!isUnavailableState(hass.states[eid].state)
|
||||||
)
|
)
|
||||||
.sort()
|
.sort()
|
||||||
.map((eid, idx) => ({
|
.map((eid, idx) => ({
|
||||||
@ -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 = (
|
||||||
|
@ -14,7 +14,13 @@ export type HvacMode =
|
|||||||
|
|
||||||
export const CLIMATE_PRESET_NONE = "none";
|
export const CLIMATE_PRESET_NONE = "none";
|
||||||
|
|
||||||
export type HvacAction = "off" | "heating" | "cooling" | "drying" | "idle";
|
export type HvacAction =
|
||||||
|
| "off"
|
||||||
|
| "heating"
|
||||||
|
| "cooling"
|
||||||
|
| "drying"
|
||||||
|
| "idle"
|
||||||
|
| "fan";
|
||||||
|
|
||||||
export type ClimateEntity = HassEntityBase & {
|
export type ClimateEntity = HassEntityBase & {
|
||||||
attributes: HassEntityAttributeBase & {
|
attributes: HassEntityAttributeBase & {
|
||||||
@ -44,13 +50,15 @@ export type ClimateEntity = HassEntityBase & {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export const CLIMATE_SUPPORT_TARGET_TEMPERATURE = 1;
|
export const enum ClimateEntityFeature {
|
||||||
export const CLIMATE_SUPPORT_TARGET_TEMPERATURE_RANGE = 2;
|
TARGET_TEMPERATURE = 1,
|
||||||
export const CLIMATE_SUPPORT_TARGET_HUMIDITY = 4;
|
TARGET_TEMPERATURE_RANGE = 2,
|
||||||
export const CLIMATE_SUPPORT_FAN_MODE = 8;
|
TARGET_HUMIDITY = 4,
|
||||||
export const CLIMATE_SUPPORT_PRESET_MODE = 16;
|
FAN_MODE = 8,
|
||||||
export const CLIMATE_SUPPORT_SWING_MODE = 32;
|
PRESET_MODE = 16,
|
||||||
export const CLIMATE_SUPPORT_AUX_HEAT = 64;
|
SWING_MODE = 32,
|
||||||
|
AUX_HEAT = 64,
|
||||||
|
}
|
||||||
|
|
||||||
const hvacModeOrdering: { [key in HvacMode]: number } = {
|
const hvacModeOrdering: { [key in HvacMode]: number } = {
|
||||||
auto: 1,
|
auto: 1,
|
||||||
|
@ -1,27 +1,74 @@
|
|||||||
import { HomeAssistant } from "../types";
|
import { HomeAssistant } from "../types";
|
||||||
|
|
||||||
interface ProcessResults {
|
interface IntentTarget {
|
||||||
card: { [key: string]: Record<string, string> };
|
type: "area" | "device" | "entity" | "domain" | "device_class" | "custom";
|
||||||
speech: {
|
name: string;
|
||||||
[SpeechType in "plain" | "ssml"]: { extra_data: any; speech: string };
|
id: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IntentResultBase {
|
||||||
|
language: string;
|
||||||
|
speech:
|
||||||
|
| {
|
||||||
|
[SpeechType in "plain" | "ssml"]: { extra_data: any; speech: string };
|
||||||
|
}
|
||||||
|
| null;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IntentResultActionDone extends IntentResultBase {
|
||||||
|
response_type: "action_done";
|
||||||
|
data: {
|
||||||
|
targets: IntentTarget[];
|
||||||
|
success: IntentTarget[];
|
||||||
|
failed: IntentTarget[];
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface IntentResultQueryAnswer extends IntentResultBase {
|
||||||
|
response_type: "query_answer";
|
||||||
|
data: {
|
||||||
|
targets: IntentTarget[];
|
||||||
|
success: IntentTarget[];
|
||||||
|
failed: IntentTarget[];
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IntentResultError extends IntentResultBase {
|
||||||
|
response_type: "error";
|
||||||
|
data: {
|
||||||
|
code:
|
||||||
|
| "no_intent_match"
|
||||||
|
| "no_valid_targets"
|
||||||
|
| "failed_to_handle"
|
||||||
|
| "unknown";
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ConversationResult {
|
||||||
|
conversation_id: string | null;
|
||||||
|
response:
|
||||||
|
| IntentResultActionDone
|
||||||
|
| IntentResultQueryAnswer
|
||||||
|
| IntentResultError;
|
||||||
|
}
|
||||||
|
|
||||||
export interface AgentInfo {
|
export interface AgentInfo {
|
||||||
attribution?: { name: string; url: string };
|
attribution?: { name: string; url: string };
|
||||||
onboarding?: { text: string; url: string };
|
onboarding?: { text: string; url: string };
|
||||||
}
|
}
|
||||||
|
|
||||||
export const processText = (
|
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,
|
||||||
): Promise<ProcessResults> =>
|
language: string
|
||||||
|
): 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 = (
|
||||||
|
@ -451,7 +451,7 @@ const getEnergyData = async (
|
|||||||
...(await fetchStatistics(
|
...(await fetchStatistics(
|
||||||
hass!,
|
hass!,
|
||||||
compareStartMinHour,
|
compareStartMinHour,
|
||||||
end,
|
endCompare,
|
||||||
waterStatIds,
|
waterStatIds,
|
||||||
period,
|
period,
|
||||||
waterUnits,
|
waterUnits,
|
||||||
|
@ -1,7 +1,12 @@
|
|||||||
|
import { arrayLiteralIncludes } from "../common/array/literal-includes";
|
||||||
|
|
||||||
export const UNAVAILABLE = "unavailable";
|
export const UNAVAILABLE = "unavailable";
|
||||||
export const UNKNOWN = "unknown";
|
export const UNKNOWN = "unknown";
|
||||||
export const ON = "on";
|
export const ON = "on";
|
||||||
export const OFF = "off";
|
export const OFF = "off";
|
||||||
|
|
||||||
export const UNAVAILABLE_STATES = [UNAVAILABLE, UNKNOWN];
|
export const UNAVAILABLE_STATES = [UNAVAILABLE, UNKNOWN] as const;
|
||||||
export const OFF_STATES = [UNAVAILABLE, UNKNOWN, OFF];
|
export const OFF_STATES = [UNAVAILABLE, UNKNOWN, OFF] as const;
|
||||||
|
|
||||||
|
export const isUnavailableState = arrayLiteralIncludes(UNAVAILABLE_STATES);
|
||||||
|
export const isOffState = arrayLiteralIncludes(OFF_STATES);
|
||||||
|
@ -22,6 +22,7 @@ export interface EntityRegistryEntry {
|
|||||||
original_name?: string;
|
original_name?: string;
|
||||||
unique_id: string;
|
unique_id: string;
|
||||||
translation_key?: string;
|
translation_key?: string;
|
||||||
|
aliases: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ExtEntityRegistryEntry extends EntityRegistryEntry {
|
export interface ExtEntityRegistryEntry extends EntityRegistryEntry {
|
||||||
@ -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 = (
|
||||||
@ -162,9 +164,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(
|
||||||
|
@ -1,4 +1,8 @@
|
|||||||
import { HassEntities, HassEntity } from "home-assistant-js-websocket";
|
import {
|
||||||
|
HassEntities,
|
||||||
|
HassEntity,
|
||||||
|
HassEntityAttributeBase,
|
||||||
|
} from "home-assistant-js-websocket";
|
||||||
import { computeDomain } from "../common/entity/compute_domain";
|
import { computeDomain } from "../common/entity/compute_domain";
|
||||||
import { computeStateDisplayFromEntityAttributes } from "../common/entity/compute_state_display";
|
import { computeStateDisplayFromEntityAttributes } from "../common/entity/compute_state_display";
|
||||||
import { computeStateNameFromEntityAttributes } from "../common/entity/compute_state_name";
|
import { computeStateNameFromEntityAttributes } from "../common/entity/compute_state_name";
|
||||||
@ -117,7 +121,7 @@ export const fetchRecent = (
|
|||||||
|
|
||||||
export const fetchRecentWS = (
|
export const fetchRecentWS = (
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
entityId: string, // This may be CSV
|
entityIds: string[],
|
||||||
startTime: Date,
|
startTime: Date,
|
||||||
endTime: Date,
|
endTime: Date,
|
||||||
skipInitialState = false,
|
skipInitialState = false,
|
||||||
@ -133,7 +137,7 @@ export const fetchRecentWS = (
|
|||||||
include_start_time_state: !skipInitialState,
|
include_start_time_state: !skipInitialState,
|
||||||
minimal_response: minimalResponse,
|
minimal_response: minimalResponse,
|
||||||
no_attributes: noAttributes || false,
|
no_attributes: noAttributes || false,
|
||||||
entity_ids: entityId.split(","),
|
entity_ids: entityIds,
|
||||||
});
|
});
|
||||||
|
|
||||||
export const fetchDate = (
|
export const fetchDate = (
|
||||||
@ -160,9 +164,9 @@ export const fetchDateWS = (
|
|||||||
start_time: startTime.toISOString(),
|
start_time: startTime.toISOString(),
|
||||||
end_time: endTime.toISOString(),
|
end_time: endTime.toISOString(),
|
||||||
minimal_response: true,
|
minimal_response: true,
|
||||||
no_attributes: !entityIds
|
no_attributes: !entityIds.some((entityId) =>
|
||||||
.map((entityId) => entityIdHistoryNeedsAttributes(hass, entityId))
|
entityIdHistoryNeedsAttributes(hass, entityId)
|
||||||
.reduce((cur, next) => cur || next, false),
|
),
|
||||||
};
|
};
|
||||||
if (entityIds.length !== 0) {
|
if (entityIds.length !== 0) {
|
||||||
return hass.callWS<HistoryStates>({ ...params, entity_ids: entityIds });
|
return hass.callWS<HistoryStates>({ ...params, entity_ids: entityIds });
|
||||||
@ -195,13 +199,22 @@ const processTimelineEntity = (
|
|||||||
if (data.length > 0 && state.s === data[data.length - 1].state) {
|
if (data.length > 0 && state.s === data[data.length - 1].state) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const currentAttributes: HassEntityAttributeBase = {};
|
||||||
|
if (current_state?.attributes.device_class) {
|
||||||
|
currentAttributes.device_class = current_state?.attributes.device_class;
|
||||||
|
}
|
||||||
|
|
||||||
data.push({
|
data.push({
|
||||||
state_localize: computeStateDisplayFromEntityAttributes(
|
state_localize: computeStateDisplayFromEntityAttributes(
|
||||||
localize,
|
localize,
|
||||||
language,
|
language,
|
||||||
entities,
|
entities,
|
||||||
entityId,
|
entityId,
|
||||||
state.a || first.a,
|
{
|
||||||
|
...(state.a || first.a),
|
||||||
|
...currentAttributes,
|
||||||
|
},
|
||||||
state.s
|
state.s
|
||||||
),
|
),
|
||||||
state: state.s,
|
state: state.s,
|
||||||
|
@ -2,14 +2,24 @@ import {
|
|||||||
HassEntityAttributeBase,
|
HassEntityAttributeBase,
|
||||||
HassEntityBase,
|
HassEntityBase,
|
||||||
} from "home-assistant-js-websocket";
|
} from "home-assistant-js-websocket";
|
||||||
|
import { FIXED_DOMAIN_STATES } from "../common/entity/get_states";
|
||||||
|
import { TranslationDict } from "../types";
|
||||||
|
import { UNAVAILABLE_STATES } from "./entity";
|
||||||
|
|
||||||
|
type HumidifierState =
|
||||||
|
| typeof FIXED_DOMAIN_STATES.humidifier[number]
|
||||||
|
| typeof UNAVAILABLE_STATES[number];
|
||||||
|
type HumidifierMode =
|
||||||
|
keyof TranslationDict["state_attributes"]["humidifier"]["mode"];
|
||||||
|
|
||||||
export type HumidifierEntity = HassEntityBase & {
|
export type HumidifierEntity = HassEntityBase & {
|
||||||
|
state: HumidifierState;
|
||||||
attributes: HassEntityAttributeBase & {
|
attributes: HassEntityAttributeBase & {
|
||||||
humidity?: number;
|
humidity?: number;
|
||||||
min_humidity?: number;
|
min_humidity?: number;
|
||||||
max_humidity?: number;
|
max_humidity?: number;
|
||||||
mode?: string;
|
mode?: HumidifierMode;
|
||||||
available_modes?: string[];
|
available_modes?: HumidifierMode[];
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -35,7 +35,7 @@ import type {
|
|||||||
import { supportsFeature } from "../common/entity/supports-feature";
|
import { supportsFeature } from "../common/entity/supports-feature";
|
||||||
import { MediaPlayerItemId } from "../components/media-player/ha-media-player-browse";
|
import { MediaPlayerItemId } from "../components/media-player/ha-media-player-browse";
|
||||||
import type { HomeAssistant, TranslationDict } from "../types";
|
import type { HomeAssistant, TranslationDict } from "../types";
|
||||||
import { UNAVAILABLE_STATES } from "./entity";
|
import { isUnavailableState } from "./entity";
|
||||||
import { isTTSMediaSource } from "./tts";
|
import { isTTSMediaSource } from "./tts";
|
||||||
|
|
||||||
interface MediaPlayerEntityAttributes extends HassEntityAttributeBase {
|
interface MediaPlayerEntityAttributes extends HassEntityAttributeBase {
|
||||||
@ -259,7 +259,7 @@ export const computeMediaControls = (
|
|||||||
|
|
||||||
const state = stateObj.state;
|
const state = stateObj.state;
|
||||||
|
|
||||||
if (UNAVAILABLE_STATES.includes(state)) {
|
if (isUnavailableState(state)) {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -37,11 +37,13 @@ export interface MQTTDeviceDebugInfo {
|
|||||||
export const subscribeMQTTTopic = (
|
export const subscribeMQTTTopic = (
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
topic: string,
|
topic: string,
|
||||||
callback: (message: MQTTMessage) => void
|
callback: (message: MQTTMessage) => void,
|
||||||
|
qos?: number
|
||||||
) =>
|
) =>
|
||||||
hass.connection.subscribeMessage<MQTTMessage>(callback, {
|
hass.connection.subscribeMessage<MQTTMessage>(callback, {
|
||||||
type: "mqtt/subscribe",
|
type: "mqtt/subscribe",
|
||||||
topic,
|
topic,
|
||||||
|
qos,
|
||||||
});
|
});
|
||||||
|
|
||||||
export const fetchMQTTDebugInfo = (
|
export const fetchMQTTDebugInfo = (
|
||||||
|
@ -44,6 +44,7 @@ declare global {
|
|||||||
export type TranslationCategory =
|
export type TranslationCategory =
|
||||||
| "title"
|
| "title"
|
||||||
| "state"
|
| "state"
|
||||||
|
| "state_attributes"
|
||||||
| "entity"
|
| "entity"
|
||||||
| "config"
|
| "config"
|
||||||
| "config_panel"
|
| "config_panel"
|
||||||
|
@ -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
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -319,6 +319,12 @@ export const weatherSVGStyles = css`
|
|||||||
.cloud-front {
|
.cloud-front {
|
||||||
fill: var(--weather-icon-cloud-front-color, #f9f9f9);
|
fill: var(--weather-icon-cloud-front-color, #f9f9f9);
|
||||||
}
|
}
|
||||||
|
.snow {
|
||||||
|
fill: var(--weather-icon-snow-color, #f9f9f9);
|
||||||
|
stroke: var(--weather-icon-snow-stroke-color, #d4d4d4);
|
||||||
|
stroke-width: 1;
|
||||||
|
paint-order: stroke;
|
||||||
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const getWeatherStateSVG = (
|
const getWeatherStateSVG = (
|
||||||
@ -434,15 +440,15 @@ const getWeatherStateSVG = (
|
|||||||
snowyStates.has(state)
|
snowyStates.has(state)
|
||||||
? svg`
|
? svg`
|
||||||
<path
|
<path
|
||||||
class="rain"
|
class="snow"
|
||||||
d="m 8.4319893,15.348341 c 0,0.257881 -0.209197,0.467079 -0.467078,0.467079 -0.258586,0 -0.46743,-0.209198 -0.46743,-0.467079 0,-0.258233 0.208844,-0.467431 0.46743,-0.467431 0.257881,0 0.467078,0.209198 0.467078,0.467431"
|
d="m 8.4319893,15.348341 c 0,0.257881 -0.209197,0.467079 -0.467078,0.467079 -0.258586,0 -0.46743,-0.209198 -0.46743,-0.467079 0,-0.258233 0.208844,-0.467431 0.46743,-0.467431 0.257881,0 0.467078,0.209198 0.467078,0.467431"
|
||||||
/>
|
/>
|
||||||
<path
|
<path
|
||||||
class="rain"
|
class="snow"
|
||||||
d="m 11.263878,14.358553 c 0,0.364067 -0.295275,0.659694 -0.659695,0.659694 -0.364419,0 -0.6596937,-0.295627 -0.6596937,-0.659694 0,-0.364419 0.2952747,-0.659694 0.6596937,-0.659694 0.36442,0 0.659695,0.295275 0.659695,0.659694"
|
d="m 11.263878,14.358553 c 0,0.364067 -0.295275,0.659694 -0.659695,0.659694 -0.364419,0 -0.6596937,-0.295627 -0.6596937,-0.659694 0,-0.364419 0.2952747,-0.659694 0.6596937,-0.659694 0.36442,0 0.659695,0.295275 0.659695,0.659694"
|
||||||
/>
|
/>
|
||||||
<path
|
<path
|
||||||
class="rain"
|
class="snow"
|
||||||
d="m 5.3252173,13.69847 c 0,0.364419 -0.295275,0.660047 -0.659695,0.660047 -0.364067,0 -0.659694,-0.295628 -0.659694,-0.660047 0,-0.364067 0.295627,-0.659694 0.659694,-0.659694 0.36442,0 0.659695,0.295627 0.659695,0.659694"
|
d="m 5.3252173,13.69847 c 0,0.364419 -0.295275,0.660047 -0.659695,0.660047 -0.364067,0 -0.659694,-0.295628 -0.659694,-0.660047 0,-0.364067 0.295627,-0.659694 0.659694,-0.659694 0.36442,0 0.659695,0.295627 0.659695,0.659694"
|
||||||
/>
|
/>
|
||||||
`
|
`
|
||||||
|
@ -4,7 +4,7 @@ import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
|
|||||||
import { customElement, property } from "lit/decorators";
|
import { customElement, property } from "lit/decorators";
|
||||||
import "../../../components/ha-relative-time";
|
import "../../../components/ha-relative-time";
|
||||||
import { triggerAutomationActions } from "../../../data/automation";
|
import { triggerAutomationActions } from "../../../data/automation";
|
||||||
import { UNAVAILABLE_STATES } from "../../../data/entity";
|
import { isUnavailableState } from "../../../data/entity";
|
||||||
import { HomeAssistant } from "../../../types";
|
import { HomeAssistant } from "../../../types";
|
||||||
|
|
||||||
@customElement("more-info-automation")
|
@customElement("more-info-automation")
|
||||||
@ -32,7 +32,7 @@ class MoreInfoAutomation extends LitElement {
|
|||||||
<div class="actions">
|
<div class="actions">
|
||||||
<mwc-button
|
<mwc-button
|
||||||
@click=${this._runActions}
|
@click=${this._runActions}
|
||||||
.disabled=${UNAVAILABLE_STATES.includes(this.stateObj!.state)}
|
.disabled=${isUnavailableState(this.stateObj!.state)}
|
||||||
>
|
>
|
||||||
${this.hass.localize("ui.card.automation.trigger")}
|
${this.hass.localize("ui.card.automation.trigger")}
|
||||||
</mwc-button>
|
</mwc-button>
|
||||||
|
@ -11,6 +11,11 @@ import { property } 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 { stopPropagation } from "../../../common/dom/stop_propagation";
|
import { stopPropagation } from "../../../common/dom/stop_propagation";
|
||||||
|
import {
|
||||||
|
computeAttributeNameDisplay,
|
||||||
|
computeAttributeValueDisplay,
|
||||||
|
} from "../../../common/entity/compute_attribute_display";
|
||||||
|
import { computeStateDisplay } from "../../../common/entity/compute_state_display";
|
||||||
import { supportsFeature } from "../../../common/entity/supports-feature";
|
import { supportsFeature } from "../../../common/entity/supports-feature";
|
||||||
import { computeRTLDirection } from "../../../common/util/compute_rtl";
|
import { computeRTLDirection } from "../../../common/util/compute_rtl";
|
||||||
import "../../../components/ha-climate-control";
|
import "../../../components/ha-climate-control";
|
||||||
@ -19,13 +24,7 @@ import "../../../components/ha-slider";
|
|||||||
import "../../../components/ha-switch";
|
import "../../../components/ha-switch";
|
||||||
import {
|
import {
|
||||||
ClimateEntity,
|
ClimateEntity,
|
||||||
CLIMATE_SUPPORT_AUX_HEAT,
|
ClimateEntityFeature,
|
||||||
CLIMATE_SUPPORT_FAN_MODE,
|
|
||||||
CLIMATE_SUPPORT_PRESET_MODE,
|
|
||||||
CLIMATE_SUPPORT_SWING_MODE,
|
|
||||||
CLIMATE_SUPPORT_TARGET_HUMIDITY,
|
|
||||||
CLIMATE_SUPPORT_TARGET_TEMPERATURE,
|
|
||||||
CLIMATE_SUPPORT_TARGET_TEMPERATURE_RANGE,
|
|
||||||
compareClimateHvacModes,
|
compareClimateHvacModes,
|
||||||
} from "../../../data/climate";
|
} from "../../../data/climate";
|
||||||
import { HomeAssistant } from "../../../types";
|
import { HomeAssistant } from "../../../types";
|
||||||
@ -47,26 +46,32 @@ class MoreInfoClimate extends LitElement {
|
|||||||
|
|
||||||
const supportTargetTemperature = supportsFeature(
|
const supportTargetTemperature = supportsFeature(
|
||||||
stateObj,
|
stateObj,
|
||||||
CLIMATE_SUPPORT_TARGET_TEMPERATURE
|
ClimateEntityFeature.TARGET_TEMPERATURE
|
||||||
);
|
);
|
||||||
const supportTargetTemperatureRange = supportsFeature(
|
const supportTargetTemperatureRange = supportsFeature(
|
||||||
stateObj,
|
stateObj,
|
||||||
CLIMATE_SUPPORT_TARGET_TEMPERATURE_RANGE
|
ClimateEntityFeature.TARGET_TEMPERATURE_RANGE
|
||||||
);
|
);
|
||||||
const supportTargetHumidity = supportsFeature(
|
const supportTargetHumidity = supportsFeature(
|
||||||
stateObj,
|
stateObj,
|
||||||
CLIMATE_SUPPORT_TARGET_HUMIDITY
|
ClimateEntityFeature.TARGET_HUMIDITY
|
||||||
|
);
|
||||||
|
const supportFanMode = supportsFeature(
|
||||||
|
stateObj,
|
||||||
|
ClimateEntityFeature.FAN_MODE
|
||||||
);
|
);
|
||||||
const supportFanMode = supportsFeature(stateObj, CLIMATE_SUPPORT_FAN_MODE);
|
|
||||||
const supportPresetMode = supportsFeature(
|
const supportPresetMode = supportsFeature(
|
||||||
stateObj,
|
stateObj,
|
||||||
CLIMATE_SUPPORT_PRESET_MODE
|
ClimateEntityFeature.PRESET_MODE
|
||||||
);
|
);
|
||||||
const supportSwingMode = supportsFeature(
|
const supportSwingMode = supportsFeature(
|
||||||
stateObj,
|
stateObj,
|
||||||
CLIMATE_SUPPORT_SWING_MODE
|
ClimateEntityFeature.SWING_MODE
|
||||||
|
);
|
||||||
|
const supportAuxHeat = supportsFeature(
|
||||||
|
stateObj,
|
||||||
|
ClimateEntityFeature.AUX_HEAT
|
||||||
);
|
);
|
||||||
const supportAuxHeat = supportsFeature(stateObj, CLIMATE_SUPPORT_AUX_HEAT);
|
|
||||||
|
|
||||||
const temperatureStepSize =
|
const temperatureStepSize =
|
||||||
stateObj.attributes.target_temp_step ||
|
stateObj.attributes.target_temp_step ||
|
||||||
@ -94,7 +99,12 @@ class MoreInfoClimate extends LitElement {
|
|||||||
${supportTargetTemperature || supportTargetTemperatureRange
|
${supportTargetTemperature || supportTargetTemperatureRange
|
||||||
? html`
|
? html`
|
||||||
<div>
|
<div>
|
||||||
${hass.localize("ui.card.climate.target_temperature")}
|
${computeAttributeNameDisplay(
|
||||||
|
hass.localize,
|
||||||
|
stateObj,
|
||||||
|
hass.entities,
|
||||||
|
"temperature"
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
`
|
`
|
||||||
: ""}
|
: ""}
|
||||||
@ -145,7 +155,14 @@ class MoreInfoClimate extends LitElement {
|
|||||||
${supportTargetHumidity
|
${supportTargetHumidity
|
||||||
? html`
|
? html`
|
||||||
<div class="container-humidity">
|
<div class="container-humidity">
|
||||||
<div>${hass.localize("ui.card.climate.target_humidity")}</div>
|
<div>
|
||||||
|
${computeAttributeNameDisplay(
|
||||||
|
hass.localize,
|
||||||
|
stateObj,
|
||||||
|
hass.entities,
|
||||||
|
"humidity"
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
<div class="single-row">
|
<div class="single-row">
|
||||||
<div class="target-humidity">
|
<div class="target-humidity">
|
||||||
${stateObj.attributes.humidity} %
|
${stateObj.attributes.humidity} %
|
||||||
@ -182,7 +199,13 @@ class MoreInfoClimate extends LitElement {
|
|||||||
.map(
|
.map(
|
||||||
(mode) => html`
|
(mode) => html`
|
||||||
<mwc-list-item .value=${mode}>
|
<mwc-list-item .value=${mode}>
|
||||||
${hass.localize(`component.climate.state._.${mode}`)}
|
${computeStateDisplay(
|
||||||
|
hass.localize,
|
||||||
|
stateObj,
|
||||||
|
hass.locale,
|
||||||
|
hass.entities,
|
||||||
|
mode
|
||||||
|
)}
|
||||||
</mwc-list-item>
|
</mwc-list-item>
|
||||||
`
|
`
|
||||||
)}
|
)}
|
||||||
@ -194,7 +217,12 @@ class MoreInfoClimate extends LitElement {
|
|||||||
? html`
|
? html`
|
||||||
<div class="container-preset_modes">
|
<div class="container-preset_modes">
|
||||||
<ha-select
|
<ha-select
|
||||||
.label=${hass.localize("ui.card.climate.preset_mode")}
|
.label=${computeAttributeNameDisplay(
|
||||||
|
hass.localize,
|
||||||
|
stateObj,
|
||||||
|
hass.entities,
|
||||||
|
"preset_mode"
|
||||||
|
)}
|
||||||
.value=${stateObj.attributes.preset_mode}
|
.value=${stateObj.attributes.preset_mode}
|
||||||
fixedMenuPosition
|
fixedMenuPosition
|
||||||
naturalMenuWidth
|
naturalMenuWidth
|
||||||
@ -204,9 +232,13 @@ class MoreInfoClimate extends LitElement {
|
|||||||
${stateObj.attributes.preset_modes!.map(
|
${stateObj.attributes.preset_modes!.map(
|
||||||
(mode) => html`
|
(mode) => html`
|
||||||
<mwc-list-item .value=${mode}>
|
<mwc-list-item .value=${mode}>
|
||||||
${hass.localize(
|
${computeAttributeValueDisplay(
|
||||||
`state_attributes.climate.preset_mode.${mode}`
|
hass.localize,
|
||||||
) || mode}
|
stateObj,
|
||||||
|
hass.entities,
|
||||||
|
"preset_mode",
|
||||||
|
mode
|
||||||
|
)}
|
||||||
</mwc-list-item>
|
</mwc-list-item>
|
||||||
`
|
`
|
||||||
)}
|
)}
|
||||||
@ -218,7 +250,12 @@ class MoreInfoClimate extends LitElement {
|
|||||||
? html`
|
? html`
|
||||||
<div class="container-fan_list">
|
<div class="container-fan_list">
|
||||||
<ha-select
|
<ha-select
|
||||||
.label=${hass.localize("ui.card.climate.fan_mode")}
|
.label=${computeAttributeNameDisplay(
|
||||||
|
hass.localize,
|
||||||
|
stateObj,
|
||||||
|
hass.entities,
|
||||||
|
"fan_mode"
|
||||||
|
)}
|
||||||
.value=${stateObj.attributes.fan_mode}
|
.value=${stateObj.attributes.fan_mode}
|
||||||
fixedMenuPosition
|
fixedMenuPosition
|
||||||
naturalMenuWidth
|
naturalMenuWidth
|
||||||
@ -228,9 +265,13 @@ class MoreInfoClimate extends LitElement {
|
|||||||
${stateObj.attributes.fan_modes!.map(
|
${stateObj.attributes.fan_modes!.map(
|
||||||
(mode) => html`
|
(mode) => html`
|
||||||
<mwc-list-item .value=${mode}>
|
<mwc-list-item .value=${mode}>
|
||||||
${hass.localize(
|
${computeAttributeValueDisplay(
|
||||||
`state_attributes.climate.fan_mode.${mode}`
|
hass.localize,
|
||||||
) || mode}
|
stateObj,
|
||||||
|
hass.entities,
|
||||||
|
"fan_mode",
|
||||||
|
mode
|
||||||
|
)}
|
||||||
</mwc-list-item>
|
</mwc-list-item>
|
||||||
`
|
`
|
||||||
)}
|
)}
|
||||||
@ -242,7 +283,12 @@ class MoreInfoClimate extends LitElement {
|
|||||||
? html`
|
? html`
|
||||||
<div class="container-swing_list">
|
<div class="container-swing_list">
|
||||||
<ha-select
|
<ha-select
|
||||||
.label=${hass.localize("ui.card.climate.swing_mode")}
|
.label=${computeAttributeNameDisplay(
|
||||||
|
hass.localize,
|
||||||
|
stateObj,
|
||||||
|
hass.entities,
|
||||||
|
"swing_mode"
|
||||||
|
)}
|
||||||
.value=${stateObj.attributes.swing_mode}
|
.value=${stateObj.attributes.swing_mode}
|
||||||
fixedMenuPosition
|
fixedMenuPosition
|
||||||
naturalMenuWidth
|
naturalMenuWidth
|
||||||
@ -251,7 +297,15 @@ class MoreInfoClimate extends LitElement {
|
|||||||
>
|
>
|
||||||
${stateObj.attributes.swing_modes!.map(
|
${stateObj.attributes.swing_modes!.map(
|
||||||
(mode) => html`
|
(mode) => html`
|
||||||
<mwc-list-item .value=${mode}>${mode}</mwc-list-item>
|
<mwc-list-item .value=${mode}>
|
||||||
|
${computeAttributeValueDisplay(
|
||||||
|
hass.localize,
|
||||||
|
stateObj,
|
||||||
|
hass.entities,
|
||||||
|
"swing_mode",
|
||||||
|
mode
|
||||||
|
)}
|
||||||
|
</mwc-list-item>
|
||||||
`
|
`
|
||||||
)}
|
)}
|
||||||
</ha-select>
|
</ha-select>
|
||||||
@ -263,7 +317,12 @@ class MoreInfoClimate extends LitElement {
|
|||||||
<div class="container-aux_heat">
|
<div class="container-aux_heat">
|
||||||
<div class="center horizontal layout single-row">
|
<div class="center horizontal layout single-row">
|
||||||
<div class="flex">
|
<div class="flex">
|
||||||
${hass.localize("ui.card.climate.aux_heat")}
|
${computeAttributeNameDisplay(
|
||||||
|
hass.localize,
|
||||||
|
stateObj,
|
||||||
|
hass.entities,
|
||||||
|
"aux_heat"
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
<ha-switch
|
<ha-switch
|
||||||
.checked=${stateObj.attributes.aux_heat === "on"}
|
.checked=${stateObj.attributes.aux_heat === "on"}
|
||||||
|
@ -2,7 +2,7 @@ import "@material/mwc-button";
|
|||||||
import { HassEntity } from "home-assistant-js-websocket";
|
import { HassEntity } from "home-assistant-js-websocket";
|
||||||
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
|
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
|
||||||
import { customElement, property } from "lit/decorators";
|
import { customElement, property } from "lit/decorators";
|
||||||
import { UNAVAILABLE_STATES } from "../../../data/entity";
|
import { isUnavailableState } from "../../../data/entity";
|
||||||
import { HomeAssistant } from "../../../types";
|
import { HomeAssistant } from "../../../types";
|
||||||
|
|
||||||
@customElement("more-info-counter")
|
@customElement("more-info-counter")
|
||||||
@ -16,7 +16,7 @@ class MoreInfoCounter extends LitElement {
|
|||||||
return html``;
|
return html``;
|
||||||
}
|
}
|
||||||
|
|
||||||
const disabled = UNAVAILABLE_STATES.includes(this.stateObj!.state);
|
const disabled = isUnavailableState(this.stateObj!.state);
|
||||||
|
|
||||||
return html`
|
return html`
|
||||||
<div class="actions">
|
<div class="actions">
|
||||||
|
@ -3,7 +3,7 @@ import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
|
|||||||
import { customElement, property } from "lit/decorators";
|
import { customElement, property } from "lit/decorators";
|
||||||
import "../../../components/ha-date-input";
|
import "../../../components/ha-date-input";
|
||||||
import "../../../components/ha-time-input";
|
import "../../../components/ha-time-input";
|
||||||
import { UNAVAILABLE_STATES, UNKNOWN } from "../../../data/entity";
|
import { isUnavailableState, UNKNOWN } from "../../../data/entity";
|
||||||
import {
|
import {
|
||||||
setInputDateTimeValue,
|
setInputDateTimeValue,
|
||||||
stateToIsoDateString,
|
stateToIsoDateString,
|
||||||
@ -28,7 +28,7 @@ class MoreInfoInputDatetime extends LitElement {
|
|||||||
<ha-date-input
|
<ha-date-input
|
||||||
.locale=${this.hass.locale}
|
.locale=${this.hass.locale}
|
||||||
.value=${stateToIsoDateString(this.stateObj)}
|
.value=${stateToIsoDateString(this.stateObj)}
|
||||||
.disabled=${UNAVAILABLE_STATES.includes(this.stateObj.state)}
|
.disabled=${isUnavailableState(this.stateObj.state)}
|
||||||
@value-changed=${this._dateChanged}
|
@value-changed=${this._dateChanged}
|
||||||
>
|
>
|
||||||
</ha-date-input>
|
</ha-date-input>
|
||||||
@ -45,7 +45,7 @@ class MoreInfoInputDatetime extends LitElement {
|
|||||||
? this.stateObj.state.split(" ")[1]
|
? this.stateObj.state.split(" ")[1]
|
||||||
: this.stateObj.state}
|
: this.stateObj.state}
|
||||||
.locale=${this.hass.locale}
|
.locale=${this.hass.locale}
|
||||||
.disabled=${UNAVAILABLE_STATES.includes(this.stateObj.state)}
|
.disabled=${isUnavailableState(this.stateObj.state)}
|
||||||
@value-changed=${this._timeChanged}
|
@value-changed=${this._timeChanged}
|
||||||
@click=${this._stopEventPropagation}
|
@click=${this._stopEventPropagation}
|
||||||
></ha-time-input>
|
></ha-time-input>
|
||||||
|
@ -9,7 +9,7 @@ import "../../../components/ha-checkbox";
|
|||||||
import "../../../components/ha-circular-progress";
|
import "../../../components/ha-circular-progress";
|
||||||
import "../../../components/ha-formfield";
|
import "../../../components/ha-formfield";
|
||||||
import "../../../components/ha-markdown";
|
import "../../../components/ha-markdown";
|
||||||
import { UNAVAILABLE_STATES } from "../../../data/entity";
|
import { isUnavailableState } from "../../../data/entity";
|
||||||
import {
|
import {
|
||||||
UpdateEntity,
|
UpdateEntity,
|
||||||
updateIsInstalling,
|
updateIsInstalling,
|
||||||
@ -37,7 +37,7 @@ class MoreInfoUpdate extends LitElement {
|
|||||||
if (
|
if (
|
||||||
!this.hass ||
|
!this.hass ||
|
||||||
!this.stateObj ||
|
!this.stateObj ||
|
||||||
UNAVAILABLE_STATES.includes(this.stateObj.state)
|
isUnavailableState(this.stateObj.state)
|
||||||
) {
|
) {
|
||||||
return html``;
|
return html``;
|
||||||
}
|
}
|
||||||
|
@ -139,7 +139,7 @@ export class MoreInfoHistory extends LitElement {
|
|||||||
}
|
}
|
||||||
this._stateHistory = await getRecentWithCache(
|
this._stateHistory = await getRecentWithCache(
|
||||||
this.hass!,
|
this.hass!,
|
||||||
this.entityId,
|
[this.entityId],
|
||||||
{
|
{
|
||||||
cacheKey: `more_info.${this.entityId}`,
|
cacheKey: `more_info.${this.entityId}`,
|
||||||
hoursToShow: 24,
|
hoursToShow: 24,
|
||||||
|
@ -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";
|
||||||
@ -22,7 +21,7 @@ import type { HaTextField } from "../../components/ha-textfield";
|
|||||||
import {
|
import {
|
||||||
AgentInfo,
|
AgentInfo,
|
||||||
getAgentInfo,
|
getAgentInfo,
|
||||||
processText,
|
processConversationInput,
|
||||||
setConversationOnboarding,
|
setConversationOnboarding,
|
||||||
} from "../../data/conversation";
|
} from "../../data/conversation";
|
||||||
import { haStyleDialog } from "../../resources/styles";
|
import { haStyleDialog } from "../../resources/styles";
|
||||||
@ -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) {
|
||||||
@ -274,13 +282,19 @@ export class HaVoiceCommandDialog extends LitElement {
|
|||||||
// To make sure the answer is placed at the right user text, we add it before we process it
|
// To make sure the answer is placed at the right user text, we add it before we process it
|
||||||
this._addMessage(message);
|
this._addMessage(message);
|
||||||
try {
|
try {
|
||||||
const response = await processText(
|
const response = await processConversationInput(
|
||||||
this.hass,
|
this.hass,
|
||||||
text,
|
text,
|
||||||
this._conversationId!
|
this._conversationId,
|
||||||
|
this.hass.language
|
||||||
);
|
);
|
||||||
const plain = response.speech.plain;
|
this._conversationId = response.conversation_id;
|
||||||
message.text = plain.speech;
|
const plain = response.response.speech?.plain;
|
||||||
|
if (plain) {
|
||||||
|
message.text = plain.speech;
|
||||||
|
} else {
|
||||||
|
message.text = "<silence>";
|
||||||
|
}
|
||||||
|
|
||||||
this.requestUpdate("_conversation");
|
this.requestUpdate("_conversation");
|
||||||
} catch {
|
} catch {
|
||||||
|
@ -138,6 +138,8 @@ export class HomeAssistantAppEl extends QuickBarMixin(HassElement) {
|
|||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
this._loadHassTranslations(this.hass!.language, "state");
|
this._loadHassTranslations(this.hass!.language, "state");
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
|
this._loadHassTranslations(this.hass!.language, "state_attributes");
|
||||||
|
// @ts-ignore
|
||||||
this._loadHassTranslations(this.hass!.language, "entity");
|
this._loadHassTranslations(this.hass!.language, "entity");
|
||||||
|
|
||||||
document.addEventListener(
|
document.addEventListener(
|
||||||
|
@ -156,13 +156,11 @@ class OnboardingCoreConfig extends LitElement {
|
|||||||
type="number"
|
type="number"
|
||||||
.disabled=${this._working}
|
.disabled=${this._working}
|
||||||
.value=${this._elevationValue}
|
.value=${this._elevationValue}
|
||||||
|
.suffix=${this.hass.localize(
|
||||||
|
"ui.panel.config.core.section.core.core_config.elevation_meters"
|
||||||
|
)}
|
||||||
@change=${this._handleChange}
|
@change=${this._handleChange}
|
||||||
>
|
>
|
||||||
<span slot="suffix">
|
|
||||||
${this.hass.localize(
|
|
||||||
"ui.panel.config.core.section.core.core_config.elevation_meters"
|
|
||||||
)}
|
|
||||||
</span>
|
|
||||||
</ha-textfield>
|
</ha-textfield>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -273,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");
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -281,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`
|
||||||
|
@ -1,17 +1,14 @@
|
|||||||
import "@material/mwc-button";
|
import "@material/mwc-button";
|
||||||
import { mdiCalendarClock, mdiClose } from "@mdi/js";
|
import { mdiCalendarClock, mdiClose } from "@mdi/js";
|
||||||
import { addDays, isSameDay } from "date-fns/esm";
|
import { addDays, isSameDay } from "date-fns/esm";
|
||||||
|
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";
|
||||||
@ -22,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;
|
||||||
@ -47,7 +44,7 @@ class DialogCalendarEventDetail extends LitElement {
|
|||||||
if (params.entry) {
|
if (params.entry) {
|
||||||
const entry = params.entry!;
|
const entry = params.entry!;
|
||||||
this._data = entry;
|
this._data = entry;
|
||||||
this._calendarId = params.calendarId || params.calendars[0].entity_id;
|
this._calendarId = params.calendarId;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -101,6 +98,7 @@ class DialogCalendarEventDetail extends LitElement {
|
|||||||
<state-info
|
<state-info
|
||||||
.hass=${this.hass}
|
.hass=${this.hass}
|
||||||
.stateObj=${stateObj}
|
.stateObj=${stateObj}
|
||||||
|
.color=${this._params.color}
|
||||||
inDialog
|
inDialog
|
||||||
></state-info>
|
></state-info>
|
||||||
</div>
|
</div>
|
||||||
@ -135,60 +133,23 @@ 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() {
|
||||||
const start = new Date(this._data!.dtstart);
|
// Parse a dates in the browser timezone
|
||||||
|
const timeZone = Intl.DateTimeFormat().resolvedOptions().timeZone;
|
||||||
|
const start = toDate(this._data!.dtstart, { timeZone: timeZone });
|
||||||
|
const endValue = toDate(this._data!.dtend, { timeZone: timeZone });
|
||||||
// All day events should be displayed as a day earlier
|
// All day events should be displayed as a day earlier
|
||||||
const end = isDate(this._data.dtend)
|
const end = isDate(this._data.dtend) ? addDays(endValue, -1) : endValue;
|
||||||
? addDays(new Date(this._data!.dtend), -1)
|
|
||||||
: new Date(this._data!.dtend);
|
|
||||||
// The range can be shortened when the start and end are on the same day.
|
// The range can be shortened when the start and end are on the same day.
|
||||||
if (isSameDay(start, end)) {
|
if (isSameDay(start, end)) {
|
||||||
if (isDate(this._data.dtstart)) {
|
if (isDate(this._data.dtstart)) {
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
import "@material/mwc-button";
|
import "@material/mwc-button";
|
||||||
import { mdiClose } from "@mdi/js";
|
import { mdiClose } from "@mdi/js";
|
||||||
import { ComboBoxLitRenderer } from "@vaadin/combo-box/lit";
|
|
||||||
import {
|
import {
|
||||||
addDays,
|
addDays,
|
||||||
addHours,
|
addHours,
|
||||||
@ -8,33 +7,35 @@ import {
|
|||||||
differenceInMilliseconds,
|
differenceInMilliseconds,
|
||||||
startOfHour,
|
startOfHour,
|
||||||
} from "date-fns/esm";
|
} from "date-fns/esm";
|
||||||
|
import { formatInTimeZone, toDate } from "date-fns-tz";
|
||||||
|
import { HassEntity } 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 memoizeOne from "memoize-one";
|
import memoizeOne from "memoize-one";
|
||||||
import { fireEvent } from "../../common/dom/fire_event";
|
import { fireEvent } from "../../common/dom/fire_event";
|
||||||
|
import { computeStateDomain } from "../../common/entity/compute_state_domain";
|
||||||
|
import { supportsFeature } from "../../common/entity/supports-feature";
|
||||||
import { isDate } from "../../common/string/is_date";
|
import { isDate } from "../../common/string/is_date";
|
||||||
|
import "../../components/entity/ha-entity-picker";
|
||||||
import "../../components/ha-date-input";
|
import "../../components/ha-date-input";
|
||||||
import "../../components/ha-textarea";
|
import "../../components/ha-textarea";
|
||||||
import "../../components/ha-time-input";
|
import "../../components/ha-time-input";
|
||||||
import {
|
import {
|
||||||
Calendar,
|
CalendarEntityFeature,
|
||||||
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";
|
||||||
import "../lovelace/components/hui-generic-entity-row";
|
import "../lovelace/components/hui-generic-entity-row";
|
||||||
import "./ha-recurrence-rule-editor";
|
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 { CalendarEventEditDialogParams } from "./show-dialog-calendar-event-editor";
|
import { CalendarEventEditDialogParams } from "./show-dialog-calendar-event-editor";
|
||||||
|
|
||||||
const rowRenderer: ComboBoxLitRenderer<Calendar> = (
|
const CALENDAR_DOMAINS = ["calendar"];
|
||||||
item
|
|
||||||
) => html`<mwc-list-item>
|
|
||||||
<span>${item.name}</span>
|
|
||||||
</mwc-list-item>`;
|
|
||||||
|
|
||||||
@customElement("dialog-calendar-event-editor")
|
@customElement("dialog-calendar-event-editor")
|
||||||
class DialogCalendarEventEditor extends LitElement {
|
class DialogCalendarEventEditor extends LitElement {
|
||||||
@ -44,9 +45,7 @@ class DialogCalendarEventEditor extends LitElement {
|
|||||||
|
|
||||||
@state() private _info?: string;
|
@state() private _info?: string;
|
||||||
|
|
||||||
@state() private _params?: CalendarEventDetailDialogParams;
|
@state() private _params?: CalendarEventEditDialogParams;
|
||||||
|
|
||||||
@state() private _calendars: Calendar[] = [];
|
|
||||||
|
|
||||||
@state() private _calendarId?: string;
|
@state() private _calendarId?: string;
|
||||||
|
|
||||||
@ -64,22 +63,36 @@ class DialogCalendarEventEditor extends LitElement {
|
|||||||
|
|
||||||
@state() private _submitting = false;
|
@state() private _submitting = false;
|
||||||
|
|
||||||
|
// Dates are manipulated and displayed in the browser timezone
|
||||||
|
// which may be different from the Home Assistant timezone. When
|
||||||
|
// events are persisted, they are relative to the Home Assistant
|
||||||
|
// timezone, but floating without a timezone.
|
||||||
|
private _timeZone?: string;
|
||||||
|
|
||||||
public showDialog(params: CalendarEventEditDialogParams): void {
|
public showDialog(params: CalendarEventEditDialogParams): void {
|
||||||
this._error = undefined;
|
this._error = undefined;
|
||||||
this._info = undefined;
|
this._info = undefined;
|
||||||
this._params = params;
|
this._params = params;
|
||||||
this._calendars = params.calendars;
|
this._calendarId =
|
||||||
this._calendarId = params.calendarId || this._calendars[0].entity_id;
|
params.calendarId ||
|
||||||
|
Object.values(this.hass.states).find(
|
||||||
|
(stateObj) =>
|
||||||
|
computeStateDomain(stateObj) === "calendar" &&
|
||||||
|
supportsFeature(stateObj, CalendarEntityFeature.CREATE_EVENT)
|
||||||
|
)?.entity_id;
|
||||||
|
this._timeZone =
|
||||||
|
Intl.DateTimeFormat().resolvedOptions().timeZone ||
|
||||||
|
this.hass.config.time_zone;
|
||||||
if (params.entry) {
|
if (params.entry) {
|
||||||
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._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);
|
||||||
@ -99,7 +112,6 @@ class DialogCalendarEventEditor extends LitElement {
|
|||||||
if (!this._params) {
|
if (!this._params) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this._calendars = [];
|
|
||||||
this._calendarId = undefined;
|
this._calendarId = undefined;
|
||||||
this._params = undefined;
|
this._params = undefined;
|
||||||
this._dtstart = undefined;
|
this._dtstart = undefined;
|
||||||
@ -158,6 +170,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")}
|
||||||
@ -169,22 +182,20 @@ 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>
|
||||||
<ha-combo-box
|
<ha-entity-picker
|
||||||
name="calendar"
|
name="calendar"
|
||||||
.hass=${this.hass}
|
.hass=${this.hass}
|
||||||
.label=${this.hass.localize("ui.components.calendar.label")}
|
.label=${this.hass.localize("ui.components.calendar.label")}
|
||||||
.value=${this._calendarId!}
|
.value=${this._calendarId!}
|
||||||
.renderer=${rowRenderer}
|
.includeDomains=${CALENDAR_DOMAINS}
|
||||||
.items=${this._calendars}
|
.entityFilter=${this._isEditableCalendar}
|
||||||
item-id-path="entity_id"
|
|
||||||
item-value-path="entity_id"
|
|
||||||
item-label-path="name"
|
|
||||||
required
|
required
|
||||||
@value-changed=${this._handleCalendarChanged}
|
@value-changed=${this._handleCalendarChanged}
|
||||||
></ha-combo-box>
|
></ha-entity-picker>
|
||||||
<ha-formfield
|
<ha-formfield
|
||||||
.label=${this.hass.localize("ui.components.calendar.event.all_day")}
|
.label=${this.hass.localize("ui.components.calendar.event.all_day")}
|
||||||
>
|
>
|
||||||
@ -237,6 +248,8 @@ class DialogCalendarEventEditor extends LitElement {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<ha-recurrence-rule-editor
|
<ha-recurrence-rule-editor
|
||||||
|
.hass=${this.hass}
|
||||||
|
.dtstart=${this._dtstart}
|
||||||
.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 || ""}
|
||||||
@ -281,20 +294,33 @@ class DialogCalendarEventEditor extends LitElement {
|
|||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
private _getLocaleStrings = memoizeOne((startDate?: Date, endDate?: Date) =>
|
private _isEditableCalendar = (entityStateObj: HassEntity) =>
|
||||||
// en-CA locale used for date format YYYY-MM-DD
|
supportsFeature(entityStateObj, CalendarEntityFeature.CREATE_EVENT);
|
||||||
// en-GB locale used for 24h time format HH:MM:SS
|
|
||||||
{
|
private _getLocaleStrings = memoizeOne(
|
||||||
const timeZone = this.hass.config.time_zone;
|
(startDate?: Date, endDate?: Date) => ({
|
||||||
return {
|
startDate: this._formatDate(startDate!),
|
||||||
startDate: startDate?.toLocaleDateString("en-CA", { timeZone }),
|
startTime: this._formatTime(startDate!),
|
||||||
startTime: startDate?.toLocaleTimeString("en-GB", { timeZone }),
|
endDate: this._formatDate(endDate!),
|
||||||
endDate: endDate?.toLocaleDateString("en-CA", { timeZone }),
|
endTime: this._formatTime(endDate!),
|
||||||
endTime: endDate?.toLocaleTimeString("en-GB", { timeZone }),
|
})
|
||||||
};
|
|
||||||
}
|
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Formats a date in specified timezone, or defaulting to browser display timezone
|
||||||
|
private _formatDate(date: Date, timeZone: string = this._timeZone!): string {
|
||||||
|
return formatInTimeZone(date, timeZone, "yyyy-MM-dd");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Formats a time in specified timezone, or defaulting to browser display timezone
|
||||||
|
private _formatTime(date: Date, timeZone: string = this._timeZone!): string {
|
||||||
|
return formatInTimeZone(date, timeZone, "HH:mm:ss"); // 24 hr
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse a date in the browser timezone
|
||||||
|
private _parseDate(dateStr: string): Date {
|
||||||
|
return toDate(dateStr, { timeZone: this._timeZone! });
|
||||||
|
}
|
||||||
|
|
||||||
private _clearInfo() {
|
private _clearInfo() {
|
||||||
this._info = undefined;
|
this._info = undefined;
|
||||||
}
|
}
|
||||||
@ -319,27 +345,14 @@ class DialogCalendarEventEditor extends LitElement {
|
|||||||
// Store previous event duration
|
// Store previous event duration
|
||||||
const duration = differenceInMilliseconds(this._dtend!, this._dtstart!);
|
const duration = differenceInMilliseconds(this._dtend!, this._dtstart!);
|
||||||
|
|
||||||
this._dtstart = new Date(
|
this._dtstart = this._parseDate(
|
||||||
ev.detail.value +
|
`${ev.detail.value}T${this._formatTime(this._dtstart!)}`
|
||||||
"T" +
|
|
||||||
this._dtstart!.toLocaleTimeString("en-GB", {
|
|
||||||
timeZone: this.hass.config.time_zone,
|
|
||||||
})
|
|
||||||
);
|
);
|
||||||
|
|
||||||
// Prevent that the end time can be before the start time. Try to keep the
|
// Prevent that the end time can be before the start time. Try to keep the
|
||||||
// duration the same.
|
// duration the same.
|
||||||
if (this._dtend! <= this._dtstart!) {
|
if (this._dtend! <= this._dtstart!) {
|
||||||
const newEnd = addMilliseconds(this._dtstart, duration);
|
this._dtend = addMilliseconds(this._dtstart, duration);
|
||||||
// en-CA locale used for date format YYYY-MM-DD
|
|
||||||
// en-GB locale used for 24h time format HH:MM:SS
|
|
||||||
this._dtend = new Date(
|
|
||||||
`${newEnd.toLocaleDateString("en-CA", {
|
|
||||||
timeZone: this.hass.config.time_zone,
|
|
||||||
})}T${newEnd.toLocaleTimeString("en-GB", {
|
|
||||||
timeZone: this.hass.config.time_zone,
|
|
||||||
})}`
|
|
||||||
);
|
|
||||||
this._info = this.hass.localize(
|
this._info = this.hass.localize(
|
||||||
"ui.components.calendar.event.end_auto_adjusted"
|
"ui.components.calendar.event.end_auto_adjusted"
|
||||||
);
|
);
|
||||||
@ -347,12 +360,8 @@ class DialogCalendarEventEditor extends LitElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private _endDateChanged(ev: CustomEvent) {
|
private _endDateChanged(ev: CustomEvent) {
|
||||||
this._dtend = new Date(
|
this._dtend = this._parseDate(
|
||||||
ev.detail.value +
|
`${ev.detail.value}T${this._formatTime(this._dtend!)}`
|
||||||
"T" +
|
|
||||||
this._dtend!.toLocaleTimeString("en-GB", {
|
|
||||||
timeZone: this.hass.config.time_zone,
|
|
||||||
})
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -360,25 +369,14 @@ class DialogCalendarEventEditor extends LitElement {
|
|||||||
// Store previous event duration
|
// Store previous event duration
|
||||||
const duration = differenceInMilliseconds(this._dtend!, this._dtstart!);
|
const duration = differenceInMilliseconds(this._dtend!, this._dtstart!);
|
||||||
|
|
||||||
this._dtstart = new Date(
|
this._dtstart = this._parseDate(
|
||||||
this._dtstart!.toLocaleDateString("en-CA", {
|
`${this._formatDate(this._dtstart!)}T${ev.detail.value}`
|
||||||
timeZone: this.hass.config.time_zone,
|
|
||||||
}) +
|
|
||||||
"T" +
|
|
||||||
ev.detail.value
|
|
||||||
);
|
);
|
||||||
|
|
||||||
// Prevent that the end time can be before the start time. Try to keep the
|
// Prevent that the end time can be before the start time. Try to keep the
|
||||||
// duration the same.
|
// duration the same.
|
||||||
if (this._dtend! <= this._dtstart!) {
|
if (this._dtend! <= this._dtstart!) {
|
||||||
const newEnd = addMilliseconds(new Date(this._dtstart), duration);
|
this._dtend = addMilliseconds(new Date(this._dtstart), duration);
|
||||||
this._dtend = new Date(
|
|
||||||
`${newEnd.toLocaleDateString("en-CA", {
|
|
||||||
timeZone: this.hass.config.time_zone,
|
|
||||||
})}T${newEnd.toLocaleTimeString("en-GB", {
|
|
||||||
timeZone: this.hass.config.time_zone,
|
|
||||||
})}`
|
|
||||||
);
|
|
||||||
this._info = this.hass.localize(
|
this._info = this.hass.localize(
|
||||||
"ui.components.calendar.event.end_auto_adjusted"
|
"ui.components.calendar.event.end_auto_adjusted"
|
||||||
);
|
);
|
||||||
@ -386,36 +384,32 @@ class DialogCalendarEventEditor extends LitElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private _endTimeChanged(ev: CustomEvent) {
|
private _endTimeChanged(ev: CustomEvent) {
|
||||||
this._dtend = new Date(
|
this._dtend = this._parseDate(
|
||||||
this._dtend!.toLocaleDateString("en-CA", {
|
`${this._formatDate(this._dtend!)}T${ev.detail.value}`
|
||||||
timeZone: this.hass.config.time_zone,
|
|
||||||
}) +
|
|
||||||
"T" +
|
|
||||||
ev.detail.value
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
private _calculateData() {
|
private _calculateData() {
|
||||||
const { startDate, startTime, endDate, endTime } = this._getLocaleStrings(
|
|
||||||
this._dtstart,
|
|
||||||
this._dtend
|
|
||||||
);
|
|
||||||
const data: CalendarEventMutableParams = {
|
const data: CalendarEventMutableParams = {
|
||||||
summary: this._summary,
|
summary: this._summary,
|
||||||
description: this._description,
|
description: this._description,
|
||||||
rrule: this._rrule,
|
rrule: this._rrule || undefined,
|
||||||
dtstart: "",
|
dtstart: "",
|
||||||
dtend: "",
|
dtend: "",
|
||||||
};
|
};
|
||||||
if (this._allDay) {
|
if (this._allDay) {
|
||||||
data.dtstart = startDate!;
|
data.dtstart = this._formatDate(this._dtstart!);
|
||||||
// End date/time is exclusive when persisted
|
// End date/time is exclusive when persisted
|
||||||
data.dtend = addDays(new Date(this._dtend!), 1).toLocaleDateString(
|
data.dtend = this._formatDate(addDays(this._dtend!, 1));
|
||||||
"en-CA"
|
|
||||||
);
|
|
||||||
} else {
|
} else {
|
||||||
data.dtstart = `${startDate}T${startTime}`;
|
data.dtstart = `${this._formatDate(
|
||||||
data.dtend = `${endDate}T${endTime}`;
|
this._dtstart!,
|
||||||
|
this.hass.config.time_zone
|
||||||
|
)}T${this._formatTime(this._dtstart!, this.hass.config.time_zone)}`;
|
||||||
|
data.dtend = `${this._formatDate(
|
||||||
|
this._dtend!,
|
||||||
|
this.hass.config.time_zone
|
||||||
|
)}T${this._formatTime(this._dtend!, this.hass.config.time_zone)}`;
|
||||||
}
|
}
|
||||||
return data;
|
return data;
|
||||||
}
|
}
|
||||||
@ -424,6 +418,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(
|
||||||
@ -432,7 +433,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"
|
||||||
);
|
);
|
||||||
@ -457,7 +458,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() {
|
||||||
@ -557,9 +612,6 @@ class DialogCalendarEventEditor extends LitElement {
|
|||||||
ha-rrule {
|
ha-rrule {
|
||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
ha-combo-box {
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
ha-svg-icon {
|
ha-svg-icon {
|
||||||
width: 40px;
|
width: 40px;
|
||||||
margin-right: 8px;
|
margin-right: 8px;
|
||||||
|
@ -200,7 +200,7 @@ export class HAFullCalendar extends LitElement {
|
|||||||
: ""}
|
: ""}
|
||||||
|
|
||||||
<div id="calendar"></div>
|
<div id="calendar"></div>
|
||||||
${this._mutableCalendars.length > 0
|
${this._hasMutableCalendars
|
||||||
? html`<ha-fab
|
? html`<ha-fab
|
||||||
slot="fab"
|
slot="fab"
|
||||||
.label=${this.hass.localize("ui.components.calendar.event.add")}
|
.label=${this.hass.localize("ui.components.calendar.event.add")}
|
||||||
@ -270,17 +270,15 @@ export class HAFullCalendar extends LitElement {
|
|||||||
this._fireViewChanged();
|
this._fireViewChanged();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Return calendars that support creating events
|
// Return if there are calendars that support creating events
|
||||||
private get _mutableCalendars(): CalendarData[] {
|
private get _hasMutableCalendars(): boolean {
|
||||||
return this.calendars
|
return this.calendars.some((selCal) => {
|
||||||
.filter((selCal) => {
|
const entityStateObj = this.hass.states[selCal.entity_id];
|
||||||
const entityStateObj = this.hass.states[selCal.entity_id];
|
return (
|
||||||
return (
|
entityStateObj &&
|
||||||
entityStateObj &&
|
supportsFeature(entityStateObj, CalendarEntityFeature.CREATE_EVENT)
|
||||||
supportsFeature(entityStateObj, CalendarEntityFeature.CREATE_EVENT)
|
);
|
||||||
);
|
});
|
||||||
})
|
|
||||||
.map((cal) => cal);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private _createEvent(_info) {
|
private _createEvent(_info) {
|
||||||
@ -289,7 +287,6 @@ export class HAFullCalendar extends LitElement {
|
|||||||
// current actual month, as for that one the current day is automatically highlighted and
|
// current actual month, as for that one the current day is automatically highlighted and
|
||||||
// defaulting to a different day in the event creation dialog would be weird.
|
// defaulting to a different day in the event creation dialog would be weird.
|
||||||
showCalendarEventEditDialog(this, {
|
showCalendarEventEditDialog(this, {
|
||||||
calendars: this._mutableCalendars,
|
|
||||||
selectedDate:
|
selectedDate:
|
||||||
this._activeView === "dayGridWeek" ||
|
this._activeView === "dayGridWeek" ||
|
||||||
this._activeView === "dayGridDay" ||
|
this._activeView === "dayGridDay" ||
|
||||||
@ -305,16 +302,20 @@ 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);
|
||||||
showCalendarEventDetailDialog(this, {
|
showCalendarEventDetailDialog(this, {
|
||||||
calendars: this.calendars,
|
|
||||||
calendarId: info.event.extendedProps.calendar,
|
calendarId: info.event.extendedProps.calendar,
|
||||||
entry: info.event.extendedProps.eventData,
|
entry: info.event.extendedProps.eventData,
|
||||||
|
color: info.event.backgroundColor,
|
||||||
updated: () => {
|
updated: () => {
|
||||||
this._fireViewChanged();
|
this._fireViewChanged();
|
||||||
},
|
},
|
||||||
|
canEdit: canEdit,
|
||||||
canDelete: canDelete,
|
canDelete: canDelete,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import type { SelectedDetail } from "@material/mwc-list";
|
import type { SelectedDetail } from "@material/mwc-list";
|
||||||
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";
|
||||||
@ -16,22 +16,31 @@ import {
|
|||||||
convertFrequency,
|
convertFrequency,
|
||||||
convertRepeatFrequency,
|
convertRepeatFrequency,
|
||||||
DEFAULT_COUNT,
|
DEFAULT_COUNT,
|
||||||
|
getWeekday,
|
||||||
getWeekdays,
|
getWeekdays,
|
||||||
|
getMonthlyRepeatItems,
|
||||||
intervalSuffix,
|
intervalSuffix,
|
||||||
RepeatEnd,
|
RepeatEnd,
|
||||||
RepeatFrequency,
|
RepeatFrequency,
|
||||||
ruleByWeekDay,
|
ruleByWeekDay,
|
||||||
untilValue,
|
untilValue,
|
||||||
WEEKDAY_NAME,
|
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({ attribute: false }) public locale!: HomeAssistant["locale"];
|
@property({ attribute: false }) public locale!: HomeAssistant["locale"];
|
||||||
|
|
||||||
@property() public timezone?: string;
|
@property() public timezone?: string;
|
||||||
@ -44,14 +53,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 _until?: 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,12 +80,45 @@ 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("timezone") ||
|
||||||
|
changedProps.has("_freq") ||
|
||||||
|
changedProps.has("_interval") ||
|
||||||
|
changedProps.has("_weekday") ||
|
||||||
|
changedProps.has("_monthlyRepeatWeekday") ||
|
||||||
|
changedProps.has("_monthday") ||
|
||||||
|
changedProps.has("_end") ||
|
||||||
|
changedProps.has("_count") ||
|
||||||
|
changedProps.has("_until")
|
||||||
|
) {
|
||||||
|
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._until = undefined;
|
||||||
@ -88,6 +140,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 &&
|
||||||
@ -129,7 +189,28 @@ export class RecurrenceRuleEditor extends LitElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
renderMonthly() {
|
renderMonthly() {
|
||||||
return this.renderInterval();
|
return html`
|
||||||
|
${this.renderInterval()}
|
||||||
|
${this._monthlyRepeatItems.length > 0
|
||||||
|
? html`<ha-select
|
||||||
|
id="monthly"
|
||||||
|
label="Repeat Monthly"
|
||||||
|
@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() {
|
||||||
@ -222,7 +303,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 +313,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 +337,6 @@ export class RecurrenceRuleEditor extends LitElement {
|
|||||||
} else {
|
} else {
|
||||||
this._weekday.delete(value);
|
this._weekday.delete(value);
|
||||||
}
|
}
|
||||||
this._updateRule();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private _onEndSelected(e: CustomEvent<SelectedDetail<number>>) {
|
private _onEndSelected(e: CustomEvent<SelectedDetail<number>>) {
|
||||||
@ -270,31 +360,47 @@ export class RecurrenceRuleEditor extends LitElement {
|
|||||||
this._until = undefined;
|
this._until = 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._until = new Date(e.detail.value);
|
||||||
this._updateRule();
|
}
|
||||||
|
|
||||||
|
// 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,
|
until: this._until,
|
||||||
tzid: this.timezone,
|
tzid: this.timezone,
|
||||||
|
byweekday: byweekday,
|
||||||
|
bymonthday: bymonthday,
|
||||||
};
|
};
|
||||||
const contentline = RRule.optionsToString(options);
|
const contentline = RRule.optionsToString(options);
|
||||||
return contentline.slice(6); // Strip "RRULE:" prefix
|
return contentline.slice(6); // Strip "RRULE:" prefix
|
||||||
|
@ -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,6 +35,13 @@ export const DEFAULT_COUNT = {
|
|||||||
daily: 30,
|
daily: 30,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export interface MonthlyRepeatItem {
|
||||||
|
value: string;
|
||||||
|
byday?: Weekday;
|
||||||
|
bymonthday?: number;
|
||||||
|
label: string;
|
||||||
|
}
|
||||||
|
|
||||||
export function intervalSuffix(freq: RepeatFrequency) {
|
export function intervalSuffix(freq: RepeatFrequency) {
|
||||||
if (freq === "monthly") {
|
if (freq === "monthly") {
|
||||||
return "months";
|
return "months";
|
||||||
@ -101,7 +122,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 +144,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 +166,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);
|
||||||
|
}
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
@ -1,13 +1,13 @@
|
|||||||
import { fireEvent } from "../../common/dom/fire_event";
|
import { fireEvent } from "../../common/dom/fire_event";
|
||||||
import { Calendar, CalendarEventData } from "../../data/calendar";
|
import { CalendarEventData } from "../../data/calendar";
|
||||||
|
|
||||||
export interface CalendarEventDetailDialogParams {
|
export interface CalendarEventDetailDialogParams {
|
||||||
calendars: Calendar[]; // When creating new events, is the list of calendar entities that support creation
|
calendarId: string;
|
||||||
calendarId?: string;
|
entry: CalendarEventData;
|
||||||
entry?: CalendarEventData;
|
|
||||||
canDelete?: boolean;
|
canDelete?: boolean;
|
||||||
canEdit?: boolean;
|
canEdit?: boolean;
|
||||||
updated: () => void;
|
updated: () => void;
|
||||||
|
color?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const loadCalendarEventDetailDialog = () =>
|
export const loadCalendarEventDetailDialog = () =>
|
||||||
|
@ -1,8 +1,7 @@
|
|||||||
import { fireEvent } from "../../common/dom/fire_event";
|
import { fireEvent } from "../../common/dom/fire_event";
|
||||||
import { Calendar, CalendarEventData } from "../../data/calendar";
|
import { CalendarEventData } from "../../data/calendar";
|
||||||
|
|
||||||
export interface CalendarEventEditDialogParams {
|
export interface CalendarEventEditDialogParams {
|
||||||
calendars: Calendar[]; // When creating new events, is the list of calendar entities that support creation
|
|
||||||
calendarId?: string;
|
calendarId?: string;
|
||||||
selectedDate?: Date; // When provided is used as the pre-filled date for the event creation dialog
|
selectedDate?: Date; // When provided is used as the pre-filled date for the event creation dialog
|
||||||
entry?: CalendarEventData;
|
entry?: CalendarEventData;
|
||||||
|
@ -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
|
||||||
@ -507,7 +507,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 +525,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) {
|
||||||
|
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