Compare commits

..

8 Commits

Author SHA1 Message Date
Christopher Altona
8940397b23 Only show nested entities with a null category 2022-06-30 03:06:24 +00:00
Christopher Altona
8ac6ae1187 Only show config and sensor entities unless they are explicitly selected as entity ids 2022-06-30 03:01:52 +00:00
Christopher Altona
049af5b00c Optimize undefined checks and don't fetch history twice 2022-06-30 02:43:13 +00:00
Christopher Altona
8b007610f9 Clean up horizontal rendering for target picker 2022-06-30 02:23:14 +00:00
Christopher Altona
e2f20ecd48 Update to use translations 2022-06-30 02:15:53 +00:00
Christopher Altona
64533865b0 Update message when no selection is made 2022-06-30 01:55:49 +00:00
Christopher Altona
51a2983310 React to updates correctly 2022-06-30 01:48:16 +00:00
Christopher Altona
ba7351a676 Fix area and device id filtering
Add add and remove all buttons next to refresh button
Index devices and entities on updates for more efficient querying
2022-06-30 01:44:08 +00:00
403 changed files with 5817 additions and 11779 deletions

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

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

View File

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

View File

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

View File

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

View File

@@ -1,73 +0,0 @@
name: Nightly
on:
workflow_dispatch:
schedule:
- cron: "0 1 * * *"
env:
PYTHON_VERSION: "3.10"
NODE_VERSION: 16
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: Archive translations
run: tar -czvf translations.tar.gz translations
- name: Upload build artifacts
uses: actions/upload-artifact@v3
with:
name: wheels
path: dist/home_assistant_frontend*.whl
if-no-files-found: error
- name: Upload translations
uses: actions/upload-artifact@v3
with:
name: translations
path: translations.tar.gz
if-no-files-found: error

View File

@@ -6,8 +6,8 @@ on:
- published
env:
PYTHON_VERSION: "3.10"
NODE_VERSION: 16
PYTHON_VERSION: 3.8
NODE_VERSION: 14
NODE_OPTIONS: --max_old_space_size=6144
# Set default workflow permissions
@@ -21,21 +21,21 @@ jobs:
name: Release
runs-on: ubuntu-latest
permissions:
contents: write # Required to upload release assets
contents: write # Required to upload release assets
steps:
- name: Checkout the repository
uses: actions/checkout@v3
uses: actions/checkout@v2
- name: Verify version
uses: home-assistant/actions/helpers/verify-version@master
- name: Set up Python ${{ env.PYTHON_VERSION }}
uses: actions/setup-python@v4
uses: actions/setup-python@v2
with:
python-version: ${{ env.PYTHON_VERSION }}
- name: Set up Node ${{ env.NODE_VERSION }}
uses: actions/setup-node@v3
uses: actions/setup-node@v2
with:
node-version: ${{ env.NODE_VERSION }}
cache: yarn

View File

@@ -10,7 +10,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: 90 days stale policy
uses: actions/stale@v5.1.1
uses: actions/stale@v3.0.13
with:
repo-token: ${{ secrets.GITHUB_TOKEN }}
days-before-stale: 90

View File

@@ -8,7 +8,7 @@ on:
- src/translations/en.json
env:
NODE_VERSION: 16
NODE_VERSION: 14
jobs:
upload:
@@ -16,7 +16,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout the repository
uses: actions/checkout@v3
uses: actions/checkout@v2
- name: Upload Translations
run: |

View File

@@ -1,4 +0,0 @@
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"
yarn run lint-staged --relative --shell "/bin/bash"

2
.nvmrc
View File

@@ -1 +1 @@
16
14

View File

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

View File

@@ -1,30 +1 @@
[
{
"path": "M20,20H7A2,2 0 0,1 5,18V8.94L2.23,5.64C2.09,5.47 2,5.24 2,5A1,1 0 0,1 3,4H20A2,2 0 0,1 22,6V18A2,2 0 0,1 20,20M8.5,7A0.5,0.5 0 0,0 8,7.5V8.5A0.5,0.5 0 0,0 8.5,9H18.5A0.5,0.5 0 0,0 19,8.5V7.5A0.5,0.5 0 0,0 18.5,7H8.5M8.5,11A0.5,0.5 0 0,0 8,11.5V12.5A0.5,0.5 0 0,0 8.5,13H18.5A0.5,0.5 0 0,0 19,12.5V11.5A0.5,0.5 0 0,0 18.5,11H8.5M8.5,15A0.5,0.5 0 0,0 8,15.5V16.5A0.5,0.5 0 0,0 8.5,17H13.5A0.5,0.5 0 0,0 14,16.5V15.5A0.5,0.5 0 0,0 13.5,15H8.5Z",
"name": "android-messages"
},
{
"path": "M4,6H2V20A2,2 0 0,0 4,22H18V20H4V6M20,2H8A2,2 0 0,0 6,4V16A2,2 0 0,0 8,18H20A2,2 0 0,0 22,16V4A2,2 0 0,0 20,2M20,12L17.5,10.5L15,12V4H20V12Z",
"name": "book-variant-multiple"
},
{
"path": "M21,14H3V4H21M21,2H3C1.89,2 1,2.89 1,4V16A2,2 0 0,0 3,18H10L8,21V22H16V21L14,18H21A2,2 0 0,0 23,16V4C23,2.89 22.1,2 21,2Z",
"name": "desktop-mac"
},
{
"path": "M21,14V4H3V14H21M21,2A2,2 0 0,1 23,4V16A2,2 0 0,1 21,18H14L16,21V22H8V21L10,18H3C1.89,18 1,17.1 1,16V4C1,2.89 1.89,2 3,2H21M4,5H15V10H4V5M16,5H20V7H16V5M20,8V13H16V8H20M4,11H9V13H4V11M10,11H15V13H10V11Z",
"name": "desktop-mac-dashboard"
},
{
"path": "M22,24L16.75,19L17.38,21H4.5A2.5,2.5 0 0,1 2,18.5V3.5A2.5,2.5 0 0,1 4.5,1H19.5A2.5,2.5 0 0,1 22,3.5V24M12,6.8C9.32,6.8 7.44,7.95 7.44,7.95C8.47,7.03 10.27,6.5 10.27,6.5L10.1,6.33C8.41,6.36 6.88,7.53 6.88,7.53C5.16,11.12 5.27,14.22 5.27,14.22C6.67,16.03 8.75,15.9 8.75,15.9L9.46,15C8.21,14.73 7.42,13.62 7.42,13.62C7.42,13.62 9.3,14.9 12,14.9C14.7,14.9 16.58,13.62 16.58,13.62C16.58,13.62 15.79,14.73 14.54,15L15.25,15.9C15.25,15.9 17.33,16.03 18.73,14.22C18.73,14.22 18.84,11.12 17.12,7.53C17.12,7.53 15.59,6.36 13.9,6.33L13.73,6.5C13.73,6.5 15.53,7.03 16.56,7.95C16.56,7.95 14.68,6.8 12,6.8M9.93,10.59C10.58,10.59 11.11,11.16 11.1,11.86C11.1,12.55 10.58,13.13 9.93,13.13C9.29,13.13 8.77,12.55 8.77,11.86C8.77,11.16 9.28,10.59 9.93,10.59M14.1,10.59C14.75,10.59 15.27,11.16 15.27,11.86C15.27,12.55 14.75,13.13 14.1,13.13C13.46,13.13 12.94,12.55 12.94,11.86C12.94,11.16 13.45,10.59 14.1,10.59Z",
"name": "discord"
},
{
"path": "M8.06,7.78C7.5,7.78 7.17,7.73 7.08,7.64L6.66,13.73C7.19,14.05 7.88,14.3 8.72,14.5C9.56,14.71 10.78,14.77 12.38,14.67C13.97,14.58 15.63,14.23 17.34,13.64L16.55,4.22C15.67,5.09 14.38,5.91 12.66,6.66C11.13,7.31 9.81,7.69 8.72,7.78H8.06M7.97,5.34C7.28,5.94 7,6.34 7.13,6.56C7.22,6.78 7.7,6.84 8.58,6.75C9.67,6.66 10.91,6.31 12.28,5.72C13.22,5.31 14.03,4.88 14.72,4.41C15.41,3.94 15.88,3.55 16.13,3.23C16.38,2.92 16.47,2.7 16.41,2.58C16.34,2.42 16.03,2.34 15.47,2.34C14.34,2.34 12.94,2.7 11.25,3.42C9.81,4.05 8.72,4.69 7.97,5.34M17.34,2.2C17.41,2.33 17.44,2.47 17.44,2.63L18.61,17C18.61,18.73 18,20.09 16.83,21.07C15.64,22.05 14.03,22.55 12,22.55C10,22.55 8.4,22.04 7.2,21C6,20 5.39,18.64 5.39,16.92L6.09,6.47C6.09,6.22 6.2,5.94 6.42,5.63C6.64,5.31 6.84,5.06 7.03,4.88L7.36,4.59C8.33,3.78 9.5,3.08 10.88,2.5C11.81,2.08 12.73,1.77 13.62,1.57C14.5,1.37 15.3,1.3 16,1.38C16.71,1.46 17.16,1.73 17.34,2.2Z",
"name": "google-home"
},
{
"path": "M19.25,19H4.75V3H19.25M14,22H10V21H14M18,0H6A3,3 0 0,0 3,3V21A3,3 0 0,0 6,24H18A3,3 0 0,0 21,21V3A3,3 0 0,0 18,0Z",
"name": "tablet-android"
}
]
[]

View File

@@ -76,7 +76,7 @@ const createWebpackConfig = ({
chunkIds: isProdBuild && !isStatsBuild ? "deterministic" : "named",
},
plugins: [
!isStatsBuild && new WebpackBar({ fancy: !isProdBuild }),
new WebpackBar({ fancy: !isProdBuild }),
new WebpackManifestPlugin({
// Only include the JS of entrypoints
filter: (file) => file.isInitial && !file.name.endsWith(".map"),

View File

@@ -797,7 +797,7 @@ export const demoEntitiesKernehed: DemoConfig["entities"] = () =>
attributes: {
battery_level: 34,
on: true,
friendly_name: "Porch motion sensor",
friendly_name: "altan_motion_sensor",
device_class: "motion",
},
},
@@ -818,7 +818,7 @@ export const demoEntitiesKernehed: DemoConfig["entities"] = () =>
attributes: {
battery_level: 74,
on: true,
friendly_name: "Bathroom motion sensor",
friendly_name: "badrumssensor",
device_class: "motion",
},
},
@@ -829,7 +829,7 @@ export const demoEntitiesKernehed: DemoConfig["entities"] = () =>
battery_level: 47,
on: true,
dark: true,
friendly_name: "Basement motion sensor",
friendly_name: "R\u00f6relsesensor k\u00e4llaren 1",
device_class: "motion",
icon: "mdi:walk",
},
@@ -863,7 +863,7 @@ export const demoEntitiesKernehed: DemoConfig["entities"] = () =>
attributes: {
battery_level: 60,
on: true,
friendly_name: "Pantry motion sensor",
friendly_name: "R\u00f6relsesensor skafferiet",
device_class: "motion",
icon: "mdi:walk",
},
@@ -875,7 +875,7 @@ export const demoEntitiesKernehed: DemoConfig["entities"] = () =>
battery_level: 60,
on: true,
dark: true,
friendly_name: "Stair motion sensor",
friendly_name: "R\u00f6relsesensor k\u00e4llaren 2",
device_class: "motion",
icon: "mdi:walk",
},
@@ -887,7 +887,7 @@ export const demoEntitiesKernehed: DemoConfig["entities"] = () =>
battery_level: 47,
on: true,
dark: true,
friendly_name: "Bench sensor",
friendly_name: "B\u00e4nksensor",
device_class: "motion",
},
},

View File

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

View File

@@ -1,4 +1,5 @@
// Compat needs to be first import
import "../../src/resources/compatibility";
import { isNavigationClick } from "../../src/common/dom/is-navigation-click";
import { navigate } from "../../src/common/navigate";
import {
@@ -6,14 +7,9 @@ import {
provideHass,
} from "../../src/fake_data/provide_hass";
import { HomeAssistantAppEl } from "../../src/layouts/home-assistant";
import "../../src/resources/compatibility";
import { HomeAssistant } from "../../src/types";
import { selectedDemoConfig } from "./configs/demo-configs";
import { mockAuth } from "./stubs/auth";
import { mockConfigEntries } from "./stubs/config_entries";
import { mockEnergy } from "./stubs/energy";
import { energyEntities } from "./stubs/entities";
import { mockEntityRegistry } from "./stubs/entity_registry";
import { mockEvents } from "./stubs/events";
import { mockFrontend } from "./stubs/frontend";
import { mockHistory } from "./stubs/history";
@@ -24,6 +20,9 @@ import { mockShoppingList } from "./stubs/shopping_list";
import { mockSystemLog } from "./stubs/system_log";
import { mockTemplate } from "./stubs/template";
import { mockTranslations } from "./stubs/translations";
import { mockEnergy } from "./stubs/energy";
import { mockConfig } from "./stubs/config";
import { energyEntities } from "./stubs/entities";
class HaDemo extends HomeAssistantAppEl {
protected async _initializeHass() {
@@ -52,36 +51,8 @@ class HaDemo extends HomeAssistantAppEl {
mockMediaPlayer(hass);
mockFrontend(hass);
mockEnergy(hass);
mockConfig(hass);
mockPersistentNotification(hass);
mockConfigEntries(hass);
mockEntityRegistry(hass, [
{
config_entry_id: "co2signal",
device_id: "co2signal",
area_id: null,
disabled_by: null,
entity_id: "sensor.co2_intensity",
name: null,
icon: null,
platform: "co2signal",
hidden_by: null,
entity_category: null,
has_entity_name: false,
},
{
config_entry_id: "co2signal",
device_id: "co2signal",
area_id: null,
disabled_by: null,
entity_id: "sensor.grid_fossil_fuel_percentage",
name: null,
icon: null,
platform: "co2signal",
hidden_by: null,
entity_category: null,
has_entity_name: false,
},
]);
hass.addEntities(energyEntities());

41
demo/src/stubs/config.ts Normal file
View File

@@ -0,0 +1,41 @@
import { MockHomeAssistant } from "../../../src/fake_data/provide_hass";
export const mockConfig = (hass: MockHomeAssistant) => {
hass.mockAPI("config/config_entries/entry?domain=co2signal", () => [
{
entry_id: "co2signal",
domain: "co2signal",
title: "CO2 Signal",
source: "user",
state: "loaded",
supports_options: false,
supports_unload: true,
pref_disable_new_entities: false,
pref_disable_polling: false,
disabled_by: null,
reason: null,
},
]);
hass.mockWS("config/entity_registry/list", () => [
{
config_entry_id: "co2signal",
device_id: "co2signal",
area_id: null,
disabled_by: null,
entity_id: "sensor.co2_intensity",
name: null,
icon: null,
platform: "co2signal",
},
{
config_entry_id: "co2signal",
device_id: "co2signal",
area_id: null,
disabled_by: null,
entity_id: "sensor.grid_fossil_fuel_percentage",
name: null,
icon: null,
platform: "co2signal",
},
]);
};

View File

@@ -1,20 +0,0 @@
import { MockHomeAssistant } from "../../../src/fake_data/provide_hass";
export const mockConfigEntries = (hass: MockHomeAssistant) => {
hass.mockWS("config_entries/get", () => [
{
entry_id: "co2signal",
domain: "co2signal",
title: "CO2 Signal",
source: "user",
state: "loaded",
supports_options: false,
supports_remove_device: false,
supports_unload: true,
pref_disable_new_entities: false,
pref_disable_polling: false,
disabled_by: null,
reason: null,
},
]);
};

View File

@@ -4,6 +4,4 @@ import type { MockHomeAssistant } from "../../../src/fake_data/provide_hass";
export const mockEntityRegistry = (
hass: MockHomeAssistant,
data: EntityRegistryEntry[] = []
) => {
hass.mockWS("config/entity_registry/list", () => data);
};
) => hass.mockWS("config/entity_registry/list", () => data);

View File

@@ -8,7 +8,7 @@ module.exports = [
{
category: "lovelace",
// Label for in the sidebar
header: "Dashboards",
header: "Lovelace",
// Specify order of pages. Any pages in the category folder but not listed here will
// automatically be added after the pages listed here.
pages: ["introduction"],
@@ -34,7 +34,7 @@ module.exports = [
},
{
category: "misc",
header: "Miscellaneous",
header: "Miscelaneous",
},
{
category: "brand",

View File

@@ -5,7 +5,7 @@ import { html, css, LitElement, PropertyValues } from "lit";
import { customElement, property, query } from "lit/decorators";
import "../../src/components/ha-icon-button";
import "../../src/managers/notification-manager";
import { HaExpansionPanel } from "../../src/components/ha-expansion-panel";
import "../../src/components/ha-expansion-panel";
import { haStyle } from "../../src/resources/styles";
import { PAGES, SIDEBAR } from "../build/import-pages";
import { dynamicElement } from "../../src/common/dom/dynamic-element-directive";
@@ -174,10 +174,9 @@ class HaGallery extends LitElement {
const menuItem = this.shadowRoot!.querySelector(
`a[href="#${this._page}"]`
)!;
// Make sure section is expanded
if (menuItem.parentElement instanceof HaExpansionPanel) {
menuItem.parentElement.expanded = true;
if (menuItem.parentElement instanceof HTMLDetailsElement) {
menuItem.parentElement.open = true;
}
}

View File

@@ -1,9 +1,7 @@
import { dump } from "js-yaml";
import { html, css, LitElement, TemplateResult } from "lit";
import { customElement, property, state } from "lit/decorators";
import { customElement, property } from "lit/decorators";
import "../../../../src/components/ha-card";
import "../../../../src/components/ha-yaml-editor";
import { Action } from "../../../../src/data/script";
import { describeAction } from "../../../../src/data/script_i18n";
import { getEntity } from "../../../../src/fake_data/entity";
import { provideHass } from "../../../../src/fake_data/provide_hass";
@@ -90,15 +88,6 @@ const ACTIONS = [
then: [{ delay: "00:00:01" }],
else: [{ delay: "00:00:05" }],
},
{
if: [{ condition: "state" }],
then: [{ delay: "00:00:01" }],
},
{
if: [{ condition: "state" }, { condition: "state" }],
then: [{ delay: "00:00:01" }],
else: [{ delay: "00:00:05" }],
},
{
choose: [
{
@@ -114,38 +103,16 @@ const ACTIONS = [
},
];
const initialAction: Action = {
service: "light.turn_on",
target: {
entity_id: "light.kitchen",
},
};
@customElement("demo-automation-describe-action")
export class DemoAutomationDescribeAction extends LitElement {
@property({ attribute: false }) hass!: HomeAssistant;
@state() _action = initialAction;
protected render(): TemplateResult {
if (!this.hass) {
return html``;
}
return html`
<ha-card header="Actions">
<div class="action">
<span>
${this._action
? describeAction(this.hass, this._action)
: "<invalid YAML>"}
</span>
<ha-yaml-editor
label="Action Config"
.defaultValue=${initialAction}
@value-changed=${this._dataChanged}
></ha-yaml-editor>
</div>
${ACTIONS.map(
(conf) => html`
<div class="action">
@@ -165,11 +132,6 @@ export class DemoAutomationDescribeAction extends LitElement {
hass.addEntities(ENTITIES);
}
private _dataChanged(ev: CustomEvent): void {
ev.stopPropagation();
this._action = ev.detail.isValid ? ev.detail.value : undefined;
}
static get styles() {
return css`
ha-card {
@@ -185,9 +147,6 @@ export class DemoAutomationDescribeAction extends LitElement {
span {
margin-right: 16px;
}
ha-yaml-editor {
width: 50%;
}
`;
}
}

View File

@@ -1,81 +1,31 @@
import { dump } from "js-yaml";
import { css, html, LitElement, TemplateResult } from "lit";
import { customElement, property, state } from "lit/decorators";
import { html, css, LitElement, TemplateResult } from "lit";
import { customElement } from "lit/decorators";
import "../../../../src/components/ha-card";
import "../../../../src/components/ha-yaml-editor";
import { Condition } from "../../../../src/data/automation";
import { describeCondition } from "../../../../src/data/automation_i18n";
import { getEntity } from "../../../../src/fake_data/entity";
import { provideHass } from "../../../../src/fake_data/provide_hass";
import { HomeAssistant } from "../../../../src/types";
const ENTITIES = [
getEntity("light", "kitchen", "on", {
friendly_name: "Kitchen Light",
}),
getEntity("device_tracker", "person", "home", {
friendly_name: "Person",
}),
getEntity("zone", "home", "", {
friendly_name: "Home",
}),
];
const conditions = [
{ condition: "and" },
{ condition: "not" },
{ condition: "or" },
{ condition: "state", entity_id: "light.kitchen", state: "on" },
{
condition: "numeric_state",
entity_id: "light.kitchen",
attribute: "brightness",
below: 80,
above: 20,
},
{ condition: "state" },
{ condition: "numeric_state" },
{ condition: "sun", after: "sunset" },
{ condition: "sun", after: "sunrise", offset: "-01:00" },
{ condition: "zone", entity_id: "device_tracker.person", zone: "zone.home" },
{ condition: "sun", after: "sunrise" },
{ condition: "zone" },
{ condition: "time" },
{ condition: "template" },
];
const initialCondition: Condition = {
condition: "state",
entity_id: "light.kitchen",
state: "on",
};
@customElement("demo-automation-describe-condition")
export class DemoAutomationDescribeCondition extends LitElement {
@property({ attribute: false }) hass!: HomeAssistant;
@state() _condition = initialCondition;
protected render(): TemplateResult {
if (!this.hass) {
return html``;
}
return html`
<ha-card header="Conditions">
<div class="condition">
<span>
${this._condition
? describeCondition(this._condition, this.hass)
: "<invalid YAML>"}
</span>
<ha-yaml-editor
label="Condition Config"
.defaultValue=${initialCondition}
@value-changed=${this._dataChanged}
></ha-yaml-editor>
</div>
${conditions.map(
(conf) => html`
<div class="condition">
<span>${describeCondition(conf as any, this.hass)}</span>
<span>${describeCondition(conf as any)}</span>
<pre>${dump(conf)}</pre>
</div>
`
@@ -84,18 +34,6 @@ export class DemoAutomationDescribeCondition extends LitElement {
`;
}
protected firstUpdated(changedProps) {
super.firstUpdated(changedProps);
const hass = provideHass(this);
hass.updateTranslations(null, "en");
hass.addEntities(ENTITIES);
}
private _dataChanged(ev: CustomEvent): void {
ev.stopPropagation();
this._condition = ev.detail.isValid ? ev.detail.value : undefined;
}
static get styles() {
return css`
ha-card {
@@ -111,9 +49,6 @@ export class DemoAutomationDescribeCondition extends LitElement {
span {
margin-right: 16px;
}
ha-yaml-editor {
width: 50%;
}
`;
}
}

View File

@@ -1,92 +1,34 @@
import { dump } from "js-yaml";
import { css, html, LitElement, TemplateResult } from "lit";
import { customElement, property, state } from "lit/decorators";
import { html, css, LitElement, TemplateResult } from "lit";
import { customElement } from "lit/decorators";
import "../../../../src/components/ha-card";
import "../../../../src/components/ha-yaml-editor";
import { Trigger } from "../../../../src/data/automation";
import { describeTrigger } from "../../../../src/data/automation_i18n";
import { getEntity } from "../../../../src/fake_data/entity";
import { provideHass } from "../../../../src/fake_data/provide_hass";
import { HomeAssistant } from "../../../../src/types";
const ENTITIES = [
getEntity("light", "kitchen", "on", {
friendly_name: "Kitchen Light",
}),
getEntity("person", "person", "", {
friendly_name: "Person",
}),
getEntity("zone", "home", "", {
friendly_name: "Home",
}),
];
const triggers = [
{ platform: "state", entity_id: "light.kitchen", from: "off", to: "on" },
{ platform: "state" },
{ platform: "mqtt" },
{
platform: "geo_location",
source: "test_source",
zone: "zone.home",
event: "enter",
},
{ platform: "homeassistant", event: "start" },
{
platform: "numeric_state",
entity_id: "light.kitchen",
attribute: "brightness",
below: 80,
above: 20,
},
{ platform: "sun", event: "sunset" },
{ platform: "geo_location" },
{ platform: "homeassistant" },
{ platform: "numeric_state" },
{ platform: "sun" },
{ platform: "time_pattern" },
{ platform: "webhook" },
{
platform: "zone",
entity_id: "person.person",
zone: "zone.home",
event: "enter",
},
{ platform: "zone" },
{ platform: "tag" },
{ platform: "time", at: "15:32" },
{ platform: "time" },
{ platform: "template" },
{ platform: "event", event_type: "homeassistant_started" },
{ platform: "event" },
];
const initialTrigger: Trigger = {
platform: "state",
entity_id: "light.kitchen",
};
@customElement("demo-automation-describe-trigger")
export class DemoAutomationDescribeTrigger extends LitElement {
@property({ attribute: false }) hass!: HomeAssistant;
@state() _trigger = initialTrigger;
protected render(): TemplateResult {
if (!this.hass) {
return html``;
}
return html`
<ha-card header="Triggers">
<div class="trigger">
<span>
${this._trigger
? describeTrigger(this._trigger, this.hass)
: "<invalid YAML>"}
</span>
<ha-yaml-editor
label="Trigger Config"
.defaultValue=${initialTrigger}
@value-changed=${this._dataChanged}
></ha-yaml-editor>
</div>
${triggers.map(
(conf) => html`
<div class="trigger">
<span>${describeTrigger(conf as any, this.hass)}</span>
<span>${describeTrigger(conf as any)}</span>
<pre>${dump(conf)}</pre>
</div>
`
@@ -95,18 +37,6 @@ export class DemoAutomationDescribeTrigger extends LitElement {
`;
}
protected firstUpdated(changedProps) {
super.firstUpdated(changedProps);
const hass = provideHass(this);
hass.updateTranslations(null, "en");
hass.addEntities(ENTITIES);
}
private _dataChanged(ev: CustomEvent): void {
ev.stopPropagation();
this._trigger = ev.detail.isValid ? ev.detail.value : undefined;
}
static get styles() {
return css`
ha-card {
@@ -122,9 +52,6 @@ export class DemoAutomationDescribeTrigger extends LitElement {
span {
margin-right: 16px;
}
ha-yaml-editor {
width: 50%;
}
`;
}
}

View File

@@ -1,32 +0,0 @@
---
title: Dialgos
subtitle: Dialogs provide important prompts in a user flow.
---
# Material Desing 3
Our dialogs are based on the latest version of Material Design. Specs and guidelines can be found on it's [website](https://m3.material.io/components/dialogs/overview).
# Highlighted guidelines
## Content
* A best practice is to always use a title, even if it is optional by Material guidelines.
* People mainly read the title and a button. Put the most important information in those two.
* Try to avoid user generated content in the title, this could make the title unreadable long.
* If users become unsure, they read the description. Make sure this explains what will happen.
* Strive for minimalism.
## Buttons and X-icon
* Keep the labels short, for example `Save`, `Delete`, `Enable`.
* Dialog with actions must always have a discard button. On desktop a `Cancel` button and X-icon, on mobile only the X-icon.
* Destructive actions should be a red warning button.
* Alert or confirmation dialogs only have buttons and no X-icon.
* Try to avoid three buttons in one dialog. Especially when you leave the dialog task unfinished.
## Example
### Confirmation dialog
> **Delete dashboard?**
>
> Dashboard [dashboard name] will be permanently deleted from Home Assistant.
>
> Cancel / Delete

View File

@@ -3,13 +3,6 @@ title: Alerts
subtitle: An alert displays a short, important message in a way that attracts the user's attention without interrupting the user's task.
---
<style>
ha-alert {
display: block;
margin: 4px 0;
}
</style>
# Alert `<ha-alert>`
The alert offers four severity levels that set a distinctive icon and color.

View File

@@ -1,5 +0,0 @@
---
title: Expansion Panel
---
Expansion panel following all the ARIA guidelines.

View File

@@ -1,157 +0,0 @@
import { mdiPacMan } from "@mdi/js";
import { css, html, LitElement, TemplateResult } from "lit";
import { customElement } from "lit/decorators";
import "../../../../src/components/ha-card";
import "../../../../src/components/ha-expansion-panel";
import "../../../../src/components/ha-markdown";
import "../../components/demo-black-white-row";
import { LONG_TEXT } from "../../data/text";
const SHORT_TEXT = LONG_TEXT.substring(0, 113);
const SAMPLES: {
template: (slot: string, leftChevron: boolean) => TemplateResult;
}[] = [
{
template(slot, leftChevron) {
return html`
<ha-expansion-panel
slot=${slot}
.leftChevron=${leftChevron}
header="Attr header"
>
${SHORT_TEXT}
</ha-expansion-panel>
`;
},
},
{
template(slot, leftChevron) {
return html`
<ha-expansion-panel
slot=${slot}
.leftChevron=${leftChevron}
header="Attr header"
secondary="Attr secondary"
>
${SHORT_TEXT}
</ha-expansion-panel>
`;
},
},
{
template(slot, leftChevron) {
return html`
<ha-expansion-panel
slot=${slot}
.leftChevron=${leftChevron}
.header=${"Prop header"}
>
${SHORT_TEXT}
</ha-expansion-panel>
`;
},
},
{
template(slot, leftChevron) {
return html`
<ha-expansion-panel
slot=${slot}
.leftChevron=${leftChevron}
.header=${"Prop header"}
.secondary=${"Prop secondary"}
>
${SHORT_TEXT}
</ha-expansion-panel>
`;
},
},
{
template(slot, leftChevron) {
return html`
<ha-expansion-panel
slot=${slot}
.leftChevron=${leftChevron}
.header=${"Prop header"}
>
<span slot="secondary">Slot Secondary</span>
${SHORT_TEXT}
</ha-expansion-panel>
`;
},
},
{
template(slot, leftChevron) {
return html`
<ha-expansion-panel slot=${slot} .leftChevron=${leftChevron}>
<span slot="header">Slot header</span>
${SHORT_TEXT}
</ha-expansion-panel>
`;
},
},
{
template(slot, leftChevron) {
return html`
<ha-expansion-panel slot=${slot} .leftChevron=${leftChevron}>
<span slot="header">Slot header with actions</span>
<ha-icon-button
slot="icons"
label="Some Action"
.path=${mdiPacMan}
></ha-icon-button>
${SHORT_TEXT}
</ha-expansion-panel>
`;
},
},
{
template(slot, leftChevron) {
return html`
<ha-expansion-panel
slot=${slot}
.leftChevron=${leftChevron}
header="Attr Header with actions"
>
<ha-icon-button
slot="icons"
label="Some Action"
.path=${mdiPacMan}
></ha-icon-button>
${SHORT_TEXT}
</ha-expansion-panel>
`;
},
},
];
@customElement("demo-components-ha-expansion-panel")
export class DemoHaExpansionPanel extends LitElement {
protected render(): TemplateResult {
return html`
${SAMPLES.map(
(sample) => html`
<demo-black-white-row>
${["light", "dark"].map((slot) =>
sample.template(slot, slot === "dark")
)}
</demo-black-white-row>
`
)}
`;
}
static get styles() {
return css`
ha-expansion-panel {
margin: -16px;
}
`;
}
}
declare global {
interface HTMLElementTagNameMap {
"demo-components-ha-expansion-panel": DemoHaExpansionPanel;
}
}

View File

@@ -3,7 +3,6 @@ import "@material/mwc-button";
import { html, LitElement, TemplateResult } from "lit";
import { customElement, state } from "lit/decorators";
import { mockAreaRegistry } from "../../../../demo/src/stubs/area_registry";
import { mockConfigEntries } from "../../../../demo/src/stubs/config_entries";
import { mockDeviceRegistry } from "../../../../demo/src/stubs/device_registry";
import { mockEntityRegistry } from "../../../../demo/src/stubs/entity_registry";
import { mockHassioSupervisor } from "../../../../demo/src/stubs/hassio_supervisor";
@@ -21,22 +20,16 @@ const ENTITIES = [
}),
getEntity("media_player", "livingroom", "playing", {
friendly_name: "Livingroom",
media_content_type: "music",
device_class: "tv",
}),
getEntity("media_player", "lounge", "idle", {
friendly_name: "Lounge",
supported_features: 444983,
device_class: "speaker",
}),
getEntity("light", "bedroom", "on", {
friendly_name: "Bedroom",
effect: "colorloop",
effect_list: ["colorloop", "random"],
}),
getEntity("switch", "coffee", "off", {
friendly_name: "Coffee",
device_class: "switch",
}),
];
@@ -148,13 +141,7 @@ const SCHEMAS: {
selector: { attribute: { entity_id: "" } },
context: { filter_entity: "entity" },
},
{
name: "State",
selector: { state: { entity_id: "" } },
context: { filter_entity: "entity", filter_attribute: "Attribute" },
},
{ name: "Device", selector: { device: {} } },
{ name: "Config entry", selector: { config_entry: {} } },
{ name: "Duration", selector: { duration: {} } },
{ name: "area", selector: { area: {} } },
{ name: "target", selector: { target: {} } },
@@ -436,7 +423,6 @@ class DemoHaForm extends LitElement {
hass.addEntities(ENTITIES);
mockEntityRegistry(hass);
mockDeviceRegistry(hass, DEVICES);
mockConfigEntries(hass);
mockAreaRegistry(hass, AREAS);
mockHassioSupervisor(hass);
}

View File

@@ -3,7 +3,6 @@ import "@material/mwc-button";
import { css, html, LitElement, TemplateResult } from "lit";
import { customElement, state } from "lit/decorators";
import { mockAreaRegistry } from "../../../../demo/src/stubs/area_registry";
import { mockConfigEntries } from "../../../../demo/src/stubs/config_entries";
import { mockDeviceRegistry } from "../../../../demo/src/stubs/device_registry";
import { mockEntityRegistry } from "../../../../demo/src/stubs/entity_registry";
import { mockHassioSupervisor } from "../../../../demo/src/stubs/hassio_supervisor";
@@ -116,19 +115,11 @@ const SCHEMAS: {
name: "One of each",
input: {
entity: { name: "Entity", selector: { entity: {} } },
state: {
name: "State",
selector: { state: { entity_id: "alarm_control_panel.alarm" } },
},
attribute: {
name: "Attribute",
selector: { attribute: { entity_id: "" } },
},
device: { name: "Device", selector: { device: {} } },
config_entry: {
name: "Integration",
selector: { config_entry: {} },
},
duration: { name: "Duration", selector: { duration: {} } },
addon: { name: "Addon", selector: { addon: {} } },
area: { name: "Area", selector: { area: {} } },
@@ -285,7 +276,6 @@ class DemoHaSelector extends LitElement implements ProvideHassElement {
hass.addEntities(ENTITIES);
mockEntityRegistry(hass);
mockDeviceRegistry(hass, DEVICES);
mockConfigEntries(hass);
mockAreaRegistry(hass, AREAS);
mockHassioSupervisor(hass);
hass.mockWS("auth/sign_path", (params) => params);

View File

@@ -31,7 +31,7 @@ const ENTITIES = [
friendly_name: "Office Light",
}),
getEntity("fan", "kitchen", "on", {
friendly_name: "Kitchen Fan",
friendly_name: "Second Office Fan",
}),
getEntity("binary_sensor", "kitchen_door", "on", {
friendly_name: "Office Door",
@@ -102,7 +102,7 @@ class DemoArea extends LitElement {
picture: "/images/office.jpg",
},
{
name: "Kitchen",
name: "Second Office",
area_id: "kitchen",
picture: "/images/kitchen.png",
},

View File

@@ -75,10 +75,6 @@ const ENTITIES = [
timestamp: 1641801600,
friendly_name: "Date and Time",
}),
getEntity("sensor", "humidity", "23.2", {
friendly_name: "Humidity",
unit_of_measurement: "%",
}),
getEntity("input_select", "dropdown", "Soda", {
friendly_name: "Dropdown",
options: ["Soda", "Beer", "Wine"],
@@ -146,7 +142,6 @@ const CONFIGS = [
- light.non_existing
- climate.ecobee
- input_number.number
- sensor.humidity
`,
},
{

View File

@@ -1,11 +1,11 @@
---
title: Introduction
---
Dashboards have many different cards. Each card allows the user to tell
Lovelace has many different cards. Each card allows the user to tell
a different story about what is going on in their house. These cards
are very customizable, as no household is the same.
This gallery helps our developers and designers to see all the
different states that each card can be in.
Check [the Dashboards documentation](https://www.home-assistant.io/dashboards/) for instructions on how to get started with Dashboards.
Check [the Lovelace documentation](https://www.home-assistant.io/lovelace) for instructions on how to get started with Lovelace.

View File

@@ -194,7 +194,6 @@ const createEntityRegistryEntries = (
name: null,
icon: null,
platform: "updater",
has_entity_name: false,
},
];

View File

@@ -69,7 +69,7 @@ const ENTITIES = [
effect_list: ["random", "colorloop"],
}),
getEntity("light", "color_RGB_light", "on", {
friendly_name: "Color Effects Light",
friendly_name: "Color Effets Light",
brightness: 255,
rgb_color: [30, 100, 255],
supported_features: SUPPORT_EFFECT + SUPPORT_FLASH + SUPPORT_TRANSITION,

View File

@@ -81,10 +81,10 @@ class HassioAddonRepositoryEl extends LitElement {
? this.supervisor.localize(
"common.new_version_available"
)
: this.supervisor.localize("addon.state.installed")
: this.supervisor.localize("addon.installed")
: addon.available
? this.supervisor.localize("addon.state.not_installed")
: this.supervisor.localize("addon.state.not_available")}
? this.supervisor.localize("addon.not_installed")
: this.supervisor.localize("addon.not_available")}
.iconClass=${addon.installed
? addon.update_available
? "update"

View File

@@ -22,10 +22,8 @@ import {
HassioAddonRepository,
reloadHassioAddons,
} from "../../../src/data/hassio/addon";
import { extractApiErrorMessage } from "../../../src/data/hassio/common";
import { StoreAddon } from "../../../src/data/supervisor/store";
import { Supervisor } from "../../../src/data/supervisor/supervisor";
import { showAlertDialog } from "../../../src/dialogs/generic/show-dialog-box";
import "../../../src/layouts/hass-loading-screen";
import "../../../src/layouts/hass-subpage";
import { HomeAssistant, Route } from "../../../src/types";
@@ -61,15 +59,8 @@ class HassioAddonStore extends LitElement {
@state() private _filter?: string;
public async refreshData() {
try {
await reloadHassioAddons(this.hass);
} catch (err) {
showAlertDialog(this, {
text: extractApiErrorMessage(err),
});
} finally {
await this._loadData();
}
await reloadHassioAddons(this.hass);
await this._loadData();
}
protected render(): TemplateResult {

View File

@@ -336,7 +336,7 @@ class HassioAddonConfig extends LitElement {
fireEvent(this, "hass-api-called", eventdata);
} catch (err: any) {
this._error = this.supervisor.localize(
"addon.failed_to_reset",
"addon.common.update_available",
"error",
extractApiErrorMessage(err)
);

View File

@@ -81,7 +81,7 @@ class HassioAddonDocumentationDashboard extends LitElement {
);
} catch (err: any) {
this._error = this.supervisor.localize(
"addon.documentation.get_documentation",
"addon.documentation.get_logs",
"error",
extractApiErrorMessage(err)
);

View File

@@ -75,7 +75,7 @@ class HassioAddonDashboard extends LitElement {
></hass-error-screen>`;
}
if (!this.addon || !this.supervisor?.addon) {
if (!this.addon) {
return html`<hass-loading-screen></hass-loading-screen>`;
}
@@ -209,8 +209,8 @@ class HassioAddonDashboard extends LitElement {
}
if (requestedAddon) {
const store = await fetchSupervisorStore(this.hass);
const validAddon = store.addons.some(
const addonsInfo = await fetchHassioAddonsInfo(this.hass);
const validAddon = addonsInfo.addons.some(
(addon) => addon.slug === requestedAddon
);
if (!validAddon) {
@@ -238,7 +238,7 @@ class HassioAddonDashboard extends LitElement {
if (["uninstall", "install", "update", "start", "stop"].includes(path)) {
fireEvent(this, "supervisor-collection-refresh", {
collection: "addon",
collection: "supervisor",
});
}
@@ -263,10 +263,6 @@ class HassioAddonDashboard extends LitElement {
return;
}
try {
if (!this.supervisor.addon) {
const addonsInfo = await fetchHassioAddonsInfo(this.hass);
fireEvent(this, "supervisor-update", { addon: addonsInfo });
}
this.addon = await fetchAddonInfo(this.hass, this.supervisor, addon);
} catch (err: any) {
this._error = `Error fetching addon info: ${extractApiErrorMessage(err)}`;

View File

@@ -40,7 +40,6 @@ import "../../../../src/components/ha-settings-row";
import "../../../../src/components/ha-svg-icon";
import "../../../../src/components/ha-switch";
import {
AddonCapability,
fetchHassioAddonChangelog,
fetchHassioAddonInfo,
HassioAddonDetails,
@@ -702,7 +701,7 @@ class HassioAddonInfo extends LitElement {
}
private _showMoreInfo(ev): void {
const id = ev.currentTarget.id as AddonCapability;
const id = ev.currentTarget.id;
showHassioMarkdownDialog(this, {
title: this.supervisor.localize(`addon.dashboard.capability.${id}.title`),
content:

View File

@@ -176,7 +176,7 @@ export class HassioBackups extends LitElement {
: supervisorTabs(this.hass)}
.hass=${this.hass}
.localizeFunc=${this.supervisor.localize}
.searchLabel=${this.supervisor.localize("backup.search")}
.searchLabel=${this.supervisor.localize("search")}
.noDataText=${this.supervisor.localize("backup.no_backups")}
.narrow=${this.narrow}
.route=${this.route}
@@ -240,7 +240,7 @@ export class HassioBackups extends LitElement {
: html`
<ha-icon-button
.label=${this.supervisor.localize(
"backup.delete_selected"
"snapshot.delete_selected"
)}
.path=${mdiDelete}
id="delete-btn"

View File

@@ -17,12 +17,9 @@ import {
} from "../../../src/data/hassio/backup";
import { Supervisor } from "../../../src/data/supervisor/supervisor";
import { PolymerChangedEvent } from "../../../src/polymer-types";
import { HomeAssistant, TranslationDict } from "../../../src/types";
import { HomeAssistant } from "../../../src/types";
import "./supervisor-formfield-label";
type BackupOrRestoreKey = keyof TranslationDict["supervisor"]["backup"] &
keyof TranslationDict["ui"]["panel"]["page-onboarding"]["restore"];
interface CheckboxItem {
slug: string;
checked: boolean;
@@ -111,9 +108,9 @@ export class SupervisorBackupContent extends LitElement {
this._focusTarget?.focus();
}
private _localize = (key: BackupOrRestoreKey) =>
this.supervisor?.localize(`backup.${key}`) ||
this.localize!(`ui.panel.page-onboarding.restore.${key}`);
private _localize = (string: string) =>
this.supervisor?.localize(`backup.${string}`) ||
this.localize!(`ui.panel.page-onboarding.restore.${string}`);
protected render(): TemplateResult {
if (!this.onboarding && !this.supervisor) {
@@ -171,24 +168,23 @@ export class SupervisorBackupContent extends LitElement {
: ""}
${this.backupType === "partial"
? html`<div class="partial-picker">
${!this.backup || this.backup.homeassistant
? html`<ha-formfield
.label=${html`<supervisor-formfield-label
label="Home Assistant"
.iconPath=${mdiHomeAssistant}
.version=${this.backup
? this.backup.homeassistant
: this.hass.config.version}
>
</supervisor-formfield-label>`}
>
<ha-checkbox
.checked=${this.homeAssistant}
@change=${this.toggleHomeAssistant}
>
</ha-checkbox>
</ha-formfield>`
: ""}
<ha-formfield
.label=${html`<supervisor-formfield-label
label="Home Assistant"
.iconPath=${mdiHomeAssistant}
.version=${this.backup
? this.backup.homeassistant
: this.hass.config.version}
>
</supervisor-formfield-label>`}
>
<ha-checkbox
.checked=${this.homeAssistant}
@change=${this.toggleHomeAssistant}
>
</ha-checkbox>
</ha-formfield>
${foldersSection?.templates.length
? html`
<ha-formfield

View File

@@ -201,24 +201,26 @@ class HassioBackupDialog
}
if (!this._dialogParams?.onboarding) {
try {
await this.hass!.callApi(
"POST",
this.hass!.callApi(
"POST",
`hassio/${
atLeastVersion(this.hass!.config.version, 2021, 9)
? "backups"
: "snapshots"
}/${this._backup!.slug}/restore/partial`,
backupDetails
);
this.closeDialog();
} catch (error: any) {
this._error = error.body.message;
}
`hassio/${
atLeastVersion(this.hass!.config.version, 2021, 9)
? "backups"
: "snapshots"
}/${this._backup!.slug}/restore/partial`,
backupDetails
).then(
() => {
this.closeDialog();
},
(error) => {
this._error = error.body.message;
}
);
} else {
fireEvent(this, "restoring");
await fetch(`/api/hassio/backups/${this._backup!.slug}/restore/partial`, {
fetch(`/api/hassio/backups/${this._backup!.slug}/restore/partial`, {
method: "POST",
body: JSON.stringify(backupDetails),
});

View File

@@ -4,7 +4,7 @@ import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { customElement, property, state } from "lit/decorators";
import { createCloseHeading } from "../../../../src/components/ha-dialog";
import "../../../../src/components/ha-form/ha-form";
import type { SchemaUnion } from "../../../../src/components/ha-form/types";
import { HaFormSchema } from "../../../../src/components/ha-form/types";
import "../../../../src/components/ha-icon-button";
import "../../../../src/components/ha-settings-row";
import { extractApiErrorMessage } from "../../../../src/data/hassio/common";
@@ -19,7 +19,7 @@ import { haStyle, haStyleDialog } from "../../../../src/resources/styles";
import type { HomeAssistant } from "../../../../src/types";
import { RegistriesDialogParams } from "./show-dialog-registries";
const SCHEMA = [
const SCHEMA: HaFormSchema[] = [
{
name: "registry",
required: true,
@@ -35,7 +35,7 @@ const SCHEMA = [
required: true,
selector: { text: { type: "password" } },
},
] as const;
];
@customElement("dialog-hassio-registries")
class HassioRegistriesDialog extends LitElement {
@@ -135,8 +135,8 @@ class HassioRegistriesDialog extends LitElement {
`;
}
private _computeLabel = (schema: SchemaUnion<typeof SCHEMA>) =>
this.supervisor.localize(`dialog.registries.${schema.name}`);
private _computeLabel = (schema: HaFormSchema) =>
this.supervisor.localize(`dialog.registries.${schema.name}`) || schema.name;
private _valueChanged(ev: CustomEvent) {
this._input = ev.detail.value;

View File

@@ -22,7 +22,6 @@ import {
Supervisor,
SupervisorObject,
supervisorCollection,
SupervisorKeys,
} from "../../src/data/supervisor/supervisor";
import { ProvideHassLitMixin } from "../../src/mixins/provide-hass-lit-mixin";
import { urlSyncMixin } from "../../src/state/url-sync-mixin";
@@ -125,13 +124,9 @@ export class SupervisorBaseElement extends urlSyncMixin(
this.supervisor = {
...this.supervisor,
localize: await computeLocalize<SupervisorKeys>(
this.constructor.prototype,
language,
{
[language]: data,
}
),
localize: await computeLocalize(this.constructor.prototype, language, {
[language]: data,
}),
};
}

View File

@@ -26,7 +26,7 @@ import {
import {
UNHEALTHY_REASON_URL,
UNSUPPORTED_REASON_URL,
} from "../../../src/panels/config/repairs/dialog-system-information";
} from "../../../src/panels/config/system-health/ha-config-system-health";
import { haStyle } from "../../../src/resources/styles";
import { HomeAssistant } from "../../../src/types";
import { bytesToString } from "../../../src/util/bytes-to-string";

View File

@@ -1,11 +1,4 @@
module.exports = {
"*.{js,ts}": [
"prettier --write",
'eslint --ignore-pattern "**/build-scripts/**/*.js" --fix',
],
"!(/translations)*.{json,css,md,html}": "prettier --write",
"translations/*/*.json": (files) =>
'printf "%s\n" "These files should not be modified. Instead, make the necessary modifications in src/translations/en.json. Please see translations/README.md for details." ' +
files.join(" ") +
" >&2 && exit 1",
"*.{js,ts}": 'eslint --ignore-pattern "**/build-scripts/**/*.js" --fix',
"!(/translations)*.{js,ts,json,css,md,html}": "prettier --write",
};

View File

@@ -16,9 +16,6 @@
"lint:lit": "lit-analyzer \"**/src/**/*.ts\" --format markdown --outFile result.md",
"lint": "yarn run lint:eslint && yarn run lint:prettier && yarn run lint:types",
"format": "yarn run format:eslint && yarn run format:prettier",
"postinstall": "husky install",
"prepack": "pinst --disable",
"postpack": "pinst --enable",
"test": "instant-mocha --webpack-config ./test/webpack.config.js --require ./test/setup.js \"test/**/*.ts\""
},
"author": "Paulus Schoutsen <Paulus@PaulusSchoutsen.nl> (http://paulusschoutsen.nl)",
@@ -49,7 +46,6 @@
"@fullcalendar/daygrid": "5.9.0",
"@fullcalendar/interaction": "5.9.0",
"@fullcalendar/list": "5.9.0",
"@fullcalendar/timegrid": "5.9.0",
"@lit-labs/motion": "^1.0.2",
"@lit-labs/virtualizer": "patch:@lit-labs/virtualizer@0.7.0-pre.2#./.yarn/patches/@lit-labs/virtualizer/event-target-shim.patch",
"@material/chips": "14.0.0-canary.261f2db59.0",
@@ -76,8 +72,8 @@
"@material/mwc-textfield": "0.25.3",
"@material/mwc-top-app-bar-fixed": "^0.25.3",
"@material/top-app-bar": "14.0.0-canary.261f2db59.0",
"@mdi/js": "7.0.96",
"@mdi/svg": "7.0.96",
"@mdi/js": "6.7.96",
"@mdi/svg": "6.7.96",
"@polymer/app-layout": "^3.1.0",
"@polymer/iron-flex-layout": "^3.0.1",
"@polymer/iron-icon": "^3.0.1",
@@ -93,8 +89,8 @@
"@polymer/paper-tooltip": "^3.0.1",
"@polymer/polymer": "3.4.1",
"@thomasloven/round-slider": "0.5.4",
"@vaadin/combo-box": "^23.1.5",
"@vaadin/vaadin-themable-mixin": "^23.1.5",
"@vaadin/combo-box": "^23.0.10",
"@vaadin/vaadin-themable-mixin": "^23.0.10",
"@vibrant/color": "^3.2.1-alpha.1",
"@vibrant/core": "^3.2.1-alpha.1",
"@vibrant/quantizer-mmcq": "^3.2.1-alpha.1",
@@ -111,14 +107,15 @@
"deep-freeze": "^0.0.1",
"fuse.js": "^6.0.0",
"google-timezones-json": "^1.0.2",
"hls.js": "^1.2.1",
"home-assistant-js-websocket": "^8.0.0",
"hls.js": "^1.1.5",
"home-assistant-js-websocket": "^7.1.0",
"idb-keyval": "^5.1.3",
"intl-messageformat": "^9.9.1",
"js-yaml": "^4.1.0",
"leaflet": "^1.7.1",
"leaflet-draw": "^1.0.4",
"lit": "^2.1.2",
"lit-vaadin-helpers": "^0.3.0",
"marked": "^4.0.12",
"memoize-one": "^5.2.1",
"node-vibrant": "3.2.1-alpha.1",
@@ -205,9 +202,9 @@
"gulp-rename": "^2.0.0",
"gulp-zopfli-green": "^3.0.1",
"html-minifier": "^4.0.0",
"husky": "^8.0.1",
"husky": "^1.3.1",
"instant-mocha": "^1.3.1",
"lint-staged": "^13.0.3",
"lint-staged": "^11.1.2",
"lit-analyzer": "^1.2.1",
"lodash.template": "^4.5.0",
"magic-string": "^0.25.7",
@@ -216,7 +213,6 @@
"mocha": "^8.4.0",
"object-hash": "^2.0.3",
"open": "^7.0.4",
"pinst": "^3.0.0",
"prettier": "^2.4.1",
"require-dir": "^1.2.0",
"rollup": "^2.8.2",
@@ -249,6 +245,11 @@
"@lit/reactive-element": "1.2.1"
},
"main": "src/home-assistant.js",
"husky": {
"hooks": {
"pre-commit": "lint-staged"
}
},
"prettier": {
"trailingComma": "es5",
"arrowParens": "always"

View File

@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "home-assistant-frontend"
version = "20220816.0"
version = "20220629.0"
license = {text = "Apache-2.0"}
description = "The Home Assistant frontend"
readme = "README.md"
@@ -23,3 +23,8 @@ 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);
}
function nightly() {
return `${today()}.dev`;
}
const methods = {
patch,
today,
auto,
nightly,
};
async function main(args) {
@@ -62,11 +57,7 @@ async function main(args) {
console.log("Current version:", version);
console.log("New version:", newVersion);
fs.writeFileSync(
"pyproject.toml",
setup.replace(version, newVersion),
"utf-8"
);
fs.writeFileSync("pyproject.toml", setup.replace(version, newVersion), "utf-8");
if (!commit) {
return;

View File

@@ -314,8 +314,7 @@ class HaAuthFlow extends litLocalizeLiteMixin(LitElement) {
}
private _computeStepDescription(step: DataEntryFlowStepForm) {
const resourceKey =
`ui.panel.page-authorize.form.providers.${step.handler[0]}.step.${step.step_id}.description` as const;
const resourceKey = `ui.panel.page-authorize.form.providers.${step.handler[0]}.step.${step.step_id}.description`;
const args: string[] = [];
const placeholders = step.description_placeholders || {};
Object.keys(placeholders).forEach((key) => {

View File

@@ -47,7 +47,7 @@ import {
mdiRobotVacuum,
mdiScriptText,
mdiSineWave,
mdiMicrophoneMessage,
mdiTextToSpeech,
mdiThermometer,
mdiThermostat,
mdiTimerOutline,
@@ -74,9 +74,8 @@ export const FIXED_DOMAIN_ICONS = {
camera: mdiVideo,
climate: mdiThermostat,
configurator: mdiCog,
conversation: mdiMicrophoneMessage,
conversation: mdiTextToSpeech,
counter: mdiCounter,
demo: mdiHomeAssistant,
fan: mdiFan,
google_assistant: mdiGoogleAssistant,
group: mdiGoogleCirclesCommunities,
@@ -98,7 +97,6 @@ export const FIXED_DOMAIN_ICONS = {
proximity: mdiAppleSafari,
remote: mdiRemote,
scene: mdiPalette,
schedule: mdiCalendarClock,
script: mdiScriptText,
select: mdiFormatListBulleted,
sensor: mdiEye,
@@ -167,6 +165,46 @@ export const DOMAINS_WITH_CARD = [
"water_heater",
];
/** Domains with separate more info dialog. */
export const DOMAINS_WITH_MORE_INFO = [
"alarm_control_panel",
"automation",
"camera",
"climate",
"configurator",
"counter",
"cover",
"fan",
"group",
"humidifier",
"input_datetime",
"light",
"lock",
"media_player",
"person",
"remote",
"script",
"scene",
"sun",
"timer",
"update",
"vacuum",
"water_heater",
"weather",
];
/** Domains that do not show the default more info dialog content (e.g. the attribute section)
* and do not have a separate more info (so not in DOMAINS_WITH_MORE_INFO). */
export const DOMAINS_HIDE_DEFAULT_MORE_INFO = [
"input_number",
"input_select",
"input_text",
"number",
"scene",
"update",
"select",
];
/** Domains that render an input element instead of a text value when displayed in a row.
* Those rows should then not show a cursor pointer when hovered (which would normally
* be the default) unless the element itself enforces it (e.g. a button). Also those elements
@@ -198,6 +236,9 @@ export const DOMAINS_INPUT_ROW = [
"vacuum",
];
/** Domains that should have the history hidden in the more info dialog. */
export const DOMAINS_MORE_INFO_NO_HISTORY = ["camera", "configurator"];
/** States that we consider "off". */
export const STATES_OFF = ["closed", "locked", "off"];

View File

@@ -1,7 +1,7 @@
import memoizeOne from "memoize-one";
import { FrontendLocaleData } from "../../data/translation";
import { polyfillsLoaded } from "../translations/localize";
import { useAmPm } from "./use_am_pm";
import { polyfillsLoaded } from "../translations/localize";
if (__BUILD__ === "latest" && polyfillsLoaded) {
await polyfillsLoaded;
@@ -28,28 +28,6 @@ const formatDateTimeMem = memoizeOne(
)
);
// Aug 9, 8:23 AM
export const formatShortDateTime = (
dateObj: Date,
locale: FrontendLocaleData
) => formatShortDateTimeMem(locale).format(dateObj);
const formatShortDateTimeMem = memoizeOne(
(locale: FrontendLocaleData) =>
new Intl.DateTimeFormat(
locale.language === "en" && !useAmPm(locale)
? "en-u-hc-h23"
: locale.language,
{
month: "short",
day: "numeric",
hour: useAmPm(locale) ? "numeric" : "2-digit",
minute: "2-digit",
hour12: useAmPm(locale),
}
)
);
// August 9, 2021, 8:23:15 AM
export const formatDateTimeWithSeconds = (
dateObj: Date,

View File

@@ -1,7 +1,7 @@
import memoizeOne from "memoize-one";
import { FrontendLocaleData } from "../../data/translation";
import { polyfillsLoaded } from "../translations/localize";
import { useAmPm } from "./use_am_pm";
import { polyfillsLoaded } from "../translations/localize";
if (__BUILD__ === "latest" && polyfillsLoaded) {
await polyfillsLoaded;
@@ -64,17 +64,3 @@ const formatTimeWeekdayMem = memoizeOne(
}
)
);
// 21:15
export const formatTime24h = (dateObj: Date) =>
formatTime24hMem().format(dateObj);
const formatTime24hMem = memoizeOne(
() =>
// en-GB to fix Chrome 24:59 to 0:59 https://stackoverflow.com/a/60898146
new Intl.DateTimeFormat("en-GB", {
hour: "numeric",
minute: "2-digit",
hour12: false,
})
);

View File

@@ -76,11 +76,7 @@ class Storage {
public setValue(storageKey: string, value: any): any {
this._storage[storageKey] = value;
try {
if (value === undefined) {
window.localStorage.removeItem(storageKey);
} else {
window.localStorage.setItem(storageKey, JSON.stringify(value));
}
window.localStorage.setItem(storageKey, JSON.stringify(value));
} catch (err: any) {
// Safari in private mode doesn't allow localstorage
}

View File

@@ -5,7 +5,8 @@ export type LeafletModuleType = typeof import("leaflet");
export type LeafletDrawModuleType = typeof import("leaflet-draw");
export const setupLeafletMap = async (
mapElement: HTMLElement
mapElement: HTMLElement,
darkMode?: boolean
): Promise<[Map, LeafletModuleType, TileLayer]> => {
if (!mapElement.parentNode) {
throw new Error("Cannot setup Leaflet map on disconnected element");
@@ -22,7 +23,7 @@ export const setupLeafletMap = async (
mapElement.parentNode.appendChild(style);
map.setView([52.3731339, 4.8903147], 13);
const tileLayer = createTileLayer(Leaflet).addTo(map);
const tileLayer = createTileLayer(Leaflet, Boolean(darkMode)).addTo(map);
return [map, Leaflet, tileLayer];
};
@@ -30,19 +31,23 @@ export const setupLeafletMap = async (
export const replaceTileLayer = (
leaflet: LeafletModuleType,
map: Map,
tileLayer: TileLayer
tileLayer: TileLayer,
darkMode: boolean
): TileLayer => {
map.removeLayer(tileLayer);
tileLayer = createTileLayer(leaflet);
tileLayer = createTileLayer(leaflet, darkMode);
tileLayer.addTo(map);
return tileLayer;
};
const createTileLayer = (leaflet: LeafletModuleType): TileLayer =>
const createTileLayer = (
leaflet: LeafletModuleType,
darkMode: boolean
): TileLayer =>
leaflet.tileLayer(
`https://basemaps.cartocdn.com/rastertiles/voyager/{z}/{x}/{y}${
leaflet.Browser.retina ? "@2x.png" : ".png"
}`,
`https://{s}.basemaps.cartocdn.com/${
darkMode ? "dark_all" : "light_all"
}/{z}/{x}/{y}${leaflet.Browser.retina ? "@2x.png" : ".png"}`,
{
attribution:
'&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a>, &copy; <a href="https://carto.com/attributions">CARTO</a>',

View File

@@ -64,12 +64,9 @@ export const computeStateDisplayFromEntityAttributes = (
// fallback to default
}
}
const unit = !attributes.unit_of_measurement
? ""
: attributes.unit_of_measurement === "%"
? "%"
: ` ${attributes.unit_of_measurement}`;
return `${formatNumber(state, locale)}${unit}`;
return `${formatNumber(state, locale)}${
attributes.unit_of_measurement ? " " + attributes.unit_of_measurement : ""
}`;
}
const domain = computeDomain(entityId);

View File

@@ -8,7 +8,6 @@ import {
mdiCalendar,
mdiCast,
mdiCastConnected,
mdiCastOff,
mdiChartSankey,
mdiCheckCircleOutline,
mdiClock,
@@ -26,15 +25,7 @@ import {
mdiPowerPlug,
mdiPowerPlugOff,
mdiRestart,
mdiSpeaker,
mdiSpeakerOff,
mdiSpeakerPause,
mdiSpeakerPlay,
mdiSwapHorizontal,
mdiTelevision,
mdiTelevisionOff,
mdiTelevisionPause,
mdiTelevisionPlay,
mdiToggleSwitchVariant,
mdiToggleSwitchVariantOff,
mdiWeatherNight,
@@ -136,40 +127,7 @@ export const domainIconWithoutDefault = (
}
case "media_player":
switch (stateObj?.attributes.device_class) {
case "speaker":
switch (compareState) {
case "playing":
return mdiSpeakerPlay;
case "paused":
return mdiSpeakerPause;
case "off":
return mdiSpeakerOff;
default:
return mdiSpeaker;
}
case "tv":
switch (compareState) {
case "playing":
return mdiTelevisionPlay;
case "paused":
return mdiTelevisionPause;
case "off":
return mdiTelevisionOff;
default:
return mdiTelevision;
}
default:
switch (compareState) {
case "playing":
case "paused":
return mdiCastConnected;
case "off":
return mdiCastOff;
default:
return mdiCast;
}
}
return compareState === "playing" ? mdiCastConnected : mdiCast;
case "switch":
switch (stateObj?.attributes.device_class) {

View File

@@ -1,277 +0,0 @@
import { HassEntity } from "home-assistant-js-websocket";
import { computeStateDomain } from "./compute_state_domain";
import { UNAVAILABLE_STATES } from "../../data/entity";
const FIXED_DOMAIN_STATES = {
alarm_control_panel: [
"armed_away",
"armed_custom_bypass",
"armed_home",
"armed_night",
"armed_vacation",
"arming",
"disarmed",
"disarming",
"pending",
"triggered",
],
automation: ["on", "off"],
binary_sensor: ["on", "off"],
button: [],
calendar: ["on", "off"],
camera: ["idle", "recording", "streaming"],
cover: ["closed", "closing", "open", "opening"],
device_tracker: ["home", "not_home"],
fan: ["on", "off"],
humidifier: ["on", "off"],
input_boolean: ["on", "off"],
input_button: [],
light: ["on", "off"],
lock: ["jammed", "locked", "locking", "unlocked", "unlocking"],
media_player: ["idle", "off", "paused", "playing", "standby"],
person: ["home", "not_home"],
remote: ["on", "off"],
scene: [],
schedule: ["on", "off"],
script: ["on", "off"],
siren: ["on", "off"],
sun: ["above_horizon", "below_horizon"],
switch: ["on", "off"],
update: ["on", "off"],
vacuum: ["cleaning", "docked", "error", "idle", "paused", "returning"],
weather: [
"clear-night",
"cloudy",
"exceptional",
"fog",
"hail",
"lightning-rainy",
"lightning",
"partlycloudy",
"pouring",
"rainy",
"snowy-rainy",
"snowy",
"sunny",
"windy-variant",
"windy",
],
};
const FIXED_DOMAIN_ATTRIBUTE_STATES = {
alarm_control_panel: {
code_format: ["number", "text"],
},
binary_sensor: {
device_class: [
"battery",
"battery_charging",
"co",
"cold",
"connectivity",
"door",
"garage_door",
"gas",
"heat",
"light",
"lock",
"moisture",
"motion",
"moving",
"occupancy",
"opening",
"plug",
"power",
"presence",
"problem",
"running",
"safety",
"smoke",
"sound",
"tamper",
"update",
"vibration",
"window",
],
},
button: {
device_class: ["restart", "update"],
},
camera: {
frontend_stream_type: ["hls", "web_rtc"],
},
climate: {
hvac_action: ["off", "idle", "heating", "cooling", "drying", "fan"],
},
cover: {
device_class: [
"awning",
"blind",
"curtain",
"damper",
"door",
"garage",
"gate",
"shade",
"shutter",
"window",
],
},
humidifier: {
device_class: ["humidifier", "dehumidifier"],
},
media_player: {
device_class: ["tv", "speaker", "receiver"],
media_content_type: [
"app",
"channel",
"episode",
"game",
"image",
"movie",
"music",
"playlist",
"tvshow",
"url",
"video",
],
},
number: {
device_class: ["temperature"],
},
sensor: {
device_class: [
"apparent_power",
"aqi",
"battery",
"carbon_dioxide",
"carbon_monoxide",
"current",
"date",
"duration",
"energy",
"frequency",
"gas",
"humidity",
"illuminance",
"monetary",
"nitrogen_dioxide",
"nitrogen_monoxide",
"nitrous_oxide",
"ozone",
"pm1",
"pm10",
"pm25",
"power_factor",
"power",
"pressure",
"reactive_power",
"signal_strength",
"sulphur_dioxide",
"temperature",
"timestamp",
"volatile_organic_compounds",
"voltage",
],
state_class: ["measurement", "total", "total_increasing"],
},
switch: {
device_class: ["outlet", "switch"],
},
update: {
device_class: ["firmware"],
},
water_heater: {
away_mode: ["on", "off"],
},
};
export const getStates = (
state: HassEntity,
attribute: string | undefined = undefined
): string[] => {
const domain = computeStateDomain(state);
const result: string[] = [];
if (!attribute && domain in FIXED_DOMAIN_STATES) {
result.push(...FIXED_DOMAIN_STATES[domain]);
} else if (
attribute &&
domain in FIXED_DOMAIN_ATTRIBUTE_STATES &&
attribute in FIXED_DOMAIN_ATTRIBUTE_STATES[domain]
) {
result.push(...FIXED_DOMAIN_ATTRIBUTE_STATES[domain][attribute]);
}
// Dynamic values based on the entities
switch (domain) {
case "climate":
if (!attribute) {
result.push(...state.attributes.hvac_modes);
} else if (attribute === "fan_mode") {
result.push(...state.attributes.fan_modes);
} else if (attribute === "preset_mode") {
result.push(...state.attributes.preset_modes);
} else if (attribute === "swing_mode") {
result.push(...state.attributes.swing_modes);
}
break;
case "device_tracker":
case "person":
if (!attribute) {
result.push("home", "not_home");
}
break;
case "fan":
if (attribute === "preset_mode") {
result.push(...state.attributes.preset_modes);
}
break;
case "humidifier":
if (attribute === "mode") {
result.push(...state.attributes.available_modes);
}
break;
case "input_select":
case "select":
if (!attribute) {
result.push(...state.attributes.options);
}
break;
case "light":
if (attribute === "effect") {
result.push(...state.attributes.effect_list);
} else if (attribute === "color_mode") {
result.push(...state.attributes.color_modes);
}
break;
case "media_player":
if (attribute === "sound_mode") {
result.push(...state.attributes.sound_mode_list);
} else if (attribute === "source") {
result.push(...state.attributes.source_list);
}
break;
case "remote":
if (attribute === "current_activity") {
result.push(...state.attributes.activity_list);
}
break;
case "vacuum":
if (attribute === "fan_speed") {
result.push(...state.attributes.fan_speed_list);
}
break;
case "water_heater":
if (!attribute || attribute === "operation_mode") {
result.push(...state.attributes.operation_list);
}
break;
}
if (!attribute) {
// All entities can have unavailable states
result.push(...UNAVAILABLE_STATES);
}
return [...new Set(result)];
};

View File

@@ -1,88 +0,0 @@
import { html } from "lit";
import { getConfigEntries } from "../../data/config_entries";
import { showConfirmationDialog } from "../../dialogs/generic/show-dialog-box";
import { showZWaveJSAddNodeDialog } from "../../panels/config/integrations/integration-panels/zwave_js/show-dialog-zwave_js-add-node";
import type { HomeAssistant } from "../../types";
import { documentationUrl } from "../../util/documentation-url";
import { isComponentLoaded } from "../config/is_component_loaded";
import { fireEvent } from "../dom/fire_event";
import { navigate } from "../navigate";
export const protocolIntegrationPicked = async (
element: HTMLElement,
hass: HomeAssistant,
slug: string
) => {
if (slug === "zwave_js") {
const entries = await getConfigEntries(hass, {
domain: "zwave_js",
});
if (!entries.length) {
// If the component isn't loaded, ask them to load the integration first
showConfirmationDialog(element, {
text: hass.localize(
"ui.panel.config.integrations.config_flow.missing_zwave_zigbee",
{
integration: "Z-Wave",
supported_hardware_link: html`<a
href=${documentationUrl(hass, "/docs/z-wave/controllers")}
target="_blank"
rel="noreferrer"
>${hass.localize(
"ui.panel.config.integrations.config_flow.supported_hardware"
)}</a
>`,
}
),
confirmText: hass.localize(
"ui.panel.config.integrations.config_flow.proceed"
),
confirm: () => {
fireEvent(element, "handler-picked", {
handler: "zwave_js",
});
},
});
return;
}
showZWaveJSAddNodeDialog(element, {
entry_id: entries[0].entry_id,
});
} else if (slug === "zha") {
// If the component isn't loaded, ask them to load the integration first
if (!isComponentLoaded(hass, "zha")) {
showConfirmationDialog(element, {
text: hass.localize(
"ui.panel.config.integrations.config_flow.missing_zwave_zigbee",
{
integration: "Zigbee",
supported_hardware_link: html`<a
href=${documentationUrl(
hass,
"/integrations/zha/#known-working-zigbee-radio-modules"
)}
target="_blank"
rel="noreferrer"
>${hass.localize(
"ui.panel.config.integrations.config_flow.supported_hardware"
)}</a
>`,
}
),
confirmText: hass.localize(
"ui.panel.config.integrations.config_flow.proceed"
),
confirm: () => {
fireEvent(element, "handler-picked", {
handler: "zha",
});
},
});
return;
}
navigate("/config/zha/add");
}
};

View File

@@ -3,66 +3,10 @@ import { shouldPolyfill as shouldPolyfillPluralRules } from "@formatjs/intl-plur
import { shouldPolyfill as shouldPolyfillRelativeTime } from "@formatjs/intl-relativetimeformat/lib/should-polyfill";
import { shouldPolyfill as shouldPolyfillDateTime } from "@formatjs/intl-datetimeformat/lib/should-polyfill";
import IntlMessageFormat from "intl-messageformat";
import { Resources, TranslationDict } from "../../types";
import { Resources } from "../../types";
import { getLocalLanguage } from "../../util/common-translation";
// Exclude some patterns from key type checking for now
// These are intended to be removed as errors are fixed
// Fixing component category will require tighter definition of types from backend and/or web socket
export type LocalizeKeys =
| FlattenObjectKeys<Omit<TranslationDict, "supervisor">>
| `panel.${string}`
| `state.${string}`
| `state_attributes.${string}`
| `state_badge.${string}`
| `ui.card.alarm_control_panel.${string}`
| `ui.card.weather.attributes.${string}`
| `ui.card.weather.cardinal_direction.${string}`
| `ui.components.logbook.${string}`
| `ui.components.selectors.file.${string}`
| `ui.dialogs.entity_registry.editor.${string}`
| `ui.dialogs.more_info_control.vacuum.${string}`
| `ui.dialogs.options_flow.loading.${string}`
| `ui.dialogs.quick-bar.commands.${string}`
| `ui.dialogs.repair_flow.loading.${string}`
| `ui.dialogs.unhealthy.reason.${string}`
| `ui.dialogs.unsupported.reason.${string}`
| `ui.panel.config.${string}.${"caption" | "description"}`
| `ui.panel.config.automation.${string}`
| `ui.panel.config.dashboard.${string}`
| `ui.panel.config.devices.${string}`
| `ui.panel.config.energy.${string}`
| `ui.panel.config.helpers.${string}`
| `ui.panel.config.info.${string}`
| `ui.panel.config.integrations.${string}`
| `ui.panel.config.logs.${string}`
| `ui.panel.config.lovelace.${string}`
| `ui.panel.config.network.${string}`
| `ui.panel.config.scene.${string}`
| `ui.panel.config.url.${string}`
| `ui.panel.config.zha.${string}`
| `ui.panel.config.zwave_js.${string}`
| `ui.panel.developer-tools.tabs.${string}`
| `ui.panel.lovelace.card.${string}`
| `ui.panel.lovelace.editor.${string}`
| `ui.panel.page-authorize.form.${string}`
| `component.${string}`;
// Tweaked from https://www.raygesualdo.com/posts/flattening-object-keys-with-typescript-types
export type FlattenObjectKeys<
T extends Record<string, any>,
Key extends keyof T = keyof T
> = Key extends string
? T[Key] extends Record<string, unknown>
? `${Key}.${FlattenObjectKeys<T[Key]>}`
: `${Key}`
: never;
export type LocalizeFunc<Keys extends string = LocalizeKeys> = (
key: Keys,
...args: any[]
) => string;
export type LocalizeFunc = (key: string, ...args: any[]) => string;
interface FormatType {
[format: string]: any;
}
@@ -121,12 +65,12 @@ export const polyfillsLoaded =
* }
*/
export const computeLocalize = async <Keys extends string = LocalizeKeys>(
export const computeLocalize = async (
cache: any,
language: string,
resources: Resources,
formats?: FormatsType
): Promise<LocalizeFunc<Keys>> => {
): Promise<LocalizeFunc> => {
if (polyfillsLoaded) {
await polyfillsLoaded;
}

View File

@@ -188,10 +188,6 @@ export default class HaChartBase extends LitElement {
ChartConstructor.defaults.color = computedStyles.getPropertyValue(
"--secondary-text-color"
);
ChartConstructor.defaults.font.family =
computedStyles.getPropertyValue("--mdc-typography-body1-font-family") ||
computedStyles.getPropertyValue("--mdc-typography-font-family") ||
"Roboto, Noto, sans-serif";
this.chart = new ChartConstructor(ctx, {
type: this.chartType,
@@ -380,7 +376,6 @@ export default class HaChartBase extends LitElement {
.chartTooltip .title {
text-align: center;
font-weight: 500;
direction: ltr;
}
.chartTooltip .footer {
font-weight: 500;

View File

@@ -15,13 +15,13 @@ import {
import { customElement, property, state } from "lit/decorators";
import { getGraphColorByIndex } from "../../common/color/colors";
import { isComponentLoaded } from "../../common/config/is_component_loaded";
import { computeStateName } from "../../common/entity/compute_state_name";
import {
formatNumber,
numberFormatToLocale,
} from "../../common/number/format_number";
import {
getStatisticIds,
getStatisticLabel,
Statistics,
statisticsHaveType,
StatisticsMetaData,
@@ -233,18 +233,24 @@ class StatisticsChart extends LitElement {
const names = this.names || {};
statisticsData.forEach((stats) => {
const firstStat = stats[0];
let name = names[firstStat.statistic_id];
if (!name) {
const entityState = this.hass.states[firstStat.statistic_id];
if (entityState) {
name = computeStateName(entityState);
} else {
name = firstStat.statistic_id;
}
}
const meta = this.statisticIds!.find(
(stat) => stat.statistic_id === firstStat.statistic_id
);
let name = names[firstStat.statistic_id];
if (!name) {
name = getStatisticLabel(this.hass, firstStat.statistic_id, meta);
}
if (!this.unit) {
if (unit === undefined) {
unit = meta?.display_unit_of_measurement;
} else if (unit !== meta?.display_unit_of_measurement) {
unit = meta?.unit_of_measurement;
} else if (unit !== meta?.unit_of_measurement) {
unit = null;
}
}

View File

@@ -221,10 +221,6 @@ class DateRangePickerElement extends WrappedElement {
.calendar-table {
padding: 0 !important;
}
.daterangepicker.ltr {
direction: ltr;
text-align: left;
}
`;
const shadowRoot = this.shadowRoot!;
shadowRoot.appendChild(style);

View File

@@ -1,7 +1,7 @@
import "@material/mwc-button/mwc-button";
import { UnsubscribeFunc } from "home-assistant-js-websocket";
import { html, LitElement, PropertyValues, TemplateResult } from "lit";
import { ComboBoxLitRenderer } from "@vaadin/combo-box/lit";
import { ComboBoxLitRenderer } from "lit-vaadin-helpers";
import { customElement, property, state } from "lit/decorators";
import memoizeOne from "memoize-one";
import { fireEvent } from "../../common/dom/fire_event";

View File

@@ -172,7 +172,8 @@ export abstract class HaDeviceAutomationPicker<
static get styles(): CSSResultGroup {
return css`
ha-select {
display: block;
width: 100%;
margin-top: 4px;
}
`;
}

View File

@@ -1,7 +1,7 @@
import "@material/mwc-list/mwc-list-item";
import { UnsubscribeFunc } from "home-assistant-js-websocket";
import { html, LitElement, PropertyValues, TemplateResult } from "lit";
import { ComboBoxLitRenderer } from "@vaadin/combo-box/lit";
import { ComboBoxLitRenderer } from "lit-vaadin-helpers";
import { customElement, property, query, state } from "lit/decorators";
import memoizeOne from "memoize-one";
import { fireEvent } from "../../common/dom/fire_event";

View File

@@ -15,14 +15,6 @@ class HaEntityAttributePicker extends LitElement {
@property() public entityId?: string;
/**
* List of attributes to be hidden.
* @type {Array}
* @attr hide-attributes
*/
@property({ type: Array, attribute: "hide-attributes" })
public hideAttributes?: string[];
@property({ type: Boolean }) public autofocus = false;
@property({ type: Boolean }) public disabled = false;
@@ -50,12 +42,10 @@ class HaEntityAttributePicker extends LitElement {
if (changedProps.has("_opened") && this._opened) {
const state = this.entityId ? this.hass.states[this.entityId] : undefined;
(this._comboBox as any).items = state
? Object.keys(state.attributes)
.filter((key) => !this.hideAttributes?.includes(key))
.map((key) => ({
value: key,
label: formatAttributeName(key),
}))
? Object.keys(state.attributes).map((key) => ({
value: key,
label: formatAttributeName(key),
}))
: [];
}
}
@@ -68,7 +58,7 @@ class HaEntityAttributePicker extends LitElement {
return html`
<ha-combo-box
.hass=${this.hass}
.value=${this.value ? formatAttributeName(this.value) : ""}
.value=${this.value || ""}
.autofocus=${this.autofocus}
.label=${this.label ??
this.hass.localize(

View File

@@ -1,7 +1,7 @@
import "@material/mwc-list/mwc-list-item";
import { HassEntity } from "home-assistant-js-websocket";
import { html, LitElement, PropertyValues, TemplateResult } from "lit";
import { ComboBoxLitRenderer } from "@vaadin/combo-box/lit";
import { ComboBoxLitRenderer } from "lit-vaadin-helpers";
import { customElement, property, query, state } from "lit/decorators";
import memoizeOne from "memoize-one";
import { fireEvent } from "../../common/dom/fire_event";

View File

@@ -1,111 +0,0 @@
import { HassEntity } from "home-assistant-js-websocket";
import { html, LitElement, PropertyValues, TemplateResult } from "lit";
import { customElement, property, query } from "lit/decorators";
import { computeStateDisplay } from "../../common/entity/compute_state_display";
import { PolymerChangedEvent } from "../../polymer-types";
import { getStates } from "../../common/entity/get_states";
import { HomeAssistant } from "../../types";
import "../ha-combo-box";
import type { HaComboBox } from "../ha-combo-box";
export type HaEntityPickerEntityFilterFunc = (entityId: HassEntity) => boolean;
@customElement("ha-entity-state-picker")
class HaEntityStatePicker extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property() public entityId?: string;
@property() public attribute?: string;
@property({ type: Boolean }) public autofocus = false;
@property({ type: Boolean }) public disabled = false;
@property({ type: Boolean }) public required = false;
@property({ type: Boolean, attribute: "allow-custom-value" })
public allowCustomValue;
@property() public label?: string;
@property() public value?: string;
@property() public helper?: string;
@property({ type: Boolean }) private _opened = false;
@query("ha-combo-box", true) private _comboBox!: HaComboBox;
protected shouldUpdate(changedProps: PropertyValues) {
return !(!changedProps.has("_opened") && this._opened);
}
protected updated(changedProps: PropertyValues) {
if (changedProps.has("_opened") && this._opened) {
const state = this.entityId ? this.hass.states[this.entityId] : undefined;
(this._comboBox as any).items =
this.entityId && state
? getStates(state, this.attribute).map((key) => ({
value: key,
label: !this.attribute
? computeStateDisplay(
this.hass.localize,
state,
this.hass.locale,
key
)
: key,
}))
: [];
}
}
protected render(): TemplateResult {
if (!this.hass) {
return html``;
}
return html`
<ha-combo-box
.hass=${this.hass}
.value=${this.value
? this.entityId && this.hass.states[this.entityId]
? computeStateDisplay(
this.hass.localize,
this.hass.states[this.entityId],
this.hass.locale,
this.value
)
: this.value
: ""}
.autofocus=${this.autofocus}
.label=${this.label ??
this.hass.localize("ui.components.entity.entity-state-picker.state")}
.disabled=${this.disabled || !this.entityId}
.required=${this.required}
.helper=${this.helper}
.allowCustomValue=${this.allowCustomValue}
item-value-path="value"
item-label-path="label"
@opened-changed=${this._openedChanged}
@value-changed=${this._valueChanged}
>
</ha-combo-box>
`;
}
private _openedChanged(ev: PolymerChangedEvent<boolean>) {
this._opened = ev.detail.value;
}
private _valueChanged(ev: PolymerChangedEvent<string>) {
this.value = ev.detail.value;
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-entity-state-picker": HaEntityStatePicker;
}
}

View File

@@ -1,6 +1,6 @@
import { HassEntity } from "home-assistant-js-websocket";
import { html, LitElement, PropertyValues, TemplateResult } from "lit";
import { ComboBoxLitRenderer } from "@vaadin/combo-box/lit";
import { ComboBoxLitRenderer } from "lit-vaadin-helpers";
import { customElement, property, query, state } from "lit/decorators";
import memoizeOne from "memoize-one";
import { fireEvent } from "../../common/dom/fire_event";
@@ -31,23 +31,12 @@ export class HaStatisticPicker extends LitElement {
@property({ type: Boolean }) public disabled?: boolean;
/**
* Show only statistics natively stored with these units of measurements.
* Show only statistics with these unit of measuments.
* @type {Array}
* @attr include-statistics-unit-of-measurement
* @attr include-unit-of-measurement
*/
@property({
type: Array,
attribute: "include-statistics-unit-of-measurement",
})
public includeStatisticsUnitOfMeasurement?: string[];
/**
* Show only statistics displayed with these units of measurements.
* @type {Array}
* @attr include-display-unit-of-measurement
*/
@property({ type: Array, attribute: "include-display-unit-of-measurement" })
public includeDisplayUnitOfMeasurement?: string[];
@property({ type: Array, attribute: "include-unit-of-measurement" })
public includeUnitOfMeasurement?: string[];
/**
* Show only statistics with these device classes.
@@ -97,8 +86,7 @@ export class HaStatisticPicker extends LitElement {
private _getStatistics = memoizeOne(
(
statisticIds: StatisticsMetaData[],
includeStatisticsUnitOfMeasurement?: string[],
includeDisplayUnitOfMeasurement?: string[],
includeUnitOfMeasurement?: string[],
includeDeviceClasses?: string[],
entitiesOnly?: boolean
): Array<{ id: string; name: string; state?: HassEntity }> => {
@@ -113,18 +101,9 @@ export class HaStatisticPicker extends LitElement {
];
}
if (includeStatisticsUnitOfMeasurement) {
if (includeUnitOfMeasurement) {
statisticIds = statisticIds.filter((meta) =>
includeStatisticsUnitOfMeasurement.includes(
meta.statistics_unit_of_measurement
)
);
}
if (includeDisplayUnitOfMeasurement) {
statisticIds = statisticIds.filter((meta) =>
includeDisplayUnitOfMeasurement.includes(
meta.display_unit_of_measurement
)
includeUnitOfMeasurement.includes(meta.unit_of_measurement)
);
}
@@ -205,8 +184,7 @@ export class HaStatisticPicker extends LitElement {
if (this.hasUpdated) {
(this.comboBox as any).items = this._getStatistics(
this.statisticIds!,
this.includeStatisticsUnitOfMeasurement,
this.includeDisplayUnitOfMeasurement,
this.includeUnitOfMeasurement,
this.includeDeviceClasses,
this.entitiesOnly
);
@@ -214,8 +192,7 @@ export class HaStatisticPicker extends LitElement {
this.updateComplete.then(() => {
(this.comboBox as any).items = this._getStatistics(
this.statisticIds!,
this.includeStatisticsUnitOfMeasurement,
this.includeDisplayUnitOfMeasurement,
this.includeUnitOfMeasurement,
this.includeDeviceClasses,
this.entitiesOnly
);

View File

@@ -1,5 +1,5 @@
import { html, LitElement, TemplateResult } from "lit";
import { ComboBoxLitRenderer } from "@vaadin/combo-box/lit";
import { ComboBoxLitRenderer } from "lit-vaadin-helpers";
import { customElement, property, query, state } from "lit/decorators";
import { isComponentLoaded } from "../common/config/is_component_loaded";
import { fireEvent } from "../common/dom/fire_event";
@@ -84,20 +84,20 @@ class HaAddonPicker extends LitElement {
} else {
showAlertDialog(this, {
title: this.hass.localize(
"ui.components.addon-picker.error.no_supervisor.title"
"ui.componencts.addon-picker.error.no_supervisor.title"
),
text: this.hass.localize(
"ui.components.addon-picker.error.no_supervisor.description"
"ui.componencts.addon-picker.error.no_supervisor.description"
),
});
}
} catch (err: any) {
showAlertDialog(this, {
title: this.hass.localize(
"ui.components.addon-picker.error.fetch_addons.title"
"ui.componencts.addon-picker.error.fetch_addons.title"
),
text: this.hass.localize(
"ui.components.addon-picker.error.fetch_addons.description"
"ui.componencts.addon-picker.error.fetch_addons.description"
),
});
}

View File

@@ -83,6 +83,7 @@ class HaAlert extends LitElement {
position: relative;
padding: 8px;
display: flex;
margin: 4px 0;
}
.issue-type.rtl {
flex-direction: row-reverse;

View File

@@ -1,6 +1,6 @@
import { UnsubscribeFunc } from "home-assistant-js-websocket";
import { html, LitElement, PropertyValues, TemplateResult } from "lit";
import { ComboBoxLitRenderer } from "@vaadin/combo-box/lit";
import { ComboBoxLitRenderer } from "lit-vaadin-helpers";
import { customElement, property, query, state } from "lit/decorators";
import { classMap } from "lit/directives/class-map";
import memoizeOne from "memoize-one";

View File

@@ -76,7 +76,6 @@ class HaAttributes extends LitElement {
css`
.attribute-container {
margin-bottom: 8px;
direction: ltr;
}
.data-entry {
display: flex;

View File

@@ -1,11 +1,10 @@
import "@material/mwc-list/mwc-list-item";
import { css, html, LitElement, TemplateResult } from "lit";
import { customElement, property } from "lit/decorators";
import { ifDefined } from "lit/directives/if-defined";
import { fireEvent } from "../common/dom/fire_event";
import { stopPropagation } from "../common/dom/stop_propagation";
import "./ha-select";
import { HaTextField } from "./ha-textfield";
import "./ha-textfield";
import "./ha-input-helper-text";
export interface TimeChangedEvent {
@@ -37,7 +36,7 @@ export class HaBaseTimeInput extends LitElement {
/**
* determines if inputs are required
*/
@property({ type: Boolean }) public required = false;
@property({ type: Boolean }) public required?: boolean;
/**
* 12 or 24 hr format
@@ -124,6 +123,11 @@ export class HaBaseTimeInput extends LitElement {
*/
@property() amPm: "AM" | "PM" = "AM";
/**
* Formatted time string
*/
@property() value?: string;
protected render(): TemplateResult {
return html`
${this.label
@@ -136,11 +140,11 @@ export class HaBaseTimeInput extends LitElement {
id="day"
type="number"
inputmode="numeric"
.value=${this.days.toFixed()}
.value=${this.days}
.label=${this.dayLabel}
name="days"
@input=${this._valueChanged}
@focusin=${this._onFocus}
@focus=${this._onFocus}
no-spinner
.required=${this.required}
.autoValidate=${this.autoValidate}
@@ -157,16 +161,16 @@ export class HaBaseTimeInput extends LitElement {
id="hour"
type="number"
inputmode="numeric"
.value=${this.hours.toFixed()}
.value=${this.hours}
.label=${this.hourLabel}
name="hours"
@input=${this._valueChanged}
@focusin=${this._onFocus}
@focus=${this._onFocus}
no-spinner
.required=${this.required}
.autoValidate=${this.autoValidate}
maxlength="2"
max=${ifDefined(this._hourMax)}
.max=${this._hourMax}
min="0"
.disabled=${this.disabled}
suffix=":"
@@ -180,7 +184,7 @@ export class HaBaseTimeInput extends LitElement {
.value=${this._formatValue(this.minutes)}
.label=${this.minLabel}
@input=${this._valueChanged}
@focusin=${this._onFocus}
@focus=${this._onFocus}
name="minutes"
no-spinner
.required=${this.required}
@@ -201,7 +205,7 @@ export class HaBaseTimeInput extends LitElement {
.value=${this._formatValue(this.seconds)}
.label=${this.secLabel}
@input=${this._valueChanged}
@focusin=${this._onFocus}
@focus=${this._onFocus}
name="seconds"
no-spinner
.required=${this.required}
@@ -222,7 +226,7 @@ export class HaBaseTimeInput extends LitElement {
.value=${this._formatValue(this.milliseconds, 3)}
.label=${this.millisecLabel}
@input=${this._valueChanged}
@focusin=${this._onFocus}
@focus=${this._onFocus}
name="milliseconds"
no-spinner
.required=${this.required}
@@ -256,10 +260,9 @@ export class HaBaseTimeInput extends LitElement {
`;
}
private _valueChanged(ev: InputEvent) {
const textField = ev.currentTarget as HaTextField;
this[textField.name] =
textField.name === "amPm" ? textField.value : Number(textField.value);
private _valueChanged(ev) {
this[ev.target.name] =
ev.target.name === "amPm" ? ev.target.value : Number(ev.target.value);
const value: TimeChangedEvent = {
hours: this.hours,
minutes: this.minutes,
@@ -274,8 +277,8 @@ export class HaBaseTimeInput extends LitElement {
});
}
private _onFocus(ev: FocusEvent) {
(ev.currentTarget as HaTextField).select();
private _onFocus(ev) {
ev.target.select();
}
/**
@@ -290,7 +293,7 @@ export class HaBaseTimeInput extends LitElement {
*/
private get _hourMax() {
if (this.noHoursLimit) {
return undefined;
return null;
}
if (this.format === 12) {
return 12;

View File

@@ -1,18 +1,13 @@
import "@material/mwc-list/mwc-list-item";
import "./ha-select";
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { customElement, property } from "lit/decorators";
import memoizeOne from "memoize-one";
import { fireEvent } from "../common/dom/fire_event";
import { stopPropagation } from "../common/dom/stop_propagation";
import { stringCompare } from "../common/string/compare";
import {
Blueprint,
BlueprintDomain,
Blueprints,
fetchBlueprints,
} from "../data/blueprint";
import { Blueprint, Blueprints, fetchBlueprints } from "../data/blueprint";
import { HomeAssistant } from "../types";
import "./ha-select";
@customElement("ha-blueprint-picker")
class HaBluePrintPicker extends LitElement {
@@ -22,7 +17,7 @@ class HaBluePrintPicker extends LitElement {
@property() public value = "";
@property() public domain: BlueprintDomain = "automation";
@property() public domain = "automation";
@property() public blueprints?: Blueprints;
@@ -56,7 +51,7 @@ class HaBluePrintPicker extends LitElement {
return html`
<ha-select
.label=${this.label ||
this.hass.localize("ui.components.blueprint-picker.select_blueprint")}
this.hass.localize("ui.components.blueprint-picker.label")}
fixedMenuPosition
naturalMenuWidth
.value=${this.value}
@@ -64,6 +59,11 @@ class HaBluePrintPicker extends LitElement {
@selected=${this._blueprintChanged}
@closed=${stopPropagation}
>
<mwc-list-item value="">
${this.hass.localize(
"ui.components.blueprint-picker.select_blueprint"
)}
</mwc-list-item>
${this._processedBlueprints(this.blueprints).map(
(blueprint) => html`
<mwc-list-item .value=${blueprint.path}>

View File

@@ -2,7 +2,6 @@ import type {
Completion,
CompletionContext,
CompletionResult,
CompletionSource,
} from "@codemirror/autocomplete";
import type { EditorView, KeyBinding, ViewUpdate } from "@codemirror/view";
import { HassEntities } from "home-assistant-js-websocket";
@@ -49,9 +48,6 @@ export class HaCodeEditor extends ReactiveElement {
@property({ type: Boolean, attribute: "autocomplete-entities" })
public autocompleteEntities = false;
@property({ type: Boolean, attribute: "autocomplete-icons" })
public autocompleteIcons = false;
@property() public error = false;
@state() private _value = "";
@@ -164,22 +160,16 @@ export class HaCodeEditor extends ReactiveElement {
),
];
if (!this.readOnly) {
const completionSources: CompletionSource[] = [];
if (this.autocompleteEntities && this.hass) {
completionSources.push(this._entityCompletions.bind(this));
}
if (this.autocompleteIcons) {
completionSources.push(this._mdiCompletions.bind(this));
}
if (completionSources.length > 0) {
extensions.push(
this._loadedCodeMirror.autocompletion({
override: completionSources,
maxRenderedOptions: 10,
})
);
}
if (!this.readOnly && this.autocompleteEntities && this.hass) {
extensions.push(
this._loadedCodeMirror.autocompletion({
override: [
this._entityCompletions.bind(this),
this._mdiCompletions.bind(this),
],
maxRenderedOptions: 10,
})
);
}
this.codemirror = new this._loadedCodeMirror.EditorView({
@@ -209,7 +199,7 @@ export class HaCodeEditor extends ReactiveElement {
private _entityCompletions(
context: CompletionContext
): CompletionResult | null | Promise<CompletionResult | null> {
const entityWord = context.matchBefore(/[a-z_]{3,}\.\w*/);
const entityWord = context.matchBefore(/[a-z_]{3,}\./);
if (
!entityWord ||
@@ -227,7 +217,7 @@ export class HaCodeEditor extends ReactiveElement {
return {
from: Number(entityWord.from),
options: states,
span: /^[a-z_]{3,}\.\w*$/,
span: /^\w*.\w*$/,
};
}
@@ -257,7 +247,7 @@ export class HaCodeEditor extends ReactiveElement {
private async _mdiCompletions(
context: CompletionContext
): Promise<CompletionResult | null> {
const match = context.matchBefore(/mdi:\S*/);
const match = context.matchBefore(/mdi:/);
if (!match || (match.from === match.to && !context.explicit)) {
return null;
@@ -268,7 +258,7 @@ export class HaCodeEditor extends ReactiveElement {
return {
from: Number(match.from),
options: iconItems,
span: /^mdi:\S*$/,
span: /^\w*.\w*$/,
};
}

View File

@@ -9,9 +9,8 @@ import type {
} from "@vaadin/combo-box/vaadin-combo-box-light";
import { registerStyles } from "@vaadin/vaadin-themable-mixin/register-styles";
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { ComboBoxLitRenderer, comboBoxRenderer } from "@vaadin/combo-box/lit";
import { ComboBoxLitRenderer, comboBoxRenderer } from "lit-vaadin-helpers";
import { customElement, property, query } from "lit/decorators";
import { ifDefined } from "lit/directives/if-defined";
import { fireEvent } from "../common/dom/fire_event";
import { HomeAssistant } from "../types";
import "./ha-icon-button";
@@ -73,31 +72,31 @@ export class HaComboBox extends LitElement {
@property({ attribute: "error-message" }) public errorMessage?: string;
@property({ type: Boolean }) public invalid = false;
@property({ type: Boolean }) public invalid?: boolean;
@property({ type: Boolean }) public icon = false;
@property({ type: Boolean }) public icon?: boolean;
@property({ attribute: false }) public items?: any[];
@property() public items?: any[];
@property({ attribute: false }) public filteredItems?: any[];
@property() public filteredItems?: any[];
@property({ attribute: "allow-custom-value", type: Boolean })
public allowCustomValue = false;
public allowCustomValue?: boolean;
@property({ attribute: "item-value-path" }) public itemValuePath = "value";
@property({ attribute: "item-value-path" }) public itemValuePath?: string;
@property({ attribute: "item-label-path" }) public itemLabelPath = "label";
@property({ attribute: "item-label-path" }) public itemLabelPath?: string;
@property({ attribute: "item-id-path" }) public itemIdPath?: string;
@property() public renderer?: ComboBoxLitRenderer<any>;
@property({ type: Boolean }) public disabled = false;
@property({ type: Boolean }) public disabled?: boolean;
@property({ type: Boolean }) public required = false;
@property({ type: Boolean }) public required?: boolean;
@property({ type: Boolean, reflect: true, attribute: "opened" })
public opened?: boolean;
private _opened?: boolean;
@query("vaadin-combo-box-light", true) private _comboBox!: ComboBoxLight;
@@ -150,45 +149,37 @@ export class HaComboBox extends LitElement {
attr-for-value="value"
>
<ha-textfield
label=${ifDefined(this.label)}
placeholder=${ifDefined(this.placeholder)}
?disabled=${this.disabled}
?required=${this.required}
validationMessage=${ifDefined(this.validationMessage)}
.label=${this.label}
.placeholder=${this.placeholder}
.disabled=${this.disabled}
.required=${this.required}
.validationMessage=${this.validationMessage}
.errorMessage=${this.errorMessage}
class="input"
autocapitalize="none"
autocomplete="off"
autocorrect="off"
spellcheck="false"
.suffix=${html`<div
style="width: 28px;"
role="none presentation"
></div>`}
.suffix=${html`<div style="width: 28px;"></div>`}
.icon=${this.icon}
.invalid=${this.invalid}
helper=${ifDefined(this.helper)}
.helper=${this.helper}
helperPersistent
>
<slot name="icon" slot="leadingIcon"></slot>
</ha-textfield>
${this.value
? html`<ha-svg-icon
role="button"
tabindex="-1"
aria-label=${ifDefined(this.hass?.localize("ui.common.clear"))}
aria-label=${this.hass?.localize("ui.components.combo-box.clear")}
class="clear-button"
.path=${mdiClose}
@click=${this._clearValue}
></ha-svg-icon>`
: ""}
<ha-svg-icon
role="button"
tabindex="-1"
aria-label=${ifDefined(this.label)}
aria-expanded=${this.opened ? "true" : "false"}
aria-label=${this.hass?.localize("ui.components.combo-box.show")}
class="toggle-button"
.path=${this.opened ? mdiMenuUp : mdiMenuDown}
.path=${this._opened ? mdiMenuUp : mdiMenuDown}
@click=${this._toggleOpen}
></ha-svg-icon>
</vaadin-combo-box-light>
@@ -208,7 +199,7 @@ export class HaComboBox extends LitElement {
}
private _toggleOpen(ev: Event) {
if (this.opened) {
if (this._opened) {
this._comboBox?.close();
ev.stopPropagation();
} else {
@@ -220,7 +211,7 @@ export class HaComboBox extends LitElement {
const opened = ev.detail.value;
// delay this so we can handle click event before setting _opened
setTimeout(() => {
this.opened = opened;
this._opened = opened;
}, 0);
// @ts-ignore
fireEvent(this, ev.type, ev.detail);

View File

@@ -1,156 +0,0 @@
import "@material/mwc-list/mwc-list-item";
import { html, LitElement, TemplateResult } from "lit";
import { ComboBoxLitRenderer } from "@vaadin/combo-box/lit";
import { customElement, property, query, state } from "lit/decorators";
import { fireEvent } from "../common/dom/fire_event";
import { PolymerChangedEvent } from "../polymer-types";
import { HomeAssistant } from "../types";
import type { HaComboBox } from "./ha-combo-box";
import { ConfigEntry, getConfigEntries } from "../data/config_entries";
import { domainToName } from "../data/integration";
import { caseInsensitiveStringCompare } from "../common/string/compare";
import { brandsUrl } from "../util/brands-url";
import "./ha-combo-box";
export interface ConfigEntryExtended extends ConfigEntry {
localized_domain_name?: string;
}
@customElement("ha-config-entry-picker")
class HaConfigEntryPicker extends LitElement {
public hass!: HomeAssistant;
@property() public integration?: string;
@property() public label?: string;
@property() public value = "";
@property() public helper?: string;
@state() private _configEntries?: ConfigEntryExtended[];
@property({ type: Boolean }) public disabled = false;
@property({ type: Boolean }) public required = false;
@query("ha-combo-box") private _comboBox!: HaComboBox;
public open() {
this._comboBox?.open();
}
public focus() {
this._comboBox?.focus();
}
protected firstUpdated() {
this._getConfigEntries();
}
private _rowRenderer: ComboBoxLitRenderer<ConfigEntryExtended> = (
item
) => html`<mwc-list-item twoline graphic="icon">
<span
>${item.title ||
this.hass.localize(
"ui.panel.config.integrations.config_entry.unnamed_entry"
)}</span
>
<span slot="secondary">${item.localized_domain_name}</span>
<img
slot="graphic"
src=${brandsUrl({
domain: item.domain,
type: "icon",
darkOptimized: this.hass.themes?.darkMode,
})}
referrerpolicy="no-referrer"
@error=${this._onImageError}
@load=${this._onImageLoad}
/>
</mwc-list-item>`;
protected render(): TemplateResult {
if (!this._configEntries) {
return html``;
}
return html`
<ha-combo-box
.hass=${this.hass}
.label=${this.label === undefined && this.hass
? this.hass.localize("ui.components.config-entry-picker.config_entry")
: this.label}
.value=${this._value}
.required=${this.required}
.disabled=${this.disabled}
.helper=${this.helper}
.renderer=${this._rowRenderer}
.items=${this._configEntries}
item-value-path="entry_id"
item-id-path="entry_id"
item-label-path="title"
@value-changed=${this._valueChanged}
></ha-combo-box>
`;
}
private _onImageLoad(ev) {
ev.target.style.visibility = "initial";
}
private _onImageError(ev) {
ev.target.style.visibility = "hidden";
}
private async _getConfigEntries() {
getConfigEntries(this.hass, {
type: "integration",
domain: this.integration,
}).then((configEntries) => {
this._configEntries = configEntries
.map(
(entry: ConfigEntry): ConfigEntryExtended => ({
...entry,
localized_domain_name: domainToName(
this.hass.localize,
entry.domain
),
})
)
.sort((conf1, conf2) =>
caseInsensitiveStringCompare(
conf1.localized_domain_name + conf1.title,
conf2.localized_domain_name + conf2.title
)
);
});
}
private get _value() {
return this.value || "";
}
private _valueChanged(ev: PolymerChangedEvent<string>) {
ev.stopPropagation();
const newValue = ev.detail.value;
if (newValue !== this._value) {
this._setValue(newValue);
}
}
private _setValue(value: string) {
this.value = value;
setTimeout(() => {
fireEvent(this, "value-changed", { value });
fireEvent(this, "change");
}, 0);
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-config-entry-picker": HaConfigEntryPicker;
}
}

View File

@@ -11,7 +11,7 @@ export const createCloseHeading = (
hass: HomeAssistant,
title: string | TemplateResult
) => html`
<div class="header_title">${title}</div>
<span class="header_title">${title}</span>
<ha-icon-button
.label=${hass.localize("ui.dialogs.generic.close")}
.path=${mdiClose}
@@ -40,13 +40,10 @@ export class HaDialog extends DialogBase {
z-index: var(--dialog-z-index, 7);
-webkit-backdrop-filter: var(--dialog-backdrop-filter, none);
backdrop-filter: var(--dialog-backdrop-filter, none);
--mdc-dialog-box-shadow: var(--dialog-box-shadow, none);
--mdc-typography-headline6-font-weight: 400;
--mdc-typography-headline6-font-size: 1.574rem;
}
.mdc-dialog__actions {
justify-content: var(--justify-action-buttons, flex-end);
padding-bottom: max(env(safe-area-inset-bottom), 24px);
padding-bottom: max(env(safe-area-inset-bottom), 8px);
}
.mdc-dialog__actions span:nth-child(1) {
flex: var(--secondary-action-button-flex, unset);
@@ -57,32 +54,28 @@ export class HaDialog extends DialogBase {
.mdc-dialog__container {
align-items: var(--vertial-align-dialog, center);
}
.mdc-dialog__title {
padding: 24px 24px 0 24px;
}
.mdc-dialog__actions {
padding: 0 24px 24px 24px;
}
.mdc-dialog__title::before {
display: block;
height: 0px;
height: 20px;
}
.mdc-dialog .mdc-dialog__content {
position: var(--dialog-content-position, relative);
padding: var(--dialog-content-padding, 24px);
padding: var(--dialog-content-padding, 20px 24px);
}
:host([hideactions]) .mdc-dialog .mdc-dialog__content {
padding-bottom: max(
var(--dialog-content-padding, 24px),
var(--dialog-content-padding, 20px),
env(safe-area-inset-bottom)
);
}
.mdc-dialog .mdc-dialog__surface {
position: var(--dialog-surface-position, relative);
top: var(--dialog-surface-top);
margin-top: var(--dialog-surface-margin-top);
min-height: var(--mdc-dialog-min-height, auto);
border-radius: var(--ha-dialog-border-radius, 28px);
border-radius: var(
--ha-dialog-border-radius,
var(--ha-card-border-radius, 4px)
);
}
:host([flexContent]) .mdc-dialog .mdc-dialog__content {
display: flex;
@@ -96,8 +89,8 @@ export class HaDialog extends DialogBase {
color: inherit;
}
.header_title {
margin-right: 32px;
margin-inline-end: 32px;
margin-right: 40px;
margin-inline-end: 40px;
margin-inline-start: initial;
direction: var(--direction);
}

View File

@@ -14,17 +14,17 @@ export interface HaDurationData {
@customElement("ha-duration-input")
class HaDurationInput extends LitElement {
@property({ attribute: false }) public data?: HaDurationData;
@property({ attribute: false }) public data!: HaDurationData;
@property() public label?: string;
@property() public helper?: string;
@property({ type: Boolean }) public required = false;
@property({ type: Boolean }) public required?: boolean;
@property({ type: Boolean }) public enableMillisecond = false;
@property({ type: Boolean }) public enableMillisecond?: boolean;
@property({ type: Boolean }) public enableDay = false;
@property({ type: Boolean }) public enableDay?: boolean;
@property({ type: Boolean }) public disabled = false;

View File

@@ -14,13 +14,11 @@ import { nextRender } from "../common/util/render-status";
import "./ha-svg-icon";
@customElement("ha-expansion-panel")
export class HaExpansionPanel extends LitElement {
class HaExpansionPanel extends LitElement {
@property({ type: Boolean, reflect: true }) expanded = false;
@property({ type: Boolean, reflect: true }) outlined = false;
@property({ type: Boolean, reflect: true }) leftChevron = false;
@property() header?: string;
@property() secondary?: string;
@@ -31,42 +29,23 @@ export class HaExpansionPanel extends LitElement {
protected render(): TemplateResult {
return html`
<div class="top">
<div
id="summary"
@click=${this._toggleContainer}
@keydown=${this._toggleContainer}
@focus=${this._focusChanged}
@blur=${this._focusChanged}
role="button"
tabindex="0"
aria-expanded=${this.expanded}
aria-controls="sect1"
>
${this.leftChevron
? html`
<ha-svg-icon
.path=${mdiChevronDown}
class="summary-icon ${classMap({ expanded: this.expanded })}"
></ha-svg-icon>
`
: ""}
<slot name="header">
<div class="header">
${this.header}
<slot class="secondary" name="secondary">${this.secondary}</slot>
</div>
</slot>
${!this.leftChevron
? html`
<ha-svg-icon
.path=${mdiChevronDown}
class="summary-icon ${classMap({ expanded: this.expanded })}"
></ha-svg-icon>
`
: ""}
</div>
<slot name="icons"></slot>
<div
id="summary"
@click=${this._toggleContainer}
@keydown=${this._toggleContainer}
role="button"
tabindex="0"
aria-expanded=${this.expanded}
aria-controls="sect1"
>
<slot class="header" name="header">
${this.header}
<slot class="secondary" name="secondary">${this.secondary}</slot>
</slot>
<ha-svg-icon
.path=${mdiChevronDown}
class="summary-icon ${classMap({ expanded: this.expanded })}"
></ha-svg-icon>
</div>
<div
class="container ${classMap({ expanded: this.expanded })}"
@@ -82,35 +61,23 @@ export class HaExpansionPanel extends LitElement {
}
protected willUpdate(changedProps: PropertyValues) {
super.willUpdate(changedProps);
if (changedProps.has("expanded") && this.expanded) {
this._showContent = this.expanded;
setTimeout(() => {
// Verify we're still expanded
if (this.expanded) {
this._container.style.overflow = "initial";
}
}, 300);
}
}
private _handleTransitionEnd() {
this._container.style.removeProperty("height");
this._container.style.overflow = this.expanded ? "initial" : "hidden";
this._showContent = this.expanded;
}
private async _toggleContainer(ev): Promise<void> {
if (ev.defaultPrevented) {
return;
}
if (ev.type === "keydown" && ev.key !== "Enter" && ev.key !== " ") {
return;
}
ev.preventDefault();
const newExpanded = !this.expanded;
fireEvent(this, "expanded-will-change", { expanded: newExpanded });
this._container.style.overflow = "hidden";
if (newExpanded) {
this._showContent = true;
@@ -131,28 +98,12 @@ export class HaExpansionPanel extends LitElement {
fireEvent(this, "expanded-changed", { expanded: this.expanded });
}
private _focusChanged(ev) {
this.shadowRoot!.querySelector(".top")!.classList.toggle(
"focused",
ev.type === "focus"
);
}
static get styles(): CSSResultGroup {
return css`
:host {
display: block;
}
.top {
display: flex;
align-items: center;
}
.top.focused {
background: var(--input-fill-color);
}
:host([outlined]) {
box-shadow: none;
border-width: 1px;
@@ -164,17 +115,7 @@ export class HaExpansionPanel extends LitElement {
border-radius: var(--ha-card-border-radius, 4px);
}
.summary-icon {
margin-left: 8px;
}
:host([leftchevron]) .summary-icon {
margin-left: 0;
margin-right: 8px;
}
#summary {
flex: 1;
display: flex;
padding: var(--expansion-panel-summary-padding, 0 8px);
min-height: 48px;
@@ -185,8 +126,15 @@ export class HaExpansionPanel extends LitElement {
outline: none;
}
#summary:focus {
background: var(--input-fill-color);
}
.summary-icon {
transition: transform 150ms cubic-bezier(0.4, 0, 0.2, 1);
margin-left: auto;
margin-inline-start: auto;
margin-inline-end: initial;
direction: var(--direction);
}
@@ -194,11 +142,6 @@ export class HaExpansionPanel extends LitElement {
transform: rotate(180deg);
}
.header,
::slotted([slot="header"]) {
flex: 1;
}
.container {
padding: var(--expansion-panel-content-padding, 0 8px);
overflow: hidden;
@@ -210,6 +153,10 @@ export class HaExpansionPanel extends LitElement {
height: auto;
}
.header {
display: block;
}
.secondary {
display: block;
color: var(--secondary-text-color);

View File

@@ -6,10 +6,7 @@ export const computeInitialHaFormData = (
): Record<string, any> => {
const data = {};
schema.forEach((field) => {
if (
field.description?.suggested_value !== undefined &&
field.description?.suggested_value !== null
) {
if (field.description?.suggested_value) {
data[field.name] = field.description.suggested_value;
} else if ("default" in field) {
data[field.name] = field.default;
@@ -50,7 +47,6 @@ export const computeInitialHaFormData = (
"text" in selector ||
"addon" in selector ||
"attribute" in selector ||
"file" in selector ||
"icon" in selector ||
"theme" in selector
) {

View File

@@ -3,15 +3,15 @@ import {
CSSResultGroup,
html,
LitElement,
PropertyValues,
TemplateResult,
PropertyValues,
} from "lit";
import { customElement, property, query } from "lit/decorators";
import { fireEvent } from "../../common/dom/fire_event";
import { HaCheckbox } from "../ha-checkbox";
import { HaFormElement, HaFormIntegerData, HaFormIntegerSchema } from "./types";
import "../ha-slider";
import { HaTextField } from "../ha-textfield";
import { HaFormElement, HaFormIntegerData, HaFormIntegerSchema } from "./types";
@customElement("ha-form-integer")
export class HaFormInteger extends LitElement implements HaFormElement {
@@ -105,8 +105,7 @@ export class HaFormInteger extends LitElement implements HaFormElement {
}
return (
(this.schema.description?.suggested_value !== undefined &&
this.schema.description?.suggested_value !== null) ||
this.schema.description?.suggested_value ||
this.schema.default ||
this.schema.valueMin ||
0

View File

@@ -5,9 +5,9 @@ import { HaFormElement, HaFormTimeData, HaFormTimeSchema } from "./types";
@customElement("ha-form-positive_time_period_dict")
export class HaFormTimePeriod extends LitElement implements HaFormElement {
@property({ attribute: false }) public schema!: HaFormTimeSchema;
@property() public schema!: HaFormTimeSchema;
@property({ attribute: false }) public data!: HaFormTimeData;
@property() public data!: HaFormTimeData;
@property() public label!: string;
@@ -25,7 +25,7 @@ export class HaFormTimePeriod extends LitElement implements HaFormElement {
return html`
<ha-duration-input
.label=${this.label}
?required=${this.schema.required}
.required=${this.schema.required}
.data=${this.data}
.disabled=${this.disabled}
></ha-duration-input>

View File

@@ -35,20 +35,20 @@ export class HaForm extends LitElement implements HaFormElement {
@property({ attribute: false }) public data!: HaFormDataContainer;
@property({ attribute: false }) public schema!: readonly HaFormSchema[];
@property({ attribute: false }) public schema!: HaFormSchema[];
@property() public error?: Record<string, string>;
@property({ type: Boolean }) public disabled = false;
@property() public computeError?: (schema: any, error) => string;
@property() public computeError?: (schema: HaFormSchema, error) => string;
@property() public computeLabel?: (
schema: any,
data: HaFormDataContainer
schema: HaFormSchema,
data?: HaFormDataContainer
) => string;
@property() public computeHelper?: (schema: any) => string | undefined;
@property() public computeHelper?: (schema: HaFormSchema) => string;
public focus() {
const root = this.shadowRoot?.querySelector(".root");
@@ -168,7 +168,7 @@ export class HaForm extends LitElement implements HaFormElement {
return this.computeHelper ? this.computeHelper(schema) : "";
}
private _computeError(error, schema: HaFormSchema | readonly HaFormSchema[]) {
private _computeError(error, schema: HaFormSchema | HaFormSchema[]) {
return this.computeError ? this.computeError(error, schema) : error;
}

View File

@@ -31,7 +31,7 @@ export interface HaFormGridSchema extends HaFormBaseSchema {
type: "grid";
name: "";
column_min_width?: string;
schema: readonly HaFormSchema[];
schema: HaFormSchema[];
}
export interface HaFormSelector extends HaFormBaseSchema {
@@ -53,15 +53,12 @@ export interface HaFormIntegerSchema extends HaFormBaseSchema {
export interface HaFormSelectSchema extends HaFormBaseSchema {
type: "select";
options: ReadonlyArray<readonly [string, string]>;
options: Array<[string, string]>;
}
export interface HaFormMultiSelectSchema extends HaFormBaseSchema {
type: "multi_select";
options:
| Record<string, string>
| readonly string[]
| ReadonlyArray<readonly [string, string]>;
options: Record<string, string> | string[] | Array<[string, string]>;
}
export interface HaFormFloatSchema extends HaFormBaseSchema {
@@ -81,12 +78,6 @@ export interface HaFormTimeSchema extends HaFormBaseSchema {
type: "positive_time_period_dict";
}
// Type utility to unionize a schema array by flattening any grid schemas
export type SchemaUnion<
SchemaArray extends readonly HaFormSchema[],
Schema = SchemaArray[number]
> = Schema extends HaFormGridSchema ? SchemaUnion<Schema["schema"]> : Schema;
export interface HaFormDataContainer {
[key: string]: HaFormData;
}
@@ -109,7 +100,7 @@ export type HaFormMultiSelectData = string[];
export type HaFormTimeData = HaDurationData;
export interface HaFormElement extends LitElement {
schema: HaFormSchema | readonly HaFormSchema[];
schema: HaFormSchema | HaFormSchema[];
data?: HaFormDataContainer | HaFormData;
label?: string;
}

View File

@@ -132,9 +132,7 @@ export class Gauge extends LitElement {
this._segment_label
? this._segment_label
: this.valueText || formatNumber(this.value, this.locale)
}${
this._segment_label ? "" : this.label === "%" ? "%" : ` ${this.label}`
}
} ${this._segment_label ? "" : this.label}
</text>
</svg>`;
}

View File

@@ -1,5 +1,5 @@
import { css, html, LitElement, TemplateResult } from "lit";
import { ComboBoxLitRenderer } from "@vaadin/combo-box/lit";
import { ComboBoxLitRenderer } from "lit-vaadin-helpers";
import { customElement, property, query, state } from "lit/decorators";
import { fireEvent } from "../common/dom/fire_event";
import { customIcons } from "../data/custom_icons";

View File

@@ -29,102 +29,7 @@ interface DeprecatedIcon {
};
}
const mdiDeprecatedIcons: DeprecatedIcon = {
"android-messages": {
newName: "message-text",
removeIn: "2022.10",
},
"book-variant-multiple": {
newName: "bookmark-box-multiple",
removeIn: "2022.10",
},
"desktop-mac": {
newName: "monitor",
removeIn: "2022.10",
},
"desktop-mac-dashboard": {
newName: "monitor-dashboard",
removeIn: "2022.10",
},
discord: {
removeIn: "2022.10",
},
"diving-scuba": {
newName: "diving-scuba-mask",
removeIn: "2022.10",
},
"email-send": {
newName: "email-arrow-right",
removeIn: "2022.10",
},
"email-send-outline": {
newName: "email-arrow-right-outline",
removeIn: "2022.10",
},
"email-receive": {
newName: "email-arrow-left",
removeIn: "2022.10",
},
"email-receive-outline": {
newName: "email-arrow-left-outline",
removeIn: "2022.10",
},
"format-textdirection-r-to-l": {
newName: "format-pilcrow-arrow-left",
removeIn: "2022.10",
},
"format-textdirection-l-to-r": {
newName: "format-pilcrow-arrow-right",
removeIn: "2022.10",
},
"google-controller": {
newName: "controller",
removeIn: "2022.10",
},
"google-controller-off": {
newName: "controller-off",
removeIn: "2022.10",
},
"google-home": {
removeIn: "2022.10",
},
lecturn: {
newName: "lectern",
removeIn: "2022.10",
},
receipt: {
newName: "receipt-text",
removeIn: "2022.10",
},
"receipt-outline": {
newName: "receipt-text-outline",
removeIn: "2022.10",
},
"tablet-android": {
newName: "tablet",
removeIn: "2022.10",
},
"text-to-speech": {
newName: "microphone-message",
removeIn: "2022.10",
},
"text-to-speech-off": {
newName: "microphone-message-off",
removeIn: "2022.10",
},
"timeline-help": {
newName: "timeline-question",
removeIn: "2022.10",
},
"timeline-help-outline": {
newName: "timeline-question-outline",
removeIn: "2022.10",
},
"vector-point": {
newName: "vector-point-select",
removeIn: "2022.10",
},
};
const mdiDeprecatedIcons: DeprecatedIcon = {};
const chunks: Chunks = {};

View File

@@ -1,12 +1,11 @@
import { ActionDetail } from "@material/mwc-list/mwc-list";
import "@material/mwc-list/mwc-list";
import "@material/mwc-list/mwc-list-item";
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { customElement, property } from "lit/decorators";
import { ifDefined } from "lit/directives/if-defined";
import { navigate } from "../common/navigate";
import type { PageNavigation } from "../layouts/hass-tabs-subpage";
import type { HomeAssistant } from "../types";
import "./ha-clickable-list-item";
import "./ha-icon-next";
import "./ha-list-item";
import "./ha-svg-icon";
@customElement("ha-navigation-list")
@@ -19,22 +18,17 @@ class HaNavigationList extends LitElement {
@property({ type: Boolean }) public hasSecondary = false;
@property() public label?: string;
public render(): TemplateResult {
return html`
<mwc-list
innerRole="menu"
itemRoles="menuitem"
innerAriaLabel=${ifDefined(this.label)}
@action=${this._handleListAction}
>
<mwc-list>
${this.pages.map(
(page) => html`
<ha-list-item
<ha-clickable-list-item
graphic="avatar"
.twoline=${this.hasSecondary}
.hasMeta=${!this.narrow}
@click=${this._entryClicked}
href=${page.path}
>
<div
slot="graphic"
@@ -50,20 +44,15 @@ class HaNavigationList extends LitElement {
${!this.narrow
? html`<ha-icon-next slot="meta"></ha-icon-next>`
: ""}
</ha-list-item>
</ha-clickable-list-item>
`
)}
</mwc-list>
`;
}
private _handleListAction(ev: CustomEvent<ActionDetail>) {
const path = this.pages[ev.detail.index].path;
if (path.endsWith("#external-app-configuration")) {
this.hass.auth.external!.fireMessage({ type: "config_screen/show" });
} else {
navigate(path);
}
private _entryClicked(ev) {
ev.currentTarget.blur();
}
static styles: CSSResultGroup = css`
@@ -86,9 +75,10 @@ class HaNavigationList extends LitElement {
.icon-background ha-svg-icon {
color: #fff;
}
ha-list-item {
ha-clickable-list-item {
cursor: pointer;
font-size: var(--navigation-list-item-title-font-size);
padding: var(--navigation-list-item-padding) 0;
}
`;
}

View File

@@ -326,9 +326,6 @@ export class HaRelatedItems extends SubscribeMixin(LitElement) {
line-height: var(--paper-font-title_-_line-height);
opacity: var(--dark-primary-opacity);
}
h3:first-child {
margin-top: 0;
}
`;
}
}

View File

@@ -52,11 +52,6 @@ export class HaSelect extends SelectBase {
inset-inline-end: initial;
direction: var(--direction);
}
.mdc-select--filled.mdc-select--with-leading-icon .mdc-floating-label {
inset-inline-start: 48px;
inset-inline-end: initial;
direction: var(--direction);
}
.mdc-select .mdc-select__anchor {
padding-inline-start: 12px;
padding-inline-end: 0px;

View File

@@ -1,9 +1,8 @@
import { HassEntity, UnsubscribeFunc } from "home-assistant-js-websocket";
import { html, LitElement, PropertyValues, TemplateResult } from "lit";
import { UnsubscribeFunc } from "home-assistant-js-websocket";
import { html, LitElement } from "lit";
import { customElement, property, state } from "lit/decorators";
import memoizeOne from "memoize-one";
import type { DeviceRegistryEntry } from "../../data/device_registry";
import { getDeviceIntegrationLookup } from "../../data/device_registry";
import { DeviceRegistryEntry } from "../../data/device_registry";
import {
EntityRegistryEntry,
subscribeEntityRegistry,
@@ -12,11 +11,7 @@ import {
EntitySources,
fetchEntitySourcesWithCache,
} from "../../data/entity_sources";
import type { AreaSelector } from "../../data/selector";
import {
filterSelectorDevices,
filterSelectorEntities,
} from "../../data/selector";
import { AreaSelector } from "../../data/selector";
import { SubscribeMixin } from "../../mixins/subscribe-mixin";
import { HomeAssistant } from "../../types";
import "../ha-area-picker";
@@ -34,15 +29,13 @@ export class HaAreaSelector extends SubscribeMixin(LitElement) {
@property() public helper?: string;
@property({ type: Boolean }) public disabled = false;
@property({ type: Boolean }) public required = true;
@state() private _entitySources?: EntitySources;
@state() private _entities?: EntityRegistryEntry[];
private _deviceIntegrationLookup = memoizeOne(getDeviceIntegrationLookup);
@property({ type: Boolean }) public disabled = false;
@property({ type: Boolean }) public required = true;
public hassSubscribe(): UnsubscribeFunc[] {
return [
@@ -52,7 +45,7 @@ export class HaAreaSelector extends SubscribeMixin(LitElement) {
];
}
protected updated(changedProperties: PropertyValues): void {
protected updated(changedProperties) {
if (
changedProperties.has("selector") &&
(this.selector.area.device?.integration ||
@@ -65,7 +58,7 @@ export class HaAreaSelector extends SubscribeMixin(LitElement) {
}
}
protected render(): TemplateResult {
protected render() {
if (
(this.selector.area.device?.integration ||
this.selector.area.entity?.integration) &&
@@ -84,6 +77,12 @@ export class HaAreaSelector extends SubscribeMixin(LitElement) {
no-add
.deviceFilter=${this._filterDevices}
.entityFilter=${this._filterEntities}
.includeDeviceClasses=${this.selector.area.entity?.device_class
? [this.selector.area.entity.device_class]
: undefined}
.includeDomains=${this.selector.area.entity?.domain
? [this.selector.area.entity.domain]
: undefined}
.disabled=${this.disabled}
.required=${this.required}
></ha-area-picker>
@@ -99,22 +98,27 @@ export class HaAreaSelector extends SubscribeMixin(LitElement) {
no-add
.deviceFilter=${this._filterDevices}
.entityFilter=${this._filterEntities}
.includeDeviceClasses=${this.selector.area.entity?.device_class
? [this.selector.area.entity.device_class]
: undefined}
.includeDomains=${this.selector.area.entity?.domain
? [this.selector.area.entity.domain]
: undefined}
.disabled=${this.disabled}
.required=${this.required}
></ha-areas-picker>
`;
}
private _filterEntities = (entity: HassEntity): boolean => {
if (!this.selector.area.entity) {
return true;
private _filterEntities = (entity: EntityRegistryEntry): boolean => {
const filterIntegration = this.selector.area.entity?.integration;
if (
filterIntegration &&
this._entitySources?.[entity.entity_id]?.domain !== filterIntegration
) {
return false;
}
return filterSelectorEntities(
this.selector.area.entity,
entity,
this._entitySources
);
return true;
};
private _filterDevices = (device: DeviceRegistryEntry): boolean => {
@@ -122,17 +126,47 @@ export class HaAreaSelector extends SubscribeMixin(LitElement) {
return true;
}
const deviceIntegrations =
this._entitySources && this._entities
? this._deviceIntegrationLookup(this._entitySources, this._entities)
: undefined;
const {
manufacturer: filterManufacturer,
model: filterModel,
integration: filterIntegration,
} = this.selector.area.device;
return filterSelectorDevices(
this.selector.area.device,
device,
deviceIntegrations
);
if (filterManufacturer && device.manufacturer !== filterManufacturer) {
return false;
}
if (filterModel && device.model !== filterModel) {
return false;
}
if (filterIntegration && this._entitySources && this._entities) {
const deviceIntegrations = this._deviceIntegrations(
this._entitySources,
this._entities
);
if (!deviceIntegrations?.[device.id]?.includes(filterIntegration)) {
return false;
}
}
return true;
};
private _deviceIntegrations = memoizeOne(
(entitySources: EntitySources, entities: EntityRegistryEntry[]) => {
const deviceIntegrations: Record<string, string[]> = {};
for (const entity of entities) {
const source = entitySources[entity.entity_id];
if (!source?.domain) {
continue;
}
if (!deviceIntegrations[entity.device_id!]) {
deviceIntegrations[entity.device_id!] = [];
}
deviceIntegrations[entity.device_id!].push(source.domain);
}
return deviceIntegrations;
}
);
}
declare global {

View File

@@ -8,9 +8,9 @@ import "../entity/ha-entity-attribute-picker";
@customElement("ha-selector-attribute")
export class HaSelectorAttribute extends SubscribeMixin(LitElement) {
@property({ attribute: false }) public hass!: HomeAssistant;
@property() public hass!: HomeAssistant;
@property({ attribute: false }) public selector!: AttributeSelector;
@property() public selector!: AttributeSelector;
@property() public value?: any;
@@ -22,7 +22,7 @@ export class HaSelectorAttribute extends SubscribeMixin(LitElement) {
@property({ type: Boolean }) public required = true;
@property({ attribute: false }) public context?: {
@property() public context?: {
filter_entity?: string;
};
@@ -32,7 +32,6 @@ export class HaSelectorAttribute extends SubscribeMixin(LitElement) {
.hass=${this.hass}
.entityId=${this.selector.attribute.entity_id ||
this.context?.filter_entity}
.hideAttributes=${this.selector.attribute.hide_attributes}
.value=${this.value}
.label=${this.label}
.helper=${this.helper}

View File

@@ -47,7 +47,7 @@ export class HaColorTempSelector extends LitElement {
static styles = css`
ha-labeled-slider {
--ha-slider-background: -webkit-linear-gradient(
var(--float-end),
right,
rgb(255, 160, 0) 0%,
white 50%,
rgb(166, 209, 255) 100%

View File

@@ -1,47 +0,0 @@
import { css, html, LitElement } from "lit";
import { customElement, property } from "lit/decorators";
import { ConfigEntrySelector } from "../../data/selector";
import { HomeAssistant } from "../../types";
import "../ha-config-entry-picker";
@customElement("ha-selector-config_entry")
export class HaConfigEntrySelector extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public selector!: ConfigEntrySelector;
@property() public value?: any;
@property() public label?: string;
@property() public helper?: string;
@property({ type: Boolean }) public disabled = false;
@property({ type: Boolean }) public required = true;
protected render() {
return html`<ha-config-entry-picker
.hass=${this.hass}
.value=${this.value}
.label=${this.label}
.helper=${this.helper}
.disabled=${this.disabled}
.required=${this.required}
.integration=${this.selector.config_entry.integration}
allow-custom-entity
></ha-config-entry-picker>`;
}
static styles = css`
ha-config-entry-picker {
width: 100%;
}
`;
}
declare global {
interface HTMLElementTagNameMap {
"ha-selector-config_entry": HaConfigEntrySelector;
}
}

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