Compare commits

..

3 Commits

Author SHA1 Message Date
Paulus Schoutsen
d011794884 Bumped version to 20220502.0 2022-05-02 15:07:50 -07:00
Bram Kragten
9b782cabe2 Update hat-script-graph.ts 2022-05-02 15:07:15 -07:00
Bram Kragten
a355bedd77 Indicate things are disabled in trace graph 2022-05-02 15:07:15 -07:00
337 changed files with 4784 additions and 11655 deletions

View File

@@ -51,7 +51,7 @@ DO NOT DELETE ANY TEXT from this template! Otherwise, your issue may be closed w
<!-- <!--
Provide details about the versions you are using, which helps us reproducing Provide details about the versions you are using, which helps us reproducing
and finding the issue quicker. Version information is found in the and finding the issue quicker. Version information is found in the
Home Assistant frontend: Settings -> About. Home Assistant frontend: Configuration -> Info.
Browser version and operating system is important! Please try to replicate Browser version and operating system is important! Please try to replicate
your issue in a different browser and be sure to include your findings. your issue in a different browser and be sure to include your findings.

View File

@@ -1,4 +1,4 @@
name: Report a bug with the UI / Dashboards name: Report a bug with the UI, Frontend or Lovelace
description: Report an issue related to the Home Assistant frontend. description: Report an issue related to the Home Assistant frontend.
labels: bug labels: bug
body: body:
@@ -9,7 +9,7 @@ body:
If you have a feature or enhancement request for the frontend, please [start an discussion][fr] instead of creating an issue. If you have a feature or enhancement request for the frontend, please [start an discussion][fr] instead of creating an issue.
**Please not not report issues for custom cards.** **Please not not report issues for custom Lovelace cards.**
[fr]: https://github.com/home-assistant/frontend/discussions [fr]: https://github.com/home-assistant/frontend/discussions
[releases]: https://github.com/home-assistant/home-assistant/releases [releases]: https://github.com/home-assistant/home-assistant/releases
@@ -64,7 +64,7 @@ body:
label: What version of Home Assistant Core has the issue? label: What version of Home Assistant Core has the issue?
placeholder: core- placeholder: core-
description: > description: >
Can be found in: [Settings -> About](https://my.home-assistant.io/redirect/info/). Can be found in the Configuration panel -> Info.
- type: input - type: input
attributes: attributes:
label: What was the last working version of Home Assistant Core? label: What was the last working version of Home Assistant Core?

View File

@@ -1,17 +1,17 @@
blank_issues_enabled: false blank_issues_enabled: false
contact_links: contact_links:
- name: Request a feature for the UI / Dashboards - name: Request a feature for the UI, Frontend or Lovelace
url: https://github.com/home-assistant/frontend/discussions/category_choices url: https://github.com/home-assistant/frontend/discussions/category_choices
about: Request an new feature for the Home Assistant frontend. about: Request an new feature for the Home Assistant frontend.
- name: Report a bug that is NOT related to the UI / Dashboards - name: Report a bug that is NOT related to the UI, Frontend or Lovelace
url: https://github.com/home-assistant/core/issues url: https://github.com/home-assistant/core/issues
about: This is the issue tracker for our frontend. Please report other issues in the backend ("core") repository. about: This is the issue tracker for our frontend. Please report other issues with the backend repository.
- name: Report incorrect or missing information on our website - name: Report incorrect or missing information on our website
url: https://github.com/home-assistant/home-assistant.io/issues url: https://github.com/home-assistant/home-assistant.io/issues
about: Our documentation has its own issue tracker. Please report issues with the website there. about: Our documentation has its own issue tracker. Please report issues with the website there.
- name: I have a question or need support - name: I have a question or need support
url: https://www.home-assistant.io/help url: https://www.home-assistant.io/help
about: We use GitHub for tracking bugs. Check our website for resources on getting help. about: We use GitHub for tracking bugs, check our website for resources on getting help.
- name: I'm unsure where to go - name: I'm unsure where to go
url: https://www.home-assistant.io/join-chat url: https://www.home-assistant.io/join-chat
about: If you are unsure where to go, then joining our chat is recommended; Just ask! about: If you are unsure where to go, then joining our chat is recommended; Just ask!

View File

@@ -1,8 +0,0 @@
version: 2
updates:
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: weekly
time: "06:00"
open-pull-requests-limit: 10

View File

@@ -19,9 +19,9 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Check out files from GitHub - name: Check out files from GitHub
uses: actions/checkout@v3 uses: actions/checkout@v2
- name: Set up Node ${{ env.NODE_VERSION }} - name: Set up Node ${{ env.NODE_VERSION }}
uses: actions/setup-node@v3 uses: actions/setup-node@v2
with: with:
node-version: ${{ env.NODE_VERSION }} node-version: ${{ env.NODE_VERSION }}
cache: yarn cache: yarn
@@ -43,9 +43,9 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Check out files from GitHub - name: Check out files from GitHub
uses: actions/checkout@v3 uses: actions/checkout@v2
- name: Set up Node ${{ env.NODE_VERSION }} - name: Set up Node ${{ env.NODE_VERSION }}
uses: actions/setup-node@v3 uses: actions/setup-node@v2
with: with:
node-version: ${{ env.NODE_VERSION }} node-version: ${{ env.NODE_VERSION }}
cache: yarn cache: yarn
@@ -62,9 +62,9 @@ jobs:
needs: [lint, test] needs: [lint, test]
steps: steps:
- name: Check out files from GitHub - name: Check out files from GitHub
uses: actions/checkout@v3 uses: actions/checkout@v2
- name: Set up Node ${{ env.NODE_VERSION }} - name: Set up Node ${{ env.NODE_VERSION }}
uses: actions/setup-node@v3 uses: actions/setup-node@v2
with: with:
node-version: ${{ env.NODE_VERSION }} node-version: ${{ env.NODE_VERSION }}
cache: yarn cache: yarn
@@ -81,9 +81,9 @@ jobs:
needs: [lint, test] needs: [lint, test]
steps: steps:
- name: Check out files from GitHub - name: Check out files from GitHub
uses: actions/checkout@v3 uses: actions/checkout@v2
- name: Set up Node ${{ env.NODE_VERSION }} - name: Set up Node ${{ env.NODE_VERSION }}
uses: actions/setup-node@v3 uses: actions/setup-node@v2
with: with:
node-version: ${{ env.NODE_VERSION }} node-version: ${{ env.NODE_VERSION }}
cache: yarn cache: yarn

View File

@@ -23,7 +23,7 @@ jobs:
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v3 uses: actions/checkout@v2
with: with:
# We must fetch at least the immediate parents so that if this is # We must fetch at least the immediate parents so that if this is
# a pull request then we can checkout the head. # a pull request then we can checkout the head.
@@ -36,14 +36,14 @@ jobs:
# Initializes the CodeQL tools for scanning. # Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL - name: Initialize CodeQL
uses: github/codeql-action/init@v2 uses: github/codeql-action/init@v1
with: with:
languages: ${{ matrix.language }} languages: ${{ matrix.language }}
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java). # 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) # If this step fails, then you should remove it and run the build manually (see below)
- name: Autobuild - name: Autobuild
uses: github/codeql-action/autobuild@v2 uses: github/codeql-action/autobuild@v1
# Command-line programs to run using the OS shell. # Command-line programs to run using the OS shell.
# 📚 https://git.io/JvXDl # 📚 https://git.io/JvXDl
@@ -57,4 +57,4 @@ jobs:
# make release # make release
- name: Perform CodeQL Analysis - name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v2 uses: github/codeql-action/analyze@v1

View File

@@ -14,9 +14,9 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Check out files from GitHub - name: Check out files from GitHub
uses: actions/checkout@v3 uses: actions/checkout@v2
- name: Set up Node ${{ env.NODE_VERSION }} - name: Set up Node ${{ env.NODE_VERSION }}
uses: actions/setup-node@v3 uses: actions/setup-node@v2
with: with:
node-version: ${{ env.NODE_VERSION }} node-version: ${{ env.NODE_VERSION }}
cache: yarn cache: yarn

View File

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

View File

@@ -1,63 +0,0 @@
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

View File

@@ -24,18 +24,18 @@ jobs:
contents: write # Required to upload release assets contents: write # Required to upload release assets
steps: steps:
- name: Checkout the repository - name: Checkout the repository
uses: actions/checkout@v3 uses: actions/checkout@v2
- name: Verify version - name: Verify version
uses: home-assistant/actions/helpers/verify-version@master uses: home-assistant/actions/helpers/verify-version@master
- name: Set up Python ${{ env.PYTHON_VERSION }} - name: Set up Python ${{ env.PYTHON_VERSION }}
uses: actions/setup-python@v4 uses: actions/setup-python@v2
with: with:
python-version: ${{ env.PYTHON_VERSION }} python-version: ${{ env.PYTHON_VERSION }}
- name: Set up Node ${{ env.NODE_VERSION }} - name: Set up Node ${{ env.NODE_VERSION }}
uses: actions/setup-node@v3 uses: actions/setup-node@v2
with: with:
node-version: ${{ env.NODE_VERSION }} node-version: ${{ env.NODE_VERSION }}
cache: yarn cache: yarn
@@ -74,11 +74,33 @@ jobs:
version=$(echo "${{ github.ref }}" | awk -F"/" '{print $NF}' ) version=$(echo "${{ github.ref }}" | awk -F"/" '{print $NF}' )
echo "home-assistant-frontend==$version" > ./requirements.txt echo "home-assistant-frontend==$version" > ./requirements.txt
- name: Build wheels - name: Upload requirements.txt
uses: home-assistant/wheels@2022.06.7 uses: actions/upload-artifact@v2
with: with:
abi: cp310 name: requirements
tag: musllinux_1_2 path: ./requirements.txt
arch: amd64
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
with:
tag: ${{ matrix.tag }}
arch: ${{ matrix.arch }}
wheels-host: ${{ secrets.WHEELS_HOST }}
wheels-key: ${{ secrets.WHEELS_KEY }} wheels-key: ${{ secrets.WHEELS_KEY }}
wheels-user: wheels
requirements: "requirements.txt" requirements: "requirements.txt"

View File

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

2
.vscode/tasks.json vendored
View File

@@ -181,7 +181,7 @@
{ {
"label": "Run HA Core for Supervisor in devcontainer", "label": "Run HA Core for Supervisor in devcontainer",
"type": "shell", "type": "shell",
"command": "SUPERVISOR=${input:supervisorHost} SUPERVISOR_TOKEN=${input:supervisorToken} script/core", "command": "HASSIO=${input:supervisorHost} HASSIO_TOKEN=${input:supervisorToken} script/core",
"isBackground": true, "isBackground": true,
"group": { "group": {
"kind": "build", "kind": "build",

View File

@@ -26,8 +26,8 @@ module.exports = {
}, },
version() { version() {
const version = fs const version = fs
.readFileSync(path.resolve(paths.polymer_dir, "pyproject.toml"), "utf8") .readFileSync(path.resolve(paths.polymer_dir, "setup.cfg"), "utf8")
.match(/version\W+=\W"(\d{8}\.\d(?:\.dev)?)"/); .match(/version\W+=\W(\d{8}\.\d)/);
if (!version) { if (!version) {
throw Error("Version not found"); throw Error("Version not found");
} }

View File

@@ -156,12 +156,3 @@ gulp.task("gen-icons-json", (done) => {
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();
});

View File

@@ -9,7 +9,6 @@ require("./compress.js");
require("./rollup.js"); require("./rollup.js");
require("./gather-static.js"); require("./gather-static.js");
require("./translations.js"); require("./translations.js");
require("./gen-icons-json.js");
gulp.task( gulp.task(
"develop-hassio", "develop-hassio",
@@ -18,7 +17,6 @@ gulp.task(
process.env.NODE_ENV = "development"; process.env.NODE_ENV = "development";
}, },
"clean-hassio", "clean-hassio",
"gen-dummy-icons-json",
"gen-index-hassio-dev", "gen-index-hassio-dev",
"build-supervisor-translations", "build-supervisor-translations",
"copy-translations-supervisor", "copy-translations-supervisor",
@@ -35,7 +33,6 @@ gulp.task(
process.env.NODE_ENV = "production"; process.env.NODE_ENV = "production";
}, },
"clean-hassio", "clean-hassio",
"gen-dummy-icons-json",
"build-supervisor-translations", "build-supervisor-translations",
"copy-translations-supervisor", "copy-translations-supervisor",
"build-locale-data", "build-locale-data",

View File

@@ -1,9 +0,0 @@
# 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

View File

@@ -194,7 +194,7 @@ export const demoLovelaceJimpower: DemoConfig["lovelace"] = () => ({
type: "state-icon", type: "state-icon",
tap_action: { tap_action: {
action: "call-service", action: "call-service",
data: { service_data: {
entity_id: "group.downstairs_lights", entity_id: "group.downstairs_lights",
}, },
service: "homeassistant.toggle", service: "homeassistant.toggle",

View File

@@ -59,7 +59,7 @@ export const demoEntitiesKernehed: DemoConfig["entities"] = () =>
attributes: { attributes: {
hidden: true, hidden: true,
radius: 50, radius: 50,
friendly_name: "School", friendly_name: "Skolan",
icon: "mdi:school", icon: "mdi:school",
}, },
}, },
@@ -137,7 +137,7 @@ export const demoEntitiesKernehed: DemoConfig["entities"] = () =>
state: "73", state: "73",
attributes: { attributes: {
unit_of_measurement: "%", unit_of_measurement: "%",
friendly_name: "Oskar battery", friendly_name: "oskar batteri",
device_class: "battery", device_class: "battery",
}, },
}, },
@@ -146,7 +146,7 @@ export const demoEntitiesKernehed: DemoConfig["entities"] = () =>
state: "88", state: "88",
attributes: { attributes: {
unit_of_measurement: "%", unit_of_measurement: "%",
friendly_name: "Bella battery", friendly_name: "bella batteri",
device_class: "battery", device_class: "battery",
}, },
}, },
@@ -154,7 +154,7 @@ export const demoEntitiesKernehed: DemoConfig["entities"] = () =>
entity_id: "binary_sensor.unifi_camera", entity_id: "binary_sensor.unifi_camera",
state: "off", state: "off",
attributes: { attributes: {
friendly_name: "Motion sensor camera", friendly_name: "R\u00f6relsesensor kamera",
icon: "mdi:walk", icon: "mdi:walk",
}, },
}, },
@@ -707,7 +707,7 @@ export const demoEntitiesKernehed: DemoConfig["entities"] = () =>
}, },
], ],
cloudiness: 25, cloudiness: 25,
friendly_name: "Weather", friendly_name: "V\u00e4der",
}, },
}, },
"binary_sensor.ubiquiti_switch": { "binary_sensor.ubiquiti_switch": {
@@ -731,7 +731,7 @@ export const demoEntitiesKernehed: DemoConfig["entities"] = () =>
round_trip_time_max: "0.626", round_trip_time_max: "0.626",
round_trip_time_mdev: "", round_trip_time_mdev: "",
round_trip_time_min: "0.358", round_trip_time_min: "0.358",
friendly_name: "Entrance camera", friendly_name: "Entr\u00e9 kamera",
device_class: "connectivity", device_class: "connectivity",
icon: "mdi:cctv", icon: "mdi:cctv",
}, },
@@ -797,7 +797,7 @@ export const demoEntitiesKernehed: DemoConfig["entities"] = () =>
attributes: { attributes: {
battery_level: 34, battery_level: 34,
on: true, on: true,
friendly_name: "Porch motion sensor", friendly_name: "altan_motion_sensor",
device_class: "motion", device_class: "motion",
}, },
}, },
@@ -807,7 +807,7 @@ export const demoEntitiesKernehed: DemoConfig["entities"] = () =>
attributes: { attributes: {
battery_level: 88, battery_level: 88,
on: true, on: true,
friendly_name: "Back door sensor", friendly_name: "Altand\u00f6rren sensor",
device_class: "opening", device_class: "opening",
icon: "mdi:door", icon: "mdi:door",
}, },
@@ -818,7 +818,7 @@ export const demoEntitiesKernehed: DemoConfig["entities"] = () =>
attributes: { attributes: {
battery_level: 74, battery_level: 74,
on: true, on: true,
friendly_name: "Bathroom motion sensor", friendly_name: "badrumssensor",
device_class: "motion", device_class: "motion",
}, },
}, },
@@ -829,7 +829,7 @@ export const demoEntitiesKernehed: DemoConfig["entities"] = () =>
battery_level: 47, battery_level: 47,
on: true, on: true,
dark: true, dark: true,
friendly_name: "Basement motion sensor", friendly_name: "R\u00f6relsesensor k\u00e4llaren 1",
device_class: "motion", device_class: "motion",
icon: "mdi:walk", icon: "mdi:walk",
}, },
@@ -841,7 +841,7 @@ export const demoEntitiesKernehed: DemoConfig["entities"] = () =>
battery_level: 60, battery_level: 60,
on: true, on: true,
dark: true, dark: true,
friendly_name: "Laundy room motion sensor", friendly_name: "R\u00f6relsesensor tv\u00e4ttstugan",
device_class: "motion", device_class: "motion",
icon: "mdi:walk", icon: "mdi:walk",
}, },
@@ -863,7 +863,7 @@ export const demoEntitiesKernehed: DemoConfig["entities"] = () =>
attributes: { attributes: {
battery_level: 60, battery_level: 60,
on: true, on: true,
friendly_name: "Pantry motion sensor", friendly_name: "R\u00f6relsesensor skafferiet",
device_class: "motion", device_class: "motion",
icon: "mdi:walk", icon: "mdi:walk",
}, },
@@ -875,7 +875,7 @@ export const demoEntitiesKernehed: DemoConfig["entities"] = () =>
battery_level: 60, battery_level: 60,
on: true, on: true,
dark: true, dark: true,
friendly_name: "Stair motion sensor", friendly_name: "R\u00f6relsesensor k\u00e4llaren 2",
device_class: "motion", device_class: "motion",
icon: "mdi:walk", icon: "mdi:walk",
}, },
@@ -887,7 +887,7 @@ export const demoEntitiesKernehed: DemoConfig["entities"] = () =>
battery_level: 47, battery_level: 47,
on: true, on: true,
dark: true, dark: true,
friendly_name: "Bench sensor", friendly_name: "B\u00e4nksensor",
device_class: "motion", device_class: "motion",
}, },
}, },

View File

@@ -277,7 +277,7 @@ export const demoLovelaceKernehed: DemoConfig["lovelace"] = () => ({
], ],
show_header_toggle: false, show_header_toggle: false,
type: "entities", type: "entities",
title: "Bandwidth", title: "Bandbredd",
}, },
// { // {
// title: "Updater", // title: "Updater",

View File

@@ -377,7 +377,7 @@ export const demoLovelaceTeachingbirds: DemoConfig["lovelace"] = () => ({
name: "AC bed", name: "AC bed",
tap_action: { tap_action: {
action: "call-service", action: "call-service",
data: { service_data: {
entity_id: "script.air_cleaner_quiet", entity_id: "script.air_cleaner_quiet",
}, },
service: "script.turn_on", service: "script.turn_on",
@@ -390,7 +390,7 @@ export const demoLovelaceTeachingbirds: DemoConfig["lovelace"] = () => ({
name: "AC bed", name: "AC bed",
tap_action: { tap_action: {
action: "call-service", action: "call-service",
data: { service_data: {
entity_id: "script.air_cleaner_auto", entity_id: "script.air_cleaner_auto",
}, },
service: "script.turn_on", service: "script.turn_on",
@@ -403,7 +403,7 @@ export const demoLovelaceTeachingbirds: DemoConfig["lovelace"] = () => ({
name: "AC bed", name: "AC bed",
tap_action: { tap_action: {
action: "call-service", action: "call-service",
data: { service_data: {
entity_id: "script.air_cleaner_turbo", entity_id: "script.air_cleaner_turbo",
}, },
service: "script.turn_on", service: "script.turn_on",
@@ -416,7 +416,7 @@ export const demoLovelaceTeachingbirds: DemoConfig["lovelace"] = () => ({
name: "AC", name: "AC",
tap_action: { tap_action: {
action: "call-service", action: "call-service",
data: { service_data: {
entity_id: "script.ac_off", entity_id: "script.ac_off",
}, },
service: "script.turn_on", service: "script.turn_on",
@@ -429,7 +429,7 @@ export const demoLovelaceTeachingbirds: DemoConfig["lovelace"] = () => ({
name: "AC", name: "AC",
tap_action: { tap_action: {
action: "call-service", action: "call-service",
data: { service_data: {
entity_id: "script.ac_on", entity_id: "script.ac_on",
}, },
service: "script.turn_on", service: "script.turn_on",
@@ -629,7 +629,7 @@ export const demoLovelaceTeachingbirds: DemoConfig["lovelace"] = () => ({
entity: "scene.morning_lights", entity: "scene.morning_lights",
tap_action: { tap_action: {
action: "call-service", action: "call-service",
data: { service_data: {
entity_id: "scene.morning_lights", entity_id: "scene.morning_lights",
}, },
service: "scene.turn_on", service: "scene.turn_on",
@@ -641,7 +641,7 @@ export const demoLovelaceTeachingbirds: DemoConfig["lovelace"] = () => ({
entity: "scene.movie_time", entity: "scene.movie_time",
tap_action: { tap_action: {
action: "call-service", action: "call-service",
data: { service_data: {
entity_id: "scene.movie_time", entity_id: "scene.movie_time",
}, },
service: "scene.turn_on", service: "scene.turn_on",
@@ -702,7 +702,7 @@ export const demoLovelaceTeachingbirds: DemoConfig["lovelace"] = () => ({
entity: "light.downstairs_lights", entity: "light.downstairs_lights",
tap_action: { tap_action: {
action: "call-service", action: "call-service",
data: { service_data: {
entity_id: "light.downstairs_lights", entity_id: "light.downstairs_lights",
}, },
service: "light.toggle", service: "light.toggle",
@@ -714,7 +714,7 @@ export const demoLovelaceTeachingbirds: DemoConfig["lovelace"] = () => ({
entity: "light.upstairs_lights", entity: "light.upstairs_lights",
tap_action: { tap_action: {
action: "call-service", action: "call-service",
data: { service_data: {
entity_id: "light.upstairs_lights", entity_id: "light.upstairs_lights",
}, },
service: "light.toggle", service: "light.toggle",

View File

@@ -1,7 +1,7 @@
import { MockHomeAssistant } from "../../../src/fake_data/provide_hass"; import { MockHomeAssistant } from "../../../src/fake_data/provide_hass";
export const mockConfig = (hass: MockHomeAssistant) => { export const mockConfig = (hass: MockHomeAssistant) => {
hass.mockAPI("config/config_entries/entry?domain=co2signal", () => [ hass.mockAPI("config/config_entries/entry", () => [
{ {
entry_id: "co2signal", entry_id: "co2signal",
domain: "co2signal", domain: "co2signal",

View File

@@ -1,4 +1,4 @@
import { format, startOfToday, startOfTomorrow } from "date-fns/esm"; import { format, startOfToday, startOfTomorrow } from "date-fns";
import { EnergySolarForecasts } from "../../../src/data/energy"; import { EnergySolarForecasts } from "../../../src/data/energy";
import { MockHomeAssistant } from "../../../src/fake_data/provide_hass"; import { MockHomeAssistant } from "../../../src/fake_data/provide_hass";

View File

@@ -4,7 +4,7 @@ import {
addMonths, addMonths,
differenceInHours, differenceInHours,
endOfDay, endOfDay,
} from "date-fns/esm"; } from "date-fns";
import { HassEntity } from "home-assistant-js-websocket"; import { HassEntity } from "home-assistant-js-websocket";
import { StatisticValue } from "../../../src/data/history"; import { StatisticValue } from "../../../src/data/history";
import { MockHomeAssistant } from "../../../src/fake_data/provide_hass"; import { MockHomeAssistant } from "../../../src/fake_data/provide_hass";
@@ -466,7 +466,6 @@ export const mockHistory = (mockHass: MockHomeAssistant) => {
return results; return results;
} }
); );
mockHass.mockWS("recorder/get_statistics_metadata", () => []);
mockHass.mockWS("history/list_statistic_ids", () => []); mockHass.mockWS("history/list_statistic_ids", () => []);
mockHass.mockWS( mockHass.mockWS(
"history/statistics_during_period", "history/statistics_during_period",

Binary file not shown.

Before

Width:  |  Height:  |  Size: 32 KiB

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 27 KiB

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 46 KiB

After

Width:  |  Height:  |  Size: 67 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 90 KiB

After

Width:  |  Height:  |  Size: 94 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

After

Width:  |  Height:  |  Size: 32 KiB

View File

@@ -119,7 +119,7 @@ export const basicTrace: DemoTrace = {
params: { params: {
domain: "input_boolean", domain: "input_boolean",
service: "toggle", service: "toggle",
data: {}, service_data: {},
target: { target: {
entity_id: ["input_boolean.toggle_4"], entity_id: ["input_boolean.toggle_4"],
}, },
@@ -164,7 +164,7 @@ export const basicTrace: DemoTrace = {
params: { params: {
domain: "input_boolean", domain: "input_boolean",
service: "toggle", service: "toggle",
data: {}, service_data: {},
target: { target: {
entity_id: ["input_boolean.toggle_2"], entity_id: ["input_boolean.toggle_2"],
}, },
@@ -182,7 +182,7 @@ export const basicTrace: DemoTrace = {
params: { params: {
domain: "input_boolean", domain: "input_boolean",
service: "toggle", service: "toggle",
data: {}, service_data: {},
target: { target: {
entity_id: ["input_boolean.toggle_3"], entity_id: ["input_boolean.toggle_3"],
}, },
@@ -200,7 +200,7 @@ export const basicTrace: DemoTrace = {
params: { params: {
domain: "input_boolean", domain: "input_boolean",
service: "toggle", service: "toggle",
data: {}, service_data: {},
target: { target: {
entity_id: ["input_boolean.toggle_4"], entity_id: ["input_boolean.toggle_4"],
}, },
@@ -298,11 +298,11 @@ export const basicTrace: DemoTrace = {
source: "state of input_boolean.toggle_1", source: "state of input_boolean.toggle_1",
entity_id: "automation.toggle_toggles", entity_id: "automation.toggle_toggles",
context_id: "6cfcae368e7b3686fad6c59e83ae76c9", context_id: "6cfcae368e7b3686fad6c59e83ae76c9",
when: 1616647011.240832, when: "2021-03-25T04:36:51.240832+00:00",
domain: "automation", domain: "automation",
}, },
{ {
when: 1616647011.249828, when: "2021-03-25T04:36:51.249828+00:00",
name: "Toggle 4", name: "Toggle 4",
state: "on", state: "on",
entity_id: "input_boolean.toggle_4", entity_id: "input_boolean.toggle_4",
@@ -313,7 +313,7 @@ export const basicTrace: DemoTrace = {
context_name: "Ensure Party mode", context_name: "Ensure Party mode",
}, },
{ {
when: 1616647011.258947, when: "2021-03-25T04:36:51.258947+00:00",
name: "Toggle 2", name: "Toggle 2",
state: "on", state: "on",
entity_id: "input_boolean.toggle_2", entity_id: "input_boolean.toggle_2",
@@ -324,7 +324,7 @@ export const basicTrace: DemoTrace = {
context_name: "Ensure Party mode", context_name: "Ensure Party mode",
}, },
{ {
when: 1616647011.261806, when: "2021-03-25T04:36:51.261806+00:00",
name: "Toggle 3", name: "Toggle 3",
state: "off", state: "off",
entity_id: "input_boolean.toggle_3", entity_id: "input_boolean.toggle_3",
@@ -335,7 +335,7 @@ export const basicTrace: DemoTrace = {
context_name: "Ensure Party mode", context_name: "Ensure Party mode",
}, },
{ {
when: 1616647011.265246, when: "2021-03-25T04:36:51.265246+00:00",
name: "Toggle 4", name: "Toggle 4",
state: "off", state: "off",
entity_id: "input_boolean.toggle_4", entity_id: "input_boolean.toggle_4",

View File

@@ -185,11 +185,11 @@ export const motionLightTrace: DemoTrace = {
"has been triggered by state of binary_sensor.pauluss_macbook_pro_camera_in_use", "has been triggered by state of binary_sensor.pauluss_macbook_pro_camera_in_use",
source: "state of binary_sensor.pauluss_macbook_pro_camera_in_use", source: "state of binary_sensor.pauluss_macbook_pro_camera_in_use",
entity_id: "automation.auto_elgato", entity_id: "automation.auto_elgato",
when: 1615702021.768492, when: "2021-03-14T06:07:01.768492+00:00",
domain: "automation", domain: "automation",
}, },
{ {
when: 1615702021.872187, when: "2021-03-14T06:07:01.872187+00:00",
name: "Elgato Key Light Air", name: "Elgato Key Light Air",
state: "on", state: "on",
entity_id: "light.elgato_key_light_air", entity_id: "light.elgato_key_light_air",
@@ -200,7 +200,7 @@ export const motionLightTrace: DemoTrace = {
context_name: "Auto Elgato", context_name: "Auto Elgato",
}, },
{ {
when: 1615702073.284505, when: "2021-03-14T06:07:53.284505+00:00",
name: "Elgato Key Light Air", name: "Elgato Key Light Air",
state: "off", state: "off",
entity_id: "light.elgato_key_light_air", entity_id: "light.elgato_key_light_air",

View File

@@ -249,7 +249,7 @@ const CONFIGS = [
name: Bed light name: Bed light
action_name: Toggle light action_name: Toggle light
service: light.toggle service: light.toggle
data: service_data:
entity_id: light.bed_light entity_id: light.bed_light
- type: section - type: section
label: Links label: Links

View File

@@ -199,7 +199,7 @@ const CONFIGS = [
tap_action: tap_action:
action: call-service action: call-service
service: light.turn_on service: light.turn_on
data: service_data:
entity_id: light.ceiling_lights entity_id: light.ceiling_lights
- entity: sun.sun - entity: sun.sun
name: Regular name: Regular

View File

@@ -40,7 +40,7 @@ const CONFIGS = [
left: 90% left: 90%
padding: 0px padding: 0px
service: light.turn_off service: light.turn_off
data: service_data:
entity_id: group.all_lights entity_id: group.all_lights
- type: icon - type: icon
icon: mdi:cctv icon: mdi:cctv
@@ -88,7 +88,7 @@ const CONFIGS = [
left: 90% left: 90%
padding: 0px padding: 0px
service: light.turn_off service: light.turn_off
data: service_data:
entity_id: group.all_lights entity_id: group.all_lights
- type: icon - type: icon
icon: mdi:cctv icon: mdi:cctv

View File

@@ -6,8 +6,10 @@ import { atLeastVersion } from "../../../src/common/config/version";
import { navigate } from "../../../src/common/navigate"; import { navigate } from "../../../src/common/navigate";
import { caseInsensitiveStringCompare } from "../../../src/common/string/compare"; import { caseInsensitiveStringCompare } from "../../../src/common/string/compare";
import "../../../src/components/ha-card"; import "../../../src/components/ha-card";
import { HassioAddonRepository } from "../../../src/data/hassio/addon"; import {
import { StoreAddon } from "../../../src/data/supervisor/store"; HassioAddonInfo,
HassioAddonRepository,
} from "../../../src/data/hassio/addon";
import { Supervisor } from "../../../src/data/supervisor/supervisor"; import { Supervisor } from "../../../src/data/supervisor/supervisor";
import { HomeAssistant } from "../../../src/types"; import { HomeAssistant } from "../../../src/types";
import "../components/hassio-card-content"; import "../components/hassio-card-content";
@@ -21,16 +23,20 @@ class HassioAddonRepositoryEl extends LitElement {
@property({ attribute: false }) public repo!: HassioAddonRepository; @property({ attribute: false }) public repo!: HassioAddonRepository;
@property({ attribute: false }) public addons!: StoreAddon[]; @property({ attribute: false }) public addons!: HassioAddonInfo[];
@property() public filter!: string; @property() public filter!: string;
private _getAddons = memoizeOne((addons: StoreAddon[], filter?: string) => { private _getAddons = memoizeOne(
if (filter) { (addons: HassioAddonInfo[], filter?: string) => {
return filterAndSort(addons, filter); if (filter) {
return filterAndSort(addons, filter);
}
return addons.sort((a, b) =>
caseInsensitiveStringCompare(a.name, b.name)
);
} }
return addons.sort((a, b) => caseInsensitiveStringCompare(a.name, b.name)); );
});
protected render(): TemplateResult { protected render(): TemplateResult {
const repo = this.repo; const repo = this.repo;
@@ -62,7 +68,6 @@ class HassioAddonRepositoryEl extends LitElement {
${addons.map( ${addons.map(
(addon) => html` (addon) => html`
<ha-card <ha-card
outlined
.addon=${addon} .addon=${addon}
class=${addon.available ? "" : "not_available"} class=${addon.available ? "" : "not_available"}
@click=${this._addonTapped} @click=${this._addonTapped}

View File

@@ -14,15 +14,15 @@ import memoizeOne from "memoize-one";
import { atLeastVersion } from "../../../src/common/config/version"; import { atLeastVersion } from "../../../src/common/config/version";
import { fireEvent } from "../../../src/common/dom/fire_event"; import { fireEvent } from "../../../src/common/dom/fire_event";
import { navigate } from "../../../src/common/navigate"; import { navigate } from "../../../src/common/navigate";
import "../../../src/components/search-input";
import { extractSearchParam } from "../../../src/common/url/search-params"; import { extractSearchParam } from "../../../src/common/url/search-params";
import "../../../src/components/ha-button-menu"; import "../../../src/components/ha-button-menu";
import "../../../src/components/ha-icon-button"; import "../../../src/components/ha-icon-button";
import "../../../src/components/search-input";
import { import {
HassioAddonInfo,
HassioAddonRepository, HassioAddonRepository,
reloadHassioAddons, reloadHassioAddons,
} from "../../../src/data/hassio/addon"; } from "../../../src/data/hassio/addon";
import { StoreAddon } from "../../../src/data/supervisor/store";
import { Supervisor } from "../../../src/data/supervisor/supervisor"; import { Supervisor } from "../../../src/data/supervisor/supervisor";
import "../../../src/layouts/hass-loading-screen"; import "../../../src/layouts/hass-loading-screen";
import "../../../src/layouts/hass-subpage"; import "../../../src/layouts/hass-subpage";
@@ -66,10 +66,10 @@ class HassioAddonStore extends LitElement {
protected render(): TemplateResult { protected render(): TemplateResult {
let repos: TemplateResult[] = []; let repos: TemplateResult[] = [];
if (this.supervisor.store.repositories) { if (this.supervisor.addon.repositories) {
repos = this.addonRepositories( repos = this.addonRepositories(
this.supervisor.store.repositories, this.supervisor.addon.repositories,
this.supervisor.store.addons, this.supervisor.addon.addons,
this._filter this._filter
); );
} }
@@ -145,7 +145,7 @@ class HassioAddonStore extends LitElement {
private addonRepositories = memoizeOne( private addonRepositories = memoizeOne(
( (
repositories: HassioAddonRepository[], repositories: HassioAddonRepository[],
addons: StoreAddon[], addons: HassioAddonInfo[],
filter?: string filter?: string
) => ) =>
repositories.sort(sortRepos).map((repo) => { repositories.sort(sortRepos).map((repo) => {

View File

@@ -50,7 +50,6 @@ class HassioAddonAudio extends LitElement {
protected render(): TemplateResult { protected render(): TemplateResult {
return html` return html`
<ha-card <ha-card
outlined
.header=${this.supervisor.localize("addon.configuration.audio.header")} .header=${this.supervisor.localize("addon.configuration.audio.header")}
> >
<div class="card-content"> <div class="card-content">

View File

@@ -162,7 +162,7 @@ class HassioAddonConfig extends LitElement {
); );
return html` return html`
<h1>${this.addon.name}</h1> <h1>${this.addon.name}</h1>
<ha-card outlined> <ha-card>
<div class="header"> <div class="header">
<h2> <h2>
${this.supervisor.localize("addon.configuration.options.header")} ${this.supervisor.localize("addon.configuration.options.header")}

View File

@@ -58,7 +58,6 @@ class HassioAddonNetwork extends LitElement {
return html` return html`
<ha-card <ha-card
outlined
.header=${this.supervisor.localize( .header=${this.supervisor.localize(
"addon.configuration.network.header" "addon.configuration.network.header"
)} )}

View File

@@ -38,7 +38,7 @@ class HassioAddonDocumentationDashboard extends LitElement {
} }
return html` return html`
<div class="content"> <div class="content">
<ha-card outlined> <ha-card>
${this._error ${this._error
? html`<ha-alert alert-type="error">${this._error}</ha-alert>` ? html`<ha-alert alert-type="error">${this._error}</ha-alert>`
: ""} : ""}

View File

@@ -12,17 +12,12 @@ import { navigate } from "../../../src/common/navigate";
import { extractSearchParam } from "../../../src/common/url/search-params"; import { extractSearchParam } from "../../../src/common/url/search-params";
import "../../../src/components/ha-circular-progress"; import "../../../src/components/ha-circular-progress";
import { import {
fetchAddonInfo,
fetchHassioAddonInfo, fetchHassioAddonInfo,
fetchHassioAddonsInfo, fetchHassioAddonsInfo,
HassioAddonDetails, HassioAddonDetails,
} from "../../../src/data/hassio/addon"; } from "../../../src/data/hassio/addon";
import { extractApiErrorMessage } from "../../../src/data/hassio/common"; import { extractApiErrorMessage } from "../../../src/data/hassio/common";
import { import { setSupervisorOption } from "../../../src/data/hassio/supervisor";
addStoreRepository,
fetchSupervisorStore,
StoreAddonDetails,
} from "../../../src/data/supervisor/store";
import { Supervisor } from "../../../src/data/supervisor/supervisor"; import { Supervisor } from "../../../src/data/supervisor/supervisor";
import { showConfirmationDialog } from "../../../src/dialogs/generic/show-dialog-box"; import { showConfirmationDialog } from "../../../src/dialogs/generic/show-dialog-box";
import "../../../src/layouts/hass-error-screen"; import "../../../src/layouts/hass-error-screen";
@@ -47,9 +42,7 @@ class HassioAddonDashboard extends LitElement {
@property({ attribute: false }) public route!: Route; @property({ attribute: false }) public route!: Route;
@property({ attribute: false }) public addon?: @property({ attribute: false }) public addon?: HassioAddonDetails;
| HassioAddonDetails
| StoreAddonDetails;
@property({ type: Boolean }) public narrow!: boolean; @property({ type: Boolean }) public narrow!: boolean;
@@ -176,35 +169,38 @@ class HassioAddonDashboard extends LitElement {
if (this.route.path === "") { if (this.route.path === "") {
const requestedAddon = extractSearchParam("addon"); const requestedAddon = extractSearchParam("addon");
const requestedAddonRepository = extractSearchParam("repository_url"); const requestedAddonRepository = extractSearchParam("repository_url");
if (requestedAddonRepository) { if (
const storeInfo = await fetchSupervisorStore(this.hass); requestedAddonRepository &&
!this.supervisor.supervisor.addons_repositories.find(
(repo) => repo === requestedAddonRepository
)
) {
if ( if (
!storeInfo.repositories.find( !(await showConfirmationDialog(this, {
(repo) => repo.source === requestedAddonRepository title: this.supervisor.localize("my.add_addon_repository_title"),
) text: this.supervisor.localize(
"my.add_addon_repository_description",
{ addon: requestedAddon, repository: requestedAddonRepository }
),
confirmText: this.supervisor.localize("common.add"),
dismissText: this.supervisor.localize("common.cancel"),
}))
) { ) {
if ( this._error = this.supervisor.localize(
!(await showConfirmationDialog(this, { "my.error_repository_not_found"
title: this.supervisor.localize("my.add_addon_repository_title"), );
text: this.supervisor.localize( return;
"my.add_addon_repository_description", }
{ addon: requestedAddon, repository: requestedAddonRepository }
),
confirmText: this.supervisor.localize("common.add"),
dismissText: this.supervisor.localize("common.cancel"),
}))
) {
this._error = this.supervisor.localize(
"my.error_repository_not_found"
);
return;
}
try { try {
await addStoreRepository(this.hass, requestedAddonRepository); await setSupervisorOption(this.hass, {
} catch (err: any) { addons_repositories: [
this._error = extractApiErrorMessage(err); ...this.supervisor.supervisor.addons_repositories,
} requestedAddonRepository,
],
});
} catch (err: any) {
this._error = extractApiErrorMessage(err);
} }
} }
@@ -244,8 +240,6 @@ class HassioAddonDashboard extends LitElement {
if (path === "uninstall") { if (path === "uninstall") {
window.history.back(); window.history.back();
} else if (path === "install") {
this.addon = await fetchHassioAddonInfo(this.hass, this.addon!.slug);
} else { } else {
await this._routeDataChanged(); await this._routeDataChanged();
} }
@@ -263,7 +257,8 @@ class HassioAddonDashboard extends LitElement {
return; return;
} }
try { try {
this.addon = await fetchAddonInfo(this.hass, this.supervisor, addon); const addoninfo = await fetchHassioAddonInfo(this.hass, addon);
this.addon = addoninfo;
} catch (err: any) { } catch (err: any) {
this._error = `Error fetching addon info: ${extractApiErrorMessage(err)}`; this._error = `Error fetching addon info: ${extractApiErrorMessage(err)}`;
this.addon = undefined; this.addon = undefined;

View File

@@ -1,6 +1,5 @@
import { customElement, property } from "lit/decorators"; import { customElement, property } from "lit/decorators";
import { HassioAddonDetails } from "../../../src/data/hassio/addon"; import { HassioAddonDetails } from "../../../src/data/hassio/addon";
import { StoreAddonDetails } from "../../../src/data/supervisor/store";
import { Supervisor } from "../../../src/data/supervisor/supervisor"; import { Supervisor } from "../../../src/data/supervisor/supervisor";
import { import {
HassRouterPage, HassRouterPage,
@@ -21,9 +20,7 @@ class HassioAddonRouter extends HassRouterPage {
@property({ attribute: false }) public supervisor!: Supervisor; @property({ attribute: false }) public supervisor!: Supervisor;
@property({ attribute: false }) public addon!: @property({ attribute: false }) public addon!: HassioAddonDetails;
| HassioAddonDetails
| StoreAddonDetails;
protected routerOptions: RouterOptions = { protected routerOptions: RouterOptions = {
defaultPage: "info", defaultPage: "info",

View File

@@ -59,10 +59,7 @@ import {
fetchHassioStats, fetchHassioStats,
HassioStats, HassioStats,
} from "../../../../src/data/hassio/common"; } from "../../../../src/data/hassio/common";
import { import { StoreAddon } from "../../../../src/data/supervisor/store";
StoreAddon,
StoreAddonDetails,
} from "../../../../src/data/supervisor/store";
import { Supervisor } from "../../../../src/data/supervisor/supervisor"; import { Supervisor } from "../../../../src/data/supervisor/supervisor";
import { import {
showAlertDialog, showAlertDialog,
@@ -103,9 +100,7 @@ class HassioAddonInfo extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant; @property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public addon!: @property({ attribute: false }) public addon!: HassioAddonDetails;
| HassioAddonDetails
| StoreAddonDetails;
@property({ attribute: false }) public supervisor!: Supervisor; @property({ attribute: false }) public supervisor!: Supervisor;
@@ -148,7 +143,7 @@ class HassioAddonInfo extends LitElement {
></update-available-card> ></update-available-card>
` `
: ""} : ""}
${"protected" in this.addon && !this.addon.protected ${!this.addon.protected
? html` ? html`
<ha-alert <ha-alert
alert-type="error" alert-type="error"
@@ -171,7 +166,7 @@ class HassioAddonInfo extends LitElement {
` `
: ""} : ""}
<ha-card outlined> <ha-card>
<div class="card-content"> <div class="card-content">
<div class="addon-header"> <div class="addon-header">
${!this.narrow ? this.addon.name : ""} ${!this.narrow ? this.addon.name : ""}
@@ -523,7 +518,7 @@ class HassioAddonInfo extends LitElement {
: ""} : ""}
</div> </div>
<div> <div>
${this.addon.version && this.addon.state === "started" ${this.addon.state === "started"
? html`<ha-settings-row ?three-line=${this.narrow}> ? html`<ha-settings-row ?three-line=${this.narrow}>
<span slot="heading"> <span slot="heading">
${this.supervisor.localize("addon.dashboard.hostname")} ${this.supervisor.localize("addon.dashboard.hostname")}
@@ -654,7 +649,7 @@ class HassioAddonInfo extends LitElement {
${this.addon.long_description ${this.addon.long_description
? html` ? html`
<ha-card outlined> <ha-card>
<div class="card-content"> <div class="card-content">
<ha-markdown <ha-markdown
.content=${this.addon.long_description} .content=${this.addon.long_description}
@@ -674,7 +669,7 @@ class HassioAddonInfo extends LitElement {
} }
private async _loadData(): Promise<void> { private async _loadData(): Promise<void> {
if ("state" in this.addon && this.addon.state === "started") { if (this.addon.state === "started") {
this._metrics = await fetchHassioStats( this._metrics = await fetchHassioStats(
this.hass, this.hass,
`addons/${this.addon.slug}` `addons/${this.addon.slug}`
@@ -722,22 +717,18 @@ class HassioAddonInfo extends LitElement {
} }
private get _computeIsRunning(): boolean { private get _computeIsRunning(): boolean {
return (this.addon as HassioAddonDetails)?.state === "started"; return this.addon?.state === "started";
} }
private get _pathWebui(): string | null { private get _pathWebui(): string | null {
return (this.addon as HassioAddonDetails).webui!.replace( return (
"[HOST]", this.addon.webui &&
document.location.hostname this.addon.webui.replace("[HOST]", document.location.hostname)
); );
} }
private get _computeShowWebUI(): boolean | "" | null { private get _computeShowWebUI(): boolean | "" | null {
return ( return !this.addon.ingress && this.addon.webui && this._computeIsRunning;
!this.addon.ingress &&
(this.addon as HassioAddonDetails).webui &&
this._computeIsRunning
);
} }
private _openIngress(): void { private _openIngress(): void {
@@ -763,8 +754,7 @@ class HassioAddonInfo extends LitElement {
private async _startOnBootToggled(): Promise<void> { private async _startOnBootToggled(): Promise<void> {
this._error = undefined; this._error = undefined;
const data: HassioAddonSetOptionParams = { const data: HassioAddonSetOptionParams = {
boot: boot: this.addon.boot === "auto" ? "manual" : "auto",
(this.addon as HassioAddonDetails).boot === "auto" ? "manual" : "auto",
}; };
try { try {
await setHassioAddonOption(this.hass, this.addon.slug, data); await setHassioAddonOption(this.hass, this.addon.slug, data);
@@ -786,7 +776,7 @@ class HassioAddonInfo extends LitElement {
private async _watchdogToggled(): Promise<void> { private async _watchdogToggled(): Promise<void> {
this._error = undefined; this._error = undefined;
const data: HassioAddonSetOptionParams = { const data: HassioAddonSetOptionParams = {
watchdog: !(this.addon as HassioAddonDetails).watchdog, watchdog: !this.addon.watchdog,
}; };
try { try {
await setHassioAddonOption(this.hass, this.addon.slug, data); await setHassioAddonOption(this.hass, this.addon.slug, data);
@@ -808,7 +798,7 @@ class HassioAddonInfo extends LitElement {
private async _autoUpdateToggled(): Promise<void> { private async _autoUpdateToggled(): Promise<void> {
this._error = undefined; this._error = undefined;
const data: HassioAddonSetOptionParams = { const data: HassioAddonSetOptionParams = {
auto_update: !(this.addon as HassioAddonDetails).auto_update, auto_update: !this.addon.auto_update,
}; };
try { try {
await setHassioAddonOption(this.hass, this.addon.slug, data); await setHassioAddonOption(this.hass, this.addon.slug, data);
@@ -830,7 +820,7 @@ class HassioAddonInfo extends LitElement {
private async _protectionToggled(): Promise<void> { private async _protectionToggled(): Promise<void> {
this._error = undefined; this._error = undefined;
const data: HassioAddonSetSecurityParams = { const data: HassioAddonSetSecurityParams = {
protected: !(this.addon as HassioAddonDetails).protected, protected: !this.addon.protected,
}; };
try { try {
await setHassioAddonSecurity(this.hass, this.addon.slug, data); await setHassioAddonSecurity(this.hass, this.addon.slug, data);
@@ -852,7 +842,7 @@ class HassioAddonInfo extends LitElement {
private async _panelToggled(): Promise<void> { private async _panelToggled(): Promise<void> {
this._error = undefined; this._error = undefined;
const data: HassioAddonSetOptionParams = { const data: HassioAddonSetOptionParams = {
ingress_panel: !(this.addon as HassioAddonDetails).ingress_panel, ingress_panel: !this.addon.ingress_panel,
}; };
try { try {
await setHassioAddonOption(this.hass, this.addon.slug, data); await setHassioAddonOption(this.hass, this.addon.slug, data);
@@ -880,7 +870,7 @@ class HassioAddonInfo extends LitElement {
showHassioMarkdownDialog(this, { showHassioMarkdownDialog(this, {
title: this.supervisor.localize("addon.dashboard.changelog"), title: this.supervisor.localize("addon.dashboard.changelog"),
content: extractChangelog(this.addon as HassioAddonDetails, content), content: extractChangelog(this.addon, content),
}); });
} catch (err: any) { } catch (err: any) {
showAlertDialog(this, { showAlertDialog(this, {

View File

@@ -34,7 +34,7 @@ class HassioAddonLogs extends LitElement {
protected render(): TemplateResult { protected render(): TemplateResult {
return html` return html`
<h1>${this.addon.name}</h1> <h1>${this.addon.name}</h1>
<ha-card outlined> <ha-card>
${this._error ${this._error
? html`<ha-alert alert-type="error">${this._error}</ha-alert>` ? html`<ha-alert alert-type="error">${this._error}</ha-alert>`
: ""} : ""}

View File

@@ -98,8 +98,9 @@ export class HassioBackups extends LitElement {
if (backup.content.addons.length !== 0) { if (backup.content.addons.length !== 0) {
for (const addon of backup.content.addons) { for (const addon of backup.content.addons) {
content.push( content.push(
this.supervisor.addon.addons.find((entry) => entry.slug === addon) this.supervisor.supervisor.addons.find(
?.name || addon (entry) => entry.slug === addon
)?.name || addon
); );
} }
} }

View File

@@ -1,8 +1,8 @@
import Fuse from "fuse.js"; import Fuse from "fuse.js";
import { StoreAddon } from "../../../src/data/supervisor/store"; import { HassioAddonInfo } from "../../../src/data/hassio/addon";
export function filterAndSort(addons: StoreAddon[], filter: string) { export function filterAndSort(addons: HassioAddonInfo[], filter: string) {
const options: Fuse.IFuseOptions<StoreAddon> = { const options: Fuse.IFuseOptions<HassioAddonInfo> = {
keys: ["name", "description", "slug"], keys: ["name", "description", "slug"],
isCaseSensitive: false, isCaseSensitive: false,
minMatchCharLength: 2, minMatchCharLength: 2,

View File

@@ -96,7 +96,7 @@ export class SupervisorBackupContent extends LitElement {
: ["ssl", "share", "media", "addons/local"] : ["ssl", "share", "media", "addons/local"]
); );
this.addons = _computeAddons( this.addons = _computeAddons(
this.backup ? this.backup.addons : this.supervisor?.addon.addons this.backup ? this.backup.addons : this.supervisor?.supervisor.addons
); );
this.backupType = this.backup?.type || "full"; this.backupType = this.backup?.type || "full";
this.backupName = this.backup?.name || ""; this.backupName = this.backup?.name || "";

View File

@@ -24,9 +24,9 @@ class HassioAddons extends LitElement {
? html` <h1>${this.supervisor.localize("dashboard.addons")}</h1> ` ? html` <h1>${this.supervisor.localize("dashboard.addons")}</h1> `
: ""} : ""}
<div class="card-group"> <div class="card-group">
${!this.supervisor.addon.addons.length ${!this.supervisor.supervisor.addons?.length
? html` ? html`
<ha-card outlined> <ha-card>
<div class="card-content"> <div class="card-content">
<button class="link" @click=${this._openStore}> <button class="link" @click=${this._openStore}>
${this.supervisor.localize("dashboard.no_addons")} ${this.supervisor.localize("dashboard.no_addons")}
@@ -34,15 +34,11 @@ class HassioAddons extends LitElement {
</div> </div>
</ha-card> </ha-card>
` `
: this.supervisor.addon.addons : this.supervisor.supervisor.addons
.sort((a, b) => caseInsensitiveStringCompare(a.name, b.name)) .sort((a, b) => caseInsensitiveStringCompare(a.name, b.name))
.map( .map(
(addon) => html` (addon) => html`
<ha-card <ha-card .addon=${addon} @click=${this._addonTapped}>
outlined
.addon=${addon}
@click=${this._addonTapped}
>
<div class="card-content"> <div class="card-content">
<hassio-card-content <hassio-card-content
.hass=${this.hass} .hass=${this.hass}

View File

@@ -85,7 +85,7 @@ export class HassioUpdate extends LitElement {
return html``; return html``;
} }
return html` return html`
<ha-card outlined> <ha-card>
<div class="card-content"> <div class="card-content">
<div class="icon"> <div class="icon">
<ha-svg-icon .path=${mdiHomeAssistant}></ha-svg-icon> <ha-svg-icon .path=${mdiHomeAssistant}></ha-svg-icon>

View File

@@ -15,18 +15,15 @@ import "../../../../src/components/ha-circular-progress";
import { createCloseHeading } from "../../../../src/components/ha-dialog"; import { createCloseHeading } from "../../../../src/components/ha-dialog";
import "../../../../src/components/ha-icon-button"; import "../../../../src/components/ha-icon-button";
import { import {
fetchHassioAddonsInfo,
HassioAddonInfo, HassioAddonInfo,
HassioAddonRepository, HassioAddonRepository,
} from "../../../../src/data/hassio/addon"; } from "../../../../src/data/hassio/addon";
import { extractApiErrorMessage } from "../../../../src/data/hassio/common"; import { extractApiErrorMessage } from "../../../../src/data/hassio/common";
import { setSupervisorOption } from "../../../../src/data/hassio/supervisor";
import { haStyle, haStyleDialog } from "../../../../src/resources/styles"; import { haStyle, haStyleDialog } from "../../../../src/resources/styles";
import type { HomeAssistant } from "../../../../src/types"; import type { HomeAssistant } from "../../../../src/types";
import { HassioRepositoryDialogParams } from "./show-dialog-repositories"; import { HassioRepositoryDialogParams } from "./show-dialog-repositories";
import {
addStoreRepository,
fetchStoreRepositories,
removeStoreRepository,
} from "../../../../src/data/supervisor/store";
@customElement("dialog-hassio-repositories") @customElement("dialog-hassio-repositories")
class HassioRepositoriesDialog extends LitElement { class HassioRepositoriesDialog extends LitElement {
@@ -61,13 +58,7 @@ class HassioRepositoriesDialog extends LitElement {
private _filteredRepositories = memoizeOne((repos: HassioAddonRepository[]) => private _filteredRepositories = memoizeOne((repos: HassioAddonRepository[]) =>
repos repos
.filter( .filter((repo) => repo.slug !== "core" && repo.slug !== "local")
(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)) .sort((a, b) => caseInsensitiveStringCompare(a.name, b.name))
); );
@@ -87,7 +78,7 @@ class HassioRepositoriesDialog extends LitElement {
const repositories = this._filteredRepositories(this._repositories); const repositories = this._filteredRepositories(this._repositories);
const usedRepositories = this._filteredUsedRepositories( const usedRepositories = this._filteredUsedRepositories(
repositories, repositories,
this._dialogParams.supervisor.addon.addons this._dialogParams.supervisor.supervisor.addons
); );
return html` return html`
<ha-dialog <ha-dialog
@@ -224,7 +215,9 @@ class HassioRepositoriesDialog extends LitElement {
private async _loadData(): Promise<void> { private async _loadData(): Promise<void> {
try { try {
this._repositories = await fetchStoreRepositories(this.hass); const addonsinfo = await fetchHassioAddonsInfo(this.hass);
this._repositories = addonsinfo.repositories;
fireEvent(this, "supervisor-collection-refresh", { collection: "addon" }); fireEvent(this, "supervisor-collection-refresh", { collection: "addon" });
} catch (err: any) { } catch (err: any) {
@@ -238,9 +231,14 @@ class HassioRepositoriesDialog extends LitElement {
return; return;
} }
this._processing = true; this._processing = true;
const repositories = this._filteredRepositories(this._repositories!);
const newRepositories = repositories.map((repo) => repo.source);
newRepositories.push(input.value);
try { try {
await addStoreRepository(this.hass, input.value); await setSupervisorOption(this.hass, {
addons_repositories: newRepositories,
});
await this._loadData(); await this._loadData();
input.value = ""; input.value = "";
@@ -252,8 +250,19 @@ class HassioRepositoriesDialog extends LitElement {
private async _removeRepository(ev: Event) { private async _removeRepository(ev: Event) {
const slug = (ev.currentTarget as any).slug; 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 { try {
await removeStoreRepository(this.hass, slug); await setSupervisorOption(this.hass, {
addons_repositories: newRepositories,
});
await this._loadData(); await this._loadData();
} catch (err: any) { } catch (err: any) {
this._error = extractApiErrorMessage(err); this._error = extractApiErrorMessage(err);

View File

@@ -48,7 +48,7 @@ class HassioCoreInfo extends LitElement {
]; ];
return html` return html`
<ha-card header="Core" outlined> <ha-card header="Core">
<div class="card-content"> <div class="card-content">
<div> <div>
<ha-settings-row> <ha-settings-row>

View File

@@ -66,7 +66,7 @@ class HassioHostInfo extends LitElement {
}, },
]; ];
return html` return html`
<ha-card header="Host" outlined> <ha-card header="Host">
<div class="card-content"> <div class="card-content">
<div> <div>
${this.supervisor.host.features.includes("hostname") ${this.supervisor.host.features.includes("hostname")

View File

@@ -57,7 +57,7 @@ class HassioSupervisorInfo extends LitElement {
}, },
]; ];
return html` return html`
<ha-card header="Supervisor" outlined> <ha-card header="Supervisor">
<div class="card-content"> <div class="card-content">
<div> <div>
<ha-settings-row> <ha-settings-row>

View File

@@ -65,7 +65,7 @@ class HassioSupervisorLog extends LitElement {
protected render(): TemplateResult | void { protected render(): TemplateResult | void {
return html` return html`
<ha-card outlined> <ha-card>
${this._error ${this._error
? html`<ha-alert alert-type="error">${this._error}</ha-alert>` ? html`<ha-alert alert-type="error">${this._error}</ha-alert>`
: ""} : ""}

View File

@@ -128,7 +128,6 @@ class UpdateAvailableCard extends LitElement {
return html` return html`
<ha-card <ha-card
outlined
.header=${this.supervisor.localize("update_available.update_name", { .header=${this.supervisor.localize("update_available.update_name", {
name: this._name, name: this._name,
})} })}

View File

@@ -72,8 +72,8 @@
"@material/mwc-textfield": "0.25.3", "@material/mwc-textfield": "0.25.3",
"@material/mwc-top-app-bar-fixed": "^0.25.3", "@material/mwc-top-app-bar-fixed": "^0.25.3",
"@material/top-app-bar": "14.0.0-canary.261f2db59.0", "@material/top-app-bar": "14.0.0-canary.261f2db59.0",
"@mdi/js": "6.9.96", "@mdi/js": "6.6.95",
"@mdi/svg": "6.9.96", "@mdi/svg": "6.6.95",
"@polymer/app-layout": "^3.1.0", "@polymer/app-layout": "^3.1.0",
"@polymer/iron-flex-layout": "^3.0.1", "@polymer/iron-flex-layout": "^3.0.1",
"@polymer/iron-icon": "^3.0.1", "@polymer/iron-icon": "^3.0.1",
@@ -89,8 +89,8 @@
"@polymer/paper-tooltip": "^3.0.1", "@polymer/paper-tooltip": "^3.0.1",
"@polymer/polymer": "3.4.1", "@polymer/polymer": "3.4.1",
"@thomasloven/round-slider": "0.5.4", "@thomasloven/round-slider": "0.5.4",
"@vaadin/combo-box": "^23.0.10", "@vaadin/combo-box": "^22.0.4",
"@vaadin/vaadin-themable-mixin": "^23.0.10", "@vaadin/vaadin-themable-mixin": "^22.0.4",
"@vibrant/color": "^3.2.1-alpha.1", "@vibrant/color": "^3.2.1-alpha.1",
"@vibrant/core": "^3.2.1-alpha.1", "@vibrant/core": "^3.2.1-alpha.1",
"@vibrant/quantizer-mmcq": "^3.2.1-alpha.1", "@vibrant/quantizer-mmcq": "^3.2.1-alpha.1",
@@ -106,9 +106,10 @@
"deep-clone-simple": "^1.1.1", "deep-clone-simple": "^1.1.1",
"deep-freeze": "^0.0.1", "deep-freeze": "^0.0.1",
"fuse.js": "^6.0.0", "fuse.js": "^6.0.0",
"fuzzysort": "^1.2.1",
"google-timezones-json": "^1.0.2", "google-timezones-json": "^1.0.2",
"hls.js": "^1.1.5", "hls.js": "^1.1.5",
"home-assistant-js-websocket": "^7.1.0", "home-assistant-js-websocket": "^7.0.3",
"idb-keyval": "^5.1.3", "idb-keyval": "^5.1.3",
"intl-messageformat": "^9.9.1", "intl-messageformat": "^9.9.1",
"js-yaml": "^4.1.0", "js-yaml": "^4.1.0",

View File

@@ -1,30 +1,3 @@
[build-system] [build-system]
requires = ["setuptools~=62.3", "wheel~=0.37.1"] requires = ["setuptools~=60.5", "wheel~=0.37.1"]
build-backend = "setuptools.build_meta" build-backend = "setuptools.build_meta"
[project]
name = "home-assistant-frontend"
version = "20220705.0"
license = {text = "Apache-2.0"}
description = "The Home Assistant frontend"
readme = "README.md"
authors = [
{name = "The Home Assistant Authors", email = "hello@home-assistant.io"}
]
requires-python = ">=3.4.0"
[project.urls]
"Homepage" = "https://github.com/home-assistant/frontend"
[tool.setuptools]
platforms = ["any"]
zip-safe = false
include-package-data = true
[tool.setuptools.packages.find]
include = ["hass_frontend*"]
[tool.mypy]
python_version = 3.4
show_error_codes = true
strict = true

View File

@@ -24,15 +24,10 @@ function auto(version) {
return patch(version); return patch(version);
} }
function nightly() {
return `${today()}.dev`;
}
const methods = { const methods = {
patch, patch,
today, today,
auto, auto,
nightly,
}; };
async function main(args) { async function main(args) {
@@ -55,18 +50,14 @@ async function main(args) {
return; return;
} }
const setup = fs.readFileSync("pyproject.toml", "utf8"); const setup = fs.readFileSync("setup.cfg", "utf8");
const version = setup.match(/version\W+=\W"(\d{8}\.\d)"/)[1]; const version = setup.match(/\d{8}\.\d+/)[0];
const newVersion = method(version); const newVersion = method(version);
console.log("Current version:", version); console.log("Current version:", version);
console.log("New version:", newVersion); console.log("New version:", newVersion);
fs.writeFileSync( fs.writeFileSync("setup.cfg", setup.replace(version, newVersion), "utf-8");
"pyproject.toml",
setup.replace(version, newVersion),
"utf-8"
);
if (!commit) { if (!commit) {
return; return;

View File

@@ -1,2 +1,26 @@
# Setuptools v62.3 doesn't support editable installs with just 'pyproject.toml' (PEP 660). [metadata]
# Keep this file until it does! name = home-assistant-frontend
version = 20220502.0
author = The Home Assistant Authors
author_email = hello@home-assistant.io
license = Apache-2.0
platforms = any
description = The Home Assistant frontend
long_description = file: README.md
long_description_content_type = text/markdown
url = https://github.com/home-assistant/frontend
[options]
packages = find:
zip_safe = False
include_package_data = True
python_requires = >= 3.4.0
[options.packages.find]
include =
hass_frontend*
[mypy]
python_version = 3.4
show_error_codes = True
strict = True

View File

@@ -1,41 +0,0 @@
const DEFAULT_OWN = true;
// Finds the closest ancestor of an element that has a specific optionally owned property,
// traversing slot and shadow root boundaries until the body element is reached
export const closestWithProperty = (
element: Element | null,
property: string | symbol,
own = DEFAULT_OWN
) => {
if (!element || element === document.body) return null;
element = element.assignedSlot ?? element;
if (element.parentElement) {
element = element.parentElement;
} else {
const root = element.getRootNode();
element = root instanceof ShadowRoot ? root.host : null;
}
if (
own
? Object.prototype.hasOwnProperty.call(element, property)
: element && property in element
)
return element;
return closestWithProperty(element, property, own);
};
// Finds the set of all such ancestors and includes starting element as first in the set
export const ancestorsWithProperty = (
element: Element | null,
property: string | symbol,
own = DEFAULT_OWN
) => {
const ancestors: Set<Element> = new Set();
while (element) {
ancestors.add(element);
element = closestWithProperty(element, property, own);
}
return ancestors;
};

View File

@@ -1,11 +1,6 @@
import { HassEntity } from "home-assistant-js-websocket"; import { HassEntity } from "home-assistant-js-websocket";
import { UNAVAILABLE_STATES } from "../../data/entity";
export const computeActiveState = (stateObj: HassEntity): string => { export const computeActiveState = (stateObj: HassEntity): string => {
if (UNAVAILABLE_STATES.includes(stateObj.state)) {
return stateObj.state;
}
const domain = stateObj.entity_id.split(".")[0]; const domain = stateObj.entity_id.split(".")[0];
let state = stateObj.state; let state = stateObj.state;

View File

@@ -2,74 +2,67 @@ import { HassEntity } from "home-assistant-js-websocket";
import { UNAVAILABLE, UNKNOWN } from "../../data/entity"; import { UNAVAILABLE, UNKNOWN } from "../../data/entity";
import { FrontendLocaleData } from "../../data/translation"; import { FrontendLocaleData } from "../../data/translation";
import { import {
updateIsInstalling,
UpdateEntity,
UPDATE_SUPPORT_PROGRESS, UPDATE_SUPPORT_PROGRESS,
updateIsInstallingFromAttributes,
} from "../../data/update"; } from "../../data/update";
import { formatDate } from "../datetime/format_date"; import { formatDate } from "../datetime/format_date";
import { formatDateTime } from "../datetime/format_date_time"; import { formatDateTime } from "../datetime/format_date_time";
import { formatTime } from "../datetime/format_time"; import { formatTime } from "../datetime/format_time";
import { formatNumber, isNumericFromAttributes } from "../number/format_number"; import { formatNumber, isNumericState } from "../number/format_number";
import { LocalizeFunc } from "../translations/localize"; import { LocalizeFunc } from "../translations/localize";
import { supportsFeatureFromAttributes } from "./supports-feature"; import { computeStateDomain } from "./compute_state_domain";
import { supportsFeature } from "./supports-feature";
import { formatDuration, UNIT_TO_SECOND_CONVERT } from "../datetime/duration"; import { formatDuration, UNIT_TO_SECOND_CONVERT } from "../datetime/duration";
import { computeDomain } from "./compute_domain";
export const computeStateDisplay = ( export const computeStateDisplay = (
localize: LocalizeFunc, localize: LocalizeFunc,
stateObj: HassEntity, stateObj: HassEntity,
locale: FrontendLocaleData, locale: FrontendLocaleData,
state?: string state?: string
): string =>
computeStateDisplayFromEntityAttributes(
localize,
locale,
stateObj.entity_id,
stateObj.attributes,
state !== undefined ? state : stateObj.state
);
export const computeStateDisplayFromEntityAttributes = (
localize: LocalizeFunc,
locale: FrontendLocaleData,
entityId: string,
attributes: any,
state: string
): string => { ): string => {
if (state === UNKNOWN || state === UNAVAILABLE) { const compareState = state !== undefined ? state : stateObj.state;
return localize(`state.default.${state}`);
if (compareState === UNKNOWN || compareState === UNAVAILABLE) {
return localize(`state.default.${compareState}`);
} }
// Entities with a `unit_of_measurement` or `state_class` are numeric values and should use `formatNumber` // Entities with a `unit_of_measurement` or `state_class` are numeric values and should use `formatNumber`
if (isNumericFromAttributes(attributes)) { if (isNumericState(stateObj)) {
// state is duration // state is duration
if ( if (
attributes.device_class === "duration" && stateObj.attributes.device_class === "duration" &&
attributes.unit_of_measurement && stateObj.attributes.unit_of_measurement &&
UNIT_TO_SECOND_CONVERT[attributes.unit_of_measurement] UNIT_TO_SECOND_CONVERT[stateObj.attributes.unit_of_measurement]
) { ) {
try { try {
return formatDuration(state, attributes.unit_of_measurement); return formatDuration(
compareState,
stateObj.attributes.unit_of_measurement
);
} catch (_err) { } catch (_err) {
// fallback to default // fallback to default
} }
} }
if (attributes.device_class === "monetary") { if (stateObj.attributes.device_class === "monetary") {
try { try {
return formatNumber(state, locale, { return formatNumber(compareState, locale, {
style: "currency", style: "currency",
currency: attributes.unit_of_measurement, currency: stateObj.attributes.unit_of_measurement,
minimumFractionDigits: 2, minimumFractionDigits: 2,
}); });
} catch (_err) { } catch (_err) {
// fallback to default // fallback to default
} }
} }
return `${formatNumber(state, locale)}${ return `${formatNumber(compareState, locale)}${
attributes.unit_of_measurement ? " " + attributes.unit_of_measurement : "" stateObj.attributes.unit_of_measurement
? " " + stateObj.attributes.unit_of_measurement
: ""
}`; }`;
} }
const domain = computeDomain(entityId); const domain = computeStateDomain(stateObj);
if (domain === "input_datetime") { if (domain === "input_datetime") {
if (state !== undefined) { if (state !== undefined) {
@@ -104,32 +97,36 @@ export const computeStateDisplayFromEntityAttributes = (
} else { } else {
// If not trying to display an explicit state, create `Date` object from `stateObj`'s attributes then format. // If not trying to display an explicit state, create `Date` object from `stateObj`'s attributes then format.
let date: Date; let date: Date;
if (attributes.has_date && attributes.has_time) { if (stateObj.attributes.has_date && stateObj.attributes.has_time) {
date = new Date( date = new Date(
attributes.year, stateObj.attributes.year,
attributes.month - 1, stateObj.attributes.month - 1,
attributes.day, stateObj.attributes.day,
attributes.hour, stateObj.attributes.hour,
attributes.minute stateObj.attributes.minute
); );
return formatDateTime(date, locale); return formatDateTime(date, locale);
} }
if (attributes.has_date) { if (stateObj.attributes.has_date) {
date = new Date(attributes.year, attributes.month - 1, attributes.day); date = new Date(
stateObj.attributes.year,
stateObj.attributes.month - 1,
stateObj.attributes.day
);
return formatDate(date, locale); return formatDate(date, locale);
} }
if (attributes.has_time) { if (stateObj.attributes.has_time) {
date = new Date(); date = new Date();
date.setHours(attributes.hour, attributes.minute); date.setHours(stateObj.attributes.hour, stateObj.attributes.minute);
return formatTime(date, locale); return formatTime(date, locale);
} }
return state; return stateObj.state;
} }
} }
if (domain === "humidifier") { if (domain === "humidifier") {
if (state === "on" && attributes.humidity) { if (compareState === "on" && stateObj.attributes.humidity) {
return `${attributes.humidity} %`; return `${stateObj.attributes.humidity} %`;
} }
} }
@@ -139,7 +136,7 @@ export const computeStateDisplayFromEntityAttributes = (
domain === "number" || domain === "number" ||
domain === "input_number" domain === "input_number"
) { ) {
return formatNumber(state, locale); return formatNumber(compareState, locale);
} }
// state of button is a timestamp // state of button is a timestamp
@@ -147,12 +144,12 @@ export const computeStateDisplayFromEntityAttributes = (
domain === "button" || domain === "button" ||
domain === "input_button" || domain === "input_button" ||
domain === "scene" || domain === "scene" ||
(domain === "sensor" && attributes.device_class === "timestamp") (domain === "sensor" && stateObj.attributes.device_class === "timestamp")
) { ) {
try { try {
return formatDateTime(new Date(state), locale); return formatDateTime(new Date(compareState), locale);
} catch (_err) { } catch (_err) {
return state; return compareState;
} }
} }
@@ -163,28 +160,30 @@ export const computeStateDisplayFromEntityAttributes = (
// When the latest version is skipped, show the latest version // When the latest version is skipped, show the latest version
// When update is not available, show "Up-to-date" // When update is not available, show "Up-to-date"
// When update is not available and there is no latest_version show "Unavailable" // When update is not available and there is no latest_version show "Unavailable"
return state === "on" return compareState === "on"
? updateIsInstallingFromAttributes(attributes) ? updateIsInstalling(stateObj as UpdateEntity)
? supportsFeatureFromAttributes(attributes, UPDATE_SUPPORT_PROGRESS) ? supportsFeature(stateObj, UPDATE_SUPPORT_PROGRESS)
? localize("ui.card.update.installing_with_progress", { ? localize("ui.card.update.installing_with_progress", {
progress: attributes.in_progress, progress: stateObj.attributes.in_progress,
}) })
: localize("ui.card.update.installing") : localize("ui.card.update.installing")
: attributes.latest_version : stateObj.attributes.latest_version
: attributes.skipped_version === attributes.latest_version : stateObj.attributes.skipped_version ===
? attributes.latest_version ?? localize("state.default.unavailable") stateObj.attributes.latest_version
? stateObj.attributes.latest_version ??
localize("state.default.unavailable")
: localize("ui.card.update.up_to_date"); : localize("ui.card.update.up_to_date");
} }
return ( return (
// Return device class translation // Return device class translation
(attributes.device_class && (stateObj.attributes.device_class &&
localize( localize(
`component.${domain}.state.${attributes.device_class}.${state}` `component.${domain}.state.${stateObj.attributes.device_class}.${compareState}`
)) || )) ||
// Return default translation // Return default translation
localize(`component.${domain}.state._.${state}`) || localize(`component.${domain}.state._.${compareState}`) ||
// We don't know! Return the raw state. // We don't know! Return the raw state.
state compareState
); );
}; };

View File

@@ -1,13 +1,7 @@
import { HassEntity } from "home-assistant-js-websocket"; import { HassEntity } from "home-assistant-js-websocket";
import { computeObjectId } from "./compute_object_id"; import { computeObjectId } from "./compute_object_id";
export const computeStateNameFromEntityAttributes = (
entityId: string,
attributes: { [key: string]: any }
): string =>
attributes.friendly_name === undefined
? computeObjectId(entityId).replace(/_/g, " ")
: attributes.friendly_name || "";
export const computeStateName = (stateObj: HassEntity): string => export const computeStateName = (stateObj: HassEntity): string =>
computeStateNameFromEntityAttributes(stateObj.entity_id, stateObj.attributes); stateObj.attributes.friendly_name === undefined
? computeObjectId(stateObj.entity_id).replace(/_/g, " ")
: stateObj.attributes.friendly_name || "";

View File

@@ -8,7 +8,6 @@ import {
mdiCalendar, mdiCalendar,
mdiCast, mdiCast,
mdiCastConnected, mdiCastConnected,
mdiChartSankey,
mdiCheckCircleOutline, mdiCheckCircleOutline,
mdiClock, mdiClock,
mdiCloseCircleOutline, mdiCloseCircleOutline,
@@ -25,14 +24,12 @@ import {
mdiPowerPlug, mdiPowerPlug,
mdiPowerPlugOff, mdiPowerPlugOff,
mdiRestart, mdiRestart,
mdiSwapHorizontal,
mdiToggleSwitchVariant, mdiToggleSwitchVariant,
mdiToggleSwitchVariantOff, mdiToggleSwitchVariantOff,
mdiWeatherNight, mdiWeatherNight,
} from "@mdi/js"; } from "@mdi/js";
import { HassEntity } from "home-assistant-js-websocket"; import { HassEntity } from "home-assistant-js-websocket";
import { UpdateEntity, updateIsInstalling } from "../../data/update"; import { updateIsInstalling, UpdateEntity } from "../../data/update";
import { weatherIcon } from "../../data/weather";
/** /**
* Return the icon to be used for a domain. * Return the icon to be used for a domain.
* *
@@ -49,20 +46,6 @@ export const domainIcon = (
stateObj?: HassEntity, stateObj?: HassEntity,
state?: string state?: string
): string => { ): string => {
const icon = domainIconWithoutDefault(domain, stateObj, state);
if (icon) {
return icon;
}
// eslint-disable-next-line
console.warn(`Unable to find icon for domain ${domain}`);
return DEFAULT_DOMAIN_ICON;
};
export const domainIconWithoutDefault = (
domain: string,
stateObj?: HassEntity,
state?: string
): string | undefined => {
const compareState = state !== undefined ? state : stateObj?.state; const compareState = state !== undefined ? state : stateObj?.state;
switch (domain) { switch (domain) {
@@ -104,15 +87,6 @@ export const domainIconWithoutDefault = (
? mdiCheckCircleOutline ? mdiCheckCircleOutline
: mdiCloseCircleOutline; : mdiCloseCircleOutline;
case "input_datetime":
if (!stateObj?.attributes.has_date) {
return mdiClock;
}
if (!stateObj.attributes.has_time) {
return mdiCalendar;
}
break;
case "lock": case "lock":
switch (compareState) { switch (compareState) {
case "unlocked": case "unlocked":
@@ -150,31 +124,33 @@ export const domainIconWithoutDefault = (
break; break;
} }
case "input_datetime":
if (!stateObj?.attributes.has_date) {
return mdiClock;
}
if (!stateObj.attributes.has_time) {
return mdiCalendar;
}
break;
case "sun": case "sun":
return stateObj?.state === "above_horizon" return stateObj?.state === "above_horizon"
? FIXED_DOMAIN_ICONS[domain] ? FIXED_DOMAIN_ICONS[domain]
: mdiWeatherNight; : mdiWeatherNight;
case "switch_as_x":
return mdiSwapHorizontal;
case "threshold":
return mdiChartSankey;
case "update": case "update":
return compareState === "on" return compareState === "on"
? updateIsInstalling(stateObj as UpdateEntity) ? updateIsInstalling(stateObj as UpdateEntity)
? mdiPackageDown ? mdiPackageDown
: mdiPackageUp : mdiPackageUp
: mdiPackage; : mdiPackage;
case "weather":
return weatherIcon(stateObj?.state);
} }
if (domain in FIXED_DOMAIN_ICONS) { if (domain in FIXED_DOMAIN_ICONS) {
return FIXED_DOMAIN_ICONS[domain]; return FIXED_DOMAIN_ICONS[domain];
} }
return undefined; // eslint-disable-next-line
console.warn(`Unable to find icon for domain ${domain}`);
return DEFAULT_DOMAIN_ICON;
}; };

View File

@@ -3,13 +3,6 @@ import { HassEntity } from "home-assistant-js-websocket";
export const supportsFeature = ( export const supportsFeature = (
stateObj: HassEntity, stateObj: HassEntity,
feature: number feature: number
): boolean => supportsFeatureFromAttributes(stateObj.attributes, feature);
export const supportsFeatureFromAttributes = (
attributes: {
[key: string]: any;
},
feature: number
): boolean => ): boolean =>
// eslint-disable-next-line no-bitwise // eslint-disable-next-line no-bitwise
(attributes.supported_features! & feature) !== 0; (stateObj.attributes.supported_features! & feature) !== 0;

View File

@@ -5,6 +5,6 @@ export const clamp = (value: number, min: number, max: number) =>
export const conditionalClamp = (value: number, min?: number, max?: number) => { export const conditionalClamp = (value: number, min?: number, max?: number) => {
let result: number; let result: number;
result = min ? Math.max(value, min) : value; result = min ? Math.max(value, min) : value;
result = max ? Math.min(result, max) : result; result = max ? Math.min(value, max) : value;
return result; return result;
}; };

View File

@@ -7,11 +7,8 @@ import { round } from "./round";
* @param stateObj The entity state object * @param stateObj The entity state object
*/ */
export const isNumericState = (stateObj: HassEntity): boolean => export const isNumericState = (stateObj: HassEntity): boolean =>
isNumericFromAttributes(stateObj.attributes); !!stateObj.attributes.unit_of_measurement ||
!!stateObj.attributes.state_class;
export const isNumericFromAttributes = (attributes: {
[key: string]: any;
}): boolean => !!attributes.unit_of_measurement || !!attributes.state_class;
export const numberFormatToLocale = ( export const numberFormatToLocale = (
localeOptions: FrontendLocaleData localeOptions: FrontendLocaleData

View File

@@ -1,244 +0,0 @@
// MIT License
// Copyright (c) 2015 - present Microsoft Corporation
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
// Names from https://blog.codinghorror.com/ascii-pronunciation-rules-for-programmers/
/**
* An inlined enum containing useful character codes (to be used with String.charCodeAt).
* Please leave the const keyword such that it gets inlined when compiled to JavaScript!
*/
export enum CharCode {
Null = 0,
/**
* The `\b` character.
*/
Backspace = 8,
/**
* The `\t` character.
*/
Tab = 9,
/**
* The `\n` character.
*/
LineFeed = 10,
/**
* The `\r` character.
*/
CarriageReturn = 13,
Space = 32,
/**
* The `!` character.
*/
ExclamationMark = 33,
/**
* The `"` character.
*/
DoubleQuote = 34,
/**
* The `#` character.
*/
Hash = 35,
/**
* The `$` character.
*/
DollarSign = 36,
/**
* The `%` character.
*/
PercentSign = 37,
/**
* The `&` character.
*/
Ampersand = 38,
/**
* The `'` character.
*/
SingleQuote = 39,
/**
* The `(` character.
*/
OpenParen = 40,
/**
* The `)` character.
*/
CloseParen = 41,
/**
* The `*` character.
*/
Asterisk = 42,
/**
* The `+` character.
*/
Plus = 43,
/**
* The `,` character.
*/
Comma = 44,
/**
* The `-` character.
*/
Dash = 45,
/**
* The `.` character.
*/
Period = 46,
/**
* The `/` character.
*/
Slash = 47,
Digit0 = 48,
Digit1 = 49,
Digit2 = 50,
Digit3 = 51,
Digit4 = 52,
Digit5 = 53,
Digit6 = 54,
Digit7 = 55,
Digit8 = 56,
Digit9 = 57,
/**
* The `:` character.
*/
Colon = 58,
/**
* The `;` character.
*/
Semicolon = 59,
/**
* The `<` character.
*/
LessThan = 60,
/**
* The `=` character.
*/
Equals = 61,
/**
* The `>` character.
*/
GreaterThan = 62,
/**
* The `?` character.
*/
QuestionMark = 63,
/**
* The `@` character.
*/
AtSign = 64,
A = 65,
B = 66,
C = 67,
D = 68,
E = 69,
F = 70,
G = 71,
H = 72,
I = 73,
J = 74,
K = 75,
L = 76,
M = 77,
N = 78,
O = 79,
P = 80,
Q = 81,
R = 82,
S = 83,
T = 84,
U = 85,
V = 86,
W = 87,
X = 88,
Y = 89,
Z = 90,
/**
* The `[` character.
*/
OpenSquareBracket = 91,
/**
* The `\` character.
*/
Backslash = 92,
/**
* The `]` character.
*/
CloseSquareBracket = 93,
/**
* The `^` character.
*/
Caret = 94,
/**
* The `_` character.
*/
Underline = 95,
/**
* The ``(`)`` character.
*/
BackTick = 96,
a = 97,
b = 98,
c = 99,
d = 100,
e = 101,
f = 102,
g = 103,
h = 104,
i = 105,
j = 106,
k = 107,
l = 108,
m = 109,
n = 110,
o = 111,
p = 112,
q = 113,
r = 114,
s = 115,
t = 116,
u = 117,
v = 118,
w = 119,
x = 120,
y = 121,
z = 122,
/**
* The `{` character.
*/
OpenCurlyBrace = 123,
/**
* The `|` character.
*/
Pipe = 124,
/**
* The `}` character.
*/
CloseCurlyBrace = 125,
/**
* The `~` character.
*/
Tilde = 126,
}

View File

@@ -1,551 +0,0 @@
/* eslint-disable no-console */
// MIT License
// Copyright (c) 2015 - present Microsoft Corporation
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
import { CharCode } from "./char-code";
const _debug = false;
export interface Match {
start: number;
end: number;
}
const _maxLen = 128;
function initTable() {
const table: number[][] = [];
const row: number[] = [];
for (let i = 0; i <= _maxLen; i++) {
row[i] = 0;
}
for (let i = 0; i <= _maxLen; i++) {
table.push(row.slice(0));
}
return table;
}
function isSeparatorAtPos(value: string, index: number): boolean {
if (index < 0 || index >= value.length) {
return false;
}
const code = value.codePointAt(index);
switch (code) {
case CharCode.Underline:
case CharCode.Dash:
case CharCode.Period:
case CharCode.Space:
case CharCode.Slash:
case CharCode.Backslash:
case CharCode.SingleQuote:
case CharCode.DoubleQuote:
case CharCode.Colon:
case CharCode.DollarSign:
case CharCode.LessThan:
case CharCode.OpenParen:
case CharCode.OpenSquareBracket:
return true;
case undefined:
return false;
default:
if (isEmojiImprecise(code)) {
return true;
}
return false;
}
}
function isWhitespaceAtPos(value: string, index: number): boolean {
if (index < 0 || index >= value.length) {
return false;
}
const code = value.charCodeAt(index);
switch (code) {
case CharCode.Space:
case CharCode.Tab:
return true;
default:
return false;
}
}
function isUpperCaseAtPos(pos: number, word: string, wordLow: string): boolean {
return word[pos] !== wordLow[pos];
}
export function isPatternInWord(
patternLow: string,
patternPos: number,
patternLen: number,
wordLow: string,
wordPos: number,
wordLen: number,
fillMinWordPosArr = false
): boolean {
while (patternPos < patternLen && wordPos < wordLen) {
if (patternLow[patternPos] === wordLow[wordPos]) {
if (fillMinWordPosArr) {
// Remember the min word position for each pattern position
_minWordMatchPos[patternPos] = wordPos;
}
patternPos += 1;
}
wordPos += 1;
}
return patternPos === patternLen; // pattern must be exhausted
}
enum Arrow {
Diag = 1,
Left = 2,
LeftLeft = 3,
}
/**
* An array representing a fuzzy match.
*
* 0. the score
* 1. the offset at which matching started
* 2. `<match_pos_N>`
* 3. `<match_pos_1>`
* 4. `<match_pos_0>` etc
*/
// export type FuzzyScore = [score: number, wordStart: number, ...matches: number[]];// [number, number, number];
export type FuzzyScore = Array<number>;
export function fuzzyScore(
pattern: string,
patternLow: string,
patternStart: number,
word: string,
wordLow: string,
wordStart: number,
firstMatchCanBeWeak: boolean
): FuzzyScore | undefined {
const patternLen = pattern.length > _maxLen ? _maxLen : pattern.length;
const wordLen = word.length > _maxLen ? _maxLen : word.length;
if (
patternStart >= patternLen ||
wordStart >= wordLen ||
patternLen - patternStart > wordLen - wordStart
) {
return undefined;
}
// Run a simple check if the characters of pattern occur
// (in order) at all in word. If that isn't the case we
// stop because no match will be possible
if (
!isPatternInWord(
patternLow,
patternStart,
patternLen,
wordLow,
wordStart,
wordLen,
true
)
) {
return undefined;
}
// Find the max matching word position for each pattern position
// NOTE: the min matching word position was filled in above, in the `isPatternInWord` call
_fillInMaxWordMatchPos(
patternLen,
wordLen,
patternStart,
wordStart,
patternLow,
wordLow
);
let row: number;
let column = 1;
let patternPos: number;
let wordPos: number;
const hasStrongFirstMatch = [false];
// There will be a match, fill in tables
for (
row = 1, patternPos = patternStart;
patternPos < patternLen;
row++, patternPos++
) {
// Reduce search space to possible matching word positions and to possible access from next row
const minWordMatchPos = _minWordMatchPos[patternPos];
const maxWordMatchPos = _maxWordMatchPos[patternPos];
const nextMaxWordMatchPos =
patternPos + 1 < patternLen ? _maxWordMatchPos[patternPos + 1] : wordLen;
for (
column = minWordMatchPos - wordStart + 1, wordPos = minWordMatchPos;
wordPos < nextMaxWordMatchPos;
column++, wordPos++
) {
let score = Number.MIN_SAFE_INTEGER;
let canComeDiag = false;
if (wordPos <= maxWordMatchPos) {
score = _doScore(
pattern,
patternLow,
patternPos,
patternStart,
word,
wordLow,
wordPos,
wordLen,
wordStart,
_diag[row - 1][column - 1] === 0,
hasStrongFirstMatch
);
}
let diagScore = 0;
if (score !== Number.MAX_SAFE_INTEGER) {
canComeDiag = true;
diagScore = score + _table[row - 1][column - 1];
}
const canComeLeft = wordPos > minWordMatchPos;
const leftScore = canComeLeft
? _table[row][column - 1] + (_diag[row][column - 1] > 0 ? -5 : 0)
: 0; // penalty for a gap start
const canComeLeftLeft =
wordPos > minWordMatchPos + 1 && _diag[row][column - 1] > 0;
const leftLeftScore = canComeLeftLeft
? _table[row][column - 2] + (_diag[row][column - 2] > 0 ? -5 : 0)
: 0; // penalty for a gap start
if (
canComeLeftLeft &&
(!canComeLeft || leftLeftScore >= leftScore) &&
(!canComeDiag || leftLeftScore >= diagScore)
) {
// always prefer choosing left left to jump over a diagonal because that means a match is earlier in the word
_table[row][column] = leftLeftScore;
_arrows[row][column] = Arrow.LeftLeft;
_diag[row][column] = 0;
} else if (canComeLeft && (!canComeDiag || leftScore >= diagScore)) {
// always prefer choosing left since that means a match is earlier in the word
_table[row][column] = leftScore;
_arrows[row][column] = Arrow.Left;
_diag[row][column] = 0;
} else if (canComeDiag) {
_table[row][column] = diagScore;
_arrows[row][column] = Arrow.Diag;
_diag[row][column] = _diag[row - 1][column - 1] + 1;
} else {
throw new Error(`not possible`);
}
}
}
if (_debug) {
printTables(pattern, patternStart, word, wordStart);
}
if (!hasStrongFirstMatch[0] && !firstMatchCanBeWeak) {
return undefined;
}
row--;
column--;
const result: FuzzyScore = [_table[row][column], wordStart];
let backwardsDiagLength = 0;
let maxMatchColumn = 0;
while (row >= 1) {
// Find the column where we go diagonally up
let diagColumn = column;
do {
const arrow = _arrows[row][diagColumn];
if (arrow === Arrow.LeftLeft) {
diagColumn -= 2;
} else if (arrow === Arrow.Left) {
diagColumn -= 1;
} else {
// found the diagonal
break;
}
} while (diagColumn >= 1);
// Overturn the "forwards" decision if keeping the "backwards" diagonal would give a better match
if (
backwardsDiagLength > 1 && // only if we would have a contiguous match of 3 characters
patternLow[patternStart + row - 1] === wordLow[wordStart + column - 1] && // only if we can do a contiguous match diagonally
!isUpperCaseAtPos(diagColumn + wordStart - 1, word, wordLow) && // only if the forwards chose diagonal is not an uppercase
backwardsDiagLength + 1 > _diag[row][diagColumn] // only if our contiguous match would be longer than the "forwards" contiguous match
) {
diagColumn = column;
}
if (diagColumn === column) {
// this is a contiguous match
backwardsDiagLength++;
} else {
backwardsDiagLength = 1;
}
if (!maxMatchColumn) {
// remember the last matched column
maxMatchColumn = diagColumn;
}
row--;
column = diagColumn - 1;
result.push(column);
}
if (wordLen === patternLen) {
// the word matches the pattern with all characters!
// giving the score a total match boost (to come up ahead other words)
result[0] += 2;
}
// Add 1 penalty for each skipped character in the word
const skippedCharsCount = maxMatchColumn - patternLen;
result[0] -= skippedCharsCount;
return result;
}
function _doScore(
pattern: string,
patternLow: string,
patternPos: number,
patternStart: number,
word: string,
wordLow: string,
wordPos: number,
wordLen: number,
wordStart: number,
newMatchStart: boolean,
outFirstMatchStrong: boolean[]
): number {
if (patternLow[patternPos] !== wordLow[wordPos]) {
return Number.MIN_SAFE_INTEGER;
}
let score = 1;
let isGapLocation = false;
if (wordPos === patternPos - patternStart) {
// common prefix: `foobar <-> foobaz`
// ^^^^^
score = pattern[patternPos] === word[wordPos] ? 7 : 5;
} else if (
isUpperCaseAtPos(wordPos, word, wordLow) &&
(wordPos === 0 || !isUpperCaseAtPos(wordPos - 1, word, wordLow))
) {
// hitting upper-case: `foo <-> forOthers`
// ^^ ^
score = pattern[patternPos] === word[wordPos] ? 7 : 5;
isGapLocation = true;
} else if (
isSeparatorAtPos(wordLow, wordPos) &&
(wordPos === 0 || !isSeparatorAtPos(wordLow, wordPos - 1))
) {
// hitting a separator: `. <-> foo.bar`
// ^
score = 5;
} else if (
isSeparatorAtPos(wordLow, wordPos - 1) ||
isWhitespaceAtPos(wordLow, wordPos - 1)
) {
// post separator: `foo <-> bar_foo`
// ^^^
score = 5;
isGapLocation = true;
}
if (score > 1 && patternPos === patternStart) {
outFirstMatchStrong[0] = true;
}
if (!isGapLocation) {
isGapLocation =
isUpperCaseAtPos(wordPos, word, wordLow) ||
isSeparatorAtPos(wordLow, wordPos - 1) ||
isWhitespaceAtPos(wordLow, wordPos - 1);
}
//
if (patternPos === patternStart) {
// first character in pattern
if (wordPos > wordStart) {
// the first pattern character would match a word character that is not at the word start
// so introduce a penalty to account for the gap preceding this match
score -= isGapLocation ? 3 : 5;
}
} else if (newMatchStart) {
// this would be the beginning of a new match (i.e. there would be a gap before this location)
score += isGapLocation ? 2 : 0;
} else {
// this is part of a contiguous match, so give it a slight bonus, but do so only if it would not be a prefered gap location
score += isGapLocation ? 0 : 1;
}
if (wordPos + 1 === wordLen) {
// we always penalize gaps, but this gives unfair advantages to a match that would match the last character in the word
// so pretend there is a gap after the last character in the word to normalize things
score -= isGapLocation ? 3 : 5;
}
return score;
}
function printTable(
table: number[][],
pattern: string,
patternLen: number,
word: string,
wordLen: number
): string {
function pad(s: string, n: number, _pad = " ") {
while (s.length < n) {
s = _pad + s;
}
return s;
}
let ret = ` | |${word
.split("")
.map((c) => pad(c, 3))
.join("|")}\n`;
for (let i = 0; i <= patternLen; i++) {
if (i === 0) {
ret += " |";
} else {
ret += `${pattern[i - 1]}|`;
}
ret +=
table[i]
.slice(0, wordLen + 1)
.map((n) => pad(n.toString(), 3))
.join("|") + "\n";
}
return ret;
}
function printTables(
pattern: string,
patternStart: number,
word: string,
wordStart: number
): void {
pattern = pattern.substr(patternStart);
word = word.substr(wordStart);
console.log(printTable(_table, pattern, pattern.length, word, word.length));
console.log(printTable(_arrows, pattern, pattern.length, word, word.length));
console.log(printTable(_diag, pattern, pattern.length, word, word.length));
}
const _minWordMatchPos = initArr(2 * _maxLen); // min word position for a certain pattern position
const _maxWordMatchPos = initArr(2 * _maxLen); // max word position for a certain pattern position
const _diag = initTable(); // the length of a contiguous diagonal match
const _table = initTable();
const _arrows = <Arrow[][]>initTable();
function initArr(maxLen: number) {
const row: number[] = [];
for (let i = 0; i <= maxLen; i++) {
row[i] = 0;
}
return row;
}
function _fillInMaxWordMatchPos(
patternLen: number,
wordLen: number,
patternStart: number,
wordStart: number,
patternLow: string,
wordLow: string
) {
let patternPos = patternLen - 1;
let wordPos = wordLen - 1;
while (patternPos >= patternStart && wordPos >= wordStart) {
if (patternLow[patternPos] === wordLow[wordPos]) {
_maxWordMatchPos[patternPos] = wordPos;
patternPos--;
}
wordPos--;
}
}
export interface FuzzyScorer {
(
pattern: string,
lowPattern: string,
patternPos: number,
word: string,
lowWord: string,
wordPos: number,
firstMatchCanBeWeak: boolean
): FuzzyScore | undefined;
}
export function createMatches(score: undefined | FuzzyScore): Match[] {
if (typeof score === "undefined") {
return [];
}
const res: Match[] = [];
const wordPos = score[1];
for (let i = score.length - 1; i > 1; i--) {
const pos = score[i] + wordPos;
const last = res[res.length - 1];
if (last && last.end === pos) {
last.end = pos + 1;
} else {
res.push({ start: pos, end: pos + 1 });
}
}
return res;
}
/**
* A fast function (therefore imprecise) to check if code points are emojis.
* Generated using https://github.com/alexdima/unicode-utils/blob/master/generate-emoji-test.js
*/
export function isEmojiImprecise(x: number): boolean {
return (
(x >= 0x1f1e6 && x <= 0x1f1ff) ||
x === 8986 ||
x === 8987 ||
x === 9200 ||
x === 9203 ||
(x >= 9728 && x <= 10175) ||
x === 11088 ||
x === 11093 ||
(x >= 127744 && x <= 128591) ||
(x >= 128640 && x <= 128764) ||
(x >= 128992 && x <= 129003) ||
(x >= 129280 && x <= 129535) ||
(x >= 129648 && x <= 129750)
);
}

View File

@@ -1,52 +1,4 @@
import { fuzzyScore } from "./filter"; import fuzzysort from "fuzzysort";
/**
* Determine whether a sequence of letters exists in another string,
* in that order, allowing for skipping. Ex: "chdr" exists in "chandelier")
*
* @param {string} filter - Sequence of letters to check for
* @param {ScorableTextItem} item - Item against whose strings will be checked
*
* @return {number} Score representing how well the word matches the filter. Return of 0 means no match.
*/
export const fuzzySequentialMatch = (
filter: string,
item: ScorableTextItem
) => {
let topScore = Number.NEGATIVE_INFINITY;
for (const word of item.strings) {
const scores = fuzzyScore(
filter,
filter.toLowerCase(),
0,
word,
word.toLowerCase(),
0,
true
);
if (!scores) {
continue;
}
// The VS Code implementation of filter returns a 0 for a weak match.
// But if .filter() sees a "0", it considers that a failed match and will remove it.
// So, we set score to 1 in these cases so the match will be included, and mostly respect correct ordering.
const score = scores[0] === 0 ? 1 : scores[0];
if (score > topScore) {
topScore = score;
}
}
if (topScore === Number.NEGATIVE_INFINITY) {
return undefined;
}
return topScore;
};
/** /**
* An interface that objects must extend in order to use the fuzzy sequence matcher * An interface that objects must extend in order to use the fuzzy sequence matcher
@@ -66,18 +18,48 @@ export interface ScorableTextItem {
strings: string[]; strings: string[];
} }
type FuzzyFilterSort = <T extends ScorableTextItem>( export type FuzzyFilterSort = <T extends ScorableTextItem>(
filter: string, filter: string,
items: T[] items: T[]
) => T[]; ) => T[];
export const fuzzyFilterSort: FuzzyFilterSort = (filter, items) => export function fuzzyMatcher(search: string | null): (string) => boolean {
items const scorer = fuzzyScorer(search);
return (value: string) => scorer([value]) !== Number.NEGATIVE_INFINITY;
}
export function fuzzyScorer(
search: string | null
): (values: string[]) => number {
const searchTerms = (search || "").match(/("[^"]+"|[^"\s]+)/g);
if (!searchTerms) {
return () => 0;
}
return (values) =>
searchTerms
.map((term) => {
const resultsForTerm = fuzzysort.go(term, values, {
allowTypo: true,
});
if (resultsForTerm.length > 0) {
return Math.max(...resultsForTerm.map((result) => result.score));
}
return Number.NEGATIVE_INFINITY;
})
.reduce((partial, current) => partial + current, 0);
}
export const fuzzySortFilterSort: FuzzyFilterSort = (filter, items) => {
const scorer = fuzzyScorer(filter);
return items
.map((item) => { .map((item) => {
item.score = fuzzySequentialMatch(filter, item); item.score = scorer(item.strings);
return item; return item;
}) })
.filter((item) => item.score !== undefined) .filter((item) => item.score !== undefined && item.score > -100000)
.sort(({ score: scoreA = 0 }, { score: scoreB = 0 }) => .sort(({ score: scoreA = 0 }, { score: scoreB = 0 }) =>
scoreA > scoreB ? -1 : scoreA < scoreB ? 1 : 0 scoreA > scoreB ? -1 : scoreA < scoreB ? 1 : 0
); );
};
export const defaultFuzzyFilterSort = fuzzySortFilterSort;

View File

@@ -70,9 +70,7 @@ 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 */ /* Color the icon if unavailable */
ha-state-icon[data-state="unavailable"] { ha-state-icon[data-state="unavailable"] {

View File

@@ -1,4 +1,3 @@
import { LitElement } from "lit";
import { HomeAssistant } from "../../types"; import { HomeAssistant } from "../../types";
export function computeRTL(hass: HomeAssistant) { export function computeRTL(hass: HomeAssistant) {
@@ -16,21 +15,3 @@ export function computeRTLDirection(hass: HomeAssistant) {
export function emitRTLDirection(rtl: boolean) { export function emitRTLDirection(rtl: boolean) {
return rtl ? "rtl" : "ltr"; 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"
);
}

View File

@@ -13,7 +13,7 @@ export const throttle = <T extends any[]>(
) => { ) => {
let timeout: number | undefined; let timeout: number | undefined;
let previous = 0; let previous = 0;
const throttledFunc = (...args: T): void => { return (...args: T): void => {
const later = () => { const later = () => {
previous = leading === false ? 0 : Date.now(); previous = leading === false ? 0 : Date.now();
timeout = undefined; timeout = undefined;
@@ -35,10 +35,4 @@ export const throttle = <T extends any[]>(
timeout = window.setTimeout(later, remaining); timeout = window.setTimeout(later, remaining);
} }
}; };
throttledFunc.cancel = () => {
clearTimeout(timeout);
timeout = undefined;
previous = 0;
};
return throttledFunc;
}; };

View File

@@ -34,7 +34,7 @@ import {
endOfMonth, endOfMonth,
endOfQuarter, endOfQuarter,
endOfYear, endOfYear,
} from "date-fns/esm"; } from "date-fns";
import { import {
formatDate, formatDate,
formatDateMonth, formatDateMonth,

View File

@@ -11,8 +11,6 @@ import { classMap } from "lit/directives/class-map";
import { styleMap } from "lit/directives/style-map"; import { styleMap } from "lit/directives/style-map";
import { clamp } from "../../common/number/clamp"; import { clamp } from "../../common/number/clamp";
export const MIN_TIME_BETWEEN_UPDATES = 60 * 5 * 1000;
interface Tooltip extends TooltipModel<any> { interface Tooltip extends TooltipModel<any> {
top: string; top: string;
left: string; left: string;
@@ -39,26 +37,6 @@ export default class HaChartBase extends LitElement {
@state() private _hiddenDatasets: Set<number> = new Set(); @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() { protected firstUpdated() {
this._setupChart(); this._setupChart();
this.data.datasets.forEach((dataset, index) => { this.data.datasets.forEach((dataset, index) => {
@@ -326,9 +304,6 @@ export default class HaChartBase extends LitElement {
width: 16px; width: 16px;
flex-shrink: 0; flex-shrink: 0;
box-sizing: border-box; box-sizing: border-box;
margin-inline-end: 6px;
margin-inline-start: initial;
direction: var(--direction);
} }
.chartTooltip .bullet { .chartTooltip .bullet {
align-self: baseline; align-self: baseline;
@@ -337,9 +312,6 @@ export default class HaChartBase extends LitElement {
:host([rtl]) .chartTooltip .bullet { :host([rtl]) .chartTooltip .bullet {
margin-right: inherit; margin-right: inherit;
margin-left: 6px; margin-left: 6px;
margin-inline-end: inherit;
margin-inline-start: 6px;
direction: var(--direction);
} }
.chartTooltip { .chartTooltip {
padding: 8px; padding: 8px;

View File

@@ -8,7 +8,7 @@ import {
} from "../../common/number/format_number"; } from "../../common/number/format_number";
import { LineChartEntity, LineChartState } from "../../data/history"; import { LineChartEntity, LineChartState } from "../../data/history";
import { HomeAssistant } from "../../types"; import { HomeAssistant } from "../../types";
import { MIN_TIME_BETWEEN_UPDATES } from "./ha-chart-base"; import "./ha-chart-base";
const safeParseFloat = (value) => { const safeParseFloat = (value) => {
const parsed = parseFloat(value); const parsed = parseFloat(value);
@@ -28,13 +28,11 @@ class StateHistoryChartLine extends LitElement {
@property({ type: Boolean }) public isSingleDevice = false; @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 _chartData?: ChartData<"line">;
@state() private _chartOptions?: ChartOptions; @state() private _chartOptions?: ChartOptions<"line">;
private _chartTime: Date = new Date();
protected render() { protected render() {
return html` return html`
@@ -59,7 +57,6 @@ class StateHistoryChartLine extends LitElement {
locale: this.hass.locale, locale: this.hass.locale,
}, },
}, },
suggestedMax: this.endTime,
ticks: { ticks: {
maxRotation: 0, maxRotation: 0,
sampleSize: 5, sampleSize: 5,
@@ -123,13 +120,7 @@ class StateHistoryChartLine extends LitElement {
locale: numberFormatToLocale(this.hass.locale), locale: numberFormatToLocale(this.hass.locale),
}; };
} }
if ( if (changedProps.has("data")) {
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(); this._generateData();
} }
} }
@@ -139,12 +130,28 @@ class StateHistoryChartLine extends LitElement {
const computedStyles = getComputedStyle(this); const computedStyles = getComputedStyle(this);
const entityStates = this.data; const entityStates = this.data;
const datasets: ChartDataset<"line">[] = []; const datasets: ChartDataset<"line">[] = [];
let endTime: Date;
if (entityStates.length === 0) { if (entityStates.length === 0) {
return; return;
} }
this._chartTime = new Date(); endTime =
const endTime = this.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();
}
const names = this.names || {}; const names = this.names || {};
entityStates.forEach((states) => { entityStates.forEach((states) => {
const domain = states.domain; const domain = states.domain;

View File

@@ -9,7 +9,7 @@ import { numberFormatToLocale } from "../../common/number/format_number";
import { computeRTL } from "../../common/util/compute_rtl"; import { computeRTL } from "../../common/util/compute_rtl";
import { TimelineEntity } from "../../data/history"; import { TimelineEntity } from "../../data/history";
import { HomeAssistant } from "../../types"; import { HomeAssistant } from "../../types";
import { MIN_TIME_BETWEEN_UPDATES } from "./ha-chart-base"; import "./ha-chart-base";
import type { TimeLineData } from "./timeline-chart/const"; import type { TimeLineData } from "./timeline-chart/const";
/** Binary sensor device classes for which the static colors for on/off are NOT inverted. /** Binary sensor device classes for which the static colors for on/off are NOT inverted.
@@ -83,8 +83,6 @@ export class StateHistoryChartTimeline extends LitElement {
@property({ attribute: false }) public data: TimelineEntity[] = []; @property({ attribute: false }) public data: TimelineEntity[] = [];
@property() public narrow!: boolean;
@property() public names: boolean | Record<string, string> = false; @property() public names: boolean | Record<string, string> = false;
@property() public unit?: string; @property() public unit?: string;
@@ -93,18 +91,12 @@ export class StateHistoryChartTimeline extends LitElement {
@property({ type: Boolean }) public isSingleDevice = false; @property({ type: Boolean }) public isSingleDevice = false;
@property({ type: Boolean }) public chunked = false; @property({ attribute: false }) public endTime?: Date;
@property({ attribute: false }) public startTime!: Date;
@property({ attribute: false }) public endTime!: Date;
@state() private _chartData?: ChartData<"timeline">; @state() private _chartData?: ChartData<"timeline">;
@state() private _chartOptions?: ChartOptions<"timeline">; @state() private _chartOptions?: ChartOptions<"timeline">;
private _chartTime: Date = new Date();
protected render() { protected render() {
return html` return html`
<ha-chart-base <ha-chart-base
@@ -118,7 +110,6 @@ export class StateHistoryChartTimeline extends LitElement {
public willUpdate(changedProps: PropertyValues) { public willUpdate(changedProps: PropertyValues) {
if (!this.hasUpdated) { if (!this.hasUpdated) {
const narrow = this.narrow;
this._chartOptions = { this._chartOptions = {
maintainAspectRatio: false, maintainAspectRatio: false,
parsing: false, parsing: false,
@@ -132,8 +123,6 @@ export class StateHistoryChartTimeline extends LitElement {
locale: this.hass.locale, locale: this.hass.locale,
}, },
}, },
suggestedMin: this.startTime,
suggestedMax: this.endTime,
ticks: { ticks: {
autoSkip: true, autoSkip: true,
maxRotation: 0, maxRotation: 0,
@@ -164,18 +153,11 @@ export class StateHistoryChartTimeline extends LitElement {
drawTicks: false, drawTicks: false,
}, },
ticks: { ticks: {
display: display: this.data.length !== 1,
this.chunked || !this.isSingleDevice || this.data.length !== 1,
}, },
afterSetDimensions: (y) => { afterSetDimensions: (y) => {
y.maxWidth = y.chart.width * 0.18; 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", position: computeRTL(this.hass) ? "right" : "left",
}, },
}, },
@@ -213,13 +195,7 @@ export class StateHistoryChartTimeline extends LitElement {
locale: numberFormatToLocale(this.hass.locale), locale: numberFormatToLocale(this.hass.locale),
}; };
} }
if ( if (changedProps.has("data")) {
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(); this._generateData();
} }
} }
@@ -232,9 +208,34 @@ export class StateHistoryChartTimeline extends LitElement {
stateHistory = []; stateHistory = [];
} }
this._chartTime = new Date(); const startTime = new Date(
const startTime = this.startTime; stateHistory.reduce(
const endTime = this.endTime; (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();
}
const labels: string[] = []; const labels: string[] = [];
const datasets: ChartDataset<"timeline">[] = []; const datasets: ChartDataset<"timeline">[] = [];
const names = this.names || {}; const names = this.names || {};

View File

@@ -1,4 +1,3 @@
import "@lit-labs/virtualizer";
import { import {
css, css,
CSSResultGroup, CSSResultGroup,
@@ -7,29 +6,12 @@ import {
PropertyValues, PropertyValues,
TemplateResult, TemplateResult,
} from "lit"; } from "lit";
import { customElement, property, state, eventOptions } from "lit/decorators"; import { customElement, property } from "lit/decorators";
import { isComponentLoaded } from "../../common/config/is_component_loaded"; import { isComponentLoaded } from "../../common/config/is_component_loaded";
import { import { HistoryResult } from "../../data/history";
HistoryResult,
LineChartUnit,
TimelineEntity,
} from "../../data/history";
import type { HomeAssistant } from "../../types"; import type { HomeAssistant } from "../../types";
import "./state-history-chart-line"; import "./state-history-chart-line";
import "./state-history-chart-timeline"; 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") @customElement("state-history-charts")
class StateHistoryCharts extends LitElement { class StateHistoryCharts extends LitElement {
@@ -37,13 +19,8 @@ class StateHistoryCharts extends LitElement {
@property({ attribute: false }) public historyData!: HistoryResult; @property({ attribute: false }) public historyData!: HistoryResult;
@property() public narrow!: boolean;
@property({ type: Boolean }) public names = false; @property({ type: Boolean }) public names = false;
@property({ type: Boolean, attribute: "virtualize", reflect: true })
public virtualize = false;
@property({ attribute: false }) public endTime?: Date; @property({ attribute: false }) public endTime?: Date;
@property({ type: Boolean, attribute: "up-to-now" }) public upToNow = false; @property({ type: Boolean, attribute: "up-to-now" }) public upToNow = false;
@@ -52,104 +29,59 @@ class StateHistoryCharts extends LitElement {
@property({ type: Boolean }) public isLoadingData = false; @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 { protected render(): TemplateResult {
if (!isComponentLoaded(this.hass, "history")) { if (!isComponentLoaded(this.hass, "history")) {
return html`<div class="info"> return html` <div class="info">
${this.hass.localize("ui.components.history_charts.history_disabled")} ${this.hass.localize("ui.components.history_charts.history_disabled")}
</div>`; </div>`;
} }
if (this.isLoadingData && !this.historyData) { if (this.isLoadingData && !this.historyData) {
return html`<div class="info"> return html` <div class="info">
${this.hass.localize("ui.components.history_charts.loading_history")} ${this.hass.localize("ui.components.history_charts.loading_history")}
</div>`; </div>`;
} }
if (this._isHistoryEmpty()) { if (this._isHistoryEmpty()) {
return html`<div class="info"> return html` <div class="info">
${this.hass.localize("ui.components.history_charts.no_history_found")} ${this.hass.localize("ui.components.history_charts.no_history_found")}
</div>`; </div>`;
} }
const now = new Date(); const computedEndTime = this.upToNow
? new Date()
: this.endTime || new Date();
this._computedEndTime = return html`
this.upToNow || !this.endTime || this.endTime > now ? now : this.endTime; ${this.historyData.timeline.length
? html`
this._computedStartTime = new Date( <state-history-chart-timeline
this.historyData.timeline.reduce( .hass=${this.hass}
(minTime, stateInfo) => .data=${this.historyData.timeline}
Math.min(minTime, new Date(stateInfo.data[0].last_changed).getTime()), .endTime=${computedEndTime}
new Date().getTime() .noSingle=${this.noSingle}
) .names=${this.names}
); ></state-history-chart-timeline>
`
const combinedItems = this.historyData.timeline.length : html``}
? (this.virtualize ${this.historyData.line.map(
? chunkData(this.historyData.timeline, CANVAS_TIMELINE_ROWS_CHUNK) (line) => html`
: [this.historyData.timeline] <state-history-chart-line
).concat(this.historyData.line) .hass=${this.hass}
: this.historyData.line; .unit=${line.unit}
.data=${line.data}
return this.virtualize .identifier=${line.identifier}
? html`<div class="container ha-scrollbar" @scroll=${this._saveScrollPos}> .isSingleDevice=${!this.noSingle &&
<lit-virtualizer line.data &&
scroller line.data.length === 1}
class="ha-scrollbar" .endTime=${computedEndTime}
.items=${combinedItems} .names=${this.names}
.renderItem=${this._renderHistoryItem} ></state-history-chart-line>
> `
</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 { protected shouldUpdate(changedProps: PropertyValues): boolean {
return !(changedProps.size === 1 && changedProps.has("hass")); return !(changedProps.size === 1 && changedProps.has("hass"));
} }
@@ -164,11 +96,6 @@ class StateHistoryCharts extends LitElement {
return !this.isLoadingData && historyDataEmpty; return !this.isLoadingData && historyDataEmpty;
} }
@eventOptions({ passive: true })
private _saveScrollPos(e: Event) {
this._savedScrollPos = (e.target as HTMLDivElement).scrollTop;
}
static get styles(): CSSResultGroup { static get styles(): CSSResultGroup {
return css` return css`
:host { :host {
@@ -176,48 +103,11 @@ class StateHistoryCharts extends LitElement {
/* height of single timeline chart = 60px */ /* height of single timeline chart = 60px */
min-height: 60px; min-height: 60px;
} }
:host([virtualize]) {
height: 100%;
}
.info { .info {
text-align: center; text-align: center;
line-height: 60px; line-height: 60px;
color: var(--secondary-text-color); 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%;
}
`; `;
} }
} }

View File

@@ -269,8 +269,8 @@ export class HaDataTable extends LitElement {
@change=${this._handleHeaderRowCheckboxClick} @change=${this._handleHeaderRowCheckboxClick}
.indeterminate=${this._checkedRows.length && .indeterminate=${this._checkedRows.length &&
this._checkedRows.length !== this._checkableRowsCount} this._checkedRows.length !== this._checkableRowsCount}
.checked=${this._checkedRows.length && .checked=${this._checkedRows.length ===
this._checkedRows.length === this._checkableRowsCount} this._checkableRowsCount}
> >
</ha-checkbox> </ha-checkbox>
</div> </div>

View File

@@ -7,25 +7,26 @@ import type {
SortableColumnContainer, SortableColumnContainer,
SortingDirection, SortingDirection,
} from "./ha-data-table"; } from "./ha-data-table";
import { fuzzyMatcher } from "../../common/string/filter/sequence-matching";
const filterData = ( const filterData = (
data: DataTableRowData[], data: DataTableRowData[],
columns: SortableColumnContainer, columns: SortableColumnContainer,
filter: string filter: string
) => { ) => {
filter = filter.toUpperCase(); const matcher = fuzzyMatcher(filter);
return data.filter((row) => return data.filter((row) =>
Object.entries(columns).some((columnEntry) => { Object.entries(columns).some((columnEntry) => {
const [key, column] = columnEntry; const [key, column] = columnEntry;
if (column.filterable) { if (column.filterable) {
if ( if (
String( matcher(
column.filterKey String(
? row[column.valueColumn || key][column.filterKey] column.filterKey
: row[column.valueColumn || key] ? row[column.valueColumn || key][column.filterKey]
: row[column.valueColumn || key]
)
) )
.toUpperCase()
.includes(filter)
) { ) {
return true; return true;
} }

View File

@@ -15,12 +15,13 @@ import type { HaComboBox } from "../ha-combo-box";
import "../ha-icon-button"; import "../ha-icon-button";
import "../ha-svg-icon"; import "../ha-svg-icon";
import "./state-badge"; import "./state-badge";
import { defaultFuzzyFilterSort } from "../../common/string/filter/sequence-matching";
interface HassEntityWithCachedName extends HassEntity { interface HassEntityWithCachedName extends HassEntity {
friendly_name: string; friendly_name: string;
} }
export type HaEntityPickerEntityFilterFunc = (entity: HassEntity) => boolean; export type HaEntityPickerEntityFilterFunc = (entityId: HassEntity) => boolean;
// eslint-disable-next-line lit/prefer-static-styles // eslint-disable-next-line lit/prefer-static-styles
const rowRenderer: ComboBoxLitRenderer<HassEntityWithCachedName> = (item) => const rowRenderer: ComboBoxLitRenderer<HassEntityWithCachedName> = (item) =>
@@ -31,7 +32,6 @@ const rowRenderer: ComboBoxLitRenderer<HassEntityWithCachedName> = (item) =>
<span>${item.friendly_name}</span> <span>${item.friendly_name}</span>
<span slot="secondary">${item.entity_id}</span> <span slot="secondary">${item.entity_id}</span>
</mwc-list-item>`; </mwc-list-item>`;
@customElement("ha-entity-picker") @customElement("ha-entity-picker")
export class HaEntityPicker extends LitElement { export class HaEntityPicker extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant; @property({ attribute: false }) public hass!: HomeAssistant;
@@ -337,11 +337,18 @@ export class HaEntityPicker extends LitElement {
} }
private _filterChanged(ev: CustomEvent): void { private _filterChanged(ev: CustomEvent): void {
const filterString = ev.detail.value.toLowerCase(); const filterString = ev.detail.value;
(this.comboBox as any).filteredItems = this._states.filter(
(entityState) => const sortableEntityStates = this._states.map((entityState) => ({
entityState.entity_id.toLowerCase().includes(filterString) || strings: [entityState.entity_id, computeStateName(entityState)],
computeStateName(entityState).toLowerCase().includes(filterString) entityState: entityState,
}));
const sortedEntityStates = defaultFuzzyFilterSort(
filterString,
sortableEntityStates
);
(this.comboBox as any).filteredItems = sortedEntityStates.map(
(sortableItem) => sortableItem.entityState
); );
} }

View File

@@ -12,10 +12,8 @@ import { property, state } from "lit/decorators";
import { ifDefined } from "lit/directives/if-defined"; import { ifDefined } from "lit/directives/if-defined";
import { styleMap } from "lit/directives/style-map"; import { styleMap } from "lit/directives/style-map";
import { computeActiveState } from "../../common/entity/compute_active_state"; import { computeActiveState } from "../../common/entity/compute_active_state";
import { computeDomain } from "../../common/entity/compute_domain";
import { computeStateDomain } from "../../common/entity/compute_state_domain"; import { computeStateDomain } from "../../common/entity/compute_state_domain";
import { iconColorCSS } from "../../common/style/icon_color_css"; import { iconColorCSS } from "../../common/style/icon_color_css";
import { cameraUrlWithWidthHeight } from "../../data/camera";
import type { HomeAssistant } from "../../types"; import type { HomeAssistant } from "../../types";
import "../ha-state-icon"; import "../ha-state-icon";
@@ -95,9 +93,6 @@ export class StateBadge extends LitElement {
if (this.hass) { if (this.hass) {
imageUrl = this.hass.hassUrl(imageUrl); imageUrl = this.hass.hassUrl(imageUrl);
} }
if (computeDomain(stateObj.entity_id) === "camera") {
imageUrl = cameraUrlWithWidthHeight(imageUrl, 80, 80);
}
hostStyle.backgroundImage = `url(${imageUrl})`; hostStyle.backgroundImage = `url(${imageUrl})`;
this._showIcon = false; this._showIcon = false;
} else if (stateObj.state === "on") { } else if (stateObj.state === "on") {

View File

@@ -4,7 +4,8 @@ import { customElement, property, query, state } from "lit/decorators";
import { isComponentLoaded } from "../common/config/is_component_loaded"; import { isComponentLoaded } from "../common/config/is_component_loaded";
import { fireEvent } from "../common/dom/fire_event"; import { fireEvent } from "../common/dom/fire_event";
import { stringCompare } from "../common/string/compare"; import { stringCompare } from "../common/string/compare";
import { fetchHassioAddonsInfo, HassioAddonInfo } from "../data/hassio/addon"; import { HassioAddonInfo } from "../data/hassio/addon";
import { fetchHassioSupervisorInfo } from "../data/hassio/supervisor";
import { showAlertDialog } from "../dialogs/generic/show-dialog-box"; import { showAlertDialog } from "../dialogs/generic/show-dialog-box";
import { PolymerChangedEvent } from "../polymer-types"; import { PolymerChangedEvent } from "../polymer-types";
import { HomeAssistant } from "../types"; import { HomeAssistant } from "../types";
@@ -77,10 +78,10 @@ class HaAddonPicker extends LitElement {
private async _getAddons() { private async _getAddons() {
try { try {
if (isComponentLoaded(this.hass, "hassio")) { if (isComponentLoaded(this.hass, "hassio")) {
const addonsInfo = await fetchHassioAddonsInfo(this.hass); const supervisorInfo = await fetchHassioSupervisorInfo(this.hass);
this._addons = addonsInfo.addons this._addons = supervisorInfo.addons.sort((a, b) =>
.filter((addon) => addon.version) stringCompare(a.name, b.name)
.sort((a, b) => stringCompare(a.name, b.name)); );
} else { } else {
showAlertDialog(this, { showAlertDialog(this, {
title: this.hass.localize( title: this.hass.localize(

View File

@@ -1,22 +1,17 @@
import type { Button } from "@material/mwc-button";
import "@material/mwc-menu"; import "@material/mwc-menu";
import type { Corner, Menu, MenuCorner } from "@material/mwc-menu"; import type { Corner, Menu, MenuCorner } from "@material/mwc-menu";
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { customElement, property, query } from "lit/decorators"; import { customElement, property, query } from "lit/decorators";
import { FOCUS_TARGET } from "../dialogs/make-dialog-manager";
import type { HaIconButton } from "./ha-icon-button";
@customElement("ha-button-menu") @customElement("ha-button-menu")
export class HaButtonMenu extends LitElement { export class HaButtonMenu extends LitElement {
protected readonly [FOCUS_TARGET];
@property() public corner: Corner = "TOP_START"; @property() public corner: Corner = "TOP_START";
@property() public menuCorner: MenuCorner = "START"; @property() public menuCorner: MenuCorner = "START";
@property({ type: Number }) public x: number | null = null; @property({ type: Number }) public x?: number;
@property({ type: Number }) public y: number | null = null; @property({ type: Number }) public y?: number;
@property({ type: Boolean }) public multi = false; @property({ type: Boolean }) public multi = false;
@@ -36,18 +31,10 @@ export class HaButtonMenu extends LitElement {
return this._menu?.selected; return this._menu?.selected;
} }
public override focus() {
if (this._menu?.open) {
this._menu.focusItemAtIndex(0);
} else {
this._triggerButton?.focus();
}
}
protected render(): TemplateResult { protected render(): TemplateResult {
return html` return html`
<div @click=${this._handleClick}> <div @click=${this._handleClick}>
<slot name="trigger" @slotchange=${this._setTriggerAria}></slot> <slot name="trigger"></slot>
</div> </div>
<mwc-menu <mwc-menu
.corner=${this.corner} .corner=${this.corner}
@@ -63,21 +50,6 @@ export class HaButtonMenu extends LitElement {
`; `;
} }
protected firstUpdated(changedProps): void {
super.firstUpdated(changedProps);
if (document.dir === "rtl") {
this.updateComplete.then(() => {
this.querySelectorAll("mwc-list-item").forEach((item) => {
const style = document.createElement("style");
style.innerHTML =
"span.material-icons:first-of-type { margin-left: var(--mdc-list-item-graphic-margin, 32px) !important; margin-right: 0px !important;}";
item!.shadowRoot!.appendChild(style);
});
});
}
}
private _handleClick(): void { private _handleClick(): void {
if (this.disabled) { if (this.disabled) {
return; return;
@@ -86,18 +58,6 @@ export class HaButtonMenu extends LitElement {
this._menu!.show(); this._menu!.show();
} }
private get _triggerButton() {
return this.querySelector(
'ha-icon-button[slot="trigger"], mwc-button[slot="trigger"]'
) as HaIconButton | Button | null;
}
private _setTriggerAria() {
if (this._triggerButton) {
this._triggerButton.ariaHasPopup = "menu";
}
}
static get styles(): CSSResultGroup { static get styles(): CSSResultGroup {
return css` return css`
:host { :host {

View File

@@ -66,12 +66,9 @@ export class HaChip extends LitElement {
line-height: 14px; line-height: 14px;
color: var(--ha-chip-icon-color, var(--ha-chip-text-color)); 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 .mdc-chip__icon--leading:not(.mdc-chip__icon--leading-hidden) { .mdc-chip__icon--leading:not(.mdc-chip__icon--leading-hidden) {
margin-right: -4px; margin-right: -4px;
margin-inline-start: -4px;
margin-inline-end: 4px;
direction: var(--direction);
} }
span[role="gridcell"] { span[role="gridcell"] {

View File

@@ -1,13 +1,17 @@
import { css, CSSResultGroup, html } from "lit"; 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 { customElement, property, query } from "lit/decorators"; import { customElement, property, query } from "lit/decorators";
import { HaListItem } from "./ha-list-item";
@customElement("ha-clickable-list-item") @customElement("ha-clickable-list-item")
export class HaClickableListItem extends HaListItem { export class HaClickableListItem extends ListItemBase {
@property() public href?: string; @property() public href?: string;
@property({ type: Boolean }) public disableHref = false; @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; @property({ type: Boolean, reflect: true }) public openNewTab = false;
@query("a") private _anchor!: HTMLAnchorElement; @query("a") private _anchor!: HTMLAnchorElement;
@@ -35,10 +39,22 @@ export class HaClickableListItem extends HaListItem {
}); });
} }
static get styles(): CSSResultGroup { static get styles(): CSSResult[] {
return [ return [
super.styles, styles,
css` css`
:host {
padding-left: 0px;
padding-right: 0px;
}
:host([rtl]) span {
margin-left: var(--mdc-list-item-graphic-margin, 20px) !important;
margin-right: 0px !important;
}
:host([graphic="avatar"]:not([twoLine])),
:host([graphic="icon"]:not([twoLine])) {
height: 48px;
}
a { a {
width: 100%; width: 100%;
height: 100%; height: 100%;

View File

@@ -3,7 +3,6 @@ import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { customElement, property } from "lit/decorators"; import { customElement, property } from "lit/decorators";
import { formatNumber } from "../common/number/format_number"; import { formatNumber } from "../common/number/format_number";
import { CLIMATE_PRESET_NONE } from "../data/climate"; import { CLIMATE_PRESET_NONE } from "../data/climate";
import { UNAVAILABLE_STATES } from "../data/entity";
import type { HomeAssistant } from "../types"; import type { HomeAssistant } from "../types";
@customElement("ha-climate-state") @customElement("ha-climate-state")
@@ -16,22 +15,22 @@ class HaClimateState extends LitElement {
const currentStatus = this._computeCurrentStatus(); const currentStatus = this._computeCurrentStatus();
return html`<div class="target"> return html`<div class="target">
${!UNAVAILABLE_STATES.includes(this.stateObj.state) ${this.stateObj.state !== "unknown"
? html`<span class="state-label"> ? html`<span class="state-label">
${this._localizeState()} ${this._localizeState()}
${this.stateObj.attributes.preset_mode && ${this.stateObj.attributes.preset_mode &&
this.stateObj.attributes.preset_mode !== CLIMATE_PRESET_NONE this.stateObj.attributes.preset_mode !== CLIMATE_PRESET_NONE
? html`- ? html`-
${this.hass.localize( ${this.hass.localize(
`state_attributes.climate.preset_mode.${this.stateObj.attributes.preset_mode}` `state_attributes.climate.preset_mode.${this.stateObj.attributes.preset_mode}`
) || this.stateObj.attributes.preset_mode}` ) || this.stateObj.attributes.preset_mode}`
: ""} : ""}
</span> </span>`
<div class="unit">${this._computeTarget()}</div>` : ""}
: this._localizeState()} <div class="unit">${this._computeTarget()}</div>
</div> </div>
${currentStatus && !UNAVAILABLE_STATES.includes(this.stateObj.state) ${currentStatus
? html`<div class="current"> ? html`<div class="current">
${this.hass.localize("ui.card.climate.currently")}: ${this.hass.localize("ui.card.climate.currently")}:
<div class="unit">${currentStatus}</div> <div class="unit">${currentStatus}</div>
@@ -109,10 +108,6 @@ class HaClimateState extends LitElement {
} }
private _localizeState(): string { private _localizeState(): string {
if (UNAVAILABLE_STATES.includes(this.stateObj.state)) {
return this.hass.localize(`state.default.${this.stateObj.state}`);
}
const stateString = this.hass.localize( const stateString = this.hass.localize(
`component.climate.state._.${this.stateObj.state}` `component.climate.state._.${this.stateObj.state}`
); );

View File

@@ -2,7 +2,6 @@ import type {
Completion, Completion,
CompletionContext, CompletionContext,
CompletionResult, CompletionResult,
CompletionSource,
} from "@codemirror/autocomplete"; } from "@codemirror/autocomplete";
import type { EditorView, KeyBinding, ViewUpdate } from "@codemirror/view"; import type { EditorView, KeyBinding, ViewUpdate } from "@codemirror/view";
import { HassEntities } from "home-assistant-js-websocket"; import { HassEntities } from "home-assistant-js-websocket";
@@ -12,7 +11,6 @@ import memoizeOne from "memoize-one";
import { fireEvent } from "../common/dom/fire_event"; import { fireEvent } from "../common/dom/fire_event";
import { loadCodeMirror } from "../resources/codemirror.ondemand"; import { loadCodeMirror } from "../resources/codemirror.ondemand";
import { HomeAssistant } from "../types"; import { HomeAssistant } from "../types";
import "./ha-icon";
declare global { declare global {
interface HASSDomEvents { interface HASSDomEvents {
@@ -28,12 +26,6 @@ const saveKeyBinding: KeyBinding = {
}, },
}; };
const renderIcon = (completion: Completion) => {
const icon = document.createElement("ha-icon");
icon.icon = completion.label;
return icon;
};
@customElement("ha-code-editor") @customElement("ha-code-editor")
export class HaCodeEditor extends ReactiveElement { export class HaCodeEditor extends ReactiveElement {
public codemirror?: EditorView; public codemirror?: EditorView;
@@ -49,17 +41,12 @@ export class HaCodeEditor extends ReactiveElement {
@property({ type: Boolean, attribute: "autocomplete-entities" }) @property({ type: Boolean, attribute: "autocomplete-entities" })
public autocompleteEntities = false; public autocompleteEntities = false;
@property({ type: Boolean, attribute: "autocomplete-icons" })
public autocompleteIcons = false;
@property() public error = false; @property() public error = false;
@state() private _value = ""; @state() private _value = "";
private _loadedCodeMirror?: typeof import("../resources/codemirror"); private _loadedCodeMirror?: typeof import("../resources/codemirror");
private _iconList?: Completion[];
public set value(value: string) { public set value(value: string) {
this._value = value; this._value = value;
} }
@@ -164,22 +151,13 @@ export class HaCodeEditor extends ReactiveElement {
), ),
]; ];
if (!this.readOnly) { if (!this.readOnly && this.autocompleteEntities && this.hass) {
const completionSources: CompletionSource[] = []; extensions.push(
if (this.autocompleteEntities && this.hass) { this._loadedCodeMirror.autocompletion({
completionSources.push(this._entityCompletions.bind(this)); override: [this._entityCompletions.bind(this)],
} maxRenderedOptions: 10,
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({ this.codemirror = new this._loadedCodeMirror.EditorView({
@@ -209,7 +187,7 @@ export class HaCodeEditor extends ReactiveElement {
private _entityCompletions( private _entityCompletions(
context: CompletionContext context: CompletionContext
): CompletionResult | null | Promise<CompletionResult | null> { ): CompletionResult | null | Promise<CompletionResult | null> {
const entityWord = context.matchBefore(/[a-z_]{3,}\.\w*/); const entityWord = context.matchBefore(/[a-z_]{3,}\./);
if ( if (
!entityWord || !entityWord ||
@@ -227,48 +205,7 @@ export class HaCodeEditor extends ReactiveElement {
return { return {
from: Number(entityWord.from), from: Number(entityWord.from),
options: states, options: states,
span: /^[a-z_]{3,}\.\w*$/, span: /^\w*.\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*$/,
}; };
} }

View File

@@ -1,17 +1,13 @@
import "@material/mwc-list/mwc-list-item"; import "@material/mwc-list/mwc-list-item";
import { mdiClose, mdiMenuDown, mdiMenuUp } from "@mdi/js"; import { mdiClose, mdiMenuDown, mdiMenuUp } from "@mdi/js";
import "@vaadin/combo-box/theme/material/vaadin-combo-box-light"; import "@vaadin/combo-box/theme/material/vaadin-combo-box-light";
import type { import type { ComboBoxLight } from "@vaadin/combo-box/vaadin-combo-box-light";
ComboBoxLight,
ComboBoxLightFilterChangedEvent,
ComboBoxLightOpenedChangedEvent,
ComboBoxLightValueChangedEvent,
} from "@vaadin/combo-box/vaadin-combo-box-light";
import { registerStyles } from "@vaadin/vaadin-themable-mixin/register-styles"; import { registerStyles } from "@vaadin/vaadin-themable-mixin/register-styles";
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { ComboBoxLitRenderer, comboBoxRenderer } from "lit-vaadin-helpers"; import { ComboBoxLitRenderer, comboBoxRenderer } from "lit-vaadin-helpers";
import { customElement, property, query } from "lit/decorators"; import { customElement, property, query } from "lit/decorators";
import { fireEvent } from "../common/dom/fire_event"; import { fireEvent } from "../common/dom/fire_event";
import { PolymerChangedEvent } from "../polymer-types";
import { HomeAssistant } from "../types"; import { HomeAssistant } from "../types";
import "./ha-icon-button"; import "./ha-icon-button";
import "./ha-textfield"; import "./ha-textfield";
@@ -100,8 +96,6 @@ export class HaComboBox extends LitElement {
@query("vaadin-combo-box-light", true) private _comboBox!: ComboBoxLight; @query("vaadin-combo-box-light", true) private _comboBox!: ComboBoxLight;
private _overlayMutationObserver?: MutationObserver;
public open() { public open() {
this.updateComplete.then(() => { this.updateComplete.then(() => {
this._comboBox?.open(); this._comboBox?.open();
@@ -114,14 +108,6 @@ export class HaComboBox extends LitElement {
}); });
} }
public disconnectedCallback() {
super.disconnectedCallback();
if (this._overlayMutationObserver) {
this._overlayMutationObserver.disconnect();
this._overlayMutationObserver = undefined;
}
}
public get selectedItem() { public get selectedItem() {
return this._comboBox.selectedItem; return this._comboBox.selectedItem;
} }
@@ -207,64 +193,21 @@ export class HaComboBox extends LitElement {
} }
} }
private _openedChanged(ev: ComboBoxLightOpenedChangedEvent) { private _openedChanged(ev: PolymerChangedEvent<boolean>) {
const opened = ev.detail.value;
// delay this so we can handle click event before setting _opened // delay this so we can handle click event before setting _opened
setTimeout(() => { setTimeout(() => {
this._opened = opened; this._opened = ev.detail.value;
}, 0); }, 0);
// @ts-ignore // @ts-ignore
fireEvent(this, ev.type, ev.detail); 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: ComboBoxLightFilterChangedEvent) { private _filterChanged(ev: PolymerChangedEvent<string>) {
// @ts-ignore // @ts-ignore
fireEvent(this, ev.type, ev.detail, { composed: false }); fireEvent(this, ev.type, ev.detail, { composed: false });
} }
private _valueChanged(ev: ComboBoxLightValueChangedEvent) { private _valueChanged(ev: PolymerChangedEvent<string>) {
ev.stopPropagation(); ev.stopPropagation();
const newValue = ev.detail.value; const newValue = ev.detail.value;
@@ -298,9 +241,6 @@ export class HaComboBox extends LitElement {
.toggle-button { .toggle-button {
right: 12px; right: 12px;
top: -10px; top: -10px;
inset-inline-start: initial;
inset-inline-end: 12px;
direction: var(--direction);
} }
:host([opened]) .toggle-button { :host([opened]) .toggle-button {
color: var(--primary-color); color: var(--primary-color);
@@ -309,9 +249,18 @@ export class HaComboBox extends LitElement {
--mdc-icon-size: 20px; --mdc-icon-size: 20px;
top: -7px; top: -7px;
right: 36px; right: 36px;
inset-inline-start: initial; }
inset-inline-end: 36px;
direction: var(--direction); :host-context([style*="direction: rtl;"]) .toggle-button {
left: 12px;
right: auto;
top: -10px;
}
:host-context([style*="direction: rtl;"]) .clear-button {
--mdc-icon-size: 20px;
top: -7px;
left: 36px;
right: auto;
} }
`; `;
} }

View File

@@ -140,9 +140,6 @@ export class HaDateRangePicker extends LitElement {
return css` return css`
ha-svg-icon { ha-svg-icon {
margin-right: 8px; margin-right: 8px;
margin-inline-end: 8px;
margin-inline-start: initial;
direction: var(--direction);
} }
.date-range-inputs { .date-range-inputs {
@@ -169,9 +166,6 @@ export class HaDateRangePicker extends LitElement {
ha-textfield:last-child { ha-textfield:last-child {
margin-left: 8px; margin-left: 8px;
margin-inline-start: 8px;
margin-inline-end: initial;
direction: var(--direction);
} }
@media only screen and (max-width: 800px) { @media only screen and (max-width: 800px) {

View File

@@ -3,8 +3,8 @@ import { styles } from "@material/mwc-dialog/mwc-dialog.css";
import { mdiClose } from "@mdi/js"; import { mdiClose } from "@mdi/js";
import { css, html, TemplateResult } from "lit"; import { css, html, TemplateResult } from "lit";
import { customElement } from "lit/decorators"; import { customElement } from "lit/decorators";
import { computeRTLDirection } from "../common/util/compute_rtl";
import type { HomeAssistant } from "../types"; import type { HomeAssistant } from "../types";
import { FOCUS_TARGET } from "../dialogs/make-dialog-manager";
import "./ha-icon-button"; import "./ha-icon-button";
export const createCloseHeading = ( export const createCloseHeading = (
@@ -17,13 +17,12 @@ export const createCloseHeading = (
.path=${mdiClose} .path=${mdiClose}
dialogAction="close" dialogAction="close"
class="header_button" class="header_button"
dir=${computeRTLDirection(hass)}
></ha-icon-button> ></ha-icon-button>
`; `;
@customElement("ha-dialog") @customElement("ha-dialog")
export class HaDialog extends DialogBase { export class HaDialog extends DialogBase {
protected readonly [FOCUS_TARGET];
public scrollToPos(x: number, y: number) { public scrollToPos(x: number, y: number) {
this.contentElement?.scrollTo(x, y); this.contentElement?.scrollTo(x, y);
} }
@@ -90,19 +89,18 @@ export class HaDialog extends DialogBase {
} }
.header_title { .header_title {
margin-right: 40px; margin-right: 40px;
margin-inline-end: 40px;
margin-inline-start: initial;
direction: var(--direction);
} }
.header_button { [dir="rtl"].header_button {
inset-inline-start: initial; right: auto;
inset-inline-end: 16px; left: 16px;
direction: var(--direction);
} }
.dialog-actions { [dir="rtl"].header_title {
inset-inline-start: initial !important; margin-left: 40px;
inset-inline-end: 0px !important; margin-right: 0px;
direction: var(--direction); }
:host-context([style*="direction: rtl;"]) .dialog-actions {
left: 0px !important;
right: auto !important;
} }
`, `,
]; ];

View File

@@ -133,9 +133,6 @@ class HaExpansionPanel extends LitElement {
.summary-icon { .summary-icon {
transition: transform 150ms cubic-bezier(0.4, 0, 0.2, 1); transition: transform 150ms cubic-bezier(0.4, 0, 0.2, 1);
margin-left: auto; margin-left: auto;
margin-inline-start: auto;
margin-inline-end: initial;
direction: var(--direction);
} }
.summary-icon.expanded { .summary-icon.expanded {

View File

@@ -1,33 +1,24 @@
import { FabBase } from "@material/mwc-fab/mwc-fab-base"; import { Fab } from "@material/mwc-fab";
import { styles } from "@material/mwc-fab/mwc-fab.css";
import { customElement } from "lit/decorators"; import { customElement } from "lit/decorators";
import { css } from "lit"; import { css } from "lit";
@customElement("ha-fab") @customElement("ha-fab")
export class HaFab extends FabBase { export class HaFab extends Fab {
protected firstUpdated(changedProperties) { protected firstUpdated(changedProperties) {
super.firstUpdated(changedProperties); super.firstUpdated(changedProperties);
this.style.setProperty("--mdc-theme-secondary", "var(--primary-color)"); this.style.setProperty("--mdc-theme-secondary", "var(--primary-color)");
} }
static override styles = [ static override styles = Fab.styles.concat([
styles,
css` css`
:host .mdc-fab--extended .mdc-fab__icon { :host-context([style*="direction: rtl;"])
margin-inline-start: -8px; .mdc-fab--extended
margin-inline-end: 12px; .mdc-fab__icon {
direction: var(--direction); margin-left: 12px !important;
margin-right: calc(12px - 20px) !important;
} }
`, `,
// safari workaround - must be explicit ]);
document.dir === "rtl"
? css`
:host .mdc-fab--extended .mdc-fab__icon {
direction: rtl;
}
`
: css``,
];
} }
declare global { declare global {

View File

@@ -175,23 +175,24 @@ export class HaFileUpload extends LitElement {
} }
.mdc-text-field__icon--leading { .mdc-text-field__icon--leading {
margin-bottom: 12px; margin-bottom: 12px;
inset-inline-start: initial; }
inset-inline-end: 0px; :host-context([style*="direction: rtl;"])
direction: var(--direction); .mdc-text-field__icon--leading {
margin-right: 0px;
} }
.mdc-text-field--filled .mdc-floating-label--float-above { .mdc-text-field--filled .mdc-floating-label--float-above {
transform: scale(0.75); transform: scale(0.75);
top: 8px; top: 8px;
} }
.mdc-floating-label { :host-context([style*="direction: rtl;"]) .mdc-floating-label {
inset-inline-start: 16px !important; left: initial;
inset-inline-end: initial !important; right: 16px;
direction: var(--direction);
} }
.mdc-text-field--filled .mdc-floating-label { :host-context([style*="direction: rtl;"])
inset-inline-start: 48px !important; .mdc-text-field--filled
inset-inline-end: initial !important; .mdc-floating-label {
direction: var(--direction); left: initial;
right: 48px;
} }
.dragged:before { .dragged:before {
position: var(--layout-fit_-_position); position: var(--layout-fit_-_position);

View File

@@ -205,9 +205,6 @@ export class HaFormMultiSelect extends LitElement implements HaFormElement {
ha-formfield { ha-formfield {
display: block; display: block;
padding-right: 16px; padding-right: 16px;
padding-inline-end: 16px;
padding-inline-start: initial;
direction: var(--direction);
} }
ha-textfield { ha-textfield {
display: block; display: block;
@@ -219,9 +216,6 @@ export class HaFormMultiSelect extends LitElement implements HaFormElement {
right: 1em; right: 1em;
top: 1em; top: 1em;
cursor: pointer; cursor: pointer;
inset-inline-end: 1em;
inset-inline-start: initial;
direction: var(--direction);
} }
:host([opened]) ha-svg-icon { :host([opened]) ha-svg-icon {
color: var(--primary-color); color: var(--primary-color);

View File

@@ -133,10 +133,9 @@ export class HaFormString extends LitElement implements HaFormElement {
color: var(--secondary-text-color); color: var(--secondary-text-color);
} }
ha-icon-button { :host-context([style*="direction: rtl;"]) ha-icon-button {
inset-inline-start: initial; right: auto;
inset-inline-end: 12px; left: 12px;
direction: var(--direction);
} }
`; `;
} }

View File

@@ -28,15 +28,10 @@ export class HaFormfield extends FormfieldBase {
css` css`
:host(:not([alignEnd])) ::slotted(ha-switch) { :host(:not([alignEnd])) ::slotted(ha-switch) {
margin-right: 10px; margin-right: 10px;
margin-inline-end: 10px;
margin-inline-start: inline;
} }
.mdc-form-field > label { :host([dir="rtl"]:not([alignEnd])) ::slotted(ha-switch) {
direction: var(--direction); margin-left: 10px;
margin-inline-start: 0; margin-right: auto;
margin-inline-end: auto;
padding-inline-start: 4px;
padding-inline-end: 0;
} }
`, `,
]; ];

View File

@@ -14,7 +14,6 @@ const getAngle = (value: number, min: number, max: number) => {
export interface LevelDefinition { export interface LevelDefinition {
level: number; level: number;
stroke: string; stroke: string;
label?: string;
} }
@customElement("ha-gauge") @customElement("ha-gauge")
@@ -39,31 +38,22 @@ export class Gauge extends LitElement {
@state() private _updated = false; @state() private _updated = false;
@state() private _segment_label? = "";
protected firstUpdated(changedProperties: PropertyValues) { protected firstUpdated(changedProperties: PropertyValues) {
super.firstUpdated(changedProperties); super.firstUpdated(changedProperties);
// Wait for the first render for the initial animation to work // Wait for the first render for the initial animation to work
afterNextRender(() => { afterNextRender(() => {
this._updated = true; this._updated = true;
this._angle = getAngle(this.value, this.min, this.max); this._angle = getAngle(this.value, this.min, this.max);
this._segment_label = this.getSegmentLabel();
this._rescale_svg(); this._rescale_svg();
}); });
} }
protected updated(changedProperties: PropertyValues) { protected updated(changedProperties: PropertyValues) {
super.updated(changedProperties); super.updated(changedProperties);
if ( if (!this._updated || !changedProperties.has("value")) {
!this._updated ||
(!changedProperties.has("value") &&
!changedProperties.has("label") &&
!changedProperties.has("_segment_label"))
) {
return; return;
} }
this._angle = getAngle(this.value, this.min, this.max); this._angle = getAngle(this.value, this.min, this.max);
this._segment_label = this.getSegmentLabel();
this._rescale_svg(); this._rescale_svg();
} }
@@ -128,11 +118,9 @@ export class Gauge extends LitElement {
</svg> </svg>
<svg class="text"> <svg class="text">
<text class="value-text"> <text class="value-text">
${ ${this.valueText || formatNumber(this.value, this.locale)} ${
this._segment_label this.label
? this._segment_label }
: this.valueText || formatNumber(this.value, this.locale)
} ${this._segment_label ? "" : this.label}
</text> </text>
</svg>`; </svg>`;
} }
@@ -149,18 +137,6 @@ 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() { static get styles() {
return css` return css`
:host { :host {

View File

@@ -1,8 +1,6 @@
import "@material/mwc-icon-button"; import "@material/mwc-icon-button";
import type { IconButton } from "@material/mwc-icon-button";
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { customElement, property, query } from "lit/decorators"; import { customElement, property } from "lit/decorators";
import { ifDefined } from "lit/directives/if-defined";
import "./ha-svg-icon"; import "./ha-svg-icon";
@customElement("ha-icon-button") @customElement("ha-icon-button")
@@ -13,32 +11,21 @@ export class HaIconButton extends LitElement {
@property({ type: String }) path?: string; @property({ type: String }) path?: string;
// Label that is used for ARIA support and as tooltip // Label that is used for ARIA support and as tooltip
@property({ type: String }) label?: string; @property({ type: String }) label = "";
// These should always be set as properties, not attributes,
// so that only the <button> element gets the attribute
@property({ type: String, attribute: "aria-haspopup" })
override ariaHasPopup!: IconButton["ariaHasPopup"];
@property({ type: Boolean }) hideTitle = false; @property({ type: Boolean }) hideTitle = false;
@query("mwc-icon-button", true) private _button?: IconButton;
public override focus() {
this._button?.focus();
}
static shadowRootOptions: ShadowRootInit = { static shadowRootOptions: ShadowRootInit = {
mode: "open", mode: "open",
delegatesFocus: true, delegatesFocus: true,
}; };
protected render(): TemplateResult { protected render(): TemplateResult {
// Note: `ariaLabel` required despite the `mwc-icon-button` docs saying `label` should be enough
return html` return html`
<mwc-icon-button <mwc-icon-button
aria-label=${ifDefined(this.label)} .ariaLabel=${this.label}
title=${ifDefined(this.hideTitle ? undefined : this.label)} .title=${this.hideTitle ? "" : this.label}
aria-haspopup=${ifDefined(this.ariaHasPopup)}
.disabled=${this.disabled} .disabled=${this.disabled}
> >
${this.path ${this.path

View File

@@ -1,42 +0,0 @@
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;
}
}

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