Compare commits
148 Commits
fix-button
...
20220705.0
Author | SHA1 | Date | |
---|---|---|---|
![]() |
f6d6fd179f | ||
![]() |
153ebb2a20 | ||
![]() |
5d58e52eea | ||
![]() |
5038f9c3c6 | ||
![]() |
b285fda61b | ||
![]() |
6cd38472cd | ||
![]() |
8fd5f53f96 | ||
![]() |
b70eee77ef | ||
![]() |
4148b8c7aa | ||
![]() |
e22dd0c49d | ||
![]() |
30a254f98f | ||
![]() |
8fcb3a017b | ||
![]() |
5e29c7efa9 | ||
![]() |
b6cc3e3ef0 | ||
![]() |
184bdc0c85 | ||
![]() |
f7fb731dc8 | ||
![]() |
77977f64a3 | ||
![]() |
e8da573ba2 | ||
![]() |
07332bf155 | ||
![]() |
f3c7583bf7 | ||
![]() |
1cc02415d3 | ||
![]() |
6ca3f06ea0 | ||
![]() |
198e2b7bdf | ||
![]() |
5a68e2c977 | ||
![]() |
d9d29db560 | ||
![]() |
124c6dc2b8 | ||
![]() |
0f3886e053 | ||
![]() |
68bb3558b4 | ||
![]() |
b8bd15aa33 | ||
![]() |
ed39aa6a7c | ||
![]() |
405cae9b5f | ||
![]() |
3ca2cbb3f9 | ||
![]() |
c295ae56ab | ||
![]() |
830364721b | ||
![]() |
19089213e3 | ||
![]() |
b633067e5c | ||
![]() |
1b8874cbd4 | ||
![]() |
a5f8ce85ba | ||
![]() |
56eacf5733 | ||
![]() |
9324061d05 | ||
![]() |
eafcbdc65b | ||
![]() |
0175522c17 | ||
![]() |
cff3f51d34 | ||
![]() |
0f580a91c9 | ||
![]() |
389f50b29a | ||
![]() |
b689bb8fcf | ||
![]() |
36f067ede4 | ||
![]() |
c2178622dd | ||
![]() |
014448e7ea | ||
![]() |
08eff0509a | ||
![]() |
62d0882e82 | ||
![]() |
86a574dbbd | ||
![]() |
71ac4620c5 | ||
![]() |
f611049517 | ||
![]() |
45fa8c272f | ||
![]() |
28a1c97571 | ||
![]() |
d9a5ae0cf1 | ||
![]() |
c03849d30b | ||
![]() |
535fe2686b | ||
![]() |
709bc87a36 | ||
![]() |
2812b467ec | ||
![]() |
7d118a5715 | ||
![]() |
8bd7370a02 | ||
![]() |
9fa8a96d09 | ||
![]() |
508d1fffef | ||
![]() |
3633daa814 | ||
![]() |
05346ae9fc | ||
![]() |
ea667cf0b9 | ||
![]() |
048ac3965e | ||
![]() |
276b6f4d1f | ||
![]() |
e765d7749c | ||
![]() |
9a3b4d6df2 | ||
![]() |
529e27992e | ||
![]() |
6c5cf2a0ec | ||
![]() |
a4cb270f09 | ||
![]() |
5160a1f55c | ||
![]() |
6a3a0db338 | ||
![]() |
765d4eb3b4 | ||
![]() |
cc09e24d66 | ||
![]() |
e7848262ea | ||
![]() |
0926202eca | ||
![]() |
e83af02410 | ||
![]() |
74d6a52fa9 | ||
![]() |
5baa975632 | ||
![]() |
4ad49ef07f | ||
![]() |
bc47ecaa57 | ||
![]() |
2bd617ce6e | ||
![]() |
dbaf955525 | ||
![]() |
578ff5b53f | ||
![]() |
e386942ea7 | ||
![]() |
2fdd50f45f | ||
![]() |
4b36770adf | ||
![]() |
54377225ec | ||
![]() |
f020add6be | ||
![]() |
b1a3996cf1 | ||
![]() |
a47a0ed716 | ||
![]() |
91cd584b4b | ||
![]() |
75562efb79 | ||
![]() |
f464bcfc14 | ||
![]() |
f8af66d310 | ||
![]() |
4922e575f8 | ||
![]() |
ac08daa64e | ||
![]() |
97f082a384 | ||
![]() |
ced37aab4c | ||
![]() |
1938fb89e6 | ||
![]() |
6842c479d6 | ||
![]() |
881f6b0531 | ||
![]() |
a564ceb9e3 | ||
![]() |
077fa3f6b2 | ||
![]() |
ceda911670 | ||
![]() |
afd41e79f0 | ||
![]() |
10f63180eb | ||
![]() |
e54802bd87 | ||
![]() |
c1d6b51065 | ||
![]() |
ab65ce819f | ||
![]() |
1e011bfe34 | ||
![]() |
5951f5c5c4 | ||
![]() |
0183e32267 | ||
![]() |
588fd87654 | ||
![]() |
e2944b098d | ||
![]() |
cbb962f084 | ||
![]() |
93f4ae1bea | ||
![]() |
d810cae194 | ||
![]() |
6797e17fc8 | ||
![]() |
6e58cd5d12 | ||
![]() |
a72fd19b73 | ||
![]() |
41c61a2895 | ||
![]() |
f35af9ed98 | ||
![]() |
abf7cb7a74 | ||
![]() |
6ec2e32241 | ||
![]() |
b7cdd9a22f | ||
![]() |
6278eefc5d | ||
![]() |
73cf0b54c9 | ||
![]() |
00dcecabb7 | ||
![]() |
c9df93bc54 | ||
![]() |
3550a8c263 | ||
![]() |
c0d30c56d6 | ||
![]() |
10813d06b6 | ||
![]() |
d65e45ecfd | ||
![]() |
1b158d8310 | ||
![]() |
9d2fcec458 | ||
![]() |
60cd6c65f0 | ||
![]() |
a39af9c307 | ||
![]() |
02af4c2156 | ||
![]() |
d02cd122a9 | ||
![]() |
8e962fdecb | ||
![]() |
1f65193a97 | ||
![]() |
24484d0e74 |
8
.github/dependabot.yml
vendored
Normal file
@@ -0,0 +1,8 @@
|
||||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: "github-actions"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: weekly
|
||||
time: "06:00"
|
||||
open-pull-requests-limit: 10
|
16
.github/workflows/ci.yaml
vendored
@@ -19,9 +19,9 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check out files from GitHub
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v3
|
||||
- name: Set up Node ${{ env.NODE_VERSION }}
|
||||
uses: actions/setup-node@v2
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: ${{ env.NODE_VERSION }}
|
||||
cache: yarn
|
||||
@@ -43,9 +43,9 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check out files from GitHub
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v3
|
||||
- name: Set up Node ${{ env.NODE_VERSION }}
|
||||
uses: actions/setup-node@v2
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: ${{ env.NODE_VERSION }}
|
||||
cache: yarn
|
||||
@@ -62,9 +62,9 @@ jobs:
|
||||
needs: [lint, test]
|
||||
steps:
|
||||
- name: Check out files from GitHub
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v3
|
||||
- name: Set up Node ${{ env.NODE_VERSION }}
|
||||
uses: actions/setup-node@v2
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: ${{ env.NODE_VERSION }}
|
||||
cache: yarn
|
||||
@@ -81,9 +81,9 @@ jobs:
|
||||
needs: [lint, test]
|
||||
steps:
|
||||
- name: Check out files from GitHub
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v3
|
||||
- name: Set up Node ${{ env.NODE_VERSION }}
|
||||
uses: actions/setup-node@v2
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: ${{ env.NODE_VERSION }}
|
||||
cache: yarn
|
||||
|
8
.github/workflows/codeql-analysis.yml
vendored
@@ -23,7 +23,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
# We must fetch at least the immediate parents so that if this is
|
||||
# a pull request then we can checkout the head.
|
||||
@@ -36,14 +36,14 @@ jobs:
|
||||
|
||||
# Initializes the CodeQL tools for scanning.
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@v1
|
||||
uses: github/codeql-action/init@v2
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
|
||||
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
|
||||
# If this step fails, then you should remove it and run the build manually (see below)
|
||||
- name: Autobuild
|
||||
uses: github/codeql-action/autobuild@v1
|
||||
uses: github/codeql-action/autobuild@v2
|
||||
|
||||
# ℹ️ Command-line programs to run using the OS shell.
|
||||
# 📚 https://git.io/JvXDl
|
||||
@@ -57,4 +57,4 @@ jobs:
|
||||
# make release
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@v1
|
||||
uses: github/codeql-action/analyze@v2
|
||||
|
4
.github/workflows/demo.yaml
vendored
@@ -14,9 +14,9 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check out files from GitHub
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v3
|
||||
- name: Set up Node ${{ env.NODE_VERSION }}
|
||||
uses: actions/setup-node@v2
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: ${{ env.NODE_VERSION }}
|
||||
cache: yarn
|
||||
|
2
.github/workflows/lock.yml
vendored
@@ -9,7 +9,7 @@ jobs:
|
||||
lock:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: dessant/lock-threads@v2.0.1
|
||||
- uses: dessant/lock-threads@v3.0.0
|
||||
with:
|
||||
github-token: ${{ github.token }}
|
||||
issue-lock-inactive-days: "30"
|
||||
|
63
.github/workflows/nightly.yaml
vendored
Normal file
@@ -0,0 +1,63 @@
|
||||
name: Nightly
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
schedule:
|
||||
- cron: "0 1 * * *"
|
||||
|
||||
env:
|
||||
PYTHON_VERSION: 3.8
|
||||
NODE_VERSION: 14
|
||||
NODE_OPTIONS: --max_old_space_size=6144
|
||||
|
||||
permissions:
|
||||
actions: none
|
||||
|
||||
jobs:
|
||||
nightly:
|
||||
name: Nightly
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
steps:
|
||||
- name: Checkout the repository
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Set up Python ${{ env.PYTHON_VERSION }}
|
||||
uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: ${{ env.PYTHON_VERSION }}
|
||||
|
||||
- name: Set up Node ${{ env.NODE_VERSION }}
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: ${{ env.NODE_VERSION }}
|
||||
cache: yarn
|
||||
|
||||
- name: Install dependencies
|
||||
run: yarn install
|
||||
|
||||
- name: Download translations
|
||||
run: ./script/translations_download
|
||||
env:
|
||||
LOKALISE_TOKEN: ${{ secrets.LOKALISE_TOKEN }}
|
||||
|
||||
- name: Bump version
|
||||
run: script/version_bump.js nightly
|
||||
|
||||
- name: Build nightly Python wheels
|
||||
run: |
|
||||
pip install build
|
||||
yarn install
|
||||
|
||||
script/build_frontend
|
||||
|
||||
rm -rf dist home_assistant_frontend.egg-info
|
||||
python3 -m build
|
||||
|
||||
- name: Upload build artifacts
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: wheels
|
||||
path: dist/home_assistant_frontend*.whl
|
||||
if-no-files-found: error
|
36
.github/workflows/release.yaml
vendored
@@ -24,18 +24,18 @@ jobs:
|
||||
contents: write # Required to upload release assets
|
||||
steps:
|
||||
- name: Checkout the repository
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Verify version
|
||||
uses: home-assistant/actions/helpers/verify-version@master
|
||||
|
||||
- name: Set up Python ${{ env.PYTHON_VERSION }}
|
||||
uses: actions/setup-python@v2
|
||||
uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: ${{ env.PYTHON_VERSION }}
|
||||
|
||||
- name: Set up Node ${{ env.NODE_VERSION }}
|
||||
uses: actions/setup-node@v2
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: ${{ env.NODE_VERSION }}
|
||||
cache: yarn
|
||||
@@ -74,33 +74,11 @@ jobs:
|
||||
version=$(echo "${{ github.ref }}" | awk -F"/" '{print $NF}' )
|
||||
echo "home-assistant-frontend==$version" > ./requirements.txt
|
||||
|
||||
- name: Upload requirements.txt
|
||||
uses: actions/upload-artifact@v2
|
||||
with:
|
||||
name: requirements
|
||||
path: ./requirements.txt
|
||||
|
||||
build-wheels:
|
||||
name: Build wheels for ${{ matrix.arch }}
|
||||
needs: wheels-init
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
arch: ["aarch64", "armhf", "armv7", "amd64", "i386"]
|
||||
tag:
|
||||
- "3.9-alpine3.14"
|
||||
steps:
|
||||
- name: Download requirements.txt
|
||||
uses: actions/download-artifact@v2
|
||||
with:
|
||||
name: requirements
|
||||
|
||||
- name: Build wheels
|
||||
uses: home-assistant/wheels@master
|
||||
uses: home-assistant/wheels@2022.06.7
|
||||
with:
|
||||
tag: ${{ matrix.tag }}
|
||||
arch: ${{ matrix.arch }}
|
||||
wheels-host: ${{ secrets.WHEELS_HOST }}
|
||||
abi: cp310
|
||||
tag: musllinux_1_2
|
||||
arch: amd64
|
||||
wheels-key: ${{ secrets.WHEELS_KEY }}
|
||||
wheels-user: wheels
|
||||
requirements: "requirements.txt"
|
||||
|
2
.github/workflows/translations.yaml
vendored
@@ -16,7 +16,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout the repository
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Upload Translations
|
||||
run: |
|
||||
|
2
.vscode/tasks.json
vendored
@@ -181,7 +181,7 @@
|
||||
{
|
||||
"label": "Run HA Core for Supervisor in devcontainer",
|
||||
"type": "shell",
|
||||
"command": "HASSIO=${input:supervisorHost} HASSIO_TOKEN=${input:supervisorToken} script/core",
|
||||
"command": "SUPERVISOR=${input:supervisorHost} SUPERVISOR_TOKEN=${input:supervisorToken} script/core",
|
||||
"isBackground": true,
|
||||
"group": {
|
||||
"kind": "build",
|
||||
|
@@ -27,7 +27,7 @@ module.exports = {
|
||||
version() {
|
||||
const version = fs
|
||||
.readFileSync(path.resolve(paths.polymer_dir, "pyproject.toml"), "utf8")
|
||||
.match(/version\W+=\W"(\d{8}\.\d)"/);
|
||||
.match(/version\W+=\W"(\d{8}\.\d(?:\.dev)?)"/);
|
||||
if (!version) {
|
||||
throw Error("Version not found");
|
||||
}
|
||||
|
@@ -156,3 +156,12 @@ gulp.task("gen-icons-json", (done) => {
|
||||
|
||||
done();
|
||||
});
|
||||
|
||||
gulp.task("gen-dummy-icons-json", (done) => {
|
||||
if (!fs.existsSync(OUTPUT_DIR)) {
|
||||
fs.mkdirSync(OUTPUT_DIR, { recursive: true });
|
||||
}
|
||||
|
||||
fs.writeFileSync(path.resolve(OUTPUT_DIR, "iconList.json"), "[]");
|
||||
done();
|
||||
});
|
||||
|
@@ -9,6 +9,7 @@ require("./compress.js");
|
||||
require("./rollup.js");
|
||||
require("./gather-static.js");
|
||||
require("./translations.js");
|
||||
require("./gen-icons-json.js");
|
||||
|
||||
gulp.task(
|
||||
"develop-hassio",
|
||||
@@ -17,6 +18,7 @@ gulp.task(
|
||||
process.env.NODE_ENV = "development";
|
||||
},
|
||||
"clean-hassio",
|
||||
"gen-dummy-icons-json",
|
||||
"gen-index-hassio-dev",
|
||||
"build-supervisor-translations",
|
||||
"copy-translations-supervisor",
|
||||
@@ -33,6 +35,7 @@ gulp.task(
|
||||
process.env.NODE_ENV = "production";
|
||||
},
|
||||
"clean-hassio",
|
||||
"gen-dummy-icons-json",
|
||||
"build-supervisor-translations",
|
||||
"copy-translations-supervisor",
|
||||
"build-locale-data",
|
||||
|
9
cast/public/_redirects
Normal file
@@ -0,0 +1,9 @@
|
||||
# These redirects are handled by Netlify
|
||||
#
|
||||
|
||||
# Some custom cards are not prefixing the instance URL when fetching data
|
||||
# and can end up fetching the data from the Cast domain instead of HA.
|
||||
# This will make sure that some common ones are replaced with a placeholder.
|
||||
/api/camera_proxy/* /images/google-nest-hub.png
|
||||
/api/camera_proxy_stream/* /images/google-nest-hub.png
|
||||
/api/media_player_proxy/* /images/google-nest-hub.png
|
@@ -59,7 +59,7 @@ export const demoEntitiesKernehed: DemoConfig["entities"] = () =>
|
||||
attributes: {
|
||||
hidden: true,
|
||||
radius: 50,
|
||||
friendly_name: "Skolan",
|
||||
friendly_name: "School",
|
||||
icon: "mdi:school",
|
||||
},
|
||||
},
|
||||
@@ -137,7 +137,7 @@ export const demoEntitiesKernehed: DemoConfig["entities"] = () =>
|
||||
state: "73",
|
||||
attributes: {
|
||||
unit_of_measurement: "%",
|
||||
friendly_name: "oskar batteri",
|
||||
friendly_name: "Oskar battery",
|
||||
device_class: "battery",
|
||||
},
|
||||
},
|
||||
@@ -146,7 +146,7 @@ export const demoEntitiesKernehed: DemoConfig["entities"] = () =>
|
||||
state: "88",
|
||||
attributes: {
|
||||
unit_of_measurement: "%",
|
||||
friendly_name: "bella batteri",
|
||||
friendly_name: "Bella battery",
|
||||
device_class: "battery",
|
||||
},
|
||||
},
|
||||
@@ -154,7 +154,7 @@ export const demoEntitiesKernehed: DemoConfig["entities"] = () =>
|
||||
entity_id: "binary_sensor.unifi_camera",
|
||||
state: "off",
|
||||
attributes: {
|
||||
friendly_name: "R\u00f6relsesensor kamera",
|
||||
friendly_name: "Motion sensor camera",
|
||||
icon: "mdi:walk",
|
||||
},
|
||||
},
|
||||
@@ -707,7 +707,7 @@ export const demoEntitiesKernehed: DemoConfig["entities"] = () =>
|
||||
},
|
||||
],
|
||||
cloudiness: 25,
|
||||
friendly_name: "V\u00e4der",
|
||||
friendly_name: "Weather",
|
||||
},
|
||||
},
|
||||
"binary_sensor.ubiquiti_switch": {
|
||||
@@ -731,7 +731,7 @@ export const demoEntitiesKernehed: DemoConfig["entities"] = () =>
|
||||
round_trip_time_max: "0.626",
|
||||
round_trip_time_mdev: "",
|
||||
round_trip_time_min: "0.358",
|
||||
friendly_name: "Entr\u00e9 kamera",
|
||||
friendly_name: "Entrance camera",
|
||||
device_class: "connectivity",
|
||||
icon: "mdi:cctv",
|
||||
},
|
||||
@@ -797,7 +797,7 @@ export const demoEntitiesKernehed: DemoConfig["entities"] = () =>
|
||||
attributes: {
|
||||
battery_level: 34,
|
||||
on: true,
|
||||
friendly_name: "altan_motion_sensor",
|
||||
friendly_name: "Porch motion sensor",
|
||||
device_class: "motion",
|
||||
},
|
||||
},
|
||||
@@ -807,7 +807,7 @@ export const demoEntitiesKernehed: DemoConfig["entities"] = () =>
|
||||
attributes: {
|
||||
battery_level: 88,
|
||||
on: true,
|
||||
friendly_name: "Altand\u00f6rren sensor",
|
||||
friendly_name: "Back door sensor",
|
||||
device_class: "opening",
|
||||
icon: "mdi:door",
|
||||
},
|
||||
@@ -818,7 +818,7 @@ export const demoEntitiesKernehed: DemoConfig["entities"] = () =>
|
||||
attributes: {
|
||||
battery_level: 74,
|
||||
on: true,
|
||||
friendly_name: "badrumssensor",
|
||||
friendly_name: "Bathroom motion sensor",
|
||||
device_class: "motion",
|
||||
},
|
||||
},
|
||||
@@ -829,7 +829,7 @@ export const demoEntitiesKernehed: DemoConfig["entities"] = () =>
|
||||
battery_level: 47,
|
||||
on: true,
|
||||
dark: true,
|
||||
friendly_name: "R\u00f6relsesensor k\u00e4llaren 1",
|
||||
friendly_name: "Basement motion sensor",
|
||||
device_class: "motion",
|
||||
icon: "mdi:walk",
|
||||
},
|
||||
@@ -841,7 +841,7 @@ export const demoEntitiesKernehed: DemoConfig["entities"] = () =>
|
||||
battery_level: 60,
|
||||
on: true,
|
||||
dark: true,
|
||||
friendly_name: "R\u00f6relsesensor tv\u00e4ttstugan",
|
||||
friendly_name: "Laundy room motion sensor",
|
||||
device_class: "motion",
|
||||
icon: "mdi:walk",
|
||||
},
|
||||
@@ -863,7 +863,7 @@ export const demoEntitiesKernehed: DemoConfig["entities"] = () =>
|
||||
attributes: {
|
||||
battery_level: 60,
|
||||
on: true,
|
||||
friendly_name: "R\u00f6relsesensor skafferiet",
|
||||
friendly_name: "Pantry motion sensor",
|
||||
device_class: "motion",
|
||||
icon: "mdi:walk",
|
||||
},
|
||||
@@ -875,7 +875,7 @@ export const demoEntitiesKernehed: DemoConfig["entities"] = () =>
|
||||
battery_level: 60,
|
||||
on: true,
|
||||
dark: true,
|
||||
friendly_name: "R\u00f6relsesensor k\u00e4llaren 2",
|
||||
friendly_name: "Stair motion sensor",
|
||||
device_class: "motion",
|
||||
icon: "mdi:walk",
|
||||
},
|
||||
@@ -887,7 +887,7 @@ export const demoEntitiesKernehed: DemoConfig["entities"] = () =>
|
||||
battery_level: 47,
|
||||
on: true,
|
||||
dark: true,
|
||||
friendly_name: "B\u00e4nksensor",
|
||||
friendly_name: "Bench sensor",
|
||||
device_class: "motion",
|
||||
},
|
||||
},
|
||||
|
@@ -277,7 +277,7 @@ export const demoLovelaceKernehed: DemoConfig["lovelace"] = () => ({
|
||||
],
|
||||
show_header_toggle: false,
|
||||
type: "entities",
|
||||
title: "Bandbredd",
|
||||
title: "Bandwidth",
|
||||
},
|
||||
// {
|
||||
// title: "Updater",
|
||||
|
@@ -1,7 +1,7 @@
|
||||
import { MockHomeAssistant } from "../../../src/fake_data/provide_hass";
|
||||
|
||||
export const mockConfig = (hass: MockHomeAssistant) => {
|
||||
hass.mockAPI("config/config_entries/entry", () => [
|
||||
hass.mockAPI("config/config_entries/entry?domain=co2signal", () => [
|
||||
{
|
||||
entry_id: "co2signal",
|
||||
domain: "co2signal",
|
||||
|
@@ -466,6 +466,7 @@ export const mockHistory = (mockHass: MockHomeAssistant) => {
|
||||
return results;
|
||||
}
|
||||
);
|
||||
mockHass.mockWS("recorder/get_statistics_metadata", () => []);
|
||||
mockHass.mockWS("history/list_statistic_ids", () => []);
|
||||
mockHass.mockWS(
|
||||
"history/statistics_during_period",
|
||||
|
Before Width: | Height: | Size: 44 KiB After Width: | Height: | Size: 32 KiB |
Before Width: | Height: | Size: 35 KiB After Width: | Height: | Size: 27 KiB |
Before Width: | Height: | Size: 67 KiB After Width: | Height: | Size: 46 KiB |
Before Width: | Height: | Size: 27 KiB After Width: | Height: | Size: 22 KiB |
Before Width: | Height: | Size: 94 KiB After Width: | Height: | Size: 90 KiB |
Before Width: | Height: | Size: 32 KiB After Width: | Height: | Size: 25 KiB |
@@ -6,10 +6,8 @@ import { atLeastVersion } from "../../../src/common/config/version";
|
||||
import { navigate } from "../../../src/common/navigate";
|
||||
import { caseInsensitiveStringCompare } from "../../../src/common/string/compare";
|
||||
import "../../../src/components/ha-card";
|
||||
import {
|
||||
HassioAddonInfo,
|
||||
HassioAddonRepository,
|
||||
} from "../../../src/data/hassio/addon";
|
||||
import { HassioAddonRepository } from "../../../src/data/hassio/addon";
|
||||
import { StoreAddon } from "../../../src/data/supervisor/store";
|
||||
import { Supervisor } from "../../../src/data/supervisor/supervisor";
|
||||
import { HomeAssistant } from "../../../src/types";
|
||||
import "../components/hassio-card-content";
|
||||
@@ -23,20 +21,16 @@ class HassioAddonRepositoryEl extends LitElement {
|
||||
|
||||
@property({ attribute: false }) public repo!: HassioAddonRepository;
|
||||
|
||||
@property({ attribute: false }) public addons!: HassioAddonInfo[];
|
||||
@property({ attribute: false }) public addons!: StoreAddon[];
|
||||
|
||||
@property() public filter!: string;
|
||||
|
||||
private _getAddons = memoizeOne(
|
||||
(addons: HassioAddonInfo[], filter?: string) => {
|
||||
if (filter) {
|
||||
return filterAndSort(addons, filter);
|
||||
}
|
||||
return addons.sort((a, b) =>
|
||||
caseInsensitiveStringCompare(a.name, b.name)
|
||||
);
|
||||
private _getAddons = memoizeOne((addons: StoreAddon[], filter?: string) => {
|
||||
if (filter) {
|
||||
return filterAndSort(addons, filter);
|
||||
}
|
||||
);
|
||||
return addons.sort((a, b) => caseInsensitiveStringCompare(a.name, b.name));
|
||||
});
|
||||
|
||||
protected render(): TemplateResult {
|
||||
const repo = this.repo;
|
||||
|
@@ -14,15 +14,15 @@ import memoizeOne from "memoize-one";
|
||||
import { atLeastVersion } from "../../../src/common/config/version";
|
||||
import { fireEvent } from "../../../src/common/dom/fire_event";
|
||||
import { navigate } from "../../../src/common/navigate";
|
||||
import "../../../src/components/search-input";
|
||||
import { extractSearchParam } from "../../../src/common/url/search-params";
|
||||
import "../../../src/components/ha-button-menu";
|
||||
import "../../../src/components/ha-icon-button";
|
||||
import "../../../src/components/search-input";
|
||||
import {
|
||||
HassioAddonInfo,
|
||||
HassioAddonRepository,
|
||||
reloadHassioAddons,
|
||||
} from "../../../src/data/hassio/addon";
|
||||
import { StoreAddon } from "../../../src/data/supervisor/store";
|
||||
import { Supervisor } from "../../../src/data/supervisor/supervisor";
|
||||
import "../../../src/layouts/hass-loading-screen";
|
||||
import "../../../src/layouts/hass-subpage";
|
||||
@@ -66,10 +66,10 @@ class HassioAddonStore extends LitElement {
|
||||
protected render(): TemplateResult {
|
||||
let repos: TemplateResult[] = [];
|
||||
|
||||
if (this.supervisor.addon.repositories) {
|
||||
if (this.supervisor.store.repositories) {
|
||||
repos = this.addonRepositories(
|
||||
this.supervisor.addon.repositories,
|
||||
this.supervisor.addon.addons,
|
||||
this.supervisor.store.repositories,
|
||||
this.supervisor.store.addons,
|
||||
this._filter
|
||||
);
|
||||
}
|
||||
@@ -145,7 +145,7 @@ class HassioAddonStore extends LitElement {
|
||||
private addonRepositories = memoizeOne(
|
||||
(
|
||||
repositories: HassioAddonRepository[],
|
||||
addons: HassioAddonInfo[],
|
||||
addons: StoreAddon[],
|
||||
filter?: string
|
||||
) =>
|
||||
repositories.sort(sortRepos).map((repo) => {
|
||||
|
@@ -12,15 +12,17 @@ import { navigate } from "../../../src/common/navigate";
|
||||
import { extractSearchParam } from "../../../src/common/url/search-params";
|
||||
import "../../../src/components/ha-circular-progress";
|
||||
import {
|
||||
fetchAddonInfo,
|
||||
fetchHassioAddonInfo,
|
||||
fetchHassioAddonsInfo,
|
||||
HassioAddonDetails,
|
||||
} from "../../../src/data/hassio/addon";
|
||||
import { extractApiErrorMessage } from "../../../src/data/hassio/common";
|
||||
import {
|
||||
fetchHassioSupervisorInfo,
|
||||
setSupervisorOption,
|
||||
} from "../../../src/data/hassio/supervisor";
|
||||
addStoreRepository,
|
||||
fetchSupervisorStore,
|
||||
StoreAddonDetails,
|
||||
} from "../../../src/data/supervisor/store";
|
||||
import { Supervisor } from "../../../src/data/supervisor/supervisor";
|
||||
import { showConfirmationDialog } from "../../../src/dialogs/generic/show-dialog-box";
|
||||
import "../../../src/layouts/hass-error-screen";
|
||||
@@ -45,7 +47,9 @@ class HassioAddonDashboard extends LitElement {
|
||||
|
||||
@property({ attribute: false }) public route!: Route;
|
||||
|
||||
@property({ attribute: false }) public addon?: HassioAddonDetails;
|
||||
@property({ attribute: false }) public addon?:
|
||||
| HassioAddonDetails
|
||||
| StoreAddonDetails;
|
||||
|
||||
@property({ type: Boolean }) public narrow!: boolean;
|
||||
|
||||
@@ -173,10 +177,10 @@ class HassioAddonDashboard extends LitElement {
|
||||
const requestedAddon = extractSearchParam("addon");
|
||||
const requestedAddonRepository = extractSearchParam("repository_url");
|
||||
if (requestedAddonRepository) {
|
||||
const supervisorInfo = await fetchHassioSupervisorInfo(this.hass);
|
||||
const storeInfo = await fetchSupervisorStore(this.hass);
|
||||
if (
|
||||
!supervisorInfo.addons_repositories.find(
|
||||
(repo) => repo === requestedAddonRepository
|
||||
!storeInfo.repositories.find(
|
||||
(repo) => repo.source === requestedAddonRepository
|
||||
)
|
||||
) {
|
||||
if (
|
||||
@@ -197,12 +201,7 @@ class HassioAddonDashboard extends LitElement {
|
||||
}
|
||||
|
||||
try {
|
||||
await setSupervisorOption(this.hass, {
|
||||
addons_repositories: [
|
||||
...supervisorInfo.addons_repositories,
|
||||
requestedAddonRepository,
|
||||
],
|
||||
});
|
||||
await addStoreRepository(this.hass, requestedAddonRepository);
|
||||
} catch (err: any) {
|
||||
this._error = extractApiErrorMessage(err);
|
||||
}
|
||||
@@ -245,6 +244,8 @@ class HassioAddonDashboard extends LitElement {
|
||||
|
||||
if (path === "uninstall") {
|
||||
window.history.back();
|
||||
} else if (path === "install") {
|
||||
this.addon = await fetchHassioAddonInfo(this.hass, this.addon!.slug);
|
||||
} else {
|
||||
await this._routeDataChanged();
|
||||
}
|
||||
@@ -262,8 +263,7 @@ class HassioAddonDashboard extends LitElement {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const addoninfo = await fetchHassioAddonInfo(this.hass, addon);
|
||||
this.addon = addoninfo;
|
||||
this.addon = await fetchAddonInfo(this.hass, this.supervisor, addon);
|
||||
} catch (err: any) {
|
||||
this._error = `Error fetching addon info: ${extractApiErrorMessage(err)}`;
|
||||
this.addon = undefined;
|
||||
|
@@ -1,5 +1,6 @@
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { HassioAddonDetails } from "../../../src/data/hassio/addon";
|
||||
import { StoreAddonDetails } from "../../../src/data/supervisor/store";
|
||||
import { Supervisor } from "../../../src/data/supervisor/supervisor";
|
||||
import {
|
||||
HassRouterPage,
|
||||
@@ -20,7 +21,9 @@ class HassioAddonRouter extends HassRouterPage {
|
||||
|
||||
@property({ attribute: false }) public supervisor!: Supervisor;
|
||||
|
||||
@property({ attribute: false }) public addon!: HassioAddonDetails;
|
||||
@property({ attribute: false }) public addon!:
|
||||
| HassioAddonDetails
|
||||
| StoreAddonDetails;
|
||||
|
||||
protected routerOptions: RouterOptions = {
|
||||
defaultPage: "info",
|
||||
|
@@ -59,7 +59,10 @@ import {
|
||||
fetchHassioStats,
|
||||
HassioStats,
|
||||
} from "../../../../src/data/hassio/common";
|
||||
import { StoreAddon } from "../../../../src/data/supervisor/store";
|
||||
import {
|
||||
StoreAddon,
|
||||
StoreAddonDetails,
|
||||
} from "../../../../src/data/supervisor/store";
|
||||
import { Supervisor } from "../../../../src/data/supervisor/supervisor";
|
||||
import {
|
||||
showAlertDialog,
|
||||
@@ -100,7 +103,9 @@ class HassioAddonInfo extends LitElement {
|
||||
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public addon!: HassioAddonDetails;
|
||||
@property({ attribute: false }) public addon!:
|
||||
| HassioAddonDetails
|
||||
| StoreAddonDetails;
|
||||
|
||||
@property({ attribute: false }) public supervisor!: Supervisor;
|
||||
|
||||
@@ -143,7 +148,7 @@ class HassioAddonInfo extends LitElement {
|
||||
></update-available-card>
|
||||
`
|
||||
: ""}
|
||||
${!this.addon.protected
|
||||
${"protected" in this.addon && !this.addon.protected
|
||||
? html`
|
||||
<ha-alert
|
||||
alert-type="error"
|
||||
@@ -518,7 +523,7 @@ class HassioAddonInfo extends LitElement {
|
||||
: ""}
|
||||
</div>
|
||||
<div>
|
||||
${this.addon.state === "started"
|
||||
${this.addon.version && this.addon.state === "started"
|
||||
? html`<ha-settings-row ?three-line=${this.narrow}>
|
||||
<span slot="heading">
|
||||
${this.supervisor.localize("addon.dashboard.hostname")}
|
||||
@@ -669,7 +674,7 @@ class HassioAddonInfo extends LitElement {
|
||||
}
|
||||
|
||||
private async _loadData(): Promise<void> {
|
||||
if (this.addon.state === "started") {
|
||||
if ("state" in this.addon && this.addon.state === "started") {
|
||||
this._metrics = await fetchHassioStats(
|
||||
this.hass,
|
||||
`addons/${this.addon.slug}`
|
||||
@@ -717,18 +722,22 @@ class HassioAddonInfo extends LitElement {
|
||||
}
|
||||
|
||||
private get _computeIsRunning(): boolean {
|
||||
return this.addon?.state === "started";
|
||||
return (this.addon as HassioAddonDetails)?.state === "started";
|
||||
}
|
||||
|
||||
private get _pathWebui(): string | null {
|
||||
return (
|
||||
this.addon.webui &&
|
||||
this.addon.webui.replace("[HOST]", document.location.hostname)
|
||||
return (this.addon as HassioAddonDetails).webui!.replace(
|
||||
"[HOST]",
|
||||
document.location.hostname
|
||||
);
|
||||
}
|
||||
|
||||
private get _computeShowWebUI(): boolean | "" | null {
|
||||
return !this.addon.ingress && this.addon.webui && this._computeIsRunning;
|
||||
return (
|
||||
!this.addon.ingress &&
|
||||
(this.addon as HassioAddonDetails).webui &&
|
||||
this._computeIsRunning
|
||||
);
|
||||
}
|
||||
|
||||
private _openIngress(): void {
|
||||
@@ -754,7 +763,8 @@ class HassioAddonInfo extends LitElement {
|
||||
private async _startOnBootToggled(): Promise<void> {
|
||||
this._error = undefined;
|
||||
const data: HassioAddonSetOptionParams = {
|
||||
boot: this.addon.boot === "auto" ? "manual" : "auto",
|
||||
boot:
|
||||
(this.addon as HassioAddonDetails).boot === "auto" ? "manual" : "auto",
|
||||
};
|
||||
try {
|
||||
await setHassioAddonOption(this.hass, this.addon.slug, data);
|
||||
@@ -776,7 +786,7 @@ class HassioAddonInfo extends LitElement {
|
||||
private async _watchdogToggled(): Promise<void> {
|
||||
this._error = undefined;
|
||||
const data: HassioAddonSetOptionParams = {
|
||||
watchdog: !this.addon.watchdog,
|
||||
watchdog: !(this.addon as HassioAddonDetails).watchdog,
|
||||
};
|
||||
try {
|
||||
await setHassioAddonOption(this.hass, this.addon.slug, data);
|
||||
@@ -798,7 +808,7 @@ class HassioAddonInfo extends LitElement {
|
||||
private async _autoUpdateToggled(): Promise<void> {
|
||||
this._error = undefined;
|
||||
const data: HassioAddonSetOptionParams = {
|
||||
auto_update: !this.addon.auto_update,
|
||||
auto_update: !(this.addon as HassioAddonDetails).auto_update,
|
||||
};
|
||||
try {
|
||||
await setHassioAddonOption(this.hass, this.addon.slug, data);
|
||||
@@ -820,7 +830,7 @@ class HassioAddonInfo extends LitElement {
|
||||
private async _protectionToggled(): Promise<void> {
|
||||
this._error = undefined;
|
||||
const data: HassioAddonSetSecurityParams = {
|
||||
protected: !this.addon.protected,
|
||||
protected: !(this.addon as HassioAddonDetails).protected,
|
||||
};
|
||||
try {
|
||||
await setHassioAddonSecurity(this.hass, this.addon.slug, data);
|
||||
@@ -842,7 +852,7 @@ class HassioAddonInfo extends LitElement {
|
||||
private async _panelToggled(): Promise<void> {
|
||||
this._error = undefined;
|
||||
const data: HassioAddonSetOptionParams = {
|
||||
ingress_panel: !this.addon.ingress_panel,
|
||||
ingress_panel: !(this.addon as HassioAddonDetails).ingress_panel,
|
||||
};
|
||||
try {
|
||||
await setHassioAddonOption(this.hass, this.addon.slug, data);
|
||||
@@ -870,7 +880,7 @@ class HassioAddonInfo extends LitElement {
|
||||
|
||||
showHassioMarkdownDialog(this, {
|
||||
title: this.supervisor.localize("addon.dashboard.changelog"),
|
||||
content: extractChangelog(this.addon, content),
|
||||
content: extractChangelog(this.addon as HassioAddonDetails, content),
|
||||
});
|
||||
} catch (err: any) {
|
||||
showAlertDialog(this, {
|
||||
|
@@ -98,9 +98,8 @@ export class HassioBackups extends LitElement {
|
||||
if (backup.content.addons.length !== 0) {
|
||||
for (const addon of backup.content.addons) {
|
||||
content.push(
|
||||
this.supervisor.supervisor.addons.find(
|
||||
(entry) => entry.slug === addon
|
||||
)?.name || addon
|
||||
this.supervisor.addon.addons.find((entry) => entry.slug === addon)
|
||||
?.name || addon
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@@ -1,8 +1,8 @@
|
||||
import Fuse from "fuse.js";
|
||||
import { HassioAddonInfo } from "../../../src/data/hassio/addon";
|
||||
import { StoreAddon } from "../../../src/data/supervisor/store";
|
||||
|
||||
export function filterAndSort(addons: HassioAddonInfo[], filter: string) {
|
||||
const options: Fuse.IFuseOptions<HassioAddonInfo> = {
|
||||
export function filterAndSort(addons: StoreAddon[], filter: string) {
|
||||
const options: Fuse.IFuseOptions<StoreAddon> = {
|
||||
keys: ["name", "description", "slug"],
|
||||
isCaseSensitive: false,
|
||||
minMatchCharLength: 2,
|
||||
|
@@ -96,7 +96,7 @@ export class SupervisorBackupContent extends LitElement {
|
||||
: ["ssl", "share", "media", "addons/local"]
|
||||
);
|
||||
this.addons = _computeAddons(
|
||||
this.backup ? this.backup.addons : this.supervisor?.supervisor.addons
|
||||
this.backup ? this.backup.addons : this.supervisor?.addon.addons
|
||||
);
|
||||
this.backupType = this.backup?.type || "full";
|
||||
this.backupName = this.backup?.name || "";
|
||||
|
@@ -24,7 +24,7 @@ class HassioAddons extends LitElement {
|
||||
? html` <h1>${this.supervisor.localize("dashboard.addons")}</h1> `
|
||||
: ""}
|
||||
<div class="card-group">
|
||||
${!this.supervisor.supervisor.addons?.length
|
||||
${!this.supervisor.addon.addons.length
|
||||
? html`
|
||||
<ha-card outlined>
|
||||
<div class="card-content">
|
||||
@@ -34,7 +34,7 @@ class HassioAddons extends LitElement {
|
||||
</div>
|
||||
</ha-card>
|
||||
`
|
||||
: this.supervisor.supervisor.addons
|
||||
: this.supervisor.addon.addons
|
||||
.sort((a, b) => caseInsensitiveStringCompare(a.name, b.name))
|
||||
.map(
|
||||
(addon) => html`
|
||||
|
@@ -15,15 +15,18 @@ import "../../../../src/components/ha-circular-progress";
|
||||
import { createCloseHeading } from "../../../../src/components/ha-dialog";
|
||||
import "../../../../src/components/ha-icon-button";
|
||||
import {
|
||||
fetchHassioAddonsInfo,
|
||||
HassioAddonInfo,
|
||||
HassioAddonRepository,
|
||||
} from "../../../../src/data/hassio/addon";
|
||||
import { extractApiErrorMessage } from "../../../../src/data/hassio/common";
|
||||
import { setSupervisorOption } from "../../../../src/data/hassio/supervisor";
|
||||
import { haStyle, haStyleDialog } from "../../../../src/resources/styles";
|
||||
import type { HomeAssistant } from "../../../../src/types";
|
||||
import { HassioRepositoryDialogParams } from "./show-dialog-repositories";
|
||||
import {
|
||||
addStoreRepository,
|
||||
fetchStoreRepositories,
|
||||
removeStoreRepository,
|
||||
} from "../../../../src/data/supervisor/store";
|
||||
|
||||
@customElement("dialog-hassio-repositories")
|
||||
class HassioRepositoriesDialog extends LitElement {
|
||||
@@ -58,7 +61,13 @@ class HassioRepositoriesDialog extends LitElement {
|
||||
|
||||
private _filteredRepositories = memoizeOne((repos: HassioAddonRepository[]) =>
|
||||
repos
|
||||
.filter((repo) => repo.slug !== "core" && repo.slug !== "local")
|
||||
.filter(
|
||||
(repo) =>
|
||||
repo.slug !== "core" && // The core add-ons repository
|
||||
repo.slug !== "local" && // Locally managed add-ons
|
||||
repo.slug !== "a0d7b954" && // Home Assistant Community Add-ons
|
||||
repo.slug !== "5c53de3b" // The ESPHome repository
|
||||
)
|
||||
.sort((a, b) => caseInsensitiveStringCompare(a.name, b.name))
|
||||
);
|
||||
|
||||
@@ -78,7 +87,7 @@ class HassioRepositoriesDialog extends LitElement {
|
||||
const repositories = this._filteredRepositories(this._repositories);
|
||||
const usedRepositories = this._filteredUsedRepositories(
|
||||
repositories,
|
||||
this._dialogParams.supervisor.supervisor.addons
|
||||
this._dialogParams.supervisor.addon.addons
|
||||
);
|
||||
return html`
|
||||
<ha-dialog
|
||||
@@ -215,9 +224,7 @@ class HassioRepositoriesDialog extends LitElement {
|
||||
|
||||
private async _loadData(): Promise<void> {
|
||||
try {
|
||||
const addonsinfo = await fetchHassioAddonsInfo(this.hass);
|
||||
|
||||
this._repositories = addonsinfo.repositories;
|
||||
this._repositories = await fetchStoreRepositories(this.hass);
|
||||
|
||||
fireEvent(this, "supervisor-collection-refresh", { collection: "addon" });
|
||||
} catch (err: any) {
|
||||
@@ -231,14 +238,9 @@ class HassioRepositoriesDialog extends LitElement {
|
||||
return;
|
||||
}
|
||||
this._processing = true;
|
||||
const repositories = this._filteredRepositories(this._repositories!);
|
||||
const newRepositories = repositories.map((repo) => repo.source);
|
||||
newRepositories.push(input.value);
|
||||
|
||||
try {
|
||||
await setSupervisorOption(this.hass, {
|
||||
addons_repositories: newRepositories,
|
||||
});
|
||||
await addStoreRepository(this.hass, input.value);
|
||||
await this._loadData();
|
||||
|
||||
input.value = "";
|
||||
@@ -250,19 +252,8 @@ class HassioRepositoriesDialog extends LitElement {
|
||||
|
||||
private async _removeRepository(ev: Event) {
|
||||
const slug = (ev.currentTarget as any).slug;
|
||||
const repositories = this._filteredRepositories(this._repositories!);
|
||||
const repository = repositories.find((repo) => repo.slug === slug);
|
||||
if (!repository) {
|
||||
return;
|
||||
}
|
||||
const newRepositories = repositories
|
||||
.map((repo) => repo.source)
|
||||
.filter((repo) => repo !== repository.source);
|
||||
|
||||
try {
|
||||
await setSupervisorOption(this.hass, {
|
||||
addons_repositories: newRepositories,
|
||||
});
|
||||
await removeStoreRepository(this.hass, slug);
|
||||
await this._loadData();
|
||||
} catch (err: any) {
|
||||
this._error = extractApiErrorMessage(err);
|
||||
|
10
package.json
@@ -72,8 +72,8 @@
|
||||
"@material/mwc-textfield": "0.25.3",
|
||||
"@material/mwc-top-app-bar-fixed": "^0.25.3",
|
||||
"@material/top-app-bar": "14.0.0-canary.261f2db59.0",
|
||||
"@mdi/js": "6.7.96",
|
||||
"@mdi/svg": "6.7.96",
|
||||
"@mdi/js": "6.9.96",
|
||||
"@mdi/svg": "6.9.96",
|
||||
"@polymer/app-layout": "^3.1.0",
|
||||
"@polymer/iron-flex-layout": "^3.0.1",
|
||||
"@polymer/iron-icon": "^3.0.1",
|
||||
@@ -89,8 +89,8 @@
|
||||
"@polymer/paper-tooltip": "^3.0.1",
|
||||
"@polymer/polymer": "3.4.1",
|
||||
"@thomasloven/round-slider": "0.5.4",
|
||||
"@vaadin/combo-box": "^22.0.4",
|
||||
"@vaadin/vaadin-themable-mixin": "^22.0.4",
|
||||
"@vaadin/combo-box": "^23.0.10",
|
||||
"@vaadin/vaadin-themable-mixin": "^23.0.10",
|
||||
"@vibrant/color": "^3.2.1-alpha.1",
|
||||
"@vibrant/core": "^3.2.1-alpha.1",
|
||||
"@vibrant/quantizer-mmcq": "^3.2.1-alpha.1",
|
||||
@@ -108,7 +108,7 @@
|
||||
"fuse.js": "^6.0.0",
|
||||
"google-timezones-json": "^1.0.2",
|
||||
"hls.js": "^1.1.5",
|
||||
"home-assistant-js-websocket": "^7.0.3",
|
||||
"home-assistant-js-websocket": "^7.1.0",
|
||||
"idb-keyval": "^5.1.3",
|
||||
"intl-messageformat": "^9.9.1",
|
||||
"js-yaml": "^4.1.0",
|
||||
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "home-assistant-frontend"
|
||||
version = "20220525.0"
|
||||
version = "20220705.0"
|
||||
license = {text = "Apache-2.0"}
|
||||
description = "The Home Assistant frontend"
|
||||
readme = "README.md"
|
||||
|
@@ -24,10 +24,15 @@ function auto(version) {
|
||||
return patch(version);
|
||||
}
|
||||
|
||||
function nightly() {
|
||||
return `${today()}.dev`;
|
||||
}
|
||||
|
||||
const methods = {
|
||||
patch,
|
||||
today,
|
||||
auto,
|
||||
nightly,
|
||||
};
|
||||
|
||||
async function main(args) {
|
||||
@@ -57,7 +62,11 @@ async function main(args) {
|
||||
console.log("Current version:", version);
|
||||
console.log("New version:", newVersion);
|
||||
|
||||
fs.writeFileSync("pyproject.toml", setup.replace(version, newVersion), "utf-8");
|
||||
fs.writeFileSync(
|
||||
"pyproject.toml",
|
||||
setup.replace(version, newVersion),
|
||||
"utf-8"
|
||||
);
|
||||
|
||||
if (!commit) {
|
||||
return;
|
||||
|
2
setup.cfg
Normal file
@@ -0,0 +1,2 @@
|
||||
# Setuptools v62.3 doesn't support editable installs with just 'pyproject.toml' (PEP 660).
|
||||
# Keep this file until it does!
|
@@ -8,6 +8,7 @@ import {
|
||||
mdiCalendar,
|
||||
mdiCast,
|
||||
mdiCastConnected,
|
||||
mdiChartSankey,
|
||||
mdiCheckCircleOutline,
|
||||
mdiClock,
|
||||
mdiCloseCircleOutline,
|
||||
@@ -24,6 +25,7 @@ import {
|
||||
mdiPowerPlug,
|
||||
mdiPowerPlugOff,
|
||||
mdiRestart,
|
||||
mdiSwapHorizontal,
|
||||
mdiToggleSwitchVariant,
|
||||
mdiToggleSwitchVariantOff,
|
||||
mdiWeatherNight,
|
||||
@@ -153,6 +155,12 @@ export const domainIconWithoutDefault = (
|
||||
? FIXED_DOMAIN_ICONS[domain]
|
||||
: mdiWeatherNight;
|
||||
|
||||
case "switch_as_x":
|
||||
return mdiSwapHorizontal;
|
||||
|
||||
case "threshold":
|
||||
return mdiChartSankey;
|
||||
|
||||
case "update":
|
||||
return compareState === "on"
|
||||
? updateIsInstalling(stateObj as UpdateEntity)
|
||||
|
@@ -5,6 +5,6 @@ export const clamp = (value: number, min: number, max: number) =>
|
||||
export const conditionalClamp = (value: number, min?: number, max?: number) => {
|
||||
let result: number;
|
||||
result = min ? Math.max(value, min) : value;
|
||||
result = max ? Math.min(value, max) : value;
|
||||
result = max ? Math.min(result, max) : result;
|
||||
return result;
|
||||
};
|
||||
|
@@ -70,7 +70,9 @@ export const iconColorCSS = css`
|
||||
}
|
||||
}
|
||||
|
||||
ha-state-icon[data-domain="plant"][data-state="problem"],
|
||||
ha-state-icon[data-domain="plant"][data-state="problem"] {
|
||||
color: var(--state-icon-error-color);
|
||||
}
|
||||
|
||||
/* Color the icon if unavailable */
|
||||
ha-state-icon[data-state="unavailable"] {
|
||||
|
@@ -1,3 +1,4 @@
|
||||
import { LitElement } from "lit";
|
||||
import { HomeAssistant } from "../../types";
|
||||
|
||||
export function computeRTL(hass: HomeAssistant) {
|
||||
@@ -15,3 +16,21 @@ export function computeRTLDirection(hass: HomeAssistant) {
|
||||
export function emitRTLDirection(rtl: boolean) {
|
||||
return rtl ? "rtl" : "ltr";
|
||||
}
|
||||
|
||||
export function computeDirectionStyles(isRTL: boolean, element: LitElement) {
|
||||
const direction: string = emitRTLDirection(isRTL);
|
||||
setDirectionStyles(direction, element);
|
||||
}
|
||||
|
||||
export function setDirectionStyles(direction: string, element: LitElement) {
|
||||
element.style.direction = direction;
|
||||
element.style.setProperty("--direction", direction);
|
||||
element.style.setProperty(
|
||||
"--float-start",
|
||||
direction === "ltr" ? "left" : "right"
|
||||
);
|
||||
element.style.setProperty(
|
||||
"--float-end",
|
||||
direction === "ltr" ? "right" : "left"
|
||||
);
|
||||
}
|
||||
|
@@ -11,6 +11,8 @@ import { classMap } from "lit/directives/class-map";
|
||||
import { styleMap } from "lit/directives/style-map";
|
||||
import { clamp } from "../../common/number/clamp";
|
||||
|
||||
export const MIN_TIME_BETWEEN_UPDATES = 60 * 5 * 1000;
|
||||
|
||||
interface Tooltip extends TooltipModel<any> {
|
||||
top: string;
|
||||
left: string;
|
||||
@@ -37,6 +39,26 @@ export default class HaChartBase extends LitElement {
|
||||
|
||||
@state() private _hiddenDatasets: Set<number> = new Set();
|
||||
|
||||
private _releaseCanvas() {
|
||||
// release the canvas memory to prevent
|
||||
// safari from running out of memory.
|
||||
if (this.chart) {
|
||||
this.chart.destroy();
|
||||
}
|
||||
}
|
||||
|
||||
public disconnectedCallback() {
|
||||
this._releaseCanvas();
|
||||
super.disconnectedCallback();
|
||||
}
|
||||
|
||||
public connectedCallback() {
|
||||
super.connectedCallback();
|
||||
if (this.hasUpdated) {
|
||||
this._setupChart();
|
||||
}
|
||||
}
|
||||
|
||||
protected firstUpdated() {
|
||||
this._setupChart();
|
||||
this.data.datasets.forEach((dataset, index) => {
|
||||
@@ -304,6 +326,9 @@ export default class HaChartBase extends LitElement {
|
||||
width: 16px;
|
||||
flex-shrink: 0;
|
||||
box-sizing: border-box;
|
||||
margin-inline-end: 6px;
|
||||
margin-inline-start: initial;
|
||||
direction: var(--direction);
|
||||
}
|
||||
.chartTooltip .bullet {
|
||||
align-self: baseline;
|
||||
@@ -312,6 +337,9 @@ export default class HaChartBase extends LitElement {
|
||||
:host([rtl]) .chartTooltip .bullet {
|
||||
margin-right: inherit;
|
||||
margin-left: 6px;
|
||||
margin-inline-end: inherit;
|
||||
margin-inline-start: 6px;
|
||||
direction: var(--direction);
|
||||
}
|
||||
.chartTooltip {
|
||||
padding: 8px;
|
||||
|
@@ -8,7 +8,7 @@ import {
|
||||
} from "../../common/number/format_number";
|
||||
import { LineChartEntity, LineChartState } from "../../data/history";
|
||||
import { HomeAssistant } from "../../types";
|
||||
import "./ha-chart-base";
|
||||
import { MIN_TIME_BETWEEN_UPDATES } from "./ha-chart-base";
|
||||
|
||||
const safeParseFloat = (value) => {
|
||||
const parsed = parseFloat(value);
|
||||
@@ -28,11 +28,13 @@ class StateHistoryChartLine extends LitElement {
|
||||
|
||||
@property({ type: Boolean }) public isSingleDevice = false;
|
||||
|
||||
@property({ attribute: false }) public endTime?: Date;
|
||||
@property({ attribute: false }) public endTime!: Date;
|
||||
|
||||
@state() private _chartData?: ChartData<"line">;
|
||||
|
||||
@state() private _chartOptions?: ChartOptions<"line">;
|
||||
@state() private _chartOptions?: ChartOptions;
|
||||
|
||||
private _chartTime: Date = new Date();
|
||||
|
||||
protected render() {
|
||||
return html`
|
||||
@@ -57,6 +59,7 @@ class StateHistoryChartLine extends LitElement {
|
||||
locale: this.hass.locale,
|
||||
},
|
||||
},
|
||||
suggestedMax: this.endTime,
|
||||
ticks: {
|
||||
maxRotation: 0,
|
||||
sampleSize: 5,
|
||||
@@ -120,7 +123,13 @@ class StateHistoryChartLine extends LitElement {
|
||||
locale: numberFormatToLocale(this.hass.locale),
|
||||
};
|
||||
}
|
||||
if (changedProps.has("data")) {
|
||||
if (
|
||||
changedProps.has("data") ||
|
||||
this._chartTime <
|
||||
new Date(this.endTime.getTime() - MIN_TIME_BETWEEN_UPDATES)
|
||||
) {
|
||||
// If the line is more than 5 minutes old, re-gen it
|
||||
// so the X axis grows even if there is no new data
|
||||
this._generateData();
|
||||
}
|
||||
}
|
||||
@@ -130,28 +139,12 @@ class StateHistoryChartLine extends LitElement {
|
||||
const computedStyles = getComputedStyle(this);
|
||||
const entityStates = this.data;
|
||||
const datasets: ChartDataset<"line">[] = [];
|
||||
let endTime: Date;
|
||||
|
||||
if (entityStates.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
endTime =
|
||||
this.endTime ||
|
||||
// Get the highest date from the last date of each device
|
||||
new Date(
|
||||
Math.max(
|
||||
...entityStates.map((devSts) =>
|
||||
new Date(
|
||||
devSts.states[devSts.states.length - 1].last_changed
|
||||
).getTime()
|
||||
)
|
||||
)
|
||||
);
|
||||
if (endTime > new Date()) {
|
||||
endTime = new Date();
|
||||
}
|
||||
|
||||
this._chartTime = new Date();
|
||||
const endTime = this.endTime;
|
||||
const names = this.names || {};
|
||||
entityStates.forEach((states) => {
|
||||
const domain = states.domain;
|
||||
|
@@ -9,7 +9,7 @@ import { numberFormatToLocale } from "../../common/number/format_number";
|
||||
import { computeRTL } from "../../common/util/compute_rtl";
|
||||
import { TimelineEntity } from "../../data/history";
|
||||
import { HomeAssistant } from "../../types";
|
||||
import "./ha-chart-base";
|
||||
import { MIN_TIME_BETWEEN_UPDATES } from "./ha-chart-base";
|
||||
import type { TimeLineData } from "./timeline-chart/const";
|
||||
|
||||
/** Binary sensor device classes for which the static colors for on/off are NOT inverted.
|
||||
@@ -83,6 +83,8 @@ export class StateHistoryChartTimeline extends LitElement {
|
||||
|
||||
@property({ attribute: false }) public data: TimelineEntity[] = [];
|
||||
|
||||
@property() public narrow!: boolean;
|
||||
|
||||
@property() public names: boolean | Record<string, string> = false;
|
||||
|
||||
@property() public unit?: string;
|
||||
@@ -91,12 +93,18 @@ export class StateHistoryChartTimeline extends LitElement {
|
||||
|
||||
@property({ type: Boolean }) public isSingleDevice = false;
|
||||
|
||||
@property({ attribute: false }) public endTime?: Date;
|
||||
@property({ type: Boolean }) public chunked = false;
|
||||
|
||||
@property({ attribute: false }) public startTime!: Date;
|
||||
|
||||
@property({ attribute: false }) public endTime!: Date;
|
||||
|
||||
@state() private _chartData?: ChartData<"timeline">;
|
||||
|
||||
@state() private _chartOptions?: ChartOptions<"timeline">;
|
||||
|
||||
private _chartTime: Date = new Date();
|
||||
|
||||
protected render() {
|
||||
return html`
|
||||
<ha-chart-base
|
||||
@@ -110,6 +118,7 @@ export class StateHistoryChartTimeline extends LitElement {
|
||||
|
||||
public willUpdate(changedProps: PropertyValues) {
|
||||
if (!this.hasUpdated) {
|
||||
const narrow = this.narrow;
|
||||
this._chartOptions = {
|
||||
maintainAspectRatio: false,
|
||||
parsing: false,
|
||||
@@ -123,6 +132,8 @@ export class StateHistoryChartTimeline extends LitElement {
|
||||
locale: this.hass.locale,
|
||||
},
|
||||
},
|
||||
suggestedMin: this.startTime,
|
||||
suggestedMax: this.endTime,
|
||||
ticks: {
|
||||
autoSkip: true,
|
||||
maxRotation: 0,
|
||||
@@ -153,11 +164,18 @@ export class StateHistoryChartTimeline extends LitElement {
|
||||
drawTicks: false,
|
||||
},
|
||||
ticks: {
|
||||
display: this.data.length !== 1,
|
||||
display:
|
||||
this.chunked || !this.isSingleDevice || this.data.length !== 1,
|
||||
},
|
||||
afterSetDimensions: (y) => {
|
||||
y.maxWidth = y.chart.width * 0.18;
|
||||
},
|
||||
afterFit: (scaleInstance) => {
|
||||
if (this.chunked) {
|
||||
// ensure all the chart labels are the same width
|
||||
scaleInstance.width = narrow ? 105 : 185;
|
||||
}
|
||||
},
|
||||
position: computeRTL(this.hass) ? "right" : "left",
|
||||
},
|
||||
},
|
||||
@@ -195,7 +213,13 @@ export class StateHistoryChartTimeline extends LitElement {
|
||||
locale: numberFormatToLocale(this.hass.locale),
|
||||
};
|
||||
}
|
||||
if (changedProps.has("data")) {
|
||||
if (
|
||||
changedProps.has("data") ||
|
||||
this._chartTime <
|
||||
new Date(this.endTime.getTime() - MIN_TIME_BETWEEN_UPDATES)
|
||||
) {
|
||||
// If the line is more than 5 minutes old, re-gen it
|
||||
// so the X axis grows even if there is no new data
|
||||
this._generateData();
|
||||
}
|
||||
}
|
||||
@@ -208,34 +232,9 @@ export class StateHistoryChartTimeline extends LitElement {
|
||||
stateHistory = [];
|
||||
}
|
||||
|
||||
const startTime = new Date(
|
||||
stateHistory.reduce(
|
||||
(minTime, stateInfo) =>
|
||||
Math.min(minTime, new Date(stateInfo.data[0].last_changed).getTime()),
|
||||
new Date().getTime()
|
||||
)
|
||||
);
|
||||
|
||||
// end time is Math.max(startTime, last_event)
|
||||
let endTime =
|
||||
this.endTime ||
|
||||
new Date(
|
||||
stateHistory.reduce(
|
||||
(maxTime, stateInfo) =>
|
||||
Math.max(
|
||||
maxTime,
|
||||
new Date(
|
||||
stateInfo.data[stateInfo.data.length - 1].last_changed
|
||||
).getTime()
|
||||
),
|
||||
startTime.getTime()
|
||||
)
|
||||
);
|
||||
|
||||
if (endTime > new Date()) {
|
||||
endTime = new Date();
|
||||
}
|
||||
|
||||
this._chartTime = new Date();
|
||||
const startTime = this.startTime;
|
||||
const endTime = this.endTime;
|
||||
const labels: string[] = [];
|
||||
const datasets: ChartDataset<"timeline">[] = [];
|
||||
const names = this.names || {};
|
||||
|
@@ -1,3 +1,4 @@
|
||||
import "@lit-labs/virtualizer";
|
||||
import {
|
||||
css,
|
||||
CSSResultGroup,
|
||||
@@ -6,12 +7,29 @@ import {
|
||||
PropertyValues,
|
||||
TemplateResult,
|
||||
} from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { customElement, property, state, eventOptions } from "lit/decorators";
|
||||
import { isComponentLoaded } from "../../common/config/is_component_loaded";
|
||||
import { HistoryResult } from "../../data/history";
|
||||
import {
|
||||
HistoryResult,
|
||||
LineChartUnit,
|
||||
TimelineEntity,
|
||||
} from "../../data/history";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import "./state-history-chart-line";
|
||||
import "./state-history-chart-timeline";
|
||||
import { restoreScroll } from "../../common/decorators/restore-scroll";
|
||||
|
||||
const CANVAS_TIMELINE_ROWS_CHUNK = 10; // Split up the canvases to avoid hitting the render limit
|
||||
|
||||
const chunkData = (inputArray: any[], chunks: number) =>
|
||||
inputArray.reduce((results, item, idx) => {
|
||||
const chunkIdx = Math.floor(idx / chunks);
|
||||
if (!results[chunkIdx]) {
|
||||
results[chunkIdx] = [];
|
||||
}
|
||||
results[chunkIdx].push(item);
|
||||
return results;
|
||||
}, []);
|
||||
|
||||
@customElement("state-history-charts")
|
||||
class StateHistoryCharts extends LitElement {
|
||||
@@ -19,8 +37,13 @@ class StateHistoryCharts extends LitElement {
|
||||
|
||||
@property({ attribute: false }) public historyData!: HistoryResult;
|
||||
|
||||
@property() public narrow!: boolean;
|
||||
|
||||
@property({ type: Boolean }) public names = false;
|
||||
|
||||
@property({ type: Boolean, attribute: "virtualize", reflect: true })
|
||||
public virtualize = false;
|
||||
|
||||
@property({ attribute: false }) public endTime?: Date;
|
||||
|
||||
@property({ type: Boolean, attribute: "up-to-now" }) public upToNow = false;
|
||||
@@ -29,59 +52,104 @@ class StateHistoryCharts extends LitElement {
|
||||
|
||||
@property({ type: Boolean }) public isLoadingData = false;
|
||||
|
||||
@state() private _computedStartTime!: Date;
|
||||
|
||||
@state() private _computedEndTime!: Date;
|
||||
|
||||
// @ts-ignore
|
||||
@restoreScroll(".container") private _savedScrollPos?: number;
|
||||
|
||||
@eventOptions({ passive: true })
|
||||
protected render(): TemplateResult {
|
||||
if (!isComponentLoaded(this.hass, "history")) {
|
||||
return html` <div class="info">
|
||||
return html`<div class="info">
|
||||
${this.hass.localize("ui.components.history_charts.history_disabled")}
|
||||
</div>`;
|
||||
}
|
||||
|
||||
if (this.isLoadingData && !this.historyData) {
|
||||
return html` <div class="info">
|
||||
return html`<div class="info">
|
||||
${this.hass.localize("ui.components.history_charts.loading_history")}
|
||||
</div>`;
|
||||
}
|
||||
|
||||
if (this._isHistoryEmpty()) {
|
||||
return html` <div class="info">
|
||||
return html`<div class="info">
|
||||
${this.hass.localize("ui.components.history_charts.no_history_found")}
|
||||
</div>`;
|
||||
}
|
||||
|
||||
const computedEndTime = this.upToNow
|
||||
? new Date()
|
||||
: this.endTime || new Date();
|
||||
const now = new Date();
|
||||
|
||||
return html`
|
||||
${this.historyData.timeline.length
|
||||
? html`
|
||||
<state-history-chart-timeline
|
||||
.hass=${this.hass}
|
||||
.data=${this.historyData.timeline}
|
||||
.endTime=${computedEndTime}
|
||||
.noSingle=${this.noSingle}
|
||||
.names=${this.names}
|
||||
></state-history-chart-timeline>
|
||||
`
|
||||
: html``}
|
||||
${this.historyData.line.map(
|
||||
(line) => html`
|
||||
<state-history-chart-line
|
||||
.hass=${this.hass}
|
||||
.unit=${line.unit}
|
||||
.data=${line.data}
|
||||
.identifier=${line.identifier}
|
||||
.isSingleDevice=${!this.noSingle &&
|
||||
line.data &&
|
||||
line.data.length === 1}
|
||||
.endTime=${computedEndTime}
|
||||
.names=${this.names}
|
||||
></state-history-chart-line>
|
||||
`
|
||||
)}
|
||||
`;
|
||||
this._computedEndTime =
|
||||
this.upToNow || !this.endTime || this.endTime > now ? now : this.endTime;
|
||||
|
||||
this._computedStartTime = new Date(
|
||||
this.historyData.timeline.reduce(
|
||||
(minTime, stateInfo) =>
|
||||
Math.min(minTime, new Date(stateInfo.data[0].last_changed).getTime()),
|
||||
new Date().getTime()
|
||||
)
|
||||
);
|
||||
|
||||
const combinedItems = this.historyData.timeline.length
|
||||
? (this.virtualize
|
||||
? chunkData(this.historyData.timeline, CANVAS_TIMELINE_ROWS_CHUNK)
|
||||
: [this.historyData.timeline]
|
||||
).concat(this.historyData.line)
|
||||
: this.historyData.line;
|
||||
|
||||
return this.virtualize
|
||||
? html`<div class="container ha-scrollbar" @scroll=${this._saveScrollPos}>
|
||||
<lit-virtualizer
|
||||
scroller
|
||||
class="ha-scrollbar"
|
||||
.items=${combinedItems}
|
||||
.renderItem=${this._renderHistoryItem}
|
||||
>
|
||||
</lit-virtualizer>
|
||||
</div>`
|
||||
: html`${combinedItems.map((item, index) =>
|
||||
this._renderHistoryItem(item, index)
|
||||
)}`;
|
||||
}
|
||||
|
||||
private _renderHistoryItem = (
|
||||
item: TimelineEntity[] | LineChartUnit,
|
||||
index: number
|
||||
): TemplateResult => {
|
||||
if (!item || index === undefined) {
|
||||
return html``;
|
||||
}
|
||||
if (!Array.isArray(item)) {
|
||||
return html`<div class="entry-container">
|
||||
<state-history-chart-line
|
||||
.hass=${this.hass}
|
||||
.unit=${item.unit}
|
||||
.data=${item.data}
|
||||
.identifier=${item.identifier}
|
||||
.isSingleDevice=${!this.noSingle &&
|
||||
this.historyData.line?.length === 1}
|
||||
.endTime=${this._computedEndTime}
|
||||
.names=${this.names}
|
||||
></state-history-chart-line>
|
||||
</div> `;
|
||||
}
|
||||
return html`<div class="entry-container">
|
||||
<state-history-chart-timeline
|
||||
.hass=${this.hass}
|
||||
.data=${item}
|
||||
.startTime=${this._computedStartTime}
|
||||
.endTime=${this._computedEndTime}
|
||||
.isSingleDevice=${!this.noSingle &&
|
||||
this.historyData.timeline?.length === 1}
|
||||
.names=${this.names}
|
||||
.narrow=${this.narrow}
|
||||
.chunked=${this.virtualize}
|
||||
></state-history-chart-timeline>
|
||||
</div> `;
|
||||
};
|
||||
|
||||
protected shouldUpdate(changedProps: PropertyValues): boolean {
|
||||
return !(changedProps.size === 1 && changedProps.has("hass"));
|
||||
}
|
||||
@@ -96,6 +164,11 @@ class StateHistoryCharts extends LitElement {
|
||||
return !this.isLoadingData && historyDataEmpty;
|
||||
}
|
||||
|
||||
@eventOptions({ passive: true })
|
||||
private _saveScrollPos(e: Event) {
|
||||
this._savedScrollPos = (e.target as HTMLDivElement).scrollTop;
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return css`
|
||||
:host {
|
||||
@@ -103,11 +176,48 @@ class StateHistoryCharts extends LitElement {
|
||||
/* height of single timeline chart = 60px */
|
||||
min-height: 60px;
|
||||
}
|
||||
|
||||
:host([virtualize]) {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.info {
|
||||
text-align: center;
|
||||
line-height: 60px;
|
||||
color: var(--secondary-text-color);
|
||||
}
|
||||
|
||||
.container {
|
||||
max-height: var(--history-max-height);
|
||||
}
|
||||
|
||||
.entry-container {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.entry-container:hover {
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
:host([virtualize]) .entry-container {
|
||||
padding-left: 1px;
|
||||
padding-right: 1px;
|
||||
}
|
||||
|
||||
.container,
|
||||
lit-virtualizer {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
lit-virtualizer {
|
||||
contain: size layout !important;
|
||||
}
|
||||
|
||||
state-history-chart-timeline,
|
||||
state-history-chart-line {
|
||||
width: 100%;
|
||||
}
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
@@ -31,6 +31,7 @@ const rowRenderer: ComboBoxLitRenderer<HassEntityWithCachedName> = (item) =>
|
||||
<span>${item.friendly_name}</span>
|
||||
<span slot="secondary">${item.entity_id}</span>
|
||||
</mwc-list-item>`;
|
||||
|
||||
@customElement("ha-entity-picker")
|
||||
export class HaEntityPicker extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
@@ -12,8 +12,10 @@ import { property, state } from "lit/decorators";
|
||||
import { ifDefined } from "lit/directives/if-defined";
|
||||
import { styleMap } from "lit/directives/style-map";
|
||||
import { computeActiveState } from "../../common/entity/compute_active_state";
|
||||
import { computeDomain } from "../../common/entity/compute_domain";
|
||||
import { computeStateDomain } from "../../common/entity/compute_state_domain";
|
||||
import { iconColorCSS } from "../../common/style/icon_color_css";
|
||||
import { cameraUrlWithWidthHeight } from "../../data/camera";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import "../ha-state-icon";
|
||||
|
||||
@@ -93,6 +95,9 @@ export class StateBadge extends LitElement {
|
||||
if (this.hass) {
|
||||
imageUrl = this.hass.hassUrl(imageUrl);
|
||||
}
|
||||
if (computeDomain(stateObj.entity_id) === "camera") {
|
||||
imageUrl = cameraUrlWithWidthHeight(imageUrl, 80, 80);
|
||||
}
|
||||
hostStyle.backgroundImage = `url(${imageUrl})`;
|
||||
this._showIcon = false;
|
||||
} else if (stateObj.state === "on") {
|
||||
|
@@ -4,8 +4,7 @@ import { customElement, property, query, state } from "lit/decorators";
|
||||
import { isComponentLoaded } from "../common/config/is_component_loaded";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import { stringCompare } from "../common/string/compare";
|
||||
import { HassioAddonInfo } from "../data/hassio/addon";
|
||||
import { fetchHassioSupervisorInfo } from "../data/hassio/supervisor";
|
||||
import { fetchHassioAddonsInfo, HassioAddonInfo } from "../data/hassio/addon";
|
||||
import { showAlertDialog } from "../dialogs/generic/show-dialog-box";
|
||||
import { PolymerChangedEvent } from "../polymer-types";
|
||||
import { HomeAssistant } from "../types";
|
||||
@@ -78,10 +77,10 @@ class HaAddonPicker extends LitElement {
|
||||
private async _getAddons() {
|
||||
try {
|
||||
if (isComponentLoaded(this.hass, "hassio")) {
|
||||
const supervisorInfo = await fetchHassioSupervisorInfo(this.hass);
|
||||
this._addons = supervisorInfo.addons.sort((a, b) =>
|
||||
stringCompare(a.name, b.name)
|
||||
);
|
||||
const addonsInfo = await fetchHassioAddonsInfo(this.hass);
|
||||
this._addons = addonsInfo.addons
|
||||
.filter((addon) => addon.version)
|
||||
.sort((a, b) => stringCompare(a.name, b.name));
|
||||
} else {
|
||||
showAlertDialog(this, {
|
||||
title: this.hass.localize(
|
||||
|
@@ -67,8 +67,7 @@ export class HaChip extends LitElement {
|
||||
color: var(--ha-chip-icon-color, var(--ha-chip-text-color));
|
||||
}
|
||||
.mdc-chip.mdc-chip--selected .mdc-chip__checkmark,
|
||||
.mdc-chip.no-text
|
||||
.mdc-chip__icon--leading:not(.mdc-chip__icon--leading-hidden) {
|
||||
.mdc-chip .mdc-chip__icon--leading:not(.mdc-chip__icon--leading-hidden) {
|
||||
margin-right: -4px;
|
||||
margin-inline-start: -4px;
|
||||
margin-inline-end: 4px;
|
||||
|
@@ -1,17 +1,13 @@
|
||||
import { ListItemBase } from "@material/mwc-list/mwc-list-item-base";
|
||||
import { styles } from "@material/mwc-list/mwc-list-item.css";
|
||||
import { css, CSSResult, html } from "lit";
|
||||
import { css, CSSResultGroup, html } from "lit";
|
||||
import { customElement, property, query } from "lit/decorators";
|
||||
import { HaListItem } from "./ha-list-item";
|
||||
|
||||
@customElement("ha-clickable-list-item")
|
||||
export class HaClickableListItem extends ListItemBase {
|
||||
export class HaClickableListItem extends HaListItem {
|
||||
@property() public href?: string;
|
||||
|
||||
@property({ type: Boolean }) public disableHref = false;
|
||||
|
||||
// property used only in css
|
||||
@property({ type: Boolean, reflect: true }) public rtl = false;
|
||||
|
||||
@property({ type: Boolean, reflect: true }) public openNewTab = false;
|
||||
|
||||
@query("a") private _anchor!: HTMLAnchorElement;
|
||||
@@ -39,18 +35,10 @@ export class HaClickableListItem extends ListItemBase {
|
||||
});
|
||||
}
|
||||
|
||||
static get styles(): CSSResult[] {
|
||||
static get styles(): CSSResultGroup {
|
||||
return [
|
||||
styles,
|
||||
super.styles,
|
||||
css`
|
||||
:host {
|
||||
padding-left: 0px;
|
||||
padding-right: 0px;
|
||||
}
|
||||
:host([graphic="avatar"]:not([twoLine])),
|
||||
:host([graphic="icon"]:not([twoLine])) {
|
||||
height: 48px;
|
||||
}
|
||||
a {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
@@ -60,19 +48,6 @@ export class HaClickableListItem extends ListItemBase {
|
||||
padding-right: var(--mdc-list-side-padding, 20px);
|
||||
overflow: hidden;
|
||||
}
|
||||
span.material-icons:first-of-type {
|
||||
margin-inline-start: 0px !important;
|
||||
margin-inline-end: var(
|
||||
--mdc-list-item-graphic-margin,
|
||||
16px
|
||||
) !important;
|
||||
direction: var(--direction);
|
||||
}
|
||||
span.material-icons:last-of-type {
|
||||
margin-inline-start: auto !important;
|
||||
margin-inline-end: 0px !important;
|
||||
direction: var(--direction);
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
@@ -2,6 +2,7 @@ import type {
|
||||
Completion,
|
||||
CompletionContext,
|
||||
CompletionResult,
|
||||
CompletionSource,
|
||||
} from "@codemirror/autocomplete";
|
||||
import type { EditorView, KeyBinding, ViewUpdate } from "@codemirror/view";
|
||||
import { HassEntities } from "home-assistant-js-websocket";
|
||||
@@ -11,6 +12,7 @@ import memoizeOne from "memoize-one";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import { loadCodeMirror } from "../resources/codemirror.ondemand";
|
||||
import { HomeAssistant } from "../types";
|
||||
import "./ha-icon";
|
||||
|
||||
declare global {
|
||||
interface HASSDomEvents {
|
||||
@@ -26,6 +28,12 @@ const saveKeyBinding: KeyBinding = {
|
||||
},
|
||||
};
|
||||
|
||||
const renderIcon = (completion: Completion) => {
|
||||
const icon = document.createElement("ha-icon");
|
||||
icon.icon = completion.label;
|
||||
return icon;
|
||||
};
|
||||
|
||||
@customElement("ha-code-editor")
|
||||
export class HaCodeEditor extends ReactiveElement {
|
||||
public codemirror?: EditorView;
|
||||
@@ -41,12 +49,17 @@ export class HaCodeEditor extends ReactiveElement {
|
||||
@property({ type: Boolean, attribute: "autocomplete-entities" })
|
||||
public autocompleteEntities = false;
|
||||
|
||||
@property({ type: Boolean, attribute: "autocomplete-icons" })
|
||||
public autocompleteIcons = false;
|
||||
|
||||
@property() public error = false;
|
||||
|
||||
@state() private _value = "";
|
||||
|
||||
private _loadedCodeMirror?: typeof import("../resources/codemirror");
|
||||
|
||||
private _iconList?: Completion[];
|
||||
|
||||
public set value(value: string) {
|
||||
this._value = value;
|
||||
}
|
||||
@@ -151,13 +164,22 @@ export class HaCodeEditor extends ReactiveElement {
|
||||
),
|
||||
];
|
||||
|
||||
if (!this.readOnly && this.autocompleteEntities && this.hass) {
|
||||
extensions.push(
|
||||
this._loadedCodeMirror.autocompletion({
|
||||
override: [this._entityCompletions.bind(this)],
|
||||
maxRenderedOptions: 10,
|
||||
})
|
||||
);
|
||||
if (!this.readOnly) {
|
||||
const completionSources: CompletionSource[] = [];
|
||||
if (this.autocompleteEntities && this.hass) {
|
||||
completionSources.push(this._entityCompletions.bind(this));
|
||||
}
|
||||
if (this.autocompleteIcons) {
|
||||
completionSources.push(this._mdiCompletions.bind(this));
|
||||
}
|
||||
if (completionSources.length > 0) {
|
||||
extensions.push(
|
||||
this._loadedCodeMirror.autocompletion({
|
||||
override: completionSources,
|
||||
maxRenderedOptions: 10,
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
this.codemirror = new this._loadedCodeMirror.EditorView({
|
||||
@@ -187,7 +209,7 @@ export class HaCodeEditor extends ReactiveElement {
|
||||
private _entityCompletions(
|
||||
context: CompletionContext
|
||||
): CompletionResult | null | Promise<CompletionResult | null> {
|
||||
const entityWord = context.matchBefore(/[a-z_]{3,}\./);
|
||||
const entityWord = context.matchBefore(/[a-z_]{3,}\.\w*/);
|
||||
|
||||
if (
|
||||
!entityWord ||
|
||||
@@ -205,7 +227,48 @@ export class HaCodeEditor extends ReactiveElement {
|
||||
return {
|
||||
from: Number(entityWord.from),
|
||||
options: states,
|
||||
span: /^\w*.\w*$/,
|
||||
span: /^[a-z_]{3,}\.\w*$/,
|
||||
};
|
||||
}
|
||||
|
||||
private _getIconItems = async (): Promise<Completion[]> => {
|
||||
if (!this._iconList) {
|
||||
let iconList: {
|
||||
name: string;
|
||||
keywords: string[];
|
||||
}[];
|
||||
if (__SUPERVISOR__) {
|
||||
iconList = [];
|
||||
} else {
|
||||
iconList = (await import("../../build/mdi/iconList.json")).default;
|
||||
}
|
||||
|
||||
this._iconList = iconList.map((icon) => ({
|
||||
type: "variable",
|
||||
label: `mdi:${icon.name}`,
|
||||
detail: icon.keywords.join(", "),
|
||||
info: renderIcon,
|
||||
}));
|
||||
}
|
||||
|
||||
return this._iconList;
|
||||
};
|
||||
|
||||
private async _mdiCompletions(
|
||||
context: CompletionContext
|
||||
): Promise<CompletionResult | null> {
|
||||
const match = context.matchBefore(/mdi:\S*/);
|
||||
|
||||
if (!match || (match.from === match.to && !context.explicit)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const iconItems = await this._getIconItems();
|
||||
|
||||
return {
|
||||
from: Number(match.from),
|
||||
options: iconItems,
|
||||
span: /^mdi:\S*$/,
|
||||
};
|
||||
}
|
||||
|
||||
|
@@ -1,13 +1,17 @@
|
||||
import "@material/mwc-list/mwc-list-item";
|
||||
import { mdiClose, mdiMenuDown, mdiMenuUp } from "@mdi/js";
|
||||
import "@vaadin/combo-box/theme/material/vaadin-combo-box-light";
|
||||
import type { ComboBoxLight } from "@vaadin/combo-box/vaadin-combo-box-light";
|
||||
import type {
|
||||
ComboBoxLight,
|
||||
ComboBoxLightFilterChangedEvent,
|
||||
ComboBoxLightOpenedChangedEvent,
|
||||
ComboBoxLightValueChangedEvent,
|
||||
} from "@vaadin/combo-box/vaadin-combo-box-light";
|
||||
import { registerStyles } from "@vaadin/vaadin-themable-mixin/register-styles";
|
||||
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
|
||||
import { ComboBoxLitRenderer, comboBoxRenderer } from "lit-vaadin-helpers";
|
||||
import { customElement, property, query } from "lit/decorators";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import { PolymerChangedEvent } from "../polymer-types";
|
||||
import { HomeAssistant } from "../types";
|
||||
import "./ha-icon-button";
|
||||
import "./ha-textfield";
|
||||
@@ -96,6 +100,8 @@ export class HaComboBox extends LitElement {
|
||||
|
||||
@query("vaadin-combo-box-light", true) private _comboBox!: ComboBoxLight;
|
||||
|
||||
private _overlayMutationObserver?: MutationObserver;
|
||||
|
||||
public open() {
|
||||
this.updateComplete.then(() => {
|
||||
this._comboBox?.open();
|
||||
@@ -108,6 +114,14 @@ export class HaComboBox extends LitElement {
|
||||
});
|
||||
}
|
||||
|
||||
public disconnectedCallback() {
|
||||
super.disconnectedCallback();
|
||||
if (this._overlayMutationObserver) {
|
||||
this._overlayMutationObserver.disconnect();
|
||||
this._overlayMutationObserver = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
public get selectedItem() {
|
||||
return this._comboBox.selectedItem;
|
||||
}
|
||||
@@ -193,21 +207,64 @@ export class HaComboBox extends LitElement {
|
||||
}
|
||||
}
|
||||
|
||||
private _openedChanged(ev: PolymerChangedEvent<boolean>) {
|
||||
private _openedChanged(ev: ComboBoxLightOpenedChangedEvent) {
|
||||
const opened = ev.detail.value;
|
||||
// delay this so we can handle click event before setting _opened
|
||||
setTimeout(() => {
|
||||
this._opened = ev.detail.value;
|
||||
this._opened = opened;
|
||||
}, 0);
|
||||
// @ts-ignore
|
||||
fireEvent(this, ev.type, ev.detail);
|
||||
|
||||
if (
|
||||
opened &&
|
||||
"MutationObserver" in window &&
|
||||
!this._overlayMutationObserver
|
||||
) {
|
||||
const overlay = document.querySelector<HTMLElement>(
|
||||
"vaadin-combo-box-overlay"
|
||||
);
|
||||
|
||||
if (!overlay) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._overlayMutationObserver = new MutationObserver((mutations) => {
|
||||
mutations.forEach((mutation) => {
|
||||
if (
|
||||
mutation.type === "attributes" &&
|
||||
mutation.attributeName === "inert"
|
||||
) {
|
||||
this._overlayMutationObserver?.disconnect();
|
||||
this._overlayMutationObserver = undefined;
|
||||
// @ts-expect-error
|
||||
overlay.inert = false;
|
||||
} else if (mutation.type === "childList") {
|
||||
mutation.removedNodes.forEach((node) => {
|
||||
if (node.nodeName === "VAADIN-COMBO-BOX-OVERLAY") {
|
||||
this._overlayMutationObserver?.disconnect();
|
||||
this._overlayMutationObserver = undefined;
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
this._overlayMutationObserver.observe(overlay, {
|
||||
attributes: true,
|
||||
});
|
||||
this._overlayMutationObserver.observe(document.body, {
|
||||
childList: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private _filterChanged(ev: PolymerChangedEvent<string>) {
|
||||
private _filterChanged(ev: ComboBoxLightFilterChangedEvent) {
|
||||
// @ts-ignore
|
||||
fireEvent(this, ev.type, ev.detail, { composed: false });
|
||||
}
|
||||
|
||||
private _valueChanged(ev: PolymerChangedEvent<string>) {
|
||||
private _valueChanged(ev: ComboBoxLightValueChangedEvent) {
|
||||
ev.stopPropagation();
|
||||
const newValue = ev.detail.value;
|
||||
|
||||
|
@@ -91,6 +91,7 @@ export class HaDialog extends DialogBase {
|
||||
.header_title {
|
||||
margin-right: 40px;
|
||||
margin-inline-end: 40px;
|
||||
margin-inline-start: initial;
|
||||
direction: var(--direction);
|
||||
}
|
||||
.header_button {
|
||||
|
@@ -19,6 +19,14 @@ export class HaFab extends FabBase {
|
||||
direction: var(--direction);
|
||||
}
|
||||
`,
|
||||
// safari workaround - must be explicit
|
||||
document.dir === "rtl"
|
||||
? css`
|
||||
:host .mdc-fab--extended .mdc-fab__icon {
|
||||
direction: rtl;
|
||||
}
|
||||
`
|
||||
: css``,
|
||||
];
|
||||
}
|
||||
|
||||
|
@@ -205,6 +205,9 @@ export class HaFormMultiSelect extends LitElement implements HaFormElement {
|
||||
ha-formfield {
|
||||
display: block;
|
||||
padding-right: 16px;
|
||||
padding-inline-end: 16px;
|
||||
padding-inline-start: initial;
|
||||
direction: var(--direction);
|
||||
}
|
||||
ha-textfield {
|
||||
display: block;
|
||||
@@ -216,6 +219,9 @@ export class HaFormMultiSelect extends LitElement implements HaFormElement {
|
||||
right: 1em;
|
||||
top: 1em;
|
||||
cursor: pointer;
|
||||
inset-inline-end: 1em;
|
||||
inset-inline-start: initial;
|
||||
direction: var(--direction);
|
||||
}
|
||||
:host([opened]) ha-svg-icon {
|
||||
color: var(--primary-color);
|
||||
|
@@ -28,10 +28,15 @@ export class HaFormfield extends FormfieldBase {
|
||||
css`
|
||||
:host(:not([alignEnd])) ::slotted(ha-switch) {
|
||||
margin-right: 10px;
|
||||
margin-inline-end: 10px;
|
||||
margin-inline-start: inline;
|
||||
}
|
||||
:host([dir="rtl"]:not([alignEnd])) ::slotted(ha-switch) {
|
||||
margin-left: 10px;
|
||||
margin-right: auto;
|
||||
.mdc-form-field > label {
|
||||
direction: var(--direction);
|
||||
margin-inline-start: 0;
|
||||
margin-inline-end: auto;
|
||||
padding-inline-start: 4px;
|
||||
padding-inline-end: 0;
|
||||
}
|
||||
`,
|
||||
];
|
||||
|
@@ -14,6 +14,7 @@ const getAngle = (value: number, min: number, max: number) => {
|
||||
export interface LevelDefinition {
|
||||
level: number;
|
||||
stroke: string;
|
||||
label?: string;
|
||||
}
|
||||
|
||||
@customElement("ha-gauge")
|
||||
@@ -38,22 +39,31 @@ export class Gauge extends LitElement {
|
||||
|
||||
@state() private _updated = false;
|
||||
|
||||
@state() private _segment_label? = "";
|
||||
|
||||
protected firstUpdated(changedProperties: PropertyValues) {
|
||||
super.firstUpdated(changedProperties);
|
||||
// Wait for the first render for the initial animation to work
|
||||
afterNextRender(() => {
|
||||
this._updated = true;
|
||||
this._angle = getAngle(this.value, this.min, this.max);
|
||||
this._segment_label = this.getSegmentLabel();
|
||||
this._rescale_svg();
|
||||
});
|
||||
}
|
||||
|
||||
protected updated(changedProperties: PropertyValues) {
|
||||
super.updated(changedProperties);
|
||||
if (!this._updated || !changedProperties.has("value")) {
|
||||
if (
|
||||
!this._updated ||
|
||||
(!changedProperties.has("value") &&
|
||||
!changedProperties.has("label") &&
|
||||
!changedProperties.has("_segment_label"))
|
||||
) {
|
||||
return;
|
||||
}
|
||||
this._angle = getAngle(this.value, this.min, this.max);
|
||||
this._segment_label = this.getSegmentLabel();
|
||||
this._rescale_svg();
|
||||
}
|
||||
|
||||
@@ -118,9 +128,11 @@ export class Gauge extends LitElement {
|
||||
</svg>
|
||||
<svg class="text">
|
||||
<text class="value-text">
|
||||
${this.valueText || formatNumber(this.value, this.locale)} ${
|
||||
this.label
|
||||
}
|
||||
${
|
||||
this._segment_label
|
||||
? this._segment_label
|
||||
: this.valueText || formatNumber(this.value, this.locale)
|
||||
} ${this._segment_label ? "" : this.label}
|
||||
</text>
|
||||
</svg>`;
|
||||
}
|
||||
@@ -137,6 +149,18 @@ export class Gauge extends LitElement {
|
||||
);
|
||||
}
|
||||
|
||||
private getSegmentLabel() {
|
||||
if (this.levels) {
|
||||
this.levels.sort((a, b) => a.level - b.level);
|
||||
for (let i = this.levels.length - 1; i >= 0; i--) {
|
||||
if (this.value >= this.levels[i].level) {
|
||||
return this.levels[i].label;
|
||||
}
|
||||
}
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
static get styles() {
|
||||
return css`
|
||||
:host {
|
||||
|
42
src/components/ha-list-item.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
import { ListItemBase } from "@material/mwc-list/mwc-list-item-base";
|
||||
import { styles } from "@material/mwc-list/mwc-list-item.css";
|
||||
import { css, CSSResultGroup } from "lit";
|
||||
import { customElement } from "lit/decorators";
|
||||
|
||||
@customElement("ha-list-item")
|
||||
export class HaListItem extends ListItemBase {
|
||||
static get styles(): CSSResultGroup {
|
||||
return [
|
||||
styles,
|
||||
css`
|
||||
:host {
|
||||
padding-left: var(--mdc-list-side-padding, 20px);
|
||||
padding-right: var(--mdc-list-side-padding, 20px);
|
||||
}
|
||||
:host([graphic="avatar"]:not([twoLine])),
|
||||
:host([graphic="icon"]:not([twoLine])) {
|
||||
height: 48px;
|
||||
}
|
||||
span.material-icons:first-of-type {
|
||||
margin-inline-start: 0px !important;
|
||||
margin-inline-end: var(
|
||||
--mdc-list-item-graphic-margin,
|
||||
16px
|
||||
) !important;
|
||||
direction: var(--direction);
|
||||
}
|
||||
span.material-icons:last-of-type {
|
||||
margin-inline-start: auto !important;
|
||||
margin-inline-end: 0px !important;
|
||||
direction: var(--direction);
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-list-item": HaListItem;
|
||||
}
|
||||
}
|
@@ -1,4 +1,3 @@
|
||||
import "@material/mwc-formfield/mwc-formfield";
|
||||
import "@material/mwc-list/mwc-list-item";
|
||||
import { mdiClose } from "@mdi/js";
|
||||
import { css, html, LitElement } from "lit";
|
||||
@@ -47,14 +46,14 @@ export class HaSelectSelector extends LitElement {
|
||||
${this.label}
|
||||
${options.map(
|
||||
(item: SelectOption) => html`
|
||||
<mwc-formfield .label=${item.label}>
|
||||
<ha-formfield .label=${item.label}>
|
||||
<ha-radio
|
||||
.checked=${item.value === this.value}
|
||||
.value=${item.value}
|
||||
.disabled=${this.disabled}
|
||||
@change=${this._valueChanged}
|
||||
></ha-radio>
|
||||
</mwc-formfield>
|
||||
</ha-formfield>
|
||||
`
|
||||
)}
|
||||
</div>
|
||||
|
@@ -31,6 +31,7 @@ export class HaTemplateSelector extends LitElement {
|
||||
.readOnly=${this.disabled}
|
||||
autofocus
|
||||
autocomplete-entities
|
||||
autocomplete-icons
|
||||
@value-changed=${this._handleChange}
|
||||
dir="ltr"
|
||||
></ha-code-editor>
|
||||
|
@@ -103,6 +103,9 @@ export class HaTextSelector extends LitElement {
|
||||
--mdc-icon-button-size: 24px;
|
||||
--mdc-icon-size: 20px;
|
||||
color: var(--secondary-text-color);
|
||||
inset-inline-start: initial;
|
||||
inset-inline-end: 16px;
|
||||
direction: var(--direction);
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
@@ -42,8 +42,8 @@ import "./entity/ha-entity-picker";
|
||||
import type { HaEntityPickerEntityFilterFunc } from "./entity/ha-entity-picker";
|
||||
import "./ha-area-picker";
|
||||
import "./ha-icon-button";
|
||||
import "./ha-svg-icon";
|
||||
import "./ha-input-helper-text";
|
||||
import "./ha-svg-icon";
|
||||
|
||||
@customElement("ha-target-picker")
|
||||
export class HaTargetPicker extends SubscribeMixin(LitElement) {
|
||||
@@ -79,6 +79,8 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
|
||||
|
||||
@property({ type: Boolean, reflect: true }) public disabled = false;
|
||||
|
||||
@property({ type: Boolean }) public horizontal = false;
|
||||
|
||||
@state() private _areas?: { [areaId: string]: AreaRegistryEntry };
|
||||
|
||||
@state() private _devices?: {
|
||||
@@ -117,7 +119,26 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
|
||||
if (!this._areas || !this._devices || !this._entities) {
|
||||
return html``;
|
||||
}
|
||||
return html`<div class="mdc-chip-set items">
|
||||
return html`
|
||||
${this.horizontal
|
||||
? html`
|
||||
<div class="horizontal-container">
|
||||
${this._renderChips()} ${this._renderPicker()}
|
||||
</div>
|
||||
${this._renderItems()}
|
||||
`
|
||||
: html`
|
||||
<div>
|
||||
${this._renderItems()} ${this._renderPicker()}
|
||||
${this._renderChips()}
|
||||
</div>
|
||||
`}
|
||||
`;
|
||||
}
|
||||
|
||||
private _renderItems() {
|
||||
return html`
|
||||
<div class="mdc-chip-set items">
|
||||
${this.value?.area_id
|
||||
? ensureArray(this.value.area_id).map((area_id) => {
|
||||
const area = this._areas![area_id];
|
||||
@@ -154,7 +175,11 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
|
||||
})
|
||||
: ""}
|
||||
</div>
|
||||
${this._renderPicker()}
|
||||
`;
|
||||
}
|
||||
|
||||
private _renderChips() {
|
||||
return html`
|
||||
<div class="mdc-chip-set">
|
||||
<div
|
||||
class="mdc-chip area_id add"
|
||||
@@ -217,10 +242,10 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
${this.helper
|
||||
? html`<ha-input-helper-text>${this.helper}</ha-input-helper-text>`
|
||||
: ""} `;
|
||||
: ""}
|
||||
`;
|
||||
}
|
||||
|
||||
private async _showPicker(ev) {
|
||||
@@ -309,48 +334,54 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
|
||||
private _renderPicker() {
|
||||
switch (this._addMode) {
|
||||
case "area_id":
|
||||
return html`<ha-area-picker
|
||||
.hass=${this.hass}
|
||||
id="input"
|
||||
.type=${"area_id"}
|
||||
.label=${this.hass.localize(
|
||||
"ui.components.target-picker.add_area_id"
|
||||
)}
|
||||
no-add
|
||||
.deviceFilter=${this.deviceFilter}
|
||||
.entityFilter=${this.entityRegFilter}
|
||||
.includeDeviceClasses=${this.includeDeviceClasses}
|
||||
.includeDomains=${this.includeDomains}
|
||||
@value-changed=${this._targetPicked}
|
||||
></ha-area-picker>`;
|
||||
return html`
|
||||
<ha-area-picker
|
||||
.hass=${this.hass}
|
||||
id="input"
|
||||
.type=${"area_id"}
|
||||
.label=${this.hass.localize(
|
||||
"ui.components.target-picker.add_area_id"
|
||||
)}
|
||||
no-add
|
||||
.deviceFilter=${this.deviceFilter}
|
||||
.entityFilter=${this.entityRegFilter}
|
||||
.includeDeviceClasses=${this.includeDeviceClasses}
|
||||
.includeDomains=${this.includeDomains}
|
||||
@value-changed=${this._targetPicked}
|
||||
></ha-area-picker>
|
||||
`;
|
||||
case "device_id":
|
||||
return html`<ha-device-picker
|
||||
.hass=${this.hass}
|
||||
id="input"
|
||||
.type=${"device_id"}
|
||||
.label=${this.hass.localize(
|
||||
"ui.components.target-picker.add_device_id"
|
||||
)}
|
||||
.deviceFilter=${this.deviceFilter}
|
||||
.entityFilter=${this.entityRegFilter}
|
||||
.includeDeviceClasses=${this.includeDeviceClasses}
|
||||
.includeDomains=${this.includeDomains}
|
||||
@value-changed=${this._targetPicked}
|
||||
></ha-device-picker>`;
|
||||
return html`
|
||||
<ha-device-picker
|
||||
.hass=${this.hass}
|
||||
id="input"
|
||||
.type=${"device_id"}
|
||||
.label=${this.hass.localize(
|
||||
"ui.components.target-picker.add_device_id"
|
||||
)}
|
||||
.deviceFilter=${this.deviceFilter}
|
||||
.entityFilter=${this.entityRegFilter}
|
||||
.includeDeviceClasses=${this.includeDeviceClasses}
|
||||
.includeDomains=${this.includeDomains}
|
||||
@value-changed=${this._targetPicked}
|
||||
></ha-device-picker>
|
||||
`;
|
||||
case "entity_id":
|
||||
return html`<ha-entity-picker
|
||||
.hass=${this.hass}
|
||||
id="input"
|
||||
.type=${"entity_id"}
|
||||
.label=${this.hass.localize(
|
||||
"ui.components.target-picker.add_entity_id"
|
||||
)}
|
||||
.entityFilter=${this.entityFilter}
|
||||
.includeDeviceClasses=${this.includeDeviceClasses}
|
||||
.includeDomains=${this.includeDomains}
|
||||
@value-changed=${this._targetPicked}
|
||||
allow-custom-entity
|
||||
></ha-entity-picker>`;
|
||||
return html`
|
||||
<ha-entity-picker
|
||||
.hass=${this.hass}
|
||||
id="input"
|
||||
.type=${"entity_id"}
|
||||
.label=${this.hass.localize(
|
||||
"ui.components.target-picker.add_entity_id"
|
||||
)}
|
||||
.entityFilter=${this.entityFilter}
|
||||
.includeDeviceClasses=${this.includeDeviceClasses}
|
||||
.includeDomains=${this.includeDomains}
|
||||
@value-changed=${this._targetPicked}
|
||||
allow-custom-entity
|
||||
></ha-entity-picker>
|
||||
`;
|
||||
}
|
||||
return html``;
|
||||
}
|
||||
@@ -539,6 +570,12 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
|
||||
static get styles(): CSSResultGroup {
|
||||
return css`
|
||||
${unsafeCSS(chipStyles)}
|
||||
.horizontal-container {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
min-height: 56px;
|
||||
align-items: center;
|
||||
}
|
||||
.mdc-chip {
|
||||
color: var(--primary-text-color);
|
||||
}
|
||||
|
@@ -61,6 +61,11 @@ export class HaTextField extends TextFieldBase {
|
||||
padding-inline-end: var(--text-field-suffix-padding-right, 0px);
|
||||
direction: var(--direction);
|
||||
}
|
||||
.mdc-text-field--with-leading-icon {
|
||||
padding-inline-start: var(--text-field-suffix-padding-left, 0px);
|
||||
padding-inline-end: var(--text-field-suffix-padding-right, 16px);
|
||||
direction: var(--direction);
|
||||
}
|
||||
|
||||
.mdc-text-field:not(.mdc-text-field--disabled)
|
||||
.mdc-text-field__affix--suffix {
|
||||
@@ -71,6 +76,12 @@ export class HaTextField extends TextFieldBase {
|
||||
color: var(--secondary-text-color);
|
||||
}
|
||||
|
||||
.mdc-text-field__icon--leading {
|
||||
margin-inline-start: 16px;
|
||||
margin-inline-end: 8px;
|
||||
direction: var(--direction);
|
||||
}
|
||||
|
||||
input {
|
||||
text-align: var(--text-field-text-align);
|
||||
}
|
||||
@@ -100,6 +111,7 @@ export class HaTextField extends TextFieldBase {
|
||||
inset-inline-end: initial !important;
|
||||
transform-origin: var(--float-start);
|
||||
direction: var(--direction);
|
||||
transform-origin: var(--float-start);
|
||||
}
|
||||
|
||||
.mdc-text-field--with-leading-icon.mdc-text-field--filled
|
||||
@@ -109,7 +121,25 @@ export class HaTextField extends TextFieldBase {
|
||||
inset-inline-end: initial !important;
|
||||
direction: var(--direction);
|
||||
}
|
||||
|
||||
.mdc-text-field__input[type="number"] {
|
||||
direction: var(--direction);
|
||||
}
|
||||
`,
|
||||
// safari workaround - must be explicit
|
||||
document.dir === "rtl"
|
||||
? css`
|
||||
.mdc-text-field__affix--suffix,
|
||||
.mdc-text-field--with-leading-icon,
|
||||
.mdc-text-field__icon--leading,
|
||||
.mdc-floating-label,
|
||||
.mdc-text-field--with-leading-icon.mdc-text-field--filled
|
||||
.mdc-floating-label,
|
||||
.mdc-text-field__input[type="number"] {
|
||||
direction: rtl;
|
||||
}
|
||||
`
|
||||
: css``,
|
||||
];
|
||||
}
|
||||
|
||||
|
@@ -70,6 +70,7 @@ export class HaYamlEditor extends LitElement {
|
||||
.readOnly=${this.readOnly}
|
||||
mode="yaml"
|
||||
autocomplete-entities
|
||||
autocomplete-icons
|
||||
.error=${this.isValid === false}
|
||||
@value-changed=${this._onChange}
|
||||
dir="ltr"
|
||||
|
@@ -36,7 +36,7 @@ declare global {
|
||||
class BrowseMediaTTS extends LitElement {
|
||||
@property() public hass!: HomeAssistant;
|
||||
|
||||
@property() public item;
|
||||
@property() public item!: MediaPlayerItem;
|
||||
|
||||
@property() public action!: MediaPlayerBrowseAction;
|
||||
|
||||
|
@@ -28,6 +28,7 @@ import { fireEvent } from "../../common/dom/fire_event";
|
||||
import { computeRTLDirection } from "../../common/util/compute_rtl";
|
||||
import { debounce } from "../../common/util/debounce";
|
||||
import { getSignedPath } from "../../data/auth";
|
||||
import { UNAVAILABLE_STATES } from "../../data/entity";
|
||||
import type { MediaPlayerItem } from "../../data/media-player";
|
||||
import {
|
||||
browseMediaPlayer,
|
||||
@@ -42,9 +43,14 @@ import { showAlertDialog } from "../../dialogs/generic/show-dialog-box";
|
||||
import { installResizeObserver } from "../../panels/lovelace/common/install-resize-observer";
|
||||
import { haStyle } from "../../resources/styles";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import { brandsUrl, extractDomainFromBrandUrl } from "../../util/brands-url";
|
||||
import {
|
||||
brandsUrl,
|
||||
extractDomainFromBrandUrl,
|
||||
isBrandUrl,
|
||||
} from "../../util/brands-url";
|
||||
import { documentationUrl } from "../../util/documentation-url";
|
||||
import "../entity/ha-entity-picker";
|
||||
import "../ha-alert";
|
||||
import "../ha-button-menu";
|
||||
import "../ha-card";
|
||||
import "../ha-circular-progress";
|
||||
@@ -110,9 +116,6 @@ export class HaMediaPlayerBrowse extends LitElement {
|
||||
|
||||
private _resizeObserver?: ResizeObserver;
|
||||
|
||||
// @ts-ignore
|
||||
private _intersectionObserver?: IntersectionObserver;
|
||||
|
||||
public connectedCallback(): void {
|
||||
super.connectedCallback();
|
||||
this.updateComplete.then(() => this._attachResizeObserver());
|
||||
@@ -122,9 +125,6 @@ export class HaMediaPlayerBrowse extends LitElement {
|
||||
if (this._resizeObserver) {
|
||||
this._resizeObserver.disconnect();
|
||||
}
|
||||
if (this._intersectionObserver) {
|
||||
this._intersectionObserver.disconnect();
|
||||
}
|
||||
}
|
||||
|
||||
public async refresh() {
|
||||
@@ -246,6 +246,16 @@ export class HaMediaPlayerBrowse extends LitElement {
|
||||
],
|
||||
replace: true,
|
||||
});
|
||||
} else if (
|
||||
err.code === "entity_not_found" &&
|
||||
UNAVAILABLE_STATES.includes(this.hass.states[this.entityId]?.state)
|
||||
) {
|
||||
this._setError({
|
||||
message: this.hass.localize(
|
||||
`ui.components.media-browser.media_player_unavailable`
|
||||
),
|
||||
code: "entity_not_found",
|
||||
});
|
||||
} else {
|
||||
this._setError(err);
|
||||
}
|
||||
@@ -305,7 +315,11 @@ export class HaMediaPlayerBrowse extends LitElement {
|
||||
protected render(): TemplateResult {
|
||||
if (this._error) {
|
||||
return html`
|
||||
<div class="container">${this._renderError(this._error)}</div>
|
||||
<div class="container">
|
||||
<ha-alert alert-type="error">
|
||||
${this._renderError(this._error)}
|
||||
</ha-alert>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -420,7 +434,9 @@ export class HaMediaPlayerBrowse extends LitElement {
|
||||
this._error
|
||||
? html`
|
||||
<div class="container">
|
||||
${this._renderError(this._error)}
|
||||
<ha-alert alert-type="error">
|
||||
${this._renderError(this._error)}
|
||||
</ha-alert>
|
||||
</div>
|
||||
`
|
||||
: isTTSMediaSource(currentItem.media_content_id)
|
||||
@@ -463,7 +479,10 @@ export class HaMediaPlayerBrowse extends LitElement {
|
||||
.layout=${grid({
|
||||
itemSize: {
|
||||
width: "175px",
|
||||
height: "225px",
|
||||
height:
|
||||
childrenMediaClass.thumbnail_ratio === "portrait"
|
||||
? "312px"
|
||||
: "225px",
|
||||
},
|
||||
gap: "16px",
|
||||
flex: { preserve: "aspect-ratio" },
|
||||
@@ -545,6 +564,8 @@ export class HaMediaPlayerBrowse extends LitElement {
|
||||
<div
|
||||
class="${["app", "directory"].includes(child.media_class)
|
||||
? "centered-image"
|
||||
: ""} ${isBrandUrl(child.thumbnail)
|
||||
? "brand-image"
|
||||
: ""} image"
|
||||
style="background-image: ${until(backgroundImage, "")}"
|
||||
></div>
|
||||
@@ -643,7 +664,7 @@ export class HaMediaPlayerBrowse extends LitElement {
|
||||
return (await getSignedPath(this.hass, thumbnailUrl)).path;
|
||||
}
|
||||
|
||||
if (thumbnailUrl.startsWith("https://brands.home-assistant.io")) {
|
||||
if (isBrandUrl(thumbnailUrl)) {
|
||||
// The backend is not aware of the theme used by the users,
|
||||
// so we rewrite the URL to show a proper icon
|
||||
thumbnailUrl = brandsUrl({
|
||||
@@ -1032,6 +1053,10 @@ export class HaMediaPlayerBrowse extends LitElement {
|
||||
background-size: contain;
|
||||
}
|
||||
|
||||
.brand-image {
|
||||
background-size: 40%;
|
||||
}
|
||||
|
||||
.children ha-card .icon-holder {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
|
@@ -8,6 +8,7 @@ import { fetchUsers, User } from "../../data/user";
|
||||
import { HomeAssistant } from "../../types";
|
||||
import "../ha-select";
|
||||
import "./ha-user-badge";
|
||||
import "../ha-list-item";
|
||||
|
||||
class HaUserPicker extends LitElement {
|
||||
public hass?: HomeAssistant;
|
||||
@@ -48,14 +49,14 @@ class HaUserPicker extends LitElement {
|
||||
: ""}
|
||||
${this._sortedUsers(this.users).map(
|
||||
(user) => html`
|
||||
<mwc-list-item graphic="avatar" .value=${user.id}>
|
||||
<ha-list-item graphic="avatar" .value=${user.id}>
|
||||
<ha-user-badge
|
||||
.hass=${this.hass}
|
||||
.user=${user}
|
||||
slot="graphic"
|
||||
></ha-user-badge>
|
||||
${user.name}
|
||||
</mwc-list-item>
|
||||
</ha-list-item>
|
||||
`
|
||||
)}
|
||||
</ha-select>
|
||||
|
@@ -1,7 +1,11 @@
|
||||
import { HomeAssistant } from "../types";
|
||||
|
||||
export interface ApplicationCredentialsDomainConfig {
|
||||
description_placeholders: string;
|
||||
}
|
||||
|
||||
export interface ApplicationCredentialsConfig {
|
||||
domains: string[];
|
||||
integrations: Record<string, ApplicationCredentialsDomainConfig>;
|
||||
}
|
||||
|
||||
export interface ApplicationCredential {
|
||||
|
@@ -158,8 +158,14 @@ export const getRecentWithCache = (
|
||||
}
|
||||
const stateHistory = computeHistory(hass, fetchedHistory, localize);
|
||||
if (appendingToCache) {
|
||||
mergeLine(stateHistory.line, cache.data.line);
|
||||
mergeTimeline(stateHistory.timeline, cache.data.timeline);
|
||||
if (stateHistory.line.length) {
|
||||
mergeLine(stateHistory.line, cache.data.line);
|
||||
}
|
||||
if (stateHistory.timeline.length) {
|
||||
mergeTimeline(stateHistory.timeline, cache.data.timeline);
|
||||
// Replace the timeline array to force an update
|
||||
cache.data.timeline = [...cache.data.timeline];
|
||||
}
|
||||
pruneStartTime(startTime, cache.data);
|
||||
} else {
|
||||
cache.data = stateHistory;
|
||||
@@ -191,6 +197,8 @@ const mergeLine = (
|
||||
oldLine.data.push(entity);
|
||||
}
|
||||
});
|
||||
// Replace the cached line data to force an update
|
||||
oldLine.data = [...oldLine.data];
|
||||
} else {
|
||||
cacheLines.push(line);
|
||||
}
|
||||
|
@@ -41,6 +41,12 @@ export interface WebRtcAnswer {
|
||||
answer: string;
|
||||
}
|
||||
|
||||
export const cameraUrlWithWidthHeight = (
|
||||
base_url: string,
|
||||
width: number,
|
||||
height: number
|
||||
) => `${base_url}&width=${width}&height=${height}`;
|
||||
|
||||
export const computeMJPEGStreamUrl = (entity: CameraEntity) =>
|
||||
`/api/camera_proxy_stream/${entity.entity_id}?token=${entity.attributes.access_token}`;
|
||||
|
||||
@@ -57,7 +63,7 @@ export const fetchThumbnailUrlWithCache = async (
|
||||
hass,
|
||||
entityId
|
||||
);
|
||||
return `${base_url}&width=${width}&height=${height}`;
|
||||
return cameraUrlWithWidthHeight(base_url, width, height);
|
||||
};
|
||||
|
||||
export const fetchThumbnailUrl = async (
|
||||
|
@@ -38,19 +38,19 @@ export const getConfigEntries = (
|
||||
hass: HomeAssistant,
|
||||
filters?: { type?: "helper" | "integration"; domain?: string }
|
||||
): Promise<ConfigEntry[]> => {
|
||||
const params = new URLSearchParams();
|
||||
const params: any = {};
|
||||
if (filters) {
|
||||
if (filters.type) {
|
||||
params.append("type", filters.type);
|
||||
params.type_filter = filters.type;
|
||||
}
|
||||
if (filters.domain) {
|
||||
params.append("domain", filters.domain);
|
||||
params.domain = filters.domain;
|
||||
}
|
||||
}
|
||||
return hass.callApi<ConfigEntry[]>(
|
||||
"GET",
|
||||
`config/config_entries/entry?${params.toString()}`
|
||||
);
|
||||
return hass.callWS<ConfigEntry[]>({
|
||||
type: "config_entries/get",
|
||||
...params,
|
||||
});
|
||||
};
|
||||
|
||||
export const updateConfigEntry = (
|
||||
|
@@ -240,6 +240,7 @@ export interface EnergyData {
|
||||
prefs: EnergyPreferences;
|
||||
info: EnergyInfo;
|
||||
stats: Statistics;
|
||||
statsMetadata: Record<string, StatisticsMetaData>;
|
||||
statsCompare: Statistics;
|
||||
co2SignalConfigEntry?: ConfigEntry;
|
||||
co2SignalEntity?: string;
|
||||
@@ -285,15 +286,6 @@ const getEnergyData = async (
|
||||
|
||||
const consumptionStatIDs: string[] = [];
|
||||
const statIDs: string[] = [];
|
||||
const gasSources: GasSourceTypeEnergyPreference[] =
|
||||
prefs.energy_sources.filter(
|
||||
(source) => source.type === "gas"
|
||||
) as GasSourceTypeEnergyPreference[];
|
||||
const gasStatisticIdsWithMeta: StatisticsMetaData[] =
|
||||
await getStatisticMetadata(
|
||||
hass,
|
||||
gasSources.map((source) => source.stat_energy_from)
|
||||
);
|
||||
|
||||
for (const source of prefs.energy_sources) {
|
||||
if (source.type === "solar") {
|
||||
@@ -303,20 +295,6 @@ const getEnergyData = async (
|
||||
|
||||
if (source.type === "gas") {
|
||||
statIDs.push(source.stat_energy_from);
|
||||
const entity = hass.states[source.stat_energy_from];
|
||||
if (!entity) {
|
||||
for (const statisticIdWithMeta of gasStatisticIdsWithMeta) {
|
||||
if (
|
||||
statisticIdWithMeta?.statistic_id === source.stat_energy_from &&
|
||||
statisticIdWithMeta?.unit_of_measurement
|
||||
) {
|
||||
source.unit_of_measurement =
|
||||
statisticIdWithMeta?.unit_of_measurement === "Wh"
|
||||
? "kWh"
|
||||
: statisticIdWithMeta?.unit_of_measurement;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (source.stat_cost) {
|
||||
statIDs.push(source.stat_cost);
|
||||
}
|
||||
@@ -432,6 +410,12 @@ const getEnergyData = async (
|
||||
}
|
||||
});
|
||||
|
||||
const statsMetadataArray = await getStatisticMetadata(hass, statIDs);
|
||||
const statsMetadata: Record<string, StatisticsMetaData> = {};
|
||||
statsMetadataArray.forEach((x) => {
|
||||
statsMetadata[x.statistic_id] = x;
|
||||
});
|
||||
|
||||
const data: EnergyData = {
|
||||
start,
|
||||
end,
|
||||
@@ -440,6 +424,7 @@ const getEnergyData = async (
|
||||
info,
|
||||
prefs,
|
||||
stats,
|
||||
statsMetadata,
|
||||
statsCompare,
|
||||
co2SignalConfigEntry,
|
||||
co2SignalEntity,
|
||||
@@ -628,13 +613,13 @@ export const getEnergyGasUnitCategory = (
|
||||
|
||||
export const getEnergyGasUnit = (
|
||||
hass: HomeAssistant,
|
||||
prefs: EnergyPreferences
|
||||
prefs: EnergyPreferences,
|
||||
statisticsMetaData: Record<string, StatisticsMetaData> = {}
|
||||
): string | undefined => {
|
||||
for (const source of prefs.energy_sources) {
|
||||
if (source.type !== "gas") {
|
||||
continue;
|
||||
}
|
||||
|
||||
const entity = hass.states[source.stat_energy_from];
|
||||
if (entity?.attributes.unit_of_measurement) {
|
||||
// Wh is normalized to kWh by stats generation
|
||||
@@ -642,8 +627,11 @@ export const getEnergyGasUnit = (
|
||||
? "kWh"
|
||||
: entity.attributes.unit_of_measurement;
|
||||
}
|
||||
if (source.unit_of_measurement) {
|
||||
return source.unit_of_measurement;
|
||||
const statisticIdWithMeta = statisticsMetaData[source.stat_energy_from];
|
||||
if (statisticIdWithMeta?.unit_of_measurement) {
|
||||
return statisticIdWithMeta.unit_of_measurement === "Wh"
|
||||
? "kWh"
|
||||
: statisticIdWithMeta.unit_of_measurement;
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
|
@@ -33,6 +33,18 @@ export interface UpdateEntityRegistryEntryResult {
|
||||
require_restart?: boolean;
|
||||
}
|
||||
|
||||
export interface SensorEntityOptions {
|
||||
unit_of_measurement?: string | null;
|
||||
}
|
||||
|
||||
export interface WeatherEntityOptions {
|
||||
precipitation_unit?: string | null;
|
||||
pressure_unit?: string | null;
|
||||
temperature_unit?: string | null;
|
||||
visibility_unit?: string | null;
|
||||
wind_speed_unit?: string | null;
|
||||
}
|
||||
|
||||
export interface EntityRegistryEntryUpdateParams {
|
||||
name?: string | null;
|
||||
icon?: string | null;
|
||||
@@ -42,9 +54,7 @@ export interface EntityRegistryEntryUpdateParams {
|
||||
hidden_by: string | null;
|
||||
new_entity_id?: string;
|
||||
options_domain?: string;
|
||||
options?: {
|
||||
unit_of_measurement?: string | null;
|
||||
};
|
||||
options?: SensorEntityOptions | WeatherEntityOptions;
|
||||
}
|
||||
|
||||
export const findBatteryEntity = (
|
||||
|
@@ -1,7 +1,9 @@
|
||||
import { atLeastVersion } from "../../common/config/version";
|
||||
import type { HaFormSchema } from "../../components/ha-form/types";
|
||||
import { HomeAssistant } from "../../types";
|
||||
import { SupervisorArch } from "../supervisor/supervisor";
|
||||
import { supervisorApiCall } from "../supervisor/common";
|
||||
import { StoreAddonDetails } from "../supervisor/store";
|
||||
import { Supervisor, SupervisorArch } from "../supervisor/supervisor";
|
||||
import {
|
||||
extractApiErrorMessage,
|
||||
hassioApiResultExtractor,
|
||||
@@ -363,3 +365,15 @@ export const uninstallHassioAddon = async (
|
||||
`hassio/addons/${slug}/uninstall`
|
||||
);
|
||||
};
|
||||
|
||||
export const fetchAddonInfo = (
|
||||
hass: HomeAssistant,
|
||||
supervisor: Supervisor,
|
||||
addonSlug: string
|
||||
): Promise<HassioAddonDetails | StoreAddonDetails> =>
|
||||
supervisorApiCall(
|
||||
hass,
|
||||
!supervisor.addon?.addons.find((addon) => addon.slug === addonSlug)
|
||||
? `/store/addons/${addonSlug}` // Use /store/addons when add-on is not installed
|
||||
: `/addons/${addonSlug}/info` // Use /addons when add-on is installed
|
||||
);
|
||||
|
@@ -1,7 +1,6 @@
|
||||
import { atLeastVersion } from "../../common/config/version";
|
||||
import { HomeAssistant, PanelInfo } from "../../types";
|
||||
import { SupervisorArch } from "../supervisor/supervisor";
|
||||
import { HassioAddonInfo } from "./addon";
|
||||
import { hassioApiResultExtractor, HassioResponse } from "./common";
|
||||
|
||||
export type HassioHomeAssistantInfo = {
|
||||
@@ -22,7 +21,7 @@ export type HassioHomeAssistantInfo = {
|
||||
};
|
||||
|
||||
export type HassioSupervisorInfo = {
|
||||
addons: HassioAddonInfo[];
|
||||
addons: string[];
|
||||
addons_repositories: string[];
|
||||
arch: SupervisorArch;
|
||||
channel: string;
|
||||
|
@@ -1,7 +1,10 @@
|
||||
import { HassEntity } from "home-assistant-js-websocket";
|
||||
import { computeDomain } from "../common/entity/compute_domain";
|
||||
import { computeStateDisplayFromEntityAttributes } from "../common/entity/compute_state_display";
|
||||
import { computeStateNameFromEntityAttributes } from "../common/entity/compute_state_name";
|
||||
import {
|
||||
computeStateName,
|
||||
computeStateNameFromEntityAttributes,
|
||||
} from "../common/entity/compute_state_name";
|
||||
import { LocalizeFunc } from "../common/translations/localize";
|
||||
import { HomeAssistant } from "../types";
|
||||
import { FrontendLocaleData } from "./translation";
|
||||
@@ -220,16 +223,12 @@ export const fetchDate = (
|
||||
hass: HomeAssistant,
|
||||
startTime: Date,
|
||||
endTime: Date,
|
||||
entityId?: string
|
||||
entityIds: string[]
|
||||
): Promise<HassEntity[][]> =>
|
||||
hass.callApi(
|
||||
"GET",
|
||||
`history/period/${startTime.toISOString()}?end_time=${endTime.toISOString()}&minimal_response${
|
||||
entityId ? `&filter_entity_id=${entityId}` : ``
|
||||
}${
|
||||
entityId && !entityIdHistoryNeedsAttributes(hass, entityId)
|
||||
? `&no_attributes`
|
||||
: ``
|
||||
entityIds ? `&filter_entity_id=${entityIds.join(",")}` : ``
|
||||
}`
|
||||
);
|
||||
|
||||
@@ -237,19 +236,19 @@ export const fetchDateWS = (
|
||||
hass: HomeAssistant,
|
||||
startTime: Date,
|
||||
endTime: Date,
|
||||
entityId?: string
|
||||
entityIds: string[]
|
||||
) => {
|
||||
const params = {
|
||||
type: "history/history_during_period",
|
||||
start_time: startTime.toISOString(),
|
||||
end_time: endTime.toISOString(),
|
||||
minimal_response: true,
|
||||
no_attributes: !!(
|
||||
entityId && !entityIdHistoryNeedsAttributes(hass, entityId)
|
||||
),
|
||||
no_attributes: !entityIds
|
||||
.map((entityId) => entityIdHistoryNeedsAttributes(hass, entityId))
|
||||
.reduce((cur, next) => cur || next, false),
|
||||
};
|
||||
if (entityId) {
|
||||
return hass.callWS<HistoryStates>({ ...params, entity_ids: [entityId] });
|
||||
if (entityIds.length !== 0) {
|
||||
return hass.callWS<HistoryStates>({ ...params, entity_ids: entityIds });
|
||||
}
|
||||
return hass.callWS<HistoryStates>(params);
|
||||
};
|
||||
@@ -547,3 +546,16 @@ export const adjustStatisticsSum = (
|
||||
start_time,
|
||||
adjustment,
|
||||
});
|
||||
|
||||
export const getStatisticLabel = (
|
||||
hass: HomeAssistant,
|
||||
statisticsId: string,
|
||||
statisticsMetaData: Record<string, StatisticsMetaData>
|
||||
): string => {
|
||||
const entity = hass.states[statisticsId];
|
||||
if (entity) {
|
||||
return computeStateName(entity);
|
||||
}
|
||||
const statisticMetaData = statisticsMetaData[statisticsId];
|
||||
return statisticMetaData?.name || statisticsId;
|
||||
};
|
||||
|
@@ -1,4 +1,4 @@
|
||||
import { HassEntity, UnsubscribeFunc } from "home-assistant-js-websocket";
|
||||
import { HassEntity } from "home-assistant-js-websocket";
|
||||
import {
|
||||
BINARY_STATE_OFF,
|
||||
BINARY_STATE_ON,
|
||||
@@ -6,12 +6,14 @@ import {
|
||||
} from "../common/const";
|
||||
import { computeDomain } from "../common/entity/compute_domain";
|
||||
import { computeStateDisplay } from "../common/entity/compute_state_display";
|
||||
import { computeStateDomain } from "../common/entity/compute_state_domain";
|
||||
import { LocalizeFunc } from "../common/translations/localize";
|
||||
import { HaEntityPickerEntityFilterFunc } from "../components/entity/ha-entity-picker";
|
||||
import { HomeAssistant } from "../types";
|
||||
import { UNAVAILABLE_STATES } from "./entity";
|
||||
|
||||
const LOGBOOK_LOCALIZE_PATH = "ui.components.logbook.messages";
|
||||
export const CONTINUOUS_DOMAINS = ["proximity", "sensor"];
|
||||
export const CONTINUOUS_DOMAINS = ["counter", "proximity", "sensor"];
|
||||
|
||||
export interface LogbookStreamMessage {
|
||||
events: LogbookEntry[];
|
||||
@@ -175,7 +177,7 @@ export const subscribeLogbook = (
|
||||
endDate: string,
|
||||
entityIds?: string[],
|
||||
deviceIds?: string[]
|
||||
): Promise<UnsubscribeFunc> => {
|
||||
): Promise<() => Promise<void>> => {
|
||||
// If all specified filters are empty lists, we can return an empty list.
|
||||
if (
|
||||
(entityIds || deviceIds) &&
|
||||
@@ -425,3 +427,10 @@ export const localizeStateMessage = (
|
||||
: state
|
||||
);
|
||||
};
|
||||
|
||||
export const filterLogbookCompatibleEntities: HaEntityPickerEntityFilterFunc = (
|
||||
entity
|
||||
) =>
|
||||
computeStateDomain(entity) !== "sensor" ||
|
||||
(entity.attributes.unit_of_measurement === undefined &&
|
||||
entity.attributes.state_class === undefined);
|
||||
|
@@ -159,6 +159,7 @@ export interface CustomActionConfig extends BaseActionConfig {
|
||||
}
|
||||
|
||||
export interface BaseActionConfig {
|
||||
action: string;
|
||||
confirmation?: ConfirmationRestrictionConfig;
|
||||
}
|
||||
|
||||
|
@@ -36,6 +36,7 @@ import { supportsFeature } from "../common/entity/supports-feature";
|
||||
import { MediaPlayerItemId } from "../components/media-player/ha-media-player-browse";
|
||||
import type { HomeAssistant } from "../types";
|
||||
import { UNAVAILABLE_STATES } from "./entity";
|
||||
import { isTTSMediaSource } from "./tts";
|
||||
|
||||
interface MediaPlayerEntityAttributes extends HassEntityAttributeBase {
|
||||
media_content_id?: string;
|
||||
@@ -441,3 +442,29 @@ export const handleMediaControlClick = (
|
||||
entity_id: stateObj!.entity_id,
|
||||
}
|
||||
);
|
||||
|
||||
export const mediaPlayerPlayMedia = (
|
||||
hass: HomeAssistant,
|
||||
entity_id: string,
|
||||
media_content_id: string,
|
||||
media_content_type: string,
|
||||
extra: {
|
||||
enqueue?: "play" | "next" | "add" | "replace";
|
||||
announce?: boolean;
|
||||
} = {}
|
||||
) => {
|
||||
// We set text-to-speech to announce.
|
||||
if (
|
||||
!extra.enqueue &&
|
||||
extra.announce === undefined &&
|
||||
isTTSMediaSource(media_content_id)
|
||||
) {
|
||||
extra.announce = true;
|
||||
}
|
||||
return hass.callService("media_player", "play_media", {
|
||||
entity_id,
|
||||
media_content_id,
|
||||
media_content_type,
|
||||
...extra,
|
||||
});
|
||||
};
|
||||
|
34
src/data/supervisor/common.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
import { atLeastVersion } from "../../common/config/version";
|
||||
import { HomeAssistant } from "../../types";
|
||||
import { hassioApiResultExtractor, HassioResponse } from "../hassio/common";
|
||||
|
||||
export interface SupervisorApiCallOptions {
|
||||
method?: "get" | "post" | "delete";
|
||||
data?: Record<string, any>;
|
||||
timeout?: number;
|
||||
}
|
||||
|
||||
export const supervisorApiCall = async <T>(
|
||||
hass: HomeAssistant,
|
||||
endpoint: string,
|
||||
options?: SupervisorApiCallOptions
|
||||
): Promise<T> => {
|
||||
if (atLeastVersion(hass.config.version, 2021, 2, 4)) {
|
||||
// Websockets was added in 2021.2.4
|
||||
return hass.callWS<T>({
|
||||
type: "supervisor/api",
|
||||
endpoint,
|
||||
method: options?.method || "get",
|
||||
timeout: options?.timeout ?? null,
|
||||
data: options?.data,
|
||||
});
|
||||
}
|
||||
return hassioApiResultExtractor(
|
||||
await hass.callApi<HassioResponse<T>>(
|
||||
// @ts-ignore
|
||||
(options.method || "get").toUpperCase(),
|
||||
`hassio${endpoint}`,
|
||||
options?.data
|
||||
)
|
||||
);
|
||||
};
|
@@ -1,7 +1,7 @@
|
||||
import { atLeastVersion } from "../../common/config/version";
|
||||
import { HomeAssistant } from "../../types";
|
||||
import { AddonRepository, AddonStage } from "../hassio/addon";
|
||||
import { hassioApiResultExtractor, HassioResponse } from "../hassio/common";
|
||||
import { AddonStage } from "../hassio/addon";
|
||||
import { supervisorApiCall } from "./common";
|
||||
import { SupervisorArch } from "./supervisor";
|
||||
|
||||
export interface StoreAddon {
|
||||
advanced: boolean;
|
||||
@@ -13,14 +13,34 @@ export interface StoreAddon {
|
||||
installed: boolean;
|
||||
logo: boolean;
|
||||
name: string;
|
||||
repository: AddonRepository;
|
||||
repository: string;
|
||||
slug: string;
|
||||
stage: AddonStage;
|
||||
update_available: boolean;
|
||||
url: string;
|
||||
version: string | null;
|
||||
version_latest: string;
|
||||
version: null;
|
||||
}
|
||||
|
||||
export interface StoreAddonDetails extends StoreAddon {
|
||||
apparmor: boolean;
|
||||
arch: SupervisorArch[];
|
||||
auth_api: boolean;
|
||||
detached: boolean;
|
||||
docker_api: boolean;
|
||||
documentation: boolean;
|
||||
full_access: boolean;
|
||||
hassio_api: boolean;
|
||||
hassio_role: string;
|
||||
homeassistant_api: boolean;
|
||||
host_network: boolean;
|
||||
host_pid: boolean;
|
||||
ingress: boolean;
|
||||
long_description: string;
|
||||
rating: number;
|
||||
signed: boolean;
|
||||
}
|
||||
|
||||
interface StoreRepository {
|
||||
maintainer: string;
|
||||
name: string;
|
||||
@@ -36,16 +56,25 @@ export interface SupervisorStore {
|
||||
|
||||
export const fetchSupervisorStore = async (
|
||||
hass: HomeAssistant
|
||||
): Promise<SupervisorStore> => {
|
||||
if (atLeastVersion(hass.config.version, 2021, 2, 4)) {
|
||||
return hass.callWS({
|
||||
type: "supervisor/api",
|
||||
endpoint: "/store",
|
||||
method: "get",
|
||||
});
|
||||
}
|
||||
): Promise<SupervisorStore> => supervisorApiCall(hass, "/store");
|
||||
|
||||
return hassioApiResultExtractor(
|
||||
await hass.callApi<HassioResponse<SupervisorStore>>("GET", `hassio/store`)
|
||||
);
|
||||
};
|
||||
export const fetchStoreRepositories = async (
|
||||
hass: HomeAssistant
|
||||
): Promise<StoreRepository[]> => supervisorApiCall(hass, "/store/repositories");
|
||||
|
||||
export const addStoreRepository = async (
|
||||
hass: HomeAssistant,
|
||||
repository: string
|
||||
): Promise<void> =>
|
||||
supervisorApiCall(hass, "/store/repositories", {
|
||||
method: "post",
|
||||
data: { repository },
|
||||
});
|
||||
|
||||
export const removeStoreRepository = async (
|
||||
hass: HomeAssistant,
|
||||
repository: string
|
||||
): Promise<void> =>
|
||||
supervisorApiCall(hass, `/store/repositories/${repository}`, {
|
||||
method: "delete",
|
||||
});
|
||||
|
@@ -38,7 +38,8 @@ export type TranslationCategory =
|
||||
| "device_automation"
|
||||
| "mfa_setup"
|
||||
| "system_health"
|
||||
| "device_class";
|
||||
| "device_class"
|
||||
| "application_credentials";
|
||||
|
||||
export const fetchTranslationPreferences = (hass: HomeAssistant) =>
|
||||
fetchFrontendUserData(hass.connection, "language");
|
||||
|
@@ -37,14 +37,24 @@ interface ForecastAttribute {
|
||||
humidity?: number;
|
||||
condition?: string;
|
||||
daytime?: boolean;
|
||||
pressure?: number;
|
||||
wind_speed?: string;
|
||||
}
|
||||
|
||||
interface WeatherEntityAttributes extends HassEntityAttributeBase {
|
||||
temperature: number;
|
||||
attribution?: string;
|
||||
humidity?: number;
|
||||
forecast?: ForecastAttribute[];
|
||||
wind_speed: string;
|
||||
wind_bearing: string;
|
||||
pressure?: number;
|
||||
temperature?: number;
|
||||
visibility?: number;
|
||||
wind_bearing?: number | string;
|
||||
wind_speed?: number;
|
||||
precipitation_unit: string;
|
||||
pressure_unit: string;
|
||||
temperature_unit: string;
|
||||
visibility_unit: string;
|
||||
wind_speed_unit: string;
|
||||
}
|
||||
|
||||
export interface WeatherEntity extends HassEntityBase {
|
||||
@@ -138,16 +148,16 @@ const cardinalDirections = [
|
||||
"N",
|
||||
];
|
||||
|
||||
const getWindBearingText = (degree: string): string => {
|
||||
const degreenum = parseInt(degree, 10);
|
||||
const getWindBearingText = (degree: number | string): string => {
|
||||
const degreenum = typeof degree === "number" ? degree : parseInt(degree, 10);
|
||||
if (isFinite(degreenum)) {
|
||||
// eslint-disable-next-line no-bitwise
|
||||
return cardinalDirections[(((degreenum + 11.25) / 22.5) | 0) % 16];
|
||||
}
|
||||
return degree;
|
||||
return typeof degree === "number" ? degree.toString() : degree;
|
||||
};
|
||||
|
||||
const getWindBearing = (bearing: string): string => {
|
||||
const getWindBearing = (bearing: number | string): string => {
|
||||
if (bearing != null) {
|
||||
return getWindBearingText(bearing);
|
||||
}
|
||||
@@ -156,14 +166,19 @@ const getWindBearing = (bearing: string): string => {
|
||||
|
||||
export const getWind = (
|
||||
hass: HomeAssistant,
|
||||
speed: string,
|
||||
bearing: string
|
||||
stateObj: WeatherEntity,
|
||||
speed?: number,
|
||||
bearing?: number | string
|
||||
): string => {
|
||||
const speedText = `${formatNumber(speed, hass.locale)} ${getWeatherUnit(
|
||||
hass!,
|
||||
"wind_speed"
|
||||
)}`;
|
||||
if (bearing !== null) {
|
||||
const speedText =
|
||||
speed !== undefined && speed !== null
|
||||
? `${formatNumber(speed, hass.locale)} ${getWeatherUnit(
|
||||
hass!,
|
||||
stateObj,
|
||||
"wind_speed"
|
||||
)}`
|
||||
: "-";
|
||||
if (bearing !== undefined && bearing !== null) {
|
||||
const cardinalDirection = getWindBearing(bearing);
|
||||
return `${speedText} (${
|
||||
hass.localize(
|
||||
@@ -176,13 +191,28 @@ export const getWind = (
|
||||
|
||||
export const getWeatherUnit = (
|
||||
hass: HomeAssistant,
|
||||
stateObj: WeatherEntity,
|
||||
measure: string
|
||||
): string => {
|
||||
const lengthUnit = hass.config.unit_system.length || "";
|
||||
switch (measure) {
|
||||
case "visibility":
|
||||
return hass.config.unit_system.length || "";
|
||||
return stateObj.attributes.visibility_unit || lengthUnit;
|
||||
case "precipitation":
|
||||
return hass.config.unit_system.accumulated_precipitation || "";
|
||||
return stateObj.attributes.precipitation_unit || lengthUnit === "km"
|
||||
? "mm"
|
||||
: "in";
|
||||
case "pressure":
|
||||
return stateObj.attributes.pressure_unit || lengthUnit === "km"
|
||||
? "hPa"
|
||||
: "inHg";
|
||||
case "temperature":
|
||||
return (
|
||||
stateObj.attributes.temperature_unit ||
|
||||
hass.config.unit_system.temperature
|
||||
);
|
||||
case "wind_speed":
|
||||
return stateObj.attributes.wind_speed_unit || `${lengthUnit}/h`;
|
||||
case "humidity":
|
||||
case "precipitation_probability":
|
||||
return "%";
|
||||
@@ -227,7 +257,7 @@ export const getSecondaryWeatherAttribute = (
|
||||
`
|
||||
: hass!.localize(`ui.card.weather.attributes.${attribute}`)}
|
||||
${formatNumber(value, hass.locale, { maximumFractionDigits: 1 })}
|
||||
${getWeatherUnit(hass!, attribute)}
|
||||
${getWeatherUnit(hass!, stateObj, attribute)}
|
||||
`;
|
||||
};
|
||||
|
||||
@@ -262,7 +292,7 @@ const getWeatherExtrema = (
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const unit = getWeatherUnit(hass!, "temperature");
|
||||
const unit = getWeatherUnit(hass!, stateObj, "temperature");
|
||||
|
||||
return html`
|
||||
${tempHigh ? `${formatNumber(tempHigh, hass.locale)} ${unit}` : ""}
|
||||
|
@@ -1,6 +1,5 @@
|
||||
import { UnsubscribeFunc } from "home-assistant-js-websocket";
|
||||
import { HomeAssistant } from "../types";
|
||||
import { DeviceRegistryEntry } from "./device_registry";
|
||||
|
||||
export enum InclusionState {
|
||||
/** The controller isn't doing anything regarding inclusion. */
|
||||
@@ -85,6 +84,23 @@ enum Protocols {
|
||||
ZWave = 0,
|
||||
ZWaveLongRange = 1,
|
||||
}
|
||||
|
||||
export enum FirmwareUpdateStatus {
|
||||
Error_Timeout = -1,
|
||||
Error_Checksum = 0,
|
||||
Error_TransmissionFailed = 1,
|
||||
Error_InvalidManufacturerID = 2,
|
||||
Error_InvalidFirmwareID = 3,
|
||||
Error_InvalidFirmwareTarget = 4,
|
||||
Error_InvalidHeaderInformation = 5,
|
||||
Error_InvalidHeaderFormat = 6,
|
||||
Error_InsufficientMemory = 7,
|
||||
Error_InvalidHardwareVersion = 8,
|
||||
OK_WaitingForActivation = 0xfd,
|
||||
OK_NoRestart = 0xfe,
|
||||
OK_RestartPending = 0xff,
|
||||
}
|
||||
|
||||
export interface QRProvisioningInformation {
|
||||
version: QRCodeVersion;
|
||||
securityClasses: SecurityClass[];
|
||||
@@ -109,10 +125,6 @@ export interface PlannedProvisioningEntry {
|
||||
|
||||
export const MINIMUM_QR_STRING_LENGTH = 52;
|
||||
|
||||
export interface ZWaveJSNodeIdentifiers {
|
||||
home_id: string;
|
||||
node_id: number;
|
||||
}
|
||||
export interface ZWaveJSNetwork {
|
||||
client: ZWaveJSClient;
|
||||
controller: ZWaveJSController;
|
||||
@@ -151,7 +163,7 @@ export interface ZWaveJSController {
|
||||
export interface ZWaveJSNodeStatus {
|
||||
node_id: number;
|
||||
ready: boolean;
|
||||
status: number;
|
||||
status: NodeStatus;
|
||||
is_secure: boolean | string;
|
||||
is_routing: boolean | null;
|
||||
zwave_plus_version: number | null;
|
||||
@@ -244,6 +256,68 @@ export interface ZWaveJSControllerStatisticsUpdatedMessage {
|
||||
timeout_callback: number;
|
||||
}
|
||||
|
||||
export enum RssiError {
|
||||
NotAvailable = 127,
|
||||
ReceiverSaturated = 126,
|
||||
NoSignalDetected = 125,
|
||||
}
|
||||
|
||||
export enum ProtocolDataRate {
|
||||
ZWave_9k6 = 0x01,
|
||||
ZWave_40k = 0x02,
|
||||
ZWave_100k = 0x03,
|
||||
LongRange_100k = 0x04,
|
||||
}
|
||||
|
||||
export interface ZWaveJSNodeStatisticsUpdatedMessage {
|
||||
event: "statistics updated";
|
||||
source: "node";
|
||||
commands_tx: number;
|
||||
commands_rx: number;
|
||||
commands_dropped_tx: number;
|
||||
commands_dropped_rx: number;
|
||||
timeout_response: number;
|
||||
rtt: number | null;
|
||||
rssi: RssiError | number | null;
|
||||
lwr: ZWaveJSRouteStatistics | null;
|
||||
nlwr: ZWaveJSRouteStatistics | null;
|
||||
}
|
||||
|
||||
export interface ZWaveJSRouteStatistics {
|
||||
protocol_data_rate: number;
|
||||
repeaters: string[];
|
||||
rssi: RssiError | number | null;
|
||||
repeater_rssi: (RssiError | number)[];
|
||||
route_failed_between: [string, string] | null;
|
||||
}
|
||||
|
||||
export interface ZWaveJSNodeStatusUpdatedMessage {
|
||||
event: "ready" | "wake up" | "sleep" | "dead" | "alive";
|
||||
ready: boolean;
|
||||
status: NodeStatus;
|
||||
}
|
||||
|
||||
export interface ZWaveJSNodeFirmwareUpdateProgressMessage {
|
||||
event: "firmware update progress";
|
||||
sent_fragments: number;
|
||||
total_fragments: number;
|
||||
}
|
||||
|
||||
export interface ZWaveJSNodeFirmwareUpdateFinishedMessage {
|
||||
event: "firmware update finished";
|
||||
status: FirmwareUpdateStatus;
|
||||
wait_time: number;
|
||||
}
|
||||
|
||||
export type ZWaveJSNodeFirmwareUpdateCapabilities =
|
||||
| { firmware_upgradable: false }
|
||||
| {
|
||||
firmware_upgradable: true;
|
||||
firmware_targets: number[];
|
||||
continues_to_function: boolean | null;
|
||||
supports_activation: boolean | null;
|
||||
};
|
||||
|
||||
export interface ZWaveJSRemovedNode {
|
||||
node_id: number;
|
||||
manufacturer: string;
|
||||
@@ -280,25 +354,6 @@ export interface RequestedGrant {
|
||||
|
||||
export const nodeStatus = ["unknown", "asleep", "awake", "dead", "alive"];
|
||||
|
||||
export interface ZWaveJsMigrationData {
|
||||
migration_device_map: Record<string, string>;
|
||||
zwave_entity_ids: string[];
|
||||
zwave_js_entity_ids: string[];
|
||||
migration_entity_map: Record<string, string>;
|
||||
migrated: boolean;
|
||||
}
|
||||
|
||||
export const migrateZwave = (
|
||||
hass: HomeAssistant,
|
||||
entry_id: string,
|
||||
dry_run = true
|
||||
): Promise<ZWaveJsMigrationData> =>
|
||||
hass.callWS({
|
||||
type: "zwave_js/migrate_zwave",
|
||||
entry_id,
|
||||
dry_run,
|
||||
});
|
||||
|
||||
export const fetchZwaveNetworkStatus = (
|
||||
hass: HomeAssistant,
|
||||
device_or_entry_id: {
|
||||
@@ -461,6 +516,19 @@ export const fetchZwaveNodeStatus = (
|
||||
device_id,
|
||||
});
|
||||
|
||||
export const subscribeZwaveNodeStatus = (
|
||||
hass: HomeAssistant,
|
||||
device_id: string,
|
||||
callbackFunction: (message: ZWaveJSNodeStatusUpdatedMessage) => void
|
||||
): Promise<UnsubscribeFunc> =>
|
||||
hass.connection.subscribeMessage(
|
||||
(message: any) => callbackFunction(message),
|
||||
{
|
||||
type: "zwave_js/subscribe_node_status",
|
||||
device_id,
|
||||
}
|
||||
);
|
||||
|
||||
export const fetchZwaveNodeMetadata = (
|
||||
hass: HomeAssistant,
|
||||
device_id: string
|
||||
@@ -558,19 +626,6 @@ export const stopHealZwaveNetwork = (
|
||||
entry_id,
|
||||
});
|
||||
|
||||
export const subscribeZwaveNodeReady = (
|
||||
hass: HomeAssistant,
|
||||
device_id: string,
|
||||
callbackFunction: (message) => void
|
||||
): Promise<UnsubscribeFunc> =>
|
||||
hass.connection.subscribeMessage(
|
||||
(message: any) => callbackFunction(message),
|
||||
{
|
||||
type: "zwave_js/node_ready",
|
||||
device_id,
|
||||
}
|
||||
);
|
||||
|
||||
export const subscribeHealZwaveNetworkProgress = (
|
||||
hass: HomeAssistant,
|
||||
entry_id: string,
|
||||
@@ -597,27 +652,96 @@ export const subscribeZwaveControllerStatistics = (
|
||||
}
|
||||
);
|
||||
|
||||
export const getZwaveJsIdentifiersFromDevice = (
|
||||
device: DeviceRegistryEntry
|
||||
): ZWaveJSNodeIdentifiers | undefined => {
|
||||
if (!device) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const zwaveJSIdentifier = device.identifiers.find(
|
||||
(identifier) => identifier[0] === "zwave_js"
|
||||
export const subscribeZwaveNodeStatistics = (
|
||||
hass: HomeAssistant,
|
||||
device_id: string,
|
||||
callbackFunction: (message: ZWaveJSNodeStatisticsUpdatedMessage) => void
|
||||
): Promise<UnsubscribeFunc> =>
|
||||
hass.connection.subscribeMessage(
|
||||
(message: any) => callbackFunction(message),
|
||||
{
|
||||
type: "zwave_js/subscribe_node_statistics",
|
||||
device_id,
|
||||
}
|
||||
);
|
||||
if (!zwaveJSIdentifier) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const identifiers = zwaveJSIdentifier[1].split("-");
|
||||
return {
|
||||
node_id: parseInt(identifiers[1]),
|
||||
home_id: identifiers[0],
|
||||
};
|
||||
export const fetchZwaveNodeIsFirmwareUpdateInProgress = (
|
||||
hass: HomeAssistant,
|
||||
device_id: string
|
||||
): Promise<boolean> =>
|
||||
hass.callWS({
|
||||
type: "zwave_js/get_firmware_update_progress",
|
||||
device_id,
|
||||
});
|
||||
|
||||
export const fetchZwaveIsAnyFirmwareUpdateInProgress = (
|
||||
hass: HomeAssistant,
|
||||
entry_id: string
|
||||
): Promise<boolean> =>
|
||||
hass.callWS({
|
||||
type: "zwave_js/get_any_firmware_update_progress",
|
||||
entry_id,
|
||||
});
|
||||
|
||||
export const fetchZwaveNodeFirmwareUpdateCapabilities = (
|
||||
hass: HomeAssistant,
|
||||
device_id: string
|
||||
): Promise<ZWaveJSNodeFirmwareUpdateCapabilities> =>
|
||||
hass.callWS({
|
||||
type: "zwave_js/get_firmware_update_capabilities",
|
||||
device_id,
|
||||
});
|
||||
|
||||
export const uploadFirmwareAndBeginUpdate = async (
|
||||
hass: HomeAssistant,
|
||||
device_id: string,
|
||||
file: File,
|
||||
target?: number
|
||||
) => {
|
||||
const fd = new FormData();
|
||||
fd.append("file", file);
|
||||
if (target !== undefined) {
|
||||
fd.append("target", target.toString());
|
||||
}
|
||||
const resp = await hass.fetchWithAuth(
|
||||
`/api/zwave_js/firmware/upload/${device_id}`,
|
||||
{
|
||||
method: "POST",
|
||||
body: fd,
|
||||
}
|
||||
);
|
||||
|
||||
if (resp.status !== 200) {
|
||||
throw new Error(resp.statusText);
|
||||
}
|
||||
};
|
||||
|
||||
export const subscribeZwaveNodeFirmwareUpdate = (
|
||||
hass: HomeAssistant,
|
||||
device_id: string,
|
||||
callbackFunction: (
|
||||
message:
|
||||
| ZWaveJSNodeFirmwareUpdateFinishedMessage
|
||||
| ZWaveJSNodeFirmwareUpdateProgressMessage
|
||||
) => void
|
||||
): Promise<UnsubscribeFunc> =>
|
||||
hass.connection.subscribeMessage(
|
||||
(message: any) => callbackFunction(message),
|
||||
{
|
||||
type: "zwave_js/subscribe_firmware_update_status",
|
||||
device_id,
|
||||
}
|
||||
);
|
||||
|
||||
export const abortZwaveNodeFirmwareUpdate = (
|
||||
hass: HomeAssistant,
|
||||
device_id: string
|
||||
): Promise<UnsubscribeFunc> =>
|
||||
hass.callWS({
|
||||
type: "zwave_js/abort_firmware_update",
|
||||
device_id,
|
||||
});
|
||||
|
||||
export type ZWaveJSLogUpdate = ZWaveJSLogMessageUpdate | ZWaveJSLogConfigUpdate;
|
||||
|
||||
interface ZWaveJSLogMessageUpdate {
|
||||
|
@@ -47,10 +47,14 @@ class DomainTogglerDialog
|
||||
hideActions
|
||||
.heading=${createCloseHeading(
|
||||
this.hass,
|
||||
this.hass.localize("ui.dialogs.domain_toggler.title")
|
||||
this._params.title ||
|
||||
this.hass.localize("ui.dialogs.domain_toggler.title")
|
||||
)}
|
||||
>
|
||||
<div>
|
||||
${this._params.description
|
||||
? html`<div class="description">${this._params.description}</div>`
|
||||
: ""}
|
||||
<div class="domains">
|
||||
${domains.map(
|
||||
(domain) =>
|
||||
html`
|
||||
@@ -92,7 +96,10 @@ class DomainTogglerDialog
|
||||
ha-dialog {
|
||||
--mdc-dialog-max-width: 500px;
|
||||
}
|
||||
div {
|
||||
.description {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.domains {
|
||||
display: grid;
|
||||
grid-template-columns: auto auto;
|
||||
grid-row-gap: 8px;
|
||||
|
@@ -1,6 +1,8 @@
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
|
||||
export interface HaDomainTogglerDialogParams {
|
||||
title?: string;
|
||||
description?: string;
|
||||
domains: string[];
|
||||
exposedDomains: string[] | null;
|
||||
toggleDomain: (domain: string, turnOn: boolean) => void;
|
||||
|
@@ -8,7 +8,6 @@ import "../../components/ha-dialog";
|
||||
import "../../components/ha-svg-icon";
|
||||
import "../../components/ha-switch";
|
||||
import { HaTextField } from "../../components/ha-textfield";
|
||||
import { haStyleDialog } from "../../resources/styles";
|
||||
import { HomeAssistant } from "../../types";
|
||||
import { DialogBoxParams } from "./show-dialog-box";
|
||||
|
||||
@@ -135,34 +134,34 @@ class DialogBox extends LitElement {
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return [
|
||||
haStyleDialog,
|
||||
css`
|
||||
:host([inert]) {
|
||||
pointer-events: initial !important;
|
||||
cursor: initial !important;
|
||||
}
|
||||
a {
|
||||
color: var(--primary-color);
|
||||
}
|
||||
p {
|
||||
margin: 0;
|
||||
padding-top: 6px;
|
||||
padding-bottom: 24px;
|
||||
color: var(--primary-text-color);
|
||||
}
|
||||
.no-bottom-padding {
|
||||
padding-bottom: 0;
|
||||
}
|
||||
.secondary {
|
||||
color: var(--secondary-text-color);
|
||||
}
|
||||
ha-dialog {
|
||||
/* Place above other dialogs */
|
||||
--dialog-z-index: 104;
|
||||
}
|
||||
`,
|
||||
];
|
||||
return css`
|
||||
:host([inert]) {
|
||||
pointer-events: initial !important;
|
||||
cursor: initial !important;
|
||||
}
|
||||
a {
|
||||
color: var(--primary-color);
|
||||
}
|
||||
p {
|
||||
margin: 0;
|
||||
padding-top: 6px;
|
||||
padding-bottom: 24px;
|
||||
color: var(--primary-text-color);
|
||||
}
|
||||
.no-bottom-padding {
|
||||
padding-bottom: 0;
|
||||
}
|
||||
.secondary {
|
||||
color: var(--secondary-text-color);
|
||||
}
|
||||
ha-dialog {
|
||||
--mdc-dialog-heading-ink-color: var(--primary-text-color);
|
||||
--mdc-dialog-content-ink-color: var(--primary-text-color);
|
||||
--justify-action-buttons: space-between;
|
||||
/* Place above other dialogs */
|
||||
--dialog-z-index: 104;
|
||||
}
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -37,7 +37,7 @@ export class MoreInfoAlarmControlPanel extends LitElement {
|
||||
id="alarmCode"
|
||||
.label=${this.hass.localize("ui.card.alarm_control_panel.code")}
|
||||
type="password"
|
||||
.inputmode=${this.stateObj.attributes.code_format ===
|
||||
.inputMode=${this.stateObj.attributes.code_format ===
|
||||
FORMAT_NUMBER
|
||||
? "numeric"
|
||||
: "text"}
|
||||
|
@@ -11,7 +11,6 @@ import {
|
||||
} from "@mdi/js";
|
||||
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { isComponentLoaded } from "../../../common/config/is_component_loaded";
|
||||
import { stopPropagation } from "../../../common/dom/stop_propagation";
|
||||
import { supportsFeature } from "../../../common/entity/supports-feature";
|
||||
import { computeRTLDirection } from "../../../common/util/compute_rtl";
|
||||
@@ -26,8 +25,8 @@ import {
|
||||
handleMediaControlClick,
|
||||
MediaPickedEvent,
|
||||
MediaPlayerEntity,
|
||||
mediaPlayerPlayMedia,
|
||||
SUPPORT_BROWSE_MEDIA,
|
||||
SUPPORT_PLAY_MEDIA,
|
||||
SUPPORT_SELECT_SOUND_MODE,
|
||||
SUPPORT_SELECT_SOURCE,
|
||||
SUPPORT_VOLUME_BUTTONS,
|
||||
@@ -78,6 +77,7 @@ class MoreInfoMediaPlayer extends LitElement {
|
||||
@click=${this._showBrowseMedia}
|
||||
>
|
||||
<ha-svg-icon
|
||||
class="browse-media-icon"
|
||||
.path=${mdiPlayBoxMultiple}
|
||||
slot="icon"
|
||||
></ha-svg-icon>
|
||||
@@ -190,14 +190,6 @@ class MoreInfoMediaPlayer extends LitElement {
|
||||
</div>
|
||||
`
|
||||
: ""}
|
||||
${isComponentLoaded(this.hass, "tts") &&
|
||||
supportsFeature(stateObj, SUPPORT_PLAY_MEDIA)
|
||||
? html`
|
||||
<div class="tts">
|
||||
Text to speech has moved to the media browser.
|
||||
</div>
|
||||
`
|
||||
: ""}
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -211,6 +203,7 @@ class MoreInfoMediaPlayer extends LitElement {
|
||||
|
||||
.controls {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
--mdc-theme-primary: currentColor;
|
||||
}
|
||||
@@ -242,6 +235,10 @@ class MoreInfoMediaPlayer extends LitElement {
|
||||
mwc-button > ha-svg-icon {
|
||||
vertical-align: text-bottom;
|
||||
}
|
||||
|
||||
.browse-media-icon {
|
||||
margin-left: 8px;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -299,20 +296,14 @@ class MoreInfoMediaPlayer extends LitElement {
|
||||
action: "play",
|
||||
entityId: this.stateObj!.entity_id,
|
||||
mediaPickedCallback: (pickedMedia: MediaPickedEvent) =>
|
||||
this._playMedia(
|
||||
mediaPlayerPlayMedia(
|
||||
this.hass,
|
||||
this.stateObj!.entity_id,
|
||||
pickedMedia.item.media_content_id,
|
||||
pickedMedia.item.media_content_type
|
||||
),
|
||||
});
|
||||
}
|
||||
|
||||
private _playMedia(media_content_id: string, media_content_type: string) {
|
||||
this.hass!.callService("media_player", "play_media", {
|
||||
entity_id: this.stateObj!.entity_id,
|
||||
media_content_id,
|
||||
media_content_type,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
|
@@ -5,7 +5,6 @@ import {
|
||||
mdiWaterPercent,
|
||||
mdiWeatherWindy,
|
||||
} from "@mdi/js";
|
||||
import { HassEntity } from "home-assistant-js-websocket";
|
||||
import {
|
||||
css,
|
||||
CSSResultGroup,
|
||||
@@ -23,6 +22,7 @@ import {
|
||||
getWeatherUnit,
|
||||
getWind,
|
||||
isForecastHourly,
|
||||
WeatherEntity,
|
||||
weatherIcons,
|
||||
} from "../../../data/weather";
|
||||
import { HomeAssistant } from "../../../types";
|
||||
@@ -31,7 +31,7 @@ import { HomeAssistant } from "../../../types";
|
||||
class MoreInfoWeather extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property() public stateObj?: HassEntity;
|
||||
@property() public stateObj?: WeatherEntity;
|
||||
|
||||
protected shouldUpdate(changedProps: PropertyValues): boolean {
|
||||
if (changedProps.has("stateObj")) {
|
||||
@@ -58,19 +58,23 @@ class MoreInfoWeather extends LitElement {
|
||||
const hourly = isForecastHourly(this.stateObj.attributes.forecast);
|
||||
|
||||
return html`
|
||||
<div class="flex">
|
||||
<ha-svg-icon .path=${mdiThermometer}></ha-svg-icon>
|
||||
<div class="main">
|
||||
${this.hass.localize("ui.card.weather.attributes.temperature")}
|
||||
</div>
|
||||
<div>
|
||||
${formatNumber(
|
||||
this.stateObj.attributes.temperature,
|
||||
this.hass.locale
|
||||
)}
|
||||
${getWeatherUnit(this.hass, "temperature")}
|
||||
</div>
|
||||
</div>
|
||||
${this._showValue(this.stateObj.attributes.temperature)
|
||||
? html`
|
||||
<div class="flex">
|
||||
<ha-svg-icon .path=${mdiThermometer}></ha-svg-icon>
|
||||
<div class="main">
|
||||
${this.hass.localize("ui.card.weather.attributes.temperature")}
|
||||
</div>
|
||||
<div>
|
||||
${formatNumber(
|
||||
this.stateObj.attributes.temperature!,
|
||||
this.hass.locale
|
||||
)}
|
||||
${getWeatherUnit(this.hass, this.stateObj, "temperature")}
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
: ""}
|
||||
${this._showValue(this.stateObj.attributes.pressure)
|
||||
? html`
|
||||
<div class="flex">
|
||||
@@ -80,10 +84,10 @@ class MoreInfoWeather extends LitElement {
|
||||
</div>
|
||||
<div>
|
||||
${formatNumber(
|
||||
this.stateObj.attributes.pressure,
|
||||
this.stateObj.attributes.pressure!,
|
||||
this.hass.locale
|
||||
)}
|
||||
${getWeatherUnit(this.hass, "pressure")}
|
||||
${getWeatherUnit(this.hass, this.stateObj, "pressure")}
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
@@ -97,7 +101,7 @@ class MoreInfoWeather extends LitElement {
|
||||
</div>
|
||||
<div>
|
||||
${formatNumber(
|
||||
this.stateObj.attributes.humidity,
|
||||
this.stateObj.attributes.humidity!,
|
||||
this.hass.locale
|
||||
)}
|
||||
%
|
||||
@@ -115,7 +119,8 @@ class MoreInfoWeather extends LitElement {
|
||||
<div>
|
||||
${getWind(
|
||||
this.hass,
|
||||
this.stateObj.attributes.wind_speed,
|
||||
this.stateObj,
|
||||
this.stateObj.attributes.wind_speed!,
|
||||
this.stateObj.attributes.wind_bearing
|
||||
)}
|
||||
</div>
|
||||
@@ -131,10 +136,10 @@ class MoreInfoWeather extends LitElement {
|
||||
</div>
|
||||
<div>
|
||||
${formatNumber(
|
||||
this.stateObj.attributes.visibility,
|
||||
this.stateObj.attributes.visibility!,
|
||||
this.hass.locale
|
||||
)}
|
||||
${getWeatherUnit(this.hass, "length")}
|
||||
${getWeatherUnit(this.hass, this.stateObj, "visibility")}
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
@@ -173,16 +178,24 @@ class MoreInfoWeather extends LitElement {
|
||||
`}
|
||||
<div class="templow">
|
||||
${this._showValue(item.templow)
|
||||
? `${formatNumber(item.templow, this.hass.locale)}
|
||||
${getWeatherUnit(this.hass, "temperature")}`
|
||||
? `${formatNumber(item.templow!, this.hass.locale)}
|
||||
${getWeatherUnit(
|
||||
this.hass,
|
||||
this.stateObj!,
|
||||
"temperature"
|
||||
)}`
|
||||
: hourly
|
||||
? ""
|
||||
: "—"}
|
||||
</div>
|
||||
<div class="temp">
|
||||
${this._showValue(item.temperature)
|
||||
? `${formatNumber(item.temperature, this.hass.locale)}
|
||||
${getWeatherUnit(this.hass, "temperature")}`
|
||||
? `${formatNumber(item.temperature!, this.hass.locale)}
|
||||
${getWeatherUnit(
|
||||
this.hass,
|
||||
this.stateObj!,
|
||||
"temperature"
|
||||
)}`
|
||||
: "—"}
|
||||
</div>
|
||||
</div>`
|
||||
@@ -240,7 +253,7 @@ class MoreInfoWeather extends LitElement {
|
||||
`;
|
||||
}
|
||||
|
||||
private _showValue(item: string): boolean {
|
||||
private _showValue(item: number | string | undefined): boolean {
|
||||
return typeof item !== "undefined" && item !== null;
|
||||
}
|
||||
}
|
||||
|
@@ -34,7 +34,7 @@ import "../../components/ha-circular-progress";
|
||||
import "../../components/ha-header-bar";
|
||||
import "../../components/ha-icon-button";
|
||||
import "../../components/ha-textfield";
|
||||
import { fetchHassioSupervisorInfo } from "../../data/hassio/supervisor";
|
||||
import { fetchHassioAddonsInfo } from "../../data/hassio/addon";
|
||||
import { domainToName } from "../../data/integration";
|
||||
import { getPanelNameTranslationKey } from "../../data/panel";
|
||||
import { PageNavigation } from "../../layouts/hass-tabs-subpage";
|
||||
@@ -586,7 +586,7 @@ export class QuickBar extends LitElement {
|
||||
const sectionItems = this._generateNavigationConfigSectionCommands();
|
||||
const supervisorItems: BaseNavigationCommand[] = [];
|
||||
if (isComponentLoaded(this.hass, "hassio")) {
|
||||
const supervisorInfo = await fetchHassioSupervisorInfo(this.hass);
|
||||
const addonsInfo = await fetchHassioAddonsInfo(this.hass);
|
||||
supervisorItems.push({
|
||||
path: "/hassio/store",
|
||||
primaryText: this.hass.localize(
|
||||
@@ -599,7 +599,7 @@ export class QuickBar extends LitElement {
|
||||
"ui.dialogs.quick-bar.commands.navigation.addon_dashboard"
|
||||
),
|
||||
});
|
||||
for (const addon of supervisorInfo.addons) {
|
||||
for (const addon of addonsInfo.addons.filter((a) => a.version)) {
|
||||
supervisorItems.push({
|
||||
path: `/hassio/addon/${addon.slug}`,
|
||||
primaryText: this.hass.localize(
|
||||
@@ -803,6 +803,9 @@ export class QuickBar extends LitElement {
|
||||
|
||||
span.command-text {
|
||||
margin-left: 8px;
|
||||
margin-inline-start: 8px;
|
||||
margin-inline-end: initial;
|
||||
direction: var(--direction);
|
||||
}
|
||||
|
||||
mwc-list-item {
|
||||
|
@@ -51,11 +51,15 @@ function initialize(
|
||||
const style = document.createElement("style");
|
||||
|
||||
style.innerHTML = `
|
||||
body { margin:0; }
|
||||
body {
|
||||
margin:0;
|
||||
background-color: var(--primary-background-color, #fafafa);
|
||||
color: var(--primary-text-color, #212121);
|
||||
}
|
||||
@media (prefers-color-scheme: dark) {
|
||||
body {
|
||||
background-color: #111111;
|
||||
color: #e1e1e1;
|
||||
background-color: var(--primary-background-color, #111111);
|
||||
color: var(--primary-text-color, #e1e1e1);
|
||||
}
|
||||
}`;
|
||||
document.head.appendChild(style);
|
||||
|
@@ -194,6 +194,7 @@ export const provideHass = (
|
||||
socket: {
|
||||
readyState: WebSocket.OPEN,
|
||||
},
|
||||
haVersion: "DEMO",
|
||||
} as any,
|
||||
connected: true,
|
||||
states: {},
|
||||
|
@@ -3,6 +3,8 @@ import { property } from "lit/decorators";
|
||||
import { computeLocalize, LocalizeFunc } from "../common/translations/localize";
|
||||
import { Constructor, Resources } from "../types";
|
||||
import { getLocalLanguage, getTranslation } from "../util/common-translation";
|
||||
import { translationMetadata } from "../resources/translations-metadata";
|
||||
import { computeDirectionStyles } from "../common/util/compute_rtl";
|
||||
|
||||
const empty = () => "";
|
||||
|
||||
@@ -25,6 +27,14 @@ export const litLocalizeLiteMixin = <T extends Constructor<LitElement>>(
|
||||
this._initializeLocalizeLite();
|
||||
}
|
||||
|
||||
protected firstUpdated(changedProps: PropertyValues) {
|
||||
super.firstUpdated(changedProps);
|
||||
computeDirectionStyles(
|
||||
translationMetadata.translations[this.language!].isRTL,
|
||||
this
|
||||
);
|
||||
}
|
||||
|
||||
protected updated(changedProperties: PropertyValues) {
|
||||
super.updated(changedProperties);
|
||||
if (changedProperties.get("translationFragment")) {
|
||||
|
@@ -124,7 +124,6 @@ class HaOnboarding extends litLocalizeLiteMixin(HassElement) {
|
||||
protected firstUpdated(changedProps: PropertyValues) {
|
||||
super.firstUpdated(changedProps);
|
||||
this._fetchOnboardingSteps();
|
||||
this._fetchInstallationType();
|
||||
import("./onboarding-integrations");
|
||||
import("./onboarding-core-config");
|
||||
registerServiceWorker(this, false);
|
||||
@@ -215,6 +214,9 @@ class HaOnboarding extends litLocalizeLiteMixin(HassElement) {
|
||||
});
|
||||
history.replaceState(null, "", location.pathname);
|
||||
await this._connectHass(auth);
|
||||
} else {
|
||||
// User creating screen needs to know the installation type.
|
||||
this._fetchInstallationType();
|
||||
}
|
||||
|
||||
this._steps = steps;
|
||||
|
@@ -378,6 +378,10 @@ class OnboardingCoreConfig extends LitElement {
|
||||
color: var(--secondary-text-color);
|
||||
}
|
||||
|
||||
ha-locations-editor {
|
||||
height: 200px;
|
||||
}
|
||||
|
||||
.flex {
|
||||
flex: 1;
|
||||
}
|
||||
|
@@ -30,7 +30,13 @@ import { HomeAssistant } from "../types";
|
||||
import "./action-badge";
|
||||
import "./integration-badge";
|
||||
|
||||
const HIDDEN_DOMAINS = new Set(["hassio", "met", "radio_browser", "rpi_power"]);
|
||||
const HIDDEN_DOMAINS = new Set([
|
||||
"hassio",
|
||||
"met",
|
||||
"radio_browser",
|
||||
"rpi_power",
|
||||
"sun",
|
||||
]);
|
||||
|
||||
@customElement("onboarding-integrations")
|
||||
class OnboardingIntegrations extends LitElement {
|
||||
@@ -75,7 +81,10 @@ class OnboardingIntegrations extends LitElement {
|
||||
// Render discovered and existing entries together sorted by localized title.
|
||||
const entries: Array<[string, TemplateResult]> = this._entries.map(
|
||||
(entry) => {
|
||||
const title = domainToName(this.hass.localize, entry.domain);
|
||||
const title =
|
||||
entry.title ||
|
||||
domainToName(this.hass.localize, entry.domain) ||
|
||||
entry.domain;
|
||||
return [
|
||||
title,
|
||||
html`
|
||||
|
@@ -7,14 +7,15 @@ import { fireEvent } from "../../../common/dom/fire_event";
|
||||
import "../../../components/ha-circular-progress";
|
||||
import "../../../components/ha-combo-box";
|
||||
import { createCloseHeading } from "../../../components/ha-dialog";
|
||||
import "../../../components/ha-markdown";
|
||||
import "../../../components/ha-textfield";
|
||||
import {
|
||||
fetchApplicationCredentialsConfig,
|
||||
createApplicationCredential,
|
||||
ApplicationCredentialsConfig,
|
||||
ApplicationCredential,
|
||||
} from "../../../data/application_credential";
|
||||
import { domainToName } from "../../../data/integration";
|
||||
import { PolymerChangedEvent } from "../../../polymer-types";
|
||||
import { haStyleDialog } from "../../../resources/styles";
|
||||
import { HomeAssistant } from "../../../types";
|
||||
import { AddApplicationCredentialDialogParams } from "./show-dialog-add-application-credential";
|
||||
@@ -43,17 +44,22 @@ export class DialogAddApplicationCredential extends LitElement {
|
||||
|
||||
@state() private _name?: string;
|
||||
|
||||
@state() private _description?: string;
|
||||
|
||||
@state() private _clientId?: string;
|
||||
|
||||
@state() private _clientSecret?: string;
|
||||
|
||||
@state() private _domains?: Domain[];
|
||||
|
||||
@state() private _config?: ApplicationCredentialsConfig;
|
||||
|
||||
public showDialog(params: AddApplicationCredentialDialogParams) {
|
||||
this._params = params;
|
||||
this._domain =
|
||||
params.selectedDomain !== undefined ? params.selectedDomain : "";
|
||||
this._name = "";
|
||||
this._description = "";
|
||||
this._clientId = "";
|
||||
this._clientSecret = "";
|
||||
this._error = undefined;
|
||||
@@ -62,11 +68,15 @@ export class DialogAddApplicationCredential extends LitElement {
|
||||
}
|
||||
|
||||
private async _fetchConfig() {
|
||||
const config = await fetchApplicationCredentialsConfig(this.hass);
|
||||
this._domains = config.domains.map((domain) => ({
|
||||
this._config = await fetchApplicationCredentialsConfig(this.hass);
|
||||
this._domains = Object.keys(this._config.integrations).map((domain) => ({
|
||||
id: domain,
|
||||
name: domainToName(this.hass.localize, domain),
|
||||
}));
|
||||
await this.hass.loadBackendTranslation("application_credentials");
|
||||
if (this._domain !== "") {
|
||||
this._updateDescription();
|
||||
}
|
||||
}
|
||||
|
||||
protected render(): TemplateResult {
|
||||
@@ -104,6 +114,12 @@ export class DialogAddApplicationCredential extends LitElement {
|
||||
required
|
||||
@value-changed=${this._handleDomainPicked}
|
||||
></ha-combo-box>
|
||||
${this._description
|
||||
? html`<ha-markdown
|
||||
breaks
|
||||
.content=${this._description}
|
||||
></ha-markdown>`
|
||||
: ""}
|
||||
<ha-textfield
|
||||
class="name"
|
||||
name="name"
|
||||
@@ -169,11 +185,18 @@ export class DialogAddApplicationCredential extends LitElement {
|
||||
fireEvent(this, "dialog-closed", { dialog: this.localName });
|
||||
}
|
||||
|
||||
private async _handleDomainPicked(ev: PolymerChangedEvent<string>) {
|
||||
const target = ev.target as any;
|
||||
if (target.selectedItem) {
|
||||
this._domain = target.selectedItem.id;
|
||||
}
|
||||
private _handleDomainPicked(ev: CustomEvent) {
|
||||
ev.stopPropagation();
|
||||
this._domain = ev.detail.value;
|
||||
this._updateDescription();
|
||||
}
|
||||
|
||||
private _updateDescription() {
|
||||
const info = this._config!.integrations[this._domain!];
|
||||
this._description = this.hass.localize(
|
||||
`component.${this._domain}.application_credentials.description`,
|
||||
info.description_placeholders
|
||||
);
|
||||
}
|
||||
|
||||
private _handleValueChanged(ev: CustomEvent) {
|
||||
|
@@ -53,7 +53,6 @@ export class HaConfigApplicationCredentials extends LitElement {
|
||||
title: localize(
|
||||
"ui.panel.config.application_credentials.picker.headers.name"
|
||||
),
|
||||
width: "40%",
|
||||
direction: "asc",
|
||||
grows: true,
|
||||
template: (_, entry: ApplicationCredential) => html`${entry.name}`,
|
||||
|
@@ -315,7 +315,8 @@ export class HaBlueprintAutomationEditor extends LitElement {
|
||||
padding: 0 16px 16px;
|
||||
}
|
||||
ha-textarea,
|
||||
ha-textfield {
|
||||
ha-textfield,
|
||||
ha-blueprint-picker {
|
||||
display: block;
|
||||
}
|
||||
h3 {
|
||||
|