diff --git a/.eslintrc.json b/.eslintrc.json index 8527576da2..985f1da4fd 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -5,6 +5,7 @@ "plugin:@typescript-eslint/recommended", "plugin:wc/recommended", "plugin:lit/all", + "plugin:lit-a11y/recommended", "prettier" ], "parser": "@typescript-eslint/parser", @@ -58,6 +59,8 @@ "prefer-destructuring": "off", "no-restricted-globals": [2, "event"], "prefer-promise-reject-errors": "off", + "no-unsafe-optional-chaining": "warn", + "prefer-regex-literals": ["warn"], "import/prefer-default-export": "off", "import/no-default-export": "off", "import/no-unresolved": "off", @@ -65,7 +68,10 @@ "import/extensions": [ "error", "ignorePackages", - { "ts": "never", "js": "never" } + { + "ts": "never", + "js": "never" + } ], "no-restricted-syntax": ["error", "LabeledStatement", "WithStatement"], "object-curly-newline": "off", @@ -112,7 +118,15 @@ ], "unused-imports/no-unused-imports": "error", "lit/attribute-value-entities": "off", - "lit/no-template-map": "off" + "lit/no-template-map": "off", + "lit/no-native-attributes": "warn", + "lit/no-this-assign-in-render": "warn", + "lit/prefer-nothing": "warn", + "lit-a11y/click-events-have-key-events": ["off"], + "lit-a11y/no-autofocus": "off", + "lit-a11y/alt-text": "warn", + "lit-a11y/anchor-is-valid": "warn", + "lit-a11y/role-has-required-aria-attrs": "warn" }, "plugins": ["disable", "unused-imports"], "processor": "disable/disable" diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 79c8f5c7c4..99dc4f65c3 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -19,7 +19,3 @@ updates: - dependency-name: "*rollup*" - dependency-name: "@rollup/*" - dependency-name: "serve" - # Wait for fullcalendar v6+ to fix shadow DOM issue - - dependency-name: "@fullcalendar/*" - versions: - - ">=6" diff --git a/.github/workflows/cast_deployment.yaml b/.github/workflows/cast_deployment.yaml index a3ecc20db0..63957be8b1 100644 --- a/.github/workflows/cast_deployment.yaml +++ b/.github/workflows/cast_deployment.yaml @@ -33,9 +33,7 @@ jobs: cache: yarn - name: Install dependencies - run: yarn install - env: - CI: true + run: yarn install --immutable - name: Build Cast run: ./node_modules/.bin/gulp build-cast @@ -71,9 +69,7 @@ jobs: cache: yarn - name: Install dependencies - run: yarn install - env: - CI: true + run: yarn install --immutable - name: Build Cast run: ./node_modules/.bin/gulp build-cast @@ -87,4 +83,4 @@ jobs: args: deploy --dir=cast/dist --prod env: NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }} - NETLIFY_SITE_ID: ${{ secrets.NETLIFY_CAST_SITE_ID }} \ No newline at end of file + NETLIFY_SITE_ID: ${{ secrets.NETLIFY_CAST_SITE_ID }} diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 4df43edc3f..625ff3ddbe 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -15,8 +15,13 @@ env: NODE_OPTIONS: --max_old_space_size=6144 GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + jobs: lint: + name: Lint and check format runs-on: ubuntu-latest steps: - name: Check out files from GitHub @@ -27,20 +32,19 @@ jobs: node-version: ${{ env.NODE_VERSION }} cache: yarn - name: Install dependencies - run: yarn install - env: - CI: true + run: yarn install --immutable + - name: Check for duplicate dependencies + run: yarn dedupe --check - name: Build resources run: ./node_modules/.bin/gulp gen-icons-json build-translations build-locale-data gather-gallery-pages - name: Run eslint - run: yarn run lint:eslint + run: yarn run lint:eslint --quiet - name: Run tsc run: yarn run lint:types - name: Run prettier run: yarn run lint:prettier - - name: Check for duplicate dependencies - run: yarn dedupe --check test: + name: Run tests runs-on: ubuntu-latest steps: - name: Check out files from GitHub @@ -51,16 +55,15 @@ jobs: node-version: ${{ env.NODE_VERSION }} cache: yarn - name: Install dependencies - run: yarn install - env: - CI: true + run: yarn install --immutable - name: Build resources run: ./node_modules/.bin/gulp build-translations build-locale-data - name: Run Tests run: yarn run test build: - runs-on: ubuntu-latest + name: Build frontend needs: [lint, test] + runs-on: ubuntu-latest steps: - name: Check out files from GitHub uses: actions/checkout@v3.3.0 @@ -70,16 +73,15 @@ jobs: node-version: ${{ env.NODE_VERSION }} cache: yarn - name: Install dependencies - run: yarn install - env: - CI: true + run: yarn install --immutable - name: Build Application run: ./node_modules/.bin/gulp build-app env: IS_TEST: "true" supervisor: - runs-on: ubuntu-latest + name: Build supervisor needs: [lint, test] + runs-on: ubuntu-latest steps: - name: Check out files from GitHub uses: actions/checkout@v3.3.0 @@ -89,9 +91,7 @@ jobs: node-version: ${{ env.NODE_VERSION }} cache: yarn - name: Install dependencies - run: yarn install - env: - CI: true + run: yarn install --immutable - name: Build Application run: ./node_modules/.bin/gulp build-hassio env: diff --git a/.github/workflows/demo_deployment.yaml b/.github/workflows/demo_deployment.yaml index d28a5ffe11..f760ad9021 100644 --- a/.github/workflows/demo_deployment.yaml +++ b/.github/workflows/demo_deployment.yaml @@ -34,9 +34,7 @@ jobs: cache: yarn - name: Install dependencies - run: yarn install - env: - CI: true + run: yarn install --immutable - name: Build Demo run: ./node_modules/.bin/gulp build-demo @@ -72,9 +70,7 @@ jobs: cache: yarn - name: Install dependencies - run: yarn install - env: - CI: true + run: yarn install --immutable - name: Build Demo run: ./node_modules/.bin/gulp build-demo @@ -88,4 +84,4 @@ jobs: args: deploy --dir=demo/dist --prod env: NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }} - NETLIFY_SITE_ID: ${{ secrets.NETLIFY_DEMO_SITE_ID }} \ No newline at end of file + NETLIFY_SITE_ID: ${{ secrets.NETLIFY_DEMO_SITE_ID }} diff --git a/.github/workflows/design_deployment.yaml b/.github/workflows/design_deployment.yaml index c9b37d605a..1803766c92 100644 --- a/.github/workflows/design_deployment.yaml +++ b/.github/workflows/design_deployment.yaml @@ -26,9 +26,7 @@ jobs: cache: yarn - name: Install dependencies - run: yarn install - env: - CI: true + run: yarn install --immutable - name: Build Gallery run: ./node_modules/.bin/gulp build-gallery diff --git a/.github/workflows/design_preview.yaml b/.github/workflows/design_preview.yaml index 607838b940..6677c65f43 100644 --- a/.github/workflows/design_preview.yaml +++ b/.github/workflows/design_preview.yaml @@ -31,9 +31,7 @@ jobs: cache: yarn - name: Install dependencies - run: yarn install - env: - CI: true + run: yarn install --immutable - name: Build Gallery run: ./node_modules/.bin/gulp build-gallery diff --git a/build-scripts/bundle.js b/build-scripts/bundle.js index 655bbb278b..715dbd9f93 100644 --- a/build-scripts/bundle.js +++ b/build-scripts/bundle.js @@ -67,7 +67,7 @@ module.exports.babelOptions = ({ latestBuild }) => ({ "@babel/preset-env", { useBuiltIns: "entry", - corejs: { version: "3.27", proposals: true }, + corejs: { version: "3.28", proposals: true }, bugfixes: true, }, ], diff --git a/cast/src/launcher/layout/hc-cast.ts b/cast/src/launcher/layout/hc-cast.ts index 28d26a9df1..4df33bc876 100644 --- a/cast/src/launcher/layout/hc-cast.ts +++ b/cast/src/launcher/layout/hc-cast.ts @@ -181,7 +181,7 @@ class HcCast extends LitElement { private async _handlePickView(ev: Event) { const path = (ev.currentTarget as any).getAttribute("data-path"); await ensureConnectedCastSession(this.castManager!, this.auth!); - castSendShowLovelaceView(this.castManager, path, this.auth.data.hassUrl); + castSendShowLovelaceView(this.castManager, this.auth.data.hassUrl, path); } private async _handleLogout() { diff --git a/demo/script/size_stats b/demo/script/size_stats index 6d785f36b3..999677bb42 100755 --- a/demo/script/size_stats +++ b/demo/script/size_stats @@ -6,6 +6,9 @@ set -e cd "$(dirname "$0")/.." -STATS=1 NODE_ENV=production ../node_modules/.bin/webpack --profile --json > compilation-stats.json -npx webpack-bundle-analyzer compilation-stats.json dist/frontend_latest -rm compilation-stats.json +export STATS=1 +statsfile="compilation-stats-demo.json" + +./node_modules/.bin/webpack-cli --profile --node-env=production --json=$statsfile +npx webpack-bundle-analyzer $statsfile dist/frontend_latest +rm -f $statsfile diff --git a/demo/src/custom-cards/ha-demo-card.ts b/demo/src/custom-cards/ha-demo-card.ts index 0f27328ecf..89835b5174 100644 --- a/demo/src/custom-cards/ha-demo-card.ts +++ b/demo/src/custom-cards/ha-demo-card.ts @@ -1,6 +1,6 @@ import "@material/mwc-button"; import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; -import { property, state } from "lit/decorators"; +import { customElement, property, state } from "lit/decorators"; import { until } from "lit/directives/until"; import "../../../src/components/ha-card"; import "../../../src/components/ha-circular-progress"; @@ -14,6 +14,7 @@ import { setDemoConfig, } from "../configs/demo-configs"; +@customElement("ha-demo-card") export class HADemoCard extends LitElement implements LovelaceCard { @property({ attribute: false }) public lovelace?: Lovelace; @@ -154,5 +155,3 @@ declare global { "ha-demo-card": HADemoCard; } } - -customElements.define("ha-demo-card", HADemoCard); diff --git a/demo/src/ha-demo.ts b/demo/src/ha-demo.ts index e911afce1f..d542477be9 100644 --- a/demo/src/ha-demo.ts +++ b/demo/src/ha-demo.ts @@ -1,5 +1,6 @@ // Compat needs to be first import import "../../src/resources/compatibility"; +import { customElement } from "lit/decorators"; import { isNavigationClick } from "../../src/common/dom/is-navigation-click"; import { navigate } from "../../src/common/navigate"; import { @@ -26,7 +27,8 @@ import { mockSystemLog } from "./stubs/system_log"; import { mockTemplate } from "./stubs/template"; import { mockTranslations } from "./stubs/translations"; -class HaDemo extends HomeAssistantAppEl { +@customElement("ha-demo") +export class HaDemo extends HomeAssistantAppEl { protected async _initializeHass() { const initial: Partial = { panelUrl: (this as any)._panelUrl, @@ -71,6 +73,7 @@ class HaDemo extends HomeAssistantAppEl { entity_category: null, has_entity_name: false, unique_id: "co2_intensity", + options: null, }, { config_entry_id: "co2signal", @@ -86,6 +89,7 @@ class HaDemo extends HomeAssistantAppEl { entity_category: null, has_entity_name: false, unique_id: "grid_fossil_fuel_percentage", + options: null, }, ]); @@ -121,8 +125,6 @@ class HaDemo extends HomeAssistantAppEl { } } -customElements.define("ha-demo", HaDemo); - declare global { interface HTMLElementTagNameMap { "ha-demo": HaDemo; diff --git a/demo/src/stubs/recorder.ts b/demo/src/stubs/recorder.ts index 3d367cdc0e..7ba78a805b 100644 --- a/demo/src/stubs/recorder.ts +++ b/demo/src/stubs/recorder.ts @@ -15,6 +15,7 @@ import { MockHomeAssistant } from "../../../src/fake_data/provide_hass"; const generateMeanStatistics = ( start: Date, end: Date, + // eslint-disable-next-line @typescript-eslint/default-param-last period: "5minute" | "hour" | "day" | "month" = "hour", initValue: number, maxDiff: number @@ -51,6 +52,7 @@ const generateMeanStatistics = ( const generateSumStatistics = ( start: Date, end: Date, + // eslint-disable-next-line @typescript-eslint/default-param-last period: "5minute" | "hour" | "day" | "month" = "hour", initValue: number, maxDiff: number @@ -86,6 +88,7 @@ const generateSumStatistics = ( const generateCurvedStatistics = ( start: Date, end: Date, + // eslint-disable-next-line @typescript-eslint/default-param-last _period: "5minute" | "hour" | "day" | "month" = "hour", initValue: number, maxDiff: number, diff --git a/gallery/src/pages/components/ha-alert.markdown b/gallery/src/pages/components/ha-alert.markdown index e5f5e5c059..89f2aa39d8 100644 --- a/gallery/src/pages/components/ha-alert.markdown +++ b/gallery/src/pages/components/ha-alert.markdown @@ -156,18 +156,6 @@ The `title ` option should not be used without a description. *Documentation coming soon* -**Right to left** - - - This is an info alert — check it out! - - -```html - - This is an info alert — check it out! - -``` - ### API **Properties/Attributes** diff --git a/gallery/src/pages/components/ha-bar-slider.markdown b/gallery/src/pages/components/ha-bar-slider.markdown deleted file mode 100644 index 5d45928b4e..0000000000 --- a/gallery/src/pages/components/ha-bar-slider.markdown +++ /dev/null @@ -1,3 +0,0 @@ ---- -title: Bar Slider ---- diff --git a/gallery/src/pages/components/ha-bar-switch.markdown b/gallery/src/pages/components/ha-bar-switch.markdown deleted file mode 100644 index b47b9bb183..0000000000 --- a/gallery/src/pages/components/ha-bar-switch.markdown +++ /dev/null @@ -1,3 +0,0 @@ ---- -title: Bar Switch ---- diff --git a/gallery/src/pages/components/ha-control-button.markdown b/gallery/src/pages/components/ha-control-button.markdown new file mode 100644 index 0000000000..344a3ebe1d --- /dev/null +++ b/gallery/src/pages/components/ha-control-button.markdown @@ -0,0 +1,3 @@ +--- +title: Control Button +--- diff --git a/gallery/src/pages/components/ha-control-button.ts b/gallery/src/pages/components/ha-control-button.ts new file mode 100644 index 0000000000..0c3ad180be --- /dev/null +++ b/gallery/src/pages/components/ha-control-button.ts @@ -0,0 +1,192 @@ +import { + mdiFanSpeed1, + mdiFanSpeed2, + mdiFanSpeed3, + mdiLightbulb, +} from "@mdi/js"; +import { css, html, LitElement, TemplateResult } from "lit"; +import { customElement } from "lit/decorators"; +import { ifDefined } from "lit/directives/if-defined"; +import { repeat } from "lit/directives/repeat"; +import "../../../../src/components/ha-control-button"; +import "../../../../src/components/ha-card"; +import "../../../../src/components/ha-svg-icon"; +import "../../../../src/components/ha-control-button-group"; + +type Button = { + label: string; + icon?: string; + class?: string; + disabled?: boolean; +}; + +const buttons: Button[] = [ + { + label: "Button", + }, + { + label: "Button and custom style", + class: "custom", + }, + { + label: "Disabled Button", + disabled: true, + }, +]; + +type ButtonGroup = { + vertical?: boolean; + class?: string; +}; + +const buttonGroups: ButtonGroup[] = [ + {}, + { + class: "custom-group", + }, +]; + +@customElement("demo-components-ha-control-button") +export class DemoHaBarButton extends LitElement { + protected render(): TemplateResult { + return html` + + ${repeat( + buttons, + (btn) => html` +
+
Config: ${JSON.stringify(btn)}
+ + + +
+ ` + )} +
+ + + ${repeat( + buttonGroups, + (group) => html` +
+
Config: ${JSON.stringify(group)}
+ + + + + + + + + + + +
+ ` + )} +
+ +
+

Vertical

+
+ ${repeat( + buttonGroups, + (group) => html` + + + + + + + + + + + + ` + )} +
+
+
+ `; + } + + static get styles() { + return css` + ha-card { + max-width: 600px; + margin: 24px auto; + } + pre { + margin-top: 0; + margin-bottom: 8px; + } + p { + margin: 0; + } + label { + font-weight: 600; + } + .custom { + --control-button-icon-color: var(--primary-color); + --control-button-background-color: var(--primary-color); + --control-button-background-opacity: 0.2; + --control-button-border-radius: 18px; + height: 100px; + width: 100px; + } + .custom-group { + --control-button-group-thickness: 100px; + --control-button-group-border-radius: 18px; + --control-button-group-spacing: 20px; + } + .custom-group ha-control-button { + --control-button-border-radius: 18px; + --mdc-icon-size: 32px; + } + .vertical-buttons { + height: 300px; + display: flex; + flex-direction: row; + justify-content: space-between; + } + p.title { + margin-bottom: 12px; + } + .vertical-switches > *:not(:last-child) { + margin-right: 4px; + } + `; + } +} + +declare global { + interface HTMLElementTagNameMap { + "demo-components-ha-control-button": DemoHaBarButton; + } +} diff --git a/gallery/src/pages/components/ha-control-slider.markdown b/gallery/src/pages/components/ha-control-slider.markdown new file mode 100644 index 0000000000..3825be3295 --- /dev/null +++ b/gallery/src/pages/components/ha-control-slider.markdown @@ -0,0 +1,3 @@ +--- +title: Control Slider +--- diff --git a/gallery/src/pages/components/ha-bar-slider.ts b/gallery/src/pages/components/ha-control-slider.ts similarity index 87% rename from gallery/src/pages/components/ha-bar-slider.ts rename to gallery/src/pages/components/ha-control-slider.ts index 261169d6dc..5faf01a1fc 100644 --- a/gallery/src/pages/components/ha-bar-slider.ts +++ b/gallery/src/pages/components/ha-control-slider.ts @@ -2,7 +2,7 @@ import { css, html, LitElement, TemplateResult } from "lit"; import { customElement, state } from "lit/decorators"; import { ifDefined } from "lit/directives/if-defined"; import { repeat } from "lit/directives/repeat"; -import "../../../../src/components/ha-bar-slider"; +import "../../../../src/components/ha-control-slider"; import "../../../../src/components/ha-card"; const sliders: { @@ -46,7 +46,7 @@ const sliders: { }, ]; -@customElement("demo-components-ha-bar-slider") +@customElement("demo-components-ha-control-slider") export class DemoHaBarSlider extends LitElement { @state() private value = 50; @@ -86,7 +86,7 @@ export class DemoHaBarSlider extends LitElement {
Config: ${JSON.stringify(config)}
- - +
`; @@ -106,7 +106,7 @@ export class DemoHaBarSlider extends LitElement { ${repeat(sliders, (slider) => { const { id, label, ...config } = slider; return html` - - + `; })} @@ -141,11 +141,11 @@ export class DemoHaBarSlider extends LitElement { font-weight: 600; } .custom { - --slider-bar-color: #ffcf4c; - --slider-bar-background: #ffcf4c; - --slider-bar-background-opacity: 0.2; - --slider-bar-thickness: 100px; - --slider-bar-border-radius: 24px; + --control-slider-color: #ffcf4c; + --control-slider-background: #ffcf4c; + --control-slider-background-opacity: 0.2; + --control-slider-thickness: 100px; + --control-slider-border-radius: 24px; } .vertical-sliders { height: 300px; @@ -165,6 +165,6 @@ export class DemoHaBarSlider extends LitElement { declare global { interface HTMLElementTagNameMap { - "demo-components-ha-bar-slider": DemoHaBarSlider; + "demo-components-ha-control-slider": DemoHaBarSlider; } } diff --git a/gallery/src/pages/components/ha-control-switch.markdown b/gallery/src/pages/components/ha-control-switch.markdown new file mode 100644 index 0000000000..ea4a4fadbf --- /dev/null +++ b/gallery/src/pages/components/ha-control-switch.markdown @@ -0,0 +1,3 @@ +--- +title: Control Switch +--- diff --git a/gallery/src/pages/components/ha-bar-switch.ts b/gallery/src/pages/components/ha-control-switch.ts similarity index 84% rename from gallery/src/pages/components/ha-bar-switch.ts rename to gallery/src/pages/components/ha-control-switch.ts index 7cf6bf64ef..8311e2a0a5 100644 --- a/gallery/src/pages/components/ha-bar-switch.ts +++ b/gallery/src/pages/components/ha-control-switch.ts @@ -8,7 +8,7 @@ import { css, html, LitElement, TemplateResult } from "lit"; import { customElement, state } from "lit/decorators"; import { ifDefined } from "lit/directives/if-defined"; import { repeat } from "lit/directives/repeat"; -import "../../../../src/components/ha-bar-switch"; +import "../../../../src/components/ha-control-switch"; import "../../../../src/components/ha-card"; const switches: { @@ -39,8 +39,8 @@ const switches: { }, ]; -@customElement("demo-components-ha-bar-switch") -export class DemoHaBarSwitch extends LitElement { +@customElement("demo-components-ha-control-switch") +export class DemoHaControlSwitch extends LitElement { @state() private checked = false; handleValueChanged(e: any) { @@ -56,7 +56,7 @@ export class DemoHaBarSwitch extends LitElement {
Config: ${JSON.stringify(config)}
- - +
`; @@ -78,7 +78,7 @@ export class DemoHaBarSwitch extends LitElement { ${repeat(switches, (sw) => { const { id, label, ...config } = sw; return html` - - + `; })} @@ -115,11 +115,11 @@ export class DemoHaBarSwitch extends LitElement { font-weight: 600; } .custom { - --switch-bar-on-color: var(--green-color); - --switch-bar-off-color: var(--red-color); - --switch-bar-thickness: 100px; - --switch-bar-border-radius: 24px; - --switch-bar-padding: 6px; + --control-switch-on-color: var(--green-color); + --control-switch-off-color: var(--red-color); + --control-switch-thickness: 100px; + --control-switch-border-radius: 24px; + --control-switch-padding: 6px; --mdc-icon-size: 24px; } .vertical-switches { @@ -140,6 +140,6 @@ export class DemoHaBarSwitch extends LitElement { declare global { interface HTMLElementTagNameMap { - "demo-components-ha-bar-switch": DemoHaBarSwitch; + "demo-components-ha-control-switch": DemoHaControlSwitch; } } diff --git a/gallery/src/pages/components/ha-tip.ts b/gallery/src/pages/components/ha-tip.ts index 49fa1f2c71..1f349efa7e 100644 --- a/gallery/src/pages/components/ha-tip.ts +++ b/gallery/src/pages/components/ha-tip.ts @@ -3,6 +3,7 @@ import { customElement } from "lit/decorators"; import "../../../../src/components/ha-tip"; import "../../../../src/components/ha-card"; import { applyThemesOnElement } from "../../../../src/common/dom/apply_themes_on_element"; +import { provideHass } from "../../../../src/fake_data/provide_hass"; const tips: (string | TemplateResult)[] = [ "Test tip", @@ -18,7 +19,11 @@ export class DemoHaTip extends LitElement {
- ${tips.map((tip) => html`${tip}`)} + ${tips.map( + (tip) => html`${tip}` + )}
diff --git a/gallery/src/pages/lovelace/tile-card.markdown b/gallery/src/pages/lovelace/tile-card.markdown new file mode 100644 index 0000000000..0117153269 --- /dev/null +++ b/gallery/src/pages/lovelace/tile-card.markdown @@ -0,0 +1,3 @@ +--- +title: Tile Card +--- diff --git a/gallery/src/pages/lovelace/tile-card.ts b/gallery/src/pages/lovelace/tile-card.ts new file mode 100644 index 0000000000..00e1e29376 --- /dev/null +++ b/gallery/src/pages/lovelace/tile-card.ts @@ -0,0 +1,173 @@ +import { html, LitElement, PropertyValues, TemplateResult } from "lit"; +import { customElement, query } from "lit/decorators"; +import { CoverEntityFeature } from "../../../../src/data/cover"; +import { LightColorMode } from "../../../../src/data/light"; +import { VacuumEntityFeature } from "../../../../src/data/vacuum"; +import { getEntity } from "../../../../src/fake_data/entity"; +import { provideHass } from "../../../../src/fake_data/provide_hass"; +import "../../components/demo-cards"; + +const ENTITIES = [ + getEntity("switch", "tv_outlet", "on", { + friendly_name: "TV outlet", + device_class: "outlet", + }), + getEntity("light", "bed_light", "on", { + friendly_name: "Bed Light", + supported_color_modes: [LightColorMode.HS], + }), + getEntity("light", "unavailable", "unavailable", { + friendly_name: "Unavailable entity", + }), + getEntity("climate", "thermostat", "heat", { + current_temperature: 73, + min_temp: 45, + max_temp: 95, + temperature: 80, + hvac_modes: ["heat", "cool", "auto", "off"], + friendly_name: "Thermostat", + hvac_action: "heating", + }), + getEntity("person", "paulus", "home", { + friendly_name: "Paulus", + }), + getEntity("vacuum", "first_floor_vacuum", "docked", { + friendly_name: "First floor vacuum", + supported_features: + VacuumEntityFeature.START + + VacuumEntityFeature.STOP + + VacuumEntityFeature.RETURN_HOME, + }), + getEntity("cover", "kitchen_shutter", "open", { + friendly_name: "Kitchen shutter", + device_class: "shutter", + supported_features: + CoverEntityFeature.CLOSE + + CoverEntityFeature.OPEN + + CoverEntityFeature.STOP, + }), + getEntity("cover", "pergola_roof", "open", { + friendly_name: "Pergola Roof", + supported_features: + CoverEntityFeature.CLOSE_TILT + + CoverEntityFeature.OPEN_TILT + + CoverEntityFeature.STOP_TILT, + }), +]; + +const CONFIGS = [ + { + heading: "Basic example", + config: ` +- type: tile + entity: switch.tv_outlet + `, + }, + { + heading: "Vertical example", + config: ` +- type: tile + entity: switch.tv_outlet + vertical: true + `, + }, + { + heading: "Custom color", + config: ` +- type: tile + entity: switch.tv_outlet + color: pink + `, + }, + { + heading: "Unknown entity", + config: ` +- type: tile + entity: light.unknown + `, + }, + { + heading: "Unavailable entity", + config: ` +- type: tile + entity: light.unavailable + `, + }, + { + heading: "Climate", + config: ` +- type: tile + entity: climate.thermostat + `, + }, + { + heading: "Person", + config: ` +- type: tile + entity: person.paulus + `, + }, + { + heading: "Light brightness feature", + config: ` +- type: tile + entity: light.bed_light + features: + - type: "light-brightness" + `, + }, + { + heading: "Vacuum commands feature", + config: ` +- type: tile + entity: vacuum.first_floor_vacuum + features: + - type: "vacuum-commands" + commands: + - start_pause + - stop + - return_home + `, + }, + { + heading: "Cover open close feature", + config: ` +- type: tile + entity: cover.kitchen_shutter + features: + - type: "cover-open-close" + `, + }, + { + heading: "Cover tilt feature", + config: ` +- type: tile + entity: cover.pergola_roof + features: + - type: "cover-tilt" + `, + }, +]; + +@customElement("demo-lovelace-tile-card") +class DemoTile extends LitElement { + @query("#demos") private _demoRoot!: HTMLElement; + + protected render(): TemplateResult { + return html``; + } + + protected firstUpdated(changedProperties: PropertyValues) { + super.firstUpdated(changedProperties); + const hass = provideHass(this._demoRoot); + hass.updateTranslations(null, "en"); + hass.updateTranslations("lovelace", "en"); + hass.addEntities(ENTITIES); + } +} + +declare global { + interface HTMLElementTagNameMap { + "demo-lovelace-tile-card": DemoTile; + } +} diff --git a/gallery/src/pages/misc/integration-card.ts b/gallery/src/pages/misc/integration-card.ts index 56de4308e8..168344e51b 100644 --- a/gallery/src/pages/misc/integration-card.ts +++ b/gallery/src/pages/misc/integration-card.ts @@ -197,6 +197,7 @@ const createEntityRegistryEntries = ( platform: "updater", has_entity_name: false, unique_id: "updater", + options: null, }, ]; diff --git a/hassio/src/addon-store/hassio-addon-repository.ts b/hassio/src/addon-store/hassio-addon-repository.ts index 0d22c49ad2..7b1d18f529 100644 --- a/hassio/src/addon-store/hassio-addon-repository.ts +++ b/hassio/src/addon-store/hassio-addon-repository.ts @@ -1,6 +1,6 @@ import { mdiArrowUpBoldCircle, mdiPuzzle } from "@mdi/js"; import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; -import { property } from "lit/decorators"; +import { customElement, property } from "lit/decorators"; import memoizeOne from "memoize-one"; import { atLeastVersion } from "../../../src/common/config/version"; import { navigate } from "../../../src/common/navigate"; @@ -14,7 +14,8 @@ import "../components/hassio-card-content"; import { filterAndSort } from "../components/hassio-filter-addons"; import { hassioStyle } from "../resources/hassio-style"; -class HassioAddonRepositoryEl extends LitElement { +@customElement("hassio-addon-repository") +export class HassioAddonRepositoryEl extends LitElement { @property({ attribute: false }) public hass!: HomeAssistant; @property({ attribute: false }) public supervisor!: Supervisor; @@ -140,5 +141,3 @@ class HassioAddonRepositoryEl extends LitElement { ]; } } - -customElements.define("hassio-addon-repository", HassioAddonRepositoryEl); diff --git a/hassio/src/addon-store/hassio-addon-store.ts b/hassio/src/addon-store/hassio-addon-store.ts index 0623e2bf44..0ae00d5453 100644 --- a/hassio/src/addon-store/hassio-addon-store.ts +++ b/hassio/src/addon-store/hassio-addon-store.ts @@ -9,7 +9,7 @@ import { PropertyValues, TemplateResult, } from "lit"; -import { property, state } from "lit/decorators"; +import { customElement, property, state } from "lit/decorators"; import memoizeOne from "memoize-one"; import { atLeastVersion } from "../../../src/common/config/version"; import { fireEvent } from "../../../src/common/dom/fire_event"; @@ -49,7 +49,8 @@ const sortRepos = (a: HassioAddonRepository, b: HassioAddonRepository) => { return a.name.toUpperCase() < b.name.toUpperCase() ? -1 : 1; }; -class HassioAddonStore extends LitElement { +@customElement("hassio-addon-store") +export class HassioAddonStore extends LitElement { @property({ attribute: false }) public hass!: HomeAssistant; @property({ attribute: false }) public supervisor!: Supervisor; @@ -250,5 +251,3 @@ class HassioAddonStore extends LitElement { `; } } - -customElements.define("hassio-addon-store", HassioAddonStore); diff --git a/hassio/src/dialogs/network/dialog-hassio-network.ts b/hassio/src/dialogs/network/dialog-hassio-network.ts index 103669b18b..05f6569df9 100644 --- a/hassio/src/dialogs/network/dialog-hassio-network.ts +++ b/hassio/src/dialogs/network/dialog-hassio-network.ts @@ -17,7 +17,6 @@ import "../../../../src/components/ha-formfield"; import "../../../../src/components/ha-header-bar"; import "../../../../src/components/ha-icon-button"; import "../../../../src/components/ha-radio"; -import "../../../../src/components/ha-related-items"; import { extractApiErrorMessage } from "../../../../src/data/hassio/common"; import { AccessPoints, diff --git a/lint-staged.config.js b/lint-staged.config.js index d5f63c77bf..1a5df4fdc1 100644 --- a/lint-staged.config.js +++ b/lint-staged.config.js @@ -5,5 +5,5 @@ module.exports = { 'printf "%s\n" "Translation files should not be added or modified here. Instead, make the necessary modifications in src/translations/en.json. Other languages are managed externally. Please see https://developers.home-assistant.io/docs/translations/ for details." ' + files.join(" ") + " >&2 && exit 1", - "/yarn.lock": () => "yarn dedupe", + "yarn.lock": () => "yarn dedupe", }; diff --git a/package.json b/package.json index fcfd48f9d4..998f5eda7b 100644 --- a/package.json +++ b/package.json @@ -25,25 +25,25 @@ "license": "Apache-2.0", "dependencies": { "@braintree/sanitize-url": "^6.0.2", - "@codemirror/autocomplete": "^6.4.0", - "@codemirror/commands": "^6.2.0", - "@codemirror/language": "^6.4.0", + "@codemirror/autocomplete": "^6.4.2", + "@codemirror/commands": "^6.2.1", + "@codemirror/language": "^6.6.0", "@codemirror/legacy-modes": "^6.3.1", "@codemirror/search": "^6.2.3", "@codemirror/state": "^6.2.0", - "@codemirror/view": "^6.7.1", - "@formatjs/intl-datetimeformat": "^6.4.3", - "@formatjs/intl-getcanonicallocales": "^2.0.5", - "@formatjs/intl-locale": "^3.0.11", - "@formatjs/intl-numberformat": "^8.3.3", - "@formatjs/intl-pluralrules": "^5.1.8", - "@formatjs/intl-relativetimeformat": "^11.1.8", - "@fullcalendar/common": "^5.11.4", - "@fullcalendar/core": "^5.11.4", - "@fullcalendar/daygrid": "^5.11.4", - "@fullcalendar/interaction": "^5.11.4", - "@fullcalendar/list": "^5.11.4", - "@fullcalendar/timegrid": "^5.11.4", + "@codemirror/view": "^6.9.1", + "@egjs/hammerjs": "^2.0.17", + "@formatjs/intl-datetimeformat": "^6.5.1", + "@formatjs/intl-getcanonicallocales": "^2.1.0", + "@formatjs/intl-locale": "^3.1.1", + "@formatjs/intl-numberformat": "^8.3.5", + "@formatjs/intl-pluralrules": "^5.1.10", + "@formatjs/intl-relativetimeformat": "^11.1.10", + "@fullcalendar/core": "^6.1.4", + "@fullcalendar/daygrid": "^6.1.4", + "@fullcalendar/interaction": "^6.1.4", + "@fullcalendar/list": "^6.1.4", + "@fullcalendar/timegrid": "^6.1.4", "@lezer/highlight": "^1.1.3", "@lit-labs/motion": "^1.0.3", "@lit-labs/virtualizer": "^1.0.1", @@ -71,6 +71,7 @@ "@material/mwc-textfield": "^0.27.0", "@material/mwc-top-app-bar-fixed": "^0.27.0", "@material/top-app-bar": "=14.0.0-canary.53b3cad2f.0", + "@material/web": "=1.0.0-pre.2", "@mdi/js": "7.1.96", "@mdi/svg": "7.1.96", "@polymer/app-layout": "^3.1.0", @@ -88,35 +89,34 @@ "@polymer/paper-tooltip": "^3.0.1", "@polymer/polymer": "3.4.1", "@thomasloven/round-slider": "0.6.0", - "@vaadin/combo-box": "^23.3.6", - "@vaadin/vaadin-themable-mixin": "^23.3.6", + "@vaadin/combo-box": "^23.3.7", + "@vaadin/vaadin-themable-mixin": "^23.3.7", "@vibrant/color": "^3.2.1-alpha.1", "@vibrant/core": "^3.2.1-alpha.1", "@vibrant/quantizer-mmcq": "^3.2.1-alpha.1", "@vue/web-component-wrapper": "^1.3.0", - "@webcomponents/scoped-custom-element-registry": "^0.0.5", - "@webcomponents/webcomponentsjs": "^2.2.10", + "@webcomponents/scoped-custom-element-registry": "^0.0.8", + "@webcomponents/webcomponentsjs": "^2.7.0", "app-datepicker": "^5.1.0", "chart.js": "^3.3.2", - "comlink": "^4.3.1", - "core-js": "^3.27.2", + "comlink": "^4.4.1", + "core-js": "^3.28.0", "cropperjs": "^1.5.13", "date-fns": "^2.29.3", - "date-fns-tz": "^1.3.7", + "date-fns-tz": "^2.0.0", "deep-clone-simple": "^1.1.1", "deep-freeze": "^0.0.1", "fuse.js": "^6.6.2", "google-timezones-json": "^1.0.2", - "hammerjs": "^2.0.8", - "hls.js": "^1.3.1", + "hls.js": "^1.3.3", "home-assistant-js-websocket": "^8.0.1", - "idb-keyval": "^5.1.3", - "intl-messageformat": "^10.3.0", + "idb-keyval": "^6.2.0", + "intl-messageformat": "^10.3.1", "js-yaml": "^4.1.0", - "leaflet": "^1.7.1", + "leaflet": "^1.9.3", "leaflet-draw": "^1.0.4", "lit": "^2.6.1", - "marked": "^4.0.12", + "marked": "^4.2.12", "memoize-one": "^6.0.0", "node-vibrant": "3.2.1-alpha.1", "proxy-polyfill": "^0.3.2", @@ -126,16 +126,17 @@ "regenerator-runtime": "^0.13.11", "resize-observer-polyfill": "^1.5.1", "roboto-fontface": "^0.10.0", - "rrule": "^2.7.1", - "sortablejs": "^1.14.0", + "rrule": "^2.7.2", + "sortablejs": "^1.15.0", "superstruct": "^1.0.3", - "tinykeys": "^1.1.3", - "tsparticles": "^1.34.0", - "unfetch": "^4.1.0", - "vis-data": "^7.1.2", - "vis-network": "^8.5.4", - "vue": "^2.6.12", - "vue2-daterange-picker": "^0.5.1", + "tinykeys": "^1.4.0", + "tsparticles-engine": "^2.9.3", + "tsparticles-preset-links": "^2.9.3", + "unfetch": "^5.0.0", + "vis-data": "^7.1.4", + "vis-network": "^9.1.2", + "vue": "^2.7.14", + "vue2-daterange-picker": "^0.6.8", "weekstart": "^1.1.0", "workbox-cacheable-response": "^6.5.4", "workbox-core": "^6.5.4", @@ -146,18 +147,18 @@ "xss": "^1.0.14" }, "devDependencies": { - "@babel/core": "^7.20.2", + "@babel/core": "^7.21.0", "@babel/plugin-external-helpers": "^7.18.6", "@babel/plugin-proposal-class-properties": "^7.18.6", - "@babel/plugin-proposal-decorators": "^7.20.7", + "@babel/plugin-proposal-decorators": "^7.21.0", "@babel/plugin-proposal-nullish-coalescing-operator": "^7.18.6", - "@babel/plugin-proposal-object-rest-spread": "^7.20.2", - "@babel/plugin-proposal-optional-chaining": "^7.20.7", + "@babel/plugin-proposal-object-rest-spread": "^7.20.7", + "@babel/plugin-proposal-optional-chaining": "^7.21.0", "@babel/plugin-syntax-dynamic-import": "^7.8.3", "@babel/plugin-syntax-import-meta": "^7.10.4", "@babel/plugin-syntax-top-level-await": "^7.14.5", "@babel/preset-env": "^7.20.2", - "@babel/preset-typescript": "^7.18.6", + "@babel/preset-typescript": "^7.21.0", "@koa/cors": "^4.0.0", "@octokit/auth-oauth-device": "^4.0.4", "@octokit/rest": "^19.0.7", @@ -169,58 +170,60 @@ "@rollup/plugin-replace": "^2.3.2", "@types/chromecast-caf-receiver": "5.0.12", "@types/chromecast-caf-sender": "^1.0.5", + "@types/esprima": "^4", "@types/glob": "^8", - "@types/hammerjs": "^2.0.41", "@types/js-yaml": "^4", "@types/leaflet": "^1", "@types/leaflet-draw": "^1", "@types/marked": "^4", - "@types/mocha": "^8", + "@types/mocha": "^10", "@types/qrcode": "^1.5.0", "@types/sortablejs": "^1", "@types/tar": "^6", "@types/webspeechapi": "^0.0.29", - "@typescript-eslint/eslint-plugin": "^5.46.1", - "@typescript-eslint/parser": "^5.49.0", - "@web/dev-server": "^0.0.24", + "@typescript-eslint/eslint-plugin": "^5.53.0", + "@typescript-eslint/parser": "^5.53.0", + "@web/dev-server": "^0.1.35", "@web/dev-server-rollup": "^0.2.11", "babel-loader": "^9.1.2", - "chai": "^4.3.4", + "chai": "^4.3.7", "del": "^7.0.0", - "eslint": "^7.32.0", - "eslint-config-airbnb-base": "^14.2.1", - "eslint-config-airbnb-typescript": "^14.0.0", + "eslint": "^8.34.0", + "eslint-config-airbnb-base": "^15.0.0", + "eslint-config-airbnb-typescript": "^17.0.0", "eslint-config-prettier": "^8.6.0", - "eslint-import-resolver-webpack": "^0.13.1", + "eslint-import-resolver-webpack": "^0.13.2", "eslint-plugin-disable": "^2.0.3", - "eslint-plugin-import": "^2.24.2", - "eslint-plugin-lit": "^1.6.1", - "eslint-plugin-unused-imports": "^1.1.5", + "eslint-plugin-import": "^2.27.5", + "eslint-plugin-lit": "^1.8.2", + "eslint-plugin-lit-a11y": "^2.3.0", + "eslint-plugin-unused-imports": "^2.0.0", "eslint-plugin-wc": "^1.4.0", + "esprima": "^4.0.1", "fancy-log": "^2.0.0", "fs-extra": "^11.1.0", "glob": "^8.1.0", "gulp": "^4.0.2", "gulp-flatmap": "^1.0.2", - "gulp-json-transform": "^0.4.6", + "gulp-json-transform": "^0.4.8", "gulp-merge-json": "^2.1.2", "gulp-rename": "^2.0.0", - "gulp-zopfli-green": "^3.0.1", + "gulp-zopfli-green": "^6.0.1", "html-minifier": "^4.0.0", "husky": "^8.0.3", "instant-mocha": "^1.5.0", "jszip": "^3.10.1", - "lint-staged": "^13.1.0", + "lint-staged": "^13.1.2", "lit-analyzer": "^1.2.1", "lodash.template": "^4.5.0", - "magic-string": "^0.25.7", + "magic-string": "^0.29.0", "map-stream": "^0.0.7", "merge-stream": "^2.0.0", - "mocha": "^8.4.0", + "mocha": "^10.2.0", "object-hash": "^3.0.0", - "open": "^8.4.0", + "open": "^8.4.1", "pinst": "^3.0.0", - "prettier": "^2.8.3", + "prettier": "^2.8.4", "require-dir": "^1.2.0", "rollup": "^2.8.2", "rollup-plugin-string": "^3.0.0", @@ -230,13 +233,13 @@ "sinon": "^15.0.1", "source-map-url": "^0.4.1", "systemjs": "^6.13.0", - "tar": "^6.1.11", - "terser-webpack-plugin": "^5.2.4", + "tar": "^6.1.13", + "terser-webpack-plugin": "^5.3.6", "ts-lit-plugin": "^1.2.1", "typescript": "^4.9.5", "vinyl-buffer": "^1.0.1", "vinyl-source-stream": "^2.0.0", - "webpack": "^5.55.1", + "webpack": "=5.72.1", "webpack-cli": "^5.0.1", "webpack-dev-server": "^4.11.1", "webpack-manifest-plugin": "^5.0.0", diff --git a/pyproject.toml b/pyproject.toml index 9440b6a7d4..01692958c7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "home-assistant-frontend" -version = "20230202.0" +version = "20230222.0" license = {text = "Apache-2.0"} description = "The Home Assistant frontend" readme = "README.md" diff --git a/script/size_stats b/script/size_stats index f93f0174dc..5c550c58b5 100755 --- a/script/size_stats +++ b/script/size_stats @@ -6,6 +6,9 @@ set -e cd "$(dirname "$0")/.." -STATS=1 NODE_ENV=production ./node_modules/.bin/webpack --profile --json > compilation-stats.json -npx webpack-bundle-analyzer compilation-stats.json hass_frontend/frontend_latest -rm compilation-stats.json +export STATS=1 +statsfile="compilation-stats.json" + +./node_modules/.bin/webpack-cli --profile --node-env=production --json=$statsfile +npx webpack-bundle-analyzer $statsfile hass_frontend/frontend_latest +rm -f $statsfile diff --git a/src/auth/ha-auth-flow.ts b/src/auth/ha-auth-flow.ts index 1fea597049..f151fea546 100644 --- a/src/auth/ha-auth-flow.ts +++ b/src/auth/ha-auth-flow.ts @@ -8,7 +8,7 @@ import { PropertyValues, TemplateResult, } from "lit"; -import { property, state } from "lit/decorators"; +import { customElement, property, state } from "lit/decorators"; import "../components/ha-alert"; import "../components/ha-checkbox"; import { computeInitialHaFormData } from "../components/ha-form/compute-initial-ha-form-data"; @@ -25,7 +25,8 @@ import "./ha-password-manager-polyfill"; type State = "loading" | "error" | "step"; -class HaAuthFlow extends litLocalizeLiteMixin(LitElement) { +@customElement("ha-auth-flow") +export class HaAuthFlow extends litLocalizeLiteMixin(LitElement) { @property({ attribute: false }) public authProvider?: AuthProvider; @property() public clientId?: string; @@ -407,7 +408,6 @@ class HaAuthFlow extends litLocalizeLiteMixin(LitElement) { `; } } -customElements.define("ha-auth-flow", HaAuthFlow); declare global { interface HTMLElementTagNameMap { diff --git a/src/auth/ha-authorize.ts b/src/auth/ha-authorize.ts index add29139bc..9f780facc2 100644 --- a/src/auth/ha-authorize.ts +++ b/src/auth/ha-authorize.ts @@ -1,5 +1,5 @@ import { css, CSSResultGroup, html, LitElement, PropertyValues } from "lit"; -import { property, state } from "lit/decorators"; +import { customElement, property, state } from "lit/decorators"; import punycode from "punycode"; import { applyThemesOnElement } from "../common/dom/apply_themes_on_element"; import { extractSearchParamsObject } from "../common/url/search-params"; @@ -14,7 +14,8 @@ import "./ha-auth-flow"; import("./ha-pick-auth-provider"); -class HaAuthorize extends litLocalizeLiteMixin(LitElement) { +@customElement("ha-authorize") +export class HaAuthorize extends litLocalizeLiteMixin(LitElement) { @property() public clientId?: string; @property() public redirectUri?: string; @@ -183,4 +184,3 @@ class HaAuthorize extends litLocalizeLiteMixin(LitElement) { `; } } -customElements.define("ha-authorize", HaAuthorize); diff --git a/src/auth/ha-pick-auth-provider.ts b/src/auth/ha-pick-auth-provider.ts index 3ee7638b10..5d9b480030 100644 --- a/src/auth/ha-pick-auth-provider.ts +++ b/src/auth/ha-pick-auth-provider.ts @@ -1,7 +1,7 @@ import "@polymer/paper-item/paper-item"; import "@polymer/paper-item/paper-item-body"; import { css, html, LitElement } from "lit"; -import { property } from "lit/decorators"; +import { customElement, property } from "lit/decorators"; import { fireEvent } from "../common/dom/fire_event"; import "../components/ha-icon-next"; import { AuthProvider } from "../data/auth"; @@ -13,7 +13,8 @@ declare global { } } -class HaPickAuthProvider extends litLocalizeLiteMixin(LitElement) { +@customElement("ha-pick-auth-provider") +export class HaPickAuthProvider extends litLocalizeLiteMixin(LitElement) { @property() public authProviders: AuthProvider[] = []; protected render() { @@ -47,4 +48,3 @@ class HaPickAuthProvider extends litLocalizeLiteMixin(LitElement) { } `; } -customElements.define("ha-pick-auth-provider", HaPickAuthProvider); diff --git a/src/common/array/ensure-array.ts b/src/common/array/ensure-array.ts index cdc9ea89ed..70869a91f1 100644 --- a/src/common/array/ensure-array.ts +++ b/src/common/array/ensure-array.ts @@ -2,6 +2,7 @@ type NonUndefined = T extends undefined ? never : T; export function ensureArray(value: undefined): undefined; export function ensureArray(value: T | T[]): NonUndefined[]; +export function ensureArray(value: T | readonly T[]): NonUndefined[]; export function ensureArray(value) { if (value === undefined || Array.isArray(value)) { return value; diff --git a/src/common/datetime/create_duration_data.ts b/src/common/datetime/create_duration_data.ts index 3b743471ca..d8e717b7e3 100644 --- a/src/common/datetime/create_duration_data.ts +++ b/src/common/datetime/create_duration_data.ts @@ -10,11 +10,19 @@ export const createDurationData = ( if (typeof duration !== "object") { if (typeof duration === "string" || isNaN(duration)) { const parts = duration?.toString().split(":") || []; + if (parts.length === 1) { + return { seconds: Number(parts[0]) }; + } + if (parts.length > 3) { + return undefined; + } + const seconds = Number(parts[2]) || 0; + const seconds_whole = Math.floor(seconds); return { hours: Number(parts[0]) || 0, minutes: Number(parts[1]) || 0, - seconds: Number(parts[2]) || 0, - milliseconds: Number(parts[3]) || 0, + seconds: seconds_whole, + milliseconds: Math.floor((seconds - seconds_whole) * 1000), }; } return { seconds: duration }; diff --git a/src/common/dom/setup-leaflet-map.ts b/src/common/dom/setup-leaflet-map.ts index 1ce5c20f4e..444e12f472 100644 --- a/src/common/dom/setup-leaflet-map.ts +++ b/src/common/dom/setup-leaflet-map.ts @@ -11,8 +11,7 @@ export const setupLeafletMap = async ( throw new Error("Cannot setup Leaflet map on disconnected element"); } // eslint-disable-next-line - const Leaflet = ((await import("leaflet")) as any) - .default as LeafletModuleType; + const Leaflet = (await import("leaflet")).default as LeafletModuleType; Leaflet.Icon.Default.imagePath = "/static/images/leaflet/images/"; const map = Leaflet.map(mapElement); diff --git a/src/common/entity/compute_state_display.ts b/src/common/entity/compute_state_display.ts index 18a05de5ac..c469585a9d 100644 --- a/src/common/entity/compute_state_display.ts +++ b/src/common/entity/compute_state_display.ts @@ -49,6 +49,8 @@ export const computeStateDisplayFromEntityAttributes = ( return localize(`state.default.${state}`); } + const entity = entities[entityId] as EntityRegistryEntry | undefined; + // Entities with a `unit_of_measurement` or `state_class` are numeric values and should use `formatNumber` if (isNumericFromAttributes(attributes)) { // state is duration @@ -82,7 +84,7 @@ export const computeStateDisplayFromEntityAttributes = ( return `${formatNumber( state, locale, - getNumberFormatOptions({ state, attributes } as HassEntity) + getNumberFormatOptions({ state, attributes } as HassEntity, entity) )}${unit}`; } @@ -160,7 +162,7 @@ export const computeStateDisplayFromEntityAttributes = ( return formatNumber( state, locale, - getNumberFormatOptions({ state, attributes } as HassEntity) + getNumberFormatOptions({ state, attributes } as HassEntity, entity) ); } @@ -199,8 +201,6 @@ export const computeStateDisplayFromEntityAttributes = ( : localize("ui.card.update.up_to_date"); } - const entity = entities[entityId] as EntityRegistryEntry | undefined; - return ( (entity?.translation_key && localize( diff --git a/src/common/integrations/protocolIntegrationPicked.ts b/src/common/integrations/protocolIntegrationPicked.ts index a1f11551c4..558bee552d 100644 --- a/src/common/integrations/protocolIntegrationPicked.ts +++ b/src/common/integrations/protocolIntegrationPicked.ts @@ -4,12 +4,15 @@ import { domainToName } from "../../data/integration"; import { getIntegrationDescriptions } from "../../data/integrations"; import { showConfigFlowDialog } from "../../dialogs/config-flow/show-dialog-config-flow"; import { showConfirmationDialog } from "../../dialogs/generic/show-dialog-box"; +import { showMatterAddDeviceDialog } from "../../panels/config/integrations/integration-panels/matter/show-dialog-add-matter-device"; 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 { navigate } from "../navigate"; +export const PROTOCOL_INTEGRATIONS = ["zha", "zwave_js", "matter"] as const; + export const protocolIntegrationPicked = async ( element: HTMLElement, hass: HomeAssistant, @@ -113,5 +116,43 @@ export const protocolIntegrationPicked = async ( } navigate("/config/zha/add"); + } else if (domain === "matter") { + const entries = await getConfigEntries(hass, { + domain, + }); + if (!isComponentLoaded(hass, domain) || !entries.length) { + // If the component isn't loaded, ask them to load the integration first + showConfirmationDialog(element, { + title: hass.localize( + "ui.panel.config.integrations.config_flow.missing_zwave_zigbee_title", + { integration: "Matter" } + ), + text: hass.localize( + "ui.panel.config.integrations.config_flow.missing_matter", + { + integration: "Matter", + brand: options?.brand || options?.domain || "Matter", + supported_hardware_link: html`${hass.localize( + "ui.panel.config.integrations.config_flow.supported_hardware" + )}`, + } + ), + confirmText: hass.localize( + "ui.panel.config.integrations.config_flow.proceed" + ), + confirm: () => { + showConfigFlowDialog(element, { + startFlowHandler: "matter", + }); + }, + }); + return; + } + showMatterAddDeviceDialog(element); } }; diff --git a/src/common/number/format_number.ts b/src/common/number/format_number.ts index e3f78f1b79..2e461ab822 100644 --- a/src/common/number/format_number.ts +++ b/src/common/number/format_number.ts @@ -2,6 +2,7 @@ import { HassEntity, HassEntityAttributeBase, } from "home-assistant-js-websocket"; +import { EntityRegistryEntry } from "../../data/entity_registry"; import { FrontendLocaleData, NumberFormat } from "../../data/translation"; import { round } from "./round"; @@ -90,8 +91,18 @@ export const formatNumber = ( * @returns An `Intl.NumberFormatOptions` object with `maximumFractionDigits` set to 0, or `undefined` */ export const getNumberFormatOptions = ( - entityState: HassEntity + entityState: HassEntity, + entity?: EntityRegistryEntry ): Intl.NumberFormatOptions | undefined => { + const precision = + entity?.options?.sensor?.display_precision ?? + entity?.options?.sensor?.suggested_display_precision; + if (precision != null) { + return { + maximumFractionDigits: precision, + minimumFractionDigits: precision, + }; + } if ( Number.isInteger(Number(entityState.attributes?.step)) && Number.isInteger(Number(entityState.state)) diff --git a/src/common/structs/is-custom-type.ts b/src/common/structs/is-custom-type.ts index dd5de2f637..e5a0082794 100644 --- a/src/common/structs/is-custom-type.ts +++ b/src/common/structs/is-custom-type.ts @@ -1,6 +1,5 @@ import { refine, string } from "superstruct"; - -export const isCustomType = (value: string) => value.startsWith("custom:"); +import { isCustomType } from "../../data/lovelace_custom_cards"; export const customType = () => refine(string(), "custom element type", isCustomType); diff --git a/src/common/util/select-unit.ts b/src/common/util/select-unit.ts index f68599c610..7e388a2ddb 100644 --- a/src/common/util/select-unit.ts +++ b/src/common/util/select-unit.ts @@ -19,6 +19,7 @@ const SECS_PER_HOUR = SECS_PER_MIN * 60; // Adapted from https://github.com/formatjs/formatjs/blob/186cef62f980ec66252ee232f438a42d0b51b9f9/packages/intl-utils/src/diff.ts export function selectUnit( from: Date | number, + // eslint-disable-next-line @typescript-eslint/default-param-last to: Date | number = Date.now(), locale: FrontendLocaleData, thresholds: Partial = {} diff --git a/src/components/buttons/ha-call-api-button.ts b/src/components/buttons/ha-call-api-button.ts index 34013a1102..05fc1f878d 100644 --- a/src/components/buttons/ha-call-api-button.ts +++ b/src/components/buttons/ha-call-api-button.ts @@ -1,9 +1,10 @@ import { css, CSSResultGroup, html, LitElement } from "lit"; -import { property, query } from "lit/decorators"; +import { customElement, property, query } from "lit/decorators"; import { fireEvent } from "../../common/dom/fire_event"; import { HomeAssistant } from "../../types"; import "./ha-progress-button"; +@customElement("ha-call-api-button") class HaCallApiButton extends LitElement { @property({ attribute: false }) public hass!: HomeAssistant; @@ -69,8 +70,6 @@ class HaCallApiButton extends LitElement { } } -customElements.define("ha-call-api-button", HaCallApiButton); - declare global { interface HTMLElementTagNameMap { "ha-call-api-button": HaCallApiButton; diff --git a/src/components/chart/ha-chart-base.ts b/src/components/chart/ha-chart-base.ts index 0f7c5b9d3c..0235fcdf88 100644 --- a/src/components/chart/ha-chart-base.ts +++ b/src/components/chart/ha-chart-base.ts @@ -233,7 +233,11 @@ export default class HaChartBase extends LitElement { { id: "afterRenderHook", afterRender: (chart) => { - this._chartHeight = chart.height; + const change = chart.height - (this._chartHeight ?? 0); + if (!this._chartHeight || change > 0 || change < -12) { + // hysteresis to prevent infinite render loops + this._chartHeight = chart.height; + } }, legend: { ...this.options?.plugins?.legend, diff --git a/src/components/chart/statistics-chart.ts b/src/components/chart/statistics-chart.ts index 9b78e8ab02..551de1a562 100644 --- a/src/components/chart/statistics-chart.ts +++ b/src/components/chart/statistics-chart.ts @@ -59,7 +59,7 @@ export const statTypeMap: Record = { class StatisticsChart extends LitElement { @property({ attribute: false }) public hass!: HomeAssistant; - @property({ attribute: false }) public statisticsData!: Statistics; + @property({ attribute: false }) public statisticsData?: Statistics; @property({ attribute: false }) public metadata?: Record< string, @@ -99,7 +99,11 @@ class StatisticsChart extends LitElement { if (!this.hasUpdated || changedProps.has("unit")) { this._createOptions(); } - if (changedProps.has("statisticsData") || changedProps.has("statTypes")) { + if ( + changedProps.has("statisticsData") || + changedProps.has("statTypes") || + changedProps.has("hideLegend") + ) { this._generateData(); } } diff --git a/src/components/data-table/ha-data-table.ts b/src/components/data-table/ha-data-table.ts index dad2d26efb..34a0bc29d3 100644 --- a/src/components/data-table/ha-data-table.ts +++ b/src/components/data-table/ha-data-table.ts @@ -1,3 +1,4 @@ +import "@lit-labs/virtualizer"; import { mdiArrowDown, mdiArrowUp } from "@mdi/js"; import deepClone from "deep-clone-simple"; import { @@ -21,16 +22,15 @@ import { styleMap } from "lit/directives/style-map"; import memoizeOne from "memoize-one"; import { restoreScroll } from "../../common/decorators/restore-scroll"; import { fireEvent } from "../../common/dom/fire_event"; -import "../search-input"; import { debounce } from "../../common/util/debounce"; import { nextRender } from "../../common/util/render-status"; import { haStyleScrollbar } from "../../resources/styles"; +import { HomeAssistant } from "../../types"; import "../ha-checkbox"; import type { HaCheckbox } from "../ha-checkbox"; import "../ha-svg-icon"; +import "../search-input"; import { filterData, sortData } from "./sort-filter"; -import { HomeAssistant } from "../../types"; -import "@lit-labs/virtualizer"; declare global { // for fire event @@ -461,7 +461,9 @@ export class HaDataTable extends LitElement { const elapsed = curTime - startTime; if (elapsed < 100) { - await new Promise((resolve) => setTimeout(resolve, 100 - elapsed)); + await new Promise((resolve) => { + setTimeout(resolve, 100 - elapsed); + }); } if (this.curRequest !== curRequest) { return; diff --git a/src/components/device/ha-device-picker.ts b/src/components/device/ha-device-picker.ts index 95a3332fe0..8b9e477d6a 100644 --- a/src/components/device/ha-device-picker.ts +++ b/src/components/device/ha-device-picker.ts @@ -1,5 +1,5 @@ import "@material/mwc-list/mwc-list-item"; -import { UnsubscribeFunc } from "home-assistant-js-websocket"; +import { HassEntity, UnsubscribeFunc } from "home-assistant-js-websocket"; import { html, LitElement, PropertyValues, TemplateResult } from "lit"; import { ComboBoxLitRenderer } from "@vaadin/combo-box/lit"; import { customElement, property, query, state } from "lit/decorators"; @@ -37,6 +37,8 @@ export type HaDevicePickerDeviceFilterFunc = ( device: DeviceRegistryEntry ) => boolean; +export type HaDevicePickerEntityFilterFunc = (entity: HassEntity) => boolean; + const rowRenderer: ComboBoxLitRenderer = (item) => html` @@ -94,6 +96,8 @@ export class HaDevicePicker extends SubscribeMixin(LitElement) { @property() public deviceFilter?: HaDevicePickerDeviceFilterFunc; + @property() public entityFilter?: HaDevicePickerEntityFilterFunc; + @property({ type: Boolean }) public disabled?: boolean; @property({ type: Boolean }) public required?: boolean; @@ -113,6 +117,7 @@ export class HaDevicePicker extends SubscribeMixin(LitElement) { excludeDomains: this["excludeDomains"], includeDeviceClasses: this["includeDeviceClasses"], deviceFilter: this["deviceFilter"], + entityFilter: this["entityFilter"], excludeDevices: this["excludeDevices"] ): Device[] => { if (!devices.length) { @@ -127,7 +132,12 @@ export class HaDevicePicker extends SubscribeMixin(LitElement) { const deviceEntityLookup: DeviceEntityLookup = {}; - if (includeDomains || excludeDomains || includeDeviceClasses) { + if ( + includeDomains || + excludeDomains || + includeDeviceClasses || + entityFilter + ) { for (const entity of entities) { if (!entity.device_id) { continue; @@ -198,6 +208,22 @@ export class HaDevicePicker extends SubscribeMixin(LitElement) { }); } + if (entityFilter) { + inputDevices = inputDevices.filter((device) => { + const devEntities = deviceEntityLookup[device.id]; + if (!devEntities || !devEntities.length) { + return false; + } + return devEntities.some((entity) => { + const stateObj = this.hass.states[entity.entity_id]; + if (!stateObj) { + return false; + } + return entityFilter(stateObj); + }); + }); + } + if (deviceFilter) { inputDevices = inputDevices.filter( (device) => @@ -274,6 +300,7 @@ export class HaDevicePicker extends SubscribeMixin(LitElement) { this.excludeDomains, this.includeDeviceClasses, this.deviceFilter, + this.entityFilter, this.excludeDevices ); } diff --git a/src/components/device/ha-devices-picker.ts b/src/components/device/ha-devices-picker.ts index 1f1340ef04..327af4c8ac 100644 --- a/src/components/device/ha-devices-picker.ts +++ b/src/components/device/ha-devices-picker.ts @@ -4,7 +4,10 @@ import { fireEvent } from "../../common/dom/fire_event"; import { PolymerChangedEvent } from "../../polymer-types"; import { HomeAssistant } from "../../types"; import "./ha-device-picker"; -import type { HaDevicePickerDeviceFilterFunc } from "./ha-device-picker"; +import type { + HaDevicePickerDeviceFilterFunc, + HaDevicePickerEntityFilterFunc, +} from "./ha-device-picker"; @customElement("ha-devices-picker") class HaDevicesPicker extends LitElement { @@ -44,6 +47,8 @@ class HaDevicesPicker extends LitElement { @property() public deviceFilter?: HaDevicePickerDeviceFilterFunc; + @property() public entityFilter?: HaDevicePickerEntityFilterFunc; + protected render(): TemplateResult { if (!this.hass) { return html``; @@ -59,6 +64,7 @@ class HaDevicesPicker extends LitElement { .curValue=${entityId} .hass=${this.hass} .deviceFilter=${this.deviceFilter} + .entityFilter=${this.entityFilter} .includeDomains=${this.includeDomains} .excludeDomains=${this.excludeDomains} .includeDeviceClasses=${this.includeDeviceClasses} @@ -76,8 +82,10 @@ class HaDevicesPicker extends LitElement { .hass=${this.hass} .helper=${this.helper} .deviceFilter=${this.deviceFilter} + .entityFilter=${this.entityFilter} .includeDomains=${this.includeDomains} .excludeDomains=${this.excludeDomains} + .excludeDevices=${currentDevices} .includeDeviceClasses=${this.includeDeviceClasses} .label=${this.pickDeviceLabel} .disabled=${this.disabled} diff --git a/src/components/entity/ha-state-label-badge.ts b/src/components/entity/ha-state-label-badge.ts index 6af024518d..03079a331f 100644 --- a/src/components/entity/ha-state-label-badge.ts +++ b/src/components/entity/ha-state-label-badge.ts @@ -186,7 +186,7 @@ export class HaStateLabelBadge extends LitElement { ? formatNumber( entityState.state, this.hass!.locale, - getNumberFormatOptions(entityState) + getNumberFormatOptions(entityState, entry) ) : computeStateDisplay( this.hass!.localize, diff --git a/src/components/entity/state-badge.ts b/src/components/entity/state-badge.ts index ff50844dc2..b0e001e9c2 100644 --- a/src/components/entity/state-badge.ts +++ b/src/components/entity/state-badge.ts @@ -133,7 +133,7 @@ export class StateBadge extends LitElement { } if (stateObj.attributes.hvac_action) { const hvacAction = stateObj.attributes.hvac_action; - if (["heating", "cooling", "drying", "fan"].includes(hvacAction)) { + if (hvacAction in HVAC_ACTION_TO_MODE) { iconStyle.color = stateColorCss( stateObj, HVAC_ACTION_TO_MODE[hvacAction] diff --git a/src/components/ha-alert.ts b/src/components/ha-alert.ts index d49c9959ac..772d2db0d6 100644 --- a/src/components/ha-alert.ts +++ b/src/components/ha-alert.ts @@ -37,13 +37,10 @@ class HaAlert extends LitElement { @property({ type: Boolean }) public dismissable = false; - @property({ type: Boolean }) public rtl = false; - public render() { return html`