Compare commits

..

9 Commits

Author SHA1 Message Date
Steve Repsher
f7e2fb5271 Allow dependabot to rebase over dedupe commit 2023-02-14 17:30:09 +00:00
Steve Repsher
6ba567c54e Merge branch 'dev' into dependabot-dedupe 2023-02-14 17:20:09 +00:00
Steve Repsher
bb6fb5fb89 Move dedupe check up to fail faster 2023-02-06 21:16:38 +00:00
Steve Repsher
7c1b2e01fe Add concurrency rules to cancel workflows in progress 2023-02-06 21:12:26 +00:00
Steve Repsher
de03c9610b Revise to use separate workflow with GitHub app 2023-02-06 20:44:02 +00:00
Steve Repsher
2fdb6f1241 Merge dev to bump action versions 2023-01-09 14:28:18 +00:00
Steve Repsher
555c43caeb Merge dev and pin action versions 2022-12-27 20:34:31 +00:00
Steve Repsher
997455932f Update bash style to quote variables and use [[ and $() 2022-12-21 14:01:39 +00:00
Steve Repsher
49e2230fb2 Add workflow job to deduplicate dependabot pull requests 2022-12-20 19:59:03 +00:00
186 changed files with 3268 additions and 5830 deletions

54
.github/workflows/dedupe.yaml vendored Normal file
View File

@@ -0,0 +1,54 @@
name: Deduplicate Dependabot
on:
push:
branches:
- dependabot/npm_and_yarn/*
env:
NODE_VERSION: 16
NODE_OPTIONS: --max_old_space_size=6144
permissions:
contents: write
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
dedupe:
name: Deduplicate dependencies
# Only trigger on initial commit from dependabot
if: github.actor == 'dependabot[bot]'
runs-on: ubuntu-latest
steps:
- name: Generate app token
# Use a GitHub app to checkout and commit in order to re-trigger the CI workflow
# (because actions with GITHUB_TOKEN do not trigger new events)
id: generate_token
uses: tibdex/github-app-token@v1.7.0
with:
app_id: ${{ secrets.HA_COMMITTER_APP_ID }}
private_key: ${{ secrets.HA_COMMITTER_PRIVATE_KEY }}
- name: Check out files from GitHub
uses: actions/checkout@v3.3.0
with:
token: ${{ steps.generate_token.outputs.token }}
- name: Set up Node ${{ env.NODE_VERSION }}
uses: actions/setup-node@v3.6.0
with:
node-version: ${{ env.NODE_VERSION }}
cache: yarn
- name: Install dependencies
# Do not run build scripts as a security measure since job has write permissions
run: yarn install --immutable --mode=skip-build
- name: Deduplicate dependencies
run: yarn dedupe --mode=skip-build
- name: Commit changes
run: |
git config user.name "Home Assistant Committer"
git config user.email "hello@home-assistant.io"
git add yarn.lock
git commit -m "Deduplicate dependencies [dependabot skip]" || exit 0
git push origin "HEAD:${GITHUB_HEAD_REF}"

View File

@@ -67,7 +67,7 @@ module.exports.babelOptions = ({ latestBuild }) => ({
"@babel/preset-env",
{
useBuiltIns: "entry",
corejs: { version: "3.28", proposals: true },
corejs: { version: "3.27", proposals: true },
bugfixes: true,
},
],

View File

@@ -2,7 +2,6 @@ const webpack = require("webpack");
const path = require("path");
const TerserPlugin = require("terser-webpack-plugin");
const { WebpackManifestPlugin } = require("webpack-manifest-plugin");
const CompressionWebpackPlugin = require("compression-webpack-plugin");
const log = require("fancy-log");
const WebpackBar = require("webpackbar");
const paths = require("./paths.js");
@@ -76,7 +75,6 @@ const createWebpackConfig = ({
chunkIds: isProdBuild && !isStatsBuild ? "deterministic" : "named",
},
plugins: [
new CompressionWebpackPlugin(),
!isStatsBuild && new WebpackBar({ fancy: !isProdBuild }),
new WebpackManifestPlugin({
// Only include the JS of entrypoints

View File

@@ -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, this.auth.data.hassUrl, path);
castSendShowLovelaceView(this.castManager, path, this.auth.data.hassUrl);
}
private async _handleLogout() {

View File

@@ -252,22 +252,6 @@ export class HcMain extends HassElement {
msg.urlPath = null;
}
this._lovelacePath = msg.viewPath;
if (msg.urlPath === "energy") {
this._lovelaceConfig = {
views: [
{
strategy: {
type: "energy",
options: { show_date_selection: true },
},
},
],
};
this._urlPath = "energy";
this._lovelacePath = 0;
this._sendStatus();
return;
}
if (!this._unsubLovelace || this._urlPath !== msg.urlPath) {
this._urlPath = msg.urlPath;
this._lovelaceConfig = undefined;

View File

@@ -6,9 +6,6 @@ set -e
cd "$(dirname "$0")/.."
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
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

View File

@@ -1,6 +1,6 @@
import "@material/mwc-button";
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { customElement, property, state } from "lit/decorators";
import { property, state } from "lit/decorators";
import { until } from "lit/directives/until";
import "../../../src/components/ha-card";
import "../../../src/components/ha-circular-progress";
@@ -14,7 +14,6 @@ import {
setDemoConfig,
} from "../configs/demo-configs";
@customElement("ha-demo-card")
export class HADemoCard extends LitElement implements LovelaceCard {
@property({ attribute: false }) public lovelace?: Lovelace;
@@ -155,3 +154,5 @@ declare global {
"ha-demo-card": HADemoCard;
}
}
customElements.define("ha-demo-card", HADemoCard);

View File

@@ -1,6 +1,5 @@
// 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 {
@@ -27,8 +26,7 @@ import { mockSystemLog } from "./stubs/system_log";
import { mockTemplate } from "./stubs/template";
import { mockTranslations } from "./stubs/translations";
@customElement("ha-demo")
export class HaDemo extends HomeAssistantAppEl {
class HaDemo extends HomeAssistantAppEl {
protected async _initializeHass() {
const initial: Partial<MockHomeAssistant> = {
panelUrl: (this as any)._panelUrl,
@@ -125,6 +123,8 @@ export class HaDemo extends HomeAssistantAppEl {
}
}
customElements.define("ha-demo", HaDemo);
declare global {
interface HTMLElementTagNameMap {
"ha-demo": HaDemo;

View File

@@ -66,7 +66,7 @@ const incrementalUnits = ["clients", "queries", "ads"];
export const mockHistory = (mockHass: MockHomeAssistant) => {
mockHass.mockAPI(
/history\/period\/.+/,
new RegExp("history/period/.+"),
(hass, _method, path, _parameters) => {
const params = parseQuery<HistoryQueryParams>(path.split("?")[1]);
const entities = params.filter_entity_id.split(",");

View File

@@ -136,7 +136,7 @@ export class DemoAutomationDescribeAction extends LitElement {
<div class="action">
<span>
${this._action
? describeAction(this.hass, [], this._action)
? describeAction(this.hass, this._action)
: "<invalid YAML>"}
</span>
<ha-yaml-editor
@@ -149,7 +149,7 @@ export class DemoAutomationDescribeAction extends LitElement {
${ACTIONS.map(
(conf) => html`
<div class="action">
<span>${describeAction(this.hass, [], conf as any)}</span>
<span>${describeAction(this.hass, conf as any)}</span>
<pre>${dump(conf)}</pre>
</div>
`

View File

@@ -0,0 +1,3 @@
---
title: Bar Slider
---

View File

@@ -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-control-slider";
import "../../../../src/components/ha-bar-slider";
import "../../../../src/components/ha-card";
const sliders: {
@@ -46,7 +46,7 @@ const sliders: {
},
];
@customElement("demo-components-ha-control-slider")
@customElement("demo-components-ha-bar-slider")
export class DemoHaBarSlider extends LitElement {
@state() private value = 50;
@@ -86,7 +86,7 @@ export class DemoHaBarSlider extends LitElement {
<div class="card-content">
<label id=${id}>${label}</label>
<pre>Config: ${JSON.stringify(config)}</pre>
<ha-control-slider
<ha-bar-slider
.value=${this.value}
.mode=${config.mode}
class=${ifDefined(config.class)}
@@ -94,7 +94,7 @@ export class DemoHaBarSlider extends LitElement {
@slider-moved=${this.handleSliderMoved}
aria-labelledby=${id}
>
</ha-control-slider>
</ha-bar-slider>
</div>
</ha-card>
`;
@@ -106,7 +106,7 @@ export class DemoHaBarSlider extends LitElement {
${repeat(sliders, (slider) => {
const { id, label, ...config } = slider;
return html`
<ha-control-slider
<ha-bar-slider
.value=${this.value}
.mode=${config.mode}
vertical
@@ -115,7 +115,7 @@ export class DemoHaBarSlider extends LitElement {
@slider-moved=${this.handleSliderMoved}
aria-label=${label}
>
</ha-control-slider>
</ha-bar-slider>
`;
})}
</div>
@@ -141,11 +141,11 @@ export class DemoHaBarSlider extends LitElement {
font-weight: 600;
}
.custom {
--control-slider-color: #ffcf4c;
--control-slider-background: #ffcf4c;
--control-slider-background-opacity: 0.2;
--control-slider-thickness: 100px;
--control-slider-border-radius: 24px;
--slider-bar-color: #ffcf4c;
--slider-bar-background: #ffcf4c;
--slider-bar-background-opacity: 0.2;
--slider-bar-thickness: 100px;
--slider-bar-border-radius: 24px;
}
.vertical-sliders {
height: 300px;
@@ -165,6 +165,6 @@ export class DemoHaBarSlider extends LitElement {
declare global {
interface HTMLElementTagNameMap {
"demo-components-ha-control-slider": DemoHaBarSlider;
"demo-components-ha-bar-slider": DemoHaBarSlider;
}
}

View File

@@ -0,0 +1,3 @@
---
title: Bar Switch
---

View File

@@ -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-control-switch";
import "../../../../src/components/ha-bar-switch";
import "../../../../src/components/ha-card";
const switches: {
@@ -39,8 +39,8 @@ const switches: {
},
];
@customElement("demo-components-ha-control-switch")
export class DemoHaControlSwitch extends LitElement {
@customElement("demo-components-ha-bar-switch")
export class DemoHaBarSwitch extends LitElement {
@state() private checked = false;
handleValueChanged(e: any) {
@@ -56,7 +56,7 @@ export class DemoHaControlSwitch extends LitElement {
<div class="card-content">
<label id=${id}>${label}</label>
<pre>Config: ${JSON.stringify(config)}</pre>
<ha-control-switch
<ha-bar-switch
.checked=${this.checked}
class=${ifDefined(config.class)}
@change=${this.handleValueChanged}
@@ -66,7 +66,7 @@ export class DemoHaControlSwitch extends LitElement {
disabled=${ifDefined(config.disabled)}
reversed=${ifDefined(config.reversed)}
>
</ha-control-switch>
</ha-bar-switch>
</div>
</ha-card>
`;
@@ -78,7 +78,7 @@ export class DemoHaControlSwitch extends LitElement {
${repeat(switches, (sw) => {
const { id, label, ...config } = sw;
return html`
<ha-control-switch
<ha-bar-switch
.checked=${this.checked}
vertical
class=${ifDefined(config.class)}
@@ -89,7 +89,7 @@ export class DemoHaControlSwitch extends LitElement {
disabled=${ifDefined(config.disabled)}
reversed=${ifDefined(config.reversed)}
>
</ha-control-switch>
</ha-bar-switch>
`;
})}
</div>
@@ -115,11 +115,11 @@ export class DemoHaControlSwitch extends LitElement {
font-weight: 600;
}
.custom {
--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;
--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;
--mdc-icon-size: 24px;
}
.vertical-switches {
@@ -140,6 +140,6 @@ export class DemoHaControlSwitch extends LitElement {
declare global {
interface HTMLElementTagNameMap {
"demo-components-ha-control-switch": DemoHaControlSwitch;
"demo-components-ha-bar-switch": DemoHaBarSwitch;
}
}

View File

@@ -1,3 +0,0 @@
---
title: Control Button
---

View File

@@ -1,192 +0,0 @@
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`
<ha-card>
${repeat(
buttons,
(btn) => html`
<div class="card-content">
<pre>Config: ${JSON.stringify(btn)}</pre>
<ha-control-button
class=${ifDefined(btn.class)}
label=${ifDefined(btn.label)}
disabled=${ifDefined(btn.disabled)}
>
<ha-svg-icon .path=${btn.icon || mdiLightbulb}></ha-svg-icon>
</ha-control-button>
</div>
`
)}
</ha-card>
<ha-card>
${repeat(
buttonGroups,
(group) => html`
<div class="card-content">
<pre>Config: ${JSON.stringify(group)}</pre>
<ha-control-button-group class=${ifDefined(group.class)}>
<ha-control-button>
<ha-svg-icon
.path=${mdiFanSpeed1}
label="Speed 1"
></ha-svg-icon>
</ha-control-button>
<ha-control-button>
<ha-svg-icon
.path=${mdiFanSpeed2}
label="Speed 2"
></ha-svg-icon>
</ha-control-button>
<ha-control-button>
<ha-svg-icon
.path=${mdiFanSpeed3}
label="Speed 3"
></ha-svg-icon>
</ha-control-button>
</ha-control-button-group>
</div>
`
)}
</ha-card>
<ha-card>
<div class="card-content">
<p class="title"><b>Vertical</b></p>
<div class="vertical-buttons">
${repeat(
buttonGroups,
(group) => html`
<ha-control-button-group
vertical
class=${ifDefined(group.class)}
>
<ha-control-button>
<ha-svg-icon
.path=${mdiFanSpeed1}
label="Speed 1"
></ha-svg-icon>
</ha-control-button>
<ha-control-button>
<ha-svg-icon
.path=${mdiFanSpeed2}
label="Speed 2"
></ha-svg-icon>
</ha-control-button>
<ha-control-button>
<ha-svg-icon
.path=${mdiFanSpeed3}
label="Speed 3"
></ha-svg-icon>
</ha-control-button>
</ha-control-button-group>
`
)}
</div>
</div>
</ha-card>
`;
}
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;
}
}

View File

@@ -1,3 +0,0 @@
---
title: Control Slider
---

View File

@@ -1,3 +0,0 @@
---
title: Control Switch
---

View File

@@ -3,7 +3,6 @@ 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",
@@ -19,11 +18,7 @@ export class DemoHaTip extends LitElement {
<div class=${mode}>
<ha-card header="ha-tip ${mode} demo">
<div class="card-content">
${tips.map(
(tip) => html`<ha-tip .hass=${provideHass(this)}
>${tip}</ha-tip
>`
)}
${tips.map((tip) => html`<ha-tip>${tip}</ha-tip>`)}
</div>
</ha-card>
</div>

View File

@@ -1,6 +1,6 @@
import { mdiArrowUpBoldCircle, mdiPuzzle } from "@mdi/js";
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { customElement, property } from "lit/decorators";
import { property } from "lit/decorators";
import memoizeOne from "memoize-one";
import { atLeastVersion } from "../../../src/common/config/version";
import { navigate } from "../../../src/common/navigate";
@@ -14,8 +14,7 @@ import "../components/hassio-card-content";
import { filterAndSort } from "../components/hassio-filter-addons";
import { hassioStyle } from "../resources/hassio-style";
@customElement("hassio-addon-repository")
export class HassioAddonRepositoryEl extends LitElement {
class HassioAddonRepositoryEl extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public supervisor!: Supervisor;
@@ -141,3 +140,5 @@ export class HassioAddonRepositoryEl extends LitElement {
];
}
}
customElements.define("hassio-addon-repository", HassioAddonRepositoryEl);

View File

@@ -9,7 +9,7 @@ import {
PropertyValues,
TemplateResult,
} from "lit";
import { customElement, property, state } from "lit/decorators";
import { 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,8 +49,7 @@ const sortRepos = (a: HassioAddonRepository, b: HassioAddonRepository) => {
return a.name.toUpperCase() < b.name.toUpperCase() ? -1 : 1;
};
@customElement("hassio-addon-store")
export class HassioAddonStore extends LitElement {
class HassioAddonStore extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public supervisor!: Supervisor;
@@ -251,3 +250,5 @@ export class HassioAddonStore extends LitElement {
`;
}
}
customElements.define("hassio-addon-store", HassioAddonStore);

View File

@@ -17,6 +17,7 @@ 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,

View File

@@ -25,20 +25,19 @@
"license": "Apache-2.0",
"dependencies": {
"@braintree/sanitize-url": "^6.0.2",
"@codemirror/autocomplete": "^6.4.2",
"@codemirror/commands": "^6.2.1",
"@codemirror/language": "^6.6.0",
"@codemirror/autocomplete": "^6.4.0",
"@codemirror/commands": "^6.2.0",
"@codemirror/language": "^6.4.0",
"@codemirror/legacy-modes": "^6.3.1",
"@codemirror/search": "^6.2.3",
"@codemirror/state": "^6.2.0",
"@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",
"@codemirror/view": "^6.8.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/core": "^6.1.4",
"@fullcalendar/daygrid": "^6.1.4",
"@fullcalendar/interaction": "^6.1.4",
@@ -71,7 +70,6 @@
"@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.3",
"@mdi/js": "7.1.96",
"@mdi/svg": "7.1.96",
"@polymer/app-layout": "^3.1.0",
@@ -89,8 +87,8 @@
"@polymer/paper-tooltip": "^3.0.1",
"@polymer/polymer": "3.4.1",
"@thomasloven/round-slider": "0.6.0",
"@vaadin/combo-box": "^23.3.7",
"@vaadin/vaadin-themable-mixin": "^23.3.7",
"@vaadin/combo-box": "^23.3.6",
"@vaadin/vaadin-themable-mixin": "^23.3.6",
"@vibrant/color": "^3.2.1-alpha.1",
"@vibrant/core": "^3.2.1-alpha.1",
"@vibrant/quantizer-mmcq": "^3.2.1-alpha.1",
@@ -100,8 +98,7 @@
"app-datepicker": "^5.1.0",
"chart.js": "^3.3.2",
"comlink": "^4.4.1",
"compression-webpack-plugin": "^10.0.0",
"core-js": "^3.28.0",
"core-js": "^3.27.2",
"cropperjs": "^1.5.13",
"date-fns": "^2.29.3",
"date-fns-tz": "^2.0.0",
@@ -109,10 +106,11 @@
"deep-freeze": "^0.0.1",
"fuse.js": "^6.6.2",
"google-timezones-json": "^1.0.2",
"hammerjs": "^2.0.8",
"hls.js": "^1.3.3",
"home-assistant-js-websocket": "^8.0.1",
"idb-keyval": "^6.2.0",
"intl-messageformat": "^10.3.1",
"intl-messageformat": "^10.3.0",
"js-yaml": "^4.1.0",
"leaflet": "^1.9.3",
"leaflet-draw": "^1.0.4",
@@ -132,13 +130,13 @@
"superstruct": "^1.0.3",
"tinykeys": "^1.4.0",
"tsparticles-engine": "^2.9.3",
"tsparticles-preset-links": "^2.9.3",
"tsparticles-preset-links": "^2.8.0",
"unfetch": "^5.0.0",
"vis-data": "^7.1.4",
"vis-network": "^9.1.4",
"vue": "^2.7.14",
"vue2-daterange-picker": "^0.6.8",
"weekstart": "^2.0.0",
"vis-network": "^8.5.4",
"vue": "^2.6.12",
"vue2-daterange-picker": "^0.5.1",
"weekstart": "^1.1.0",
"workbox-cacheable-response": "^6.5.4",
"workbox-core": "^6.5.4",
"workbox-expiration": "^6.5.4",
@@ -148,18 +146,18 @@
"xss": "^1.0.14"
},
"devDependencies": {
"@babel/core": "^7.21.0",
"@babel/core": "^7.20.12",
"@babel/plugin-external-helpers": "^7.18.6",
"@babel/plugin-proposal-class-properties": "^7.18.6",
"@babel/plugin-proposal-decorators": "^7.21.0",
"@babel/plugin-proposal-decorators": "^7.20.13",
"@babel/plugin-proposal-nullish-coalescing-operator": "^7.18.6",
"@babel/plugin-proposal-object-rest-spread": "^7.20.7",
"@babel/plugin-proposal-optional-chaining": "^7.21.0",
"@babel/plugin-proposal-optional-chaining": "^7.20.7",
"@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.21.0",
"@babel/preset-typescript": "^7.18.6",
"@koa/cors": "^4.0.0",
"@octokit/auth-oauth-device": "^4.0.4",
"@octokit/rest": "^19.0.7",
@@ -171,26 +169,26 @@
"@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": "^10",
"@types/mocha": "^8",
"@types/qrcode": "^1.5.0",
"@types/sortablejs": "^1",
"@types/tar": "^6",
"@types/webspeechapi": "^0.0.29",
"@typescript-eslint/eslint-plugin": "^5.53.0",
"@typescript-eslint/parser": "^5.53.0",
"@web/dev-server": "^0.1.35",
"@typescript-eslint/eslint-plugin": "^5.51.0",
"@typescript-eslint/parser": "^5.52.0",
"@web/dev-server": "^0.0.24",
"@web/dev-server-rollup": "^0.2.11",
"babel-loader": "^9.1.2",
"chai": "^4.3.7",
"del": "^7.0.0",
"eslint": "^8.35.0",
"eslint-config-airbnb-base": "^15.0.0",
"eslint": "^7.32.0",
"eslint-config-airbnb-base": "^14.2.1",
"eslint-config-airbnb-typescript": "^17.0.0",
"eslint-config-prettier": "^8.6.0",
"eslint-import-resolver-webpack": "^0.13.2",
@@ -198,9 +196,8 @@
"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-unused-imports": "^1.1.5",
"eslint-plugin-wc": "^1.4.0",
"esprima": "^4.0.1",
"fancy-log": "^2.0.0",
"fs-extra": "^11.1.0",
"glob": "^8.1.0",
@@ -214,15 +211,15 @@
"husky": "^8.0.3",
"instant-mocha": "^1.5.0",
"jszip": "^3.10.1",
"lint-staged": "^13.1.2",
"lint-staged": "^13.1.1",
"lit-analyzer": "^1.2.1",
"lodash.template": "^4.5.0",
"magic-string": "^0.30.0",
"magic-string": "^0.25.7",
"map-stream": "^0.0.7",
"merge-stream": "^2.0.0",
"mocha": "^10.2.0",
"mocha": "^8.4.0",
"object-hash": "^3.0.0",
"open": "^8.4.1",
"open": "^8.4.0",
"pinst": "^3.0.0",
"prettier": "^2.8.4",
"require-dir": "^1.2.0",
@@ -233,14 +230,14 @@
"serve": "^11.3.2",
"sinon": "^15.0.1",
"source-map-url": "^0.4.1",
"systemjs": "^6.14.0",
"systemjs": "^6.13.0",
"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.72.1",
"webpack": "^5.55.1",
"webpack-cli": "^5.0.1",
"webpack-dev-server": "^4.11.1",
"webpack-manifest-plugin": "^5.0.0",

View File

@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "home-assistant-frontend"
version = "20230224.0"
version = "20230202.0"
license = {text = "Apache-2.0"}
description = "The Home Assistant frontend"
readme = "README.md"

View File

@@ -6,9 +6,6 @@ set -e
cd "$(dirname "$0")/.."
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
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

View File

@@ -8,7 +8,7 @@ import {
PropertyValues,
TemplateResult,
} from "lit";
import { customElement, property, state } from "lit/decorators";
import { 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,8 +25,7 @@ import "./ha-password-manager-polyfill";
type State = "loading" | "error" | "step";
@customElement("ha-auth-flow")
export class HaAuthFlow extends litLocalizeLiteMixin(LitElement) {
class HaAuthFlow extends litLocalizeLiteMixin(LitElement) {
@property({ attribute: false }) public authProvider?: AuthProvider;
@property() public clientId?: string;
@@ -408,6 +407,7 @@ export class HaAuthFlow extends litLocalizeLiteMixin(LitElement) {
`;
}
}
customElements.define("ha-auth-flow", HaAuthFlow);
declare global {
interface HTMLElementTagNameMap {

View File

@@ -1,5 +1,5 @@
import { css, CSSResultGroup, html, LitElement, PropertyValues } from "lit";
import { customElement, property, state } from "lit/decorators";
import { 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,8 +14,7 @@ import "./ha-auth-flow";
import("./ha-pick-auth-provider");
@customElement("ha-authorize")
export class HaAuthorize extends litLocalizeLiteMixin(LitElement) {
class HaAuthorize extends litLocalizeLiteMixin(LitElement) {
@property() public clientId?: string;
@property() public redirectUri?: string;
@@ -184,3 +183,4 @@ export class HaAuthorize extends litLocalizeLiteMixin(LitElement) {
`;
}
}
customElements.define("ha-authorize", HaAuthorize);

View File

@@ -1,6 +1,6 @@
import { css, html, LitElement, nothing } from "lit";
/* eslint-disable lit/prefer-static-styles */
import { html, LitElement, TemplateResult } from "lit";
import { customElement, property } from "lit/decorators";
import { styleMap } from "lit/directives/style-map";
import { fireEvent } from "../common/dom/fire_event";
import type { HaFormSchema } from "../components/ha-form/types";
import { autocompleteLoginFields } from "../data/auth";
@@ -29,43 +29,35 @@ export class HaPasswordManagerPolyfill extends LitElement {
@property({ attribute: false }) public boundingRect?: DOMRect;
private _styleElement?: HTMLStyleElement;
public connectedCallback() {
super.connectedCallback();
this._styleElement = document.createElement("style");
this._styleElement.textContent = css`
.password-manager-polyfill {
position: absolute;
opacity: 0;
z-index: -1;
}
.password-manager-polyfill input {
width: 100%;
height: 62px;
padding: 0;
border: 0;
}
.password-manager-polyfill input[type="submit"] {
width: 0;
height: 0;
}
`.toString();
document.head.append(this._styleElement);
}
public disconnectedCallback() {
super.disconnectedCallback();
this._styleElement?.remove();
delete this._styleElement;
}
protected createRenderRoot() {
// Add under document body so the element isn't placed inside any shadow roots
return document.body;
}
protected render() {
private get styles() {
return `
.password-manager-polyfill {
position: absolute;
top: ${this.boundingRect?.y || 148}px;
left: calc(50% - ${(this.boundingRect?.width || 360) / 2}px);
width: ${this.boundingRect?.width || 360}px;
opacity: 0;
z-index: -1;
}
.password-manager-polyfill input {
width: 100%;
height: 62px;
padding: 0;
border: 0;
}
.password-manager-polyfill input[type="submit"] {
width: 0;
height: 0;
}
`;
}
protected render(): TemplateResult {
if (
this.step &&
this.step.type === "form" &&
@@ -75,11 +67,6 @@ export class HaPasswordManagerPolyfill extends LitElement {
return html`
<form
class="password-manager-polyfill"
style=${styleMap({
top: `${this.boundingRect?.y || 148}px`,
left: `calc(50% - ${(this.boundingRect?.width || 360) / 2}px)`,
width: `${this.boundingRect?.width || 360}px`,
})}
aria-hidden="true"
@submit=${this._handleSubmit}
>
@@ -87,13 +74,16 @@ export class HaPasswordManagerPolyfill extends LitElement {
this.render_input(input)
)}
<input type="submit" />
<style>
${this.styles}
</style>
</form>
`;
}
return nothing;
return html``;
}
private render_input(schema: HaFormSchema) {
private render_input(schema: HaFormSchema): TemplateResult | string {
const inputType = schema.name.includes("password") ? "password" : "text";
if (schema.type !== "string") {
return "";

View File

@@ -1,7 +1,7 @@
import "@polymer/paper-item/paper-item";
import "@polymer/paper-item/paper-item-body";
import { css, html, LitElement } from "lit";
import { customElement, property } from "lit/decorators";
import { property } from "lit/decorators";
import { fireEvent } from "../common/dom/fire_event";
import "../components/ha-icon-next";
import { AuthProvider } from "../data/auth";
@@ -13,8 +13,7 @@ declare global {
}
}
@customElement("ha-pick-auth-provider")
export class HaPickAuthProvider extends litLocalizeLiteMixin(LitElement) {
class HaPickAuthProvider extends litLocalizeLiteMixin(LitElement) {
@property() public authProviders: AuthProvider[] = [];
protected render() {
@@ -48,3 +47,4 @@ export class HaPickAuthProvider extends litLocalizeLiteMixin(LitElement) {
}
`;
}
customElements.define("ha-pick-auth-provider", HaPickAuthProvider);

View File

@@ -2,7 +2,6 @@ type NonUndefined<T> = T extends undefined ? never : T;
export function ensureArray(value: undefined): undefined;
export function ensureArray<T>(value: T | T[]): NonUndefined<T>[];
export function ensureArray<T>(value: T | readonly T[]): NonUndefined<T>[];
export function ensureArray(value) {
if (value === undefined || Array.isArray(value)) {
return value;

View File

@@ -24,6 +24,7 @@ import {
mdiDatabase,
mdiEarHearing,
mdiEye,
mdiFan,
mdiFlash,
mdiFlower,
mdiFormatListBulleted,
@@ -90,6 +91,7 @@ export const FIXED_DOMAIN_ICONS = {
conversation: mdiMicrophoneMessage,
counter: mdiCounter,
demo: mdiHomeAssistant,
fan: mdiFan,
google_assistant: mdiGoogleAssistant,
group: mdiGoogleCirclesCommunities,
homeassistant: mdiHomeAssistant,

View File

@@ -10,19 +10,11 @@ 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: seconds_whole,
milliseconds: Math.floor((seconds - seconds_whole) * 1000),
seconds: Number(parts[2]) || 0,
milliseconds: Number(parts[3]) || 0,
};
}
return { seconds: duration };

View File

@@ -1,5 +1,5 @@
import { HassEntity } from "home-assistant-js-websocket";
import { EntityRegistryDisplayEntry } from "../../data/entity_registry";
import { EntityRegistryEntry } from "../../data/entity_registry";
import { HomeAssistant } from "../../types";
import { LocalizeFunc } from "../translations/localize";
import { computeDomain } from "./compute_domain";
@@ -15,7 +15,7 @@ export const computeAttributeValueDisplay = (
const attributeValue =
value !== undefined ? value : stateObj.attributes[attribute];
const domain = computeDomain(entityId);
const entity = entities[entityId] as EntityRegistryDisplayEntry | undefined;
const entity = entities[entityId] as EntityRegistryEntry | undefined;
const translationKey = entity?.translation_key;
return (
@@ -38,7 +38,7 @@ export const computeAttributeNameDisplay = (
): string => {
const entityId = stateObj.entity_id;
const domain = computeDomain(entityId);
const entity = entities[entityId] as EntityRegistryDisplayEntry | undefined;
const entity = entities[entityId] as EntityRegistryEntry | undefined;
const translationKey = entity?.translation_key;
return (

View File

@@ -1,6 +1,6 @@
import { HassEntity } from "home-assistant-js-websocket";
import { UNAVAILABLE, UNKNOWN } from "../../data/entity";
import { EntityRegistryDisplayEntry } from "../../data/entity_registry";
import { EntityRegistryEntry } from "../../data/entity_registry";
import { FrontendLocaleData } from "../../data/translation";
import {
updateIsInstallingFromAttributes,
@@ -49,7 +49,7 @@ export const computeStateDisplayFromEntityAttributes = (
return localize(`state.default.${state}`);
}
const entity = entities[entityId] as EntityRegistryDisplayEntry | undefined;
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)) {

View File

@@ -15,8 +15,6 @@ import {
mdiCheckCircleOutline,
mdiClock,
mdiCloseCircleOutline,
mdiFan,
mdiFanOff,
mdiGestureTapButton,
mdiLanConnect,
mdiLanDisconnect,
@@ -110,9 +108,6 @@ export const domainIconWithoutDefault = (
}
return compareState === "not_home" ? mdiAccountArrowRight : mdiAccount;
case "fan":
return compareState === "off" ? mdiFanOff : mdiFan;
case "humidifier":
return compareState === "off" ? mdiAirHumidifierOff : mdiAirHumidifier;

View File

@@ -2,7 +2,7 @@ import {
HassEntity,
HassEntityAttributeBase,
} from "home-assistant-js-websocket";
import { EntityRegistryDisplayEntry } from "../../data/entity_registry";
import { EntityRegistryEntry } from "../../data/entity_registry";
import { FrontendLocaleData, NumberFormat } from "../../data/translation";
import { round } from "./round";
@@ -92,9 +92,11 @@ export const formatNumber = (
*/
export const getNumberFormatOptions = (
entityState: HassEntity,
entity?: EntityRegistryDisplayEntry
entity?: EntityRegistryEntry
): Intl.NumberFormatOptions | undefined => {
const precision = entity?.display_precision;
const precision =
entity?.options?.sensor?.display_precision ??
entity?.options?.sensor?.suggested_display_precision;
if (precision != null) {
return {
maximumFractionDigits: precision,

View File

@@ -1,4 +1,4 @@
const isTemplateRegex = /{%|{{/;
const isTemplateRegex = new RegExp("{%|{{");
export const isTemplate = (value: string): boolean =>
isTemplateRegex.test(value);

View File

@@ -1,5 +1,6 @@
import { refine, string } from "superstruct";
import { isCustomType } from "../../data/lovelace_custom_cards";
export const isCustomType = (value: string) => value.startsWith("custom:");
export const customType = () =>
refine(string(), "custom element type", isCustomType);

View File

@@ -1,10 +1,9 @@
import { css, CSSResultGroup, html, LitElement } from "lit";
import { customElement, property, query } from "lit/decorators";
import { 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;
@@ -70,6 +69,8 @@ class HaCallApiButton extends LitElement {
}
}
customElements.define("ha-call-api-button", HaCallApiButton);
declare global {
interface HTMLElementTagNameMap {
"ha-call-api-button": HaCallApiButton;

View File

@@ -59,7 +59,7 @@ export const statTypeMap: Record<ExtendedStatisticType, StatisticType> = {
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,11 +99,7 @@ class StatisticsChart extends LitElement {
if (!this.hasUpdated || changedProps.has("unit")) {
this._createOptions();
}
if (
changedProps.has("statisticsData") ||
changedProps.has("statTypes") ||
changedProps.has("hideLegend")
) {
if (changedProps.has("statisticsData") || changedProps.has("statTypes")) {
this._generateData();
}
}
@@ -332,10 +328,7 @@ class StatisticsChart extends LitElement {
prevEndTime = end;
};
const color = getGraphColorByIndex(
colorIndex,
this._computedStyle || getComputedStyle(this)
);
const color = getGraphColorByIndex(colorIndex, this._computedStyle!);
colorIndex++;
const statTypes: this["statTypes"] = [];

View File

@@ -1,4 +1,3 @@
import "@lit-labs/virtualizer";
import { mdiArrowDown, mdiArrowUp } from "@mdi/js";
import deepClone from "deep-clone-simple";
import {
@@ -22,15 +21,16 @@ 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,9 +461,7 @@ 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;

View File

@@ -1,5 +1,5 @@
import "@material/mwc-list/mwc-list-item";
import { HassEntity, UnsubscribeFunc } from "home-assistant-js-websocket";
import { 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,8 +37,6 @@ export type HaDevicePickerDeviceFilterFunc = (
device: DeviceRegistryEntry
) => boolean;
export type HaDevicePickerEntityFilterFunc = (entity: HassEntity) => boolean;
const rowRenderer: ComboBoxLitRenderer<Device> = (item) => html`<mwc-list-item
.twoline=${!!item.area}
>
@@ -96,8 +94,6 @@ 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;
@@ -117,7 +113,6 @@ 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) {
@@ -132,12 +127,7 @@ export class HaDevicePicker extends SubscribeMixin(LitElement) {
const deviceEntityLookup: DeviceEntityLookup = {};
if (
includeDomains ||
excludeDomains ||
includeDeviceClasses ||
entityFilter
) {
if (includeDomains || excludeDomains || includeDeviceClasses) {
for (const entity of entities) {
if (!entity.device_id) {
continue;
@@ -208,22 +198,6 @@ 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) =>
@@ -300,7 +274,6 @@ export class HaDevicePicker extends SubscribeMixin(LitElement) {
this.excludeDomains,
this.includeDeviceClasses,
this.deviceFilter,
this.entityFilter,
this.excludeDevices
);
}

View File

@@ -4,10 +4,7 @@ import { fireEvent } from "../../common/dom/fire_event";
import { PolymerChangedEvent } from "../../polymer-types";
import { HomeAssistant } from "../../types";
import "./ha-device-picker";
import type {
HaDevicePickerDeviceFilterFunc,
HaDevicePickerEntityFilterFunc,
} from "./ha-device-picker";
import type { HaDevicePickerDeviceFilterFunc } from "./ha-device-picker";
@customElement("ha-devices-picker")
class HaDevicesPicker extends LitElement {
@@ -47,8 +44,6 @@ class HaDevicesPicker extends LitElement {
@property() public deviceFilter?: HaDevicePickerDeviceFilterFunc;
@property() public entityFilter?: HaDevicePickerEntityFilterFunc;
protected render(): TemplateResult {
if (!this.hass) {
return html``;
@@ -64,7 +59,6 @@ class HaDevicesPicker extends LitElement {
.curValue=${entityId}
.hass=${this.hass}
.deviceFilter=${this.deviceFilter}
.entityFilter=${this.entityFilter}
.includeDomains=${this.includeDomains}
.excludeDomains=${this.excludeDomains}
.includeDeviceClasses=${this.includeDeviceClasses}
@@ -82,10 +76,8 @@ 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}

View File

@@ -22,7 +22,7 @@ import {
isNumericState,
} from "../../common/number/format_number";
import { isUnavailableState, UNAVAILABLE, UNKNOWN } from "../../data/entity";
import { EntityRegistryDisplayEntry } from "../../data/entity_registry";
import { EntityRegistryEntry } from "../../data/entity_registry";
import { timerTimeRemaining } from "../../data/timer";
import { HomeAssistant } from "../../types";
import "../ha-label-badge";
@@ -160,7 +160,7 @@ export class HaStateLabelBadge extends LitElement {
private _computeValue(
domain: string,
entityState: HassEntity,
entry?: EntityRegistryDisplayEntry
entry?: EntityRegistryEntry
) {
switch (domain) {
case "alarm_control_panel":
@@ -200,7 +200,7 @@ export class HaStateLabelBadge extends LitElement {
private _computeShowIcon(
domain: string,
entityState: HassEntity,
entry?: EntityRegistryDisplayEntry
entry?: EntityRegistryEntry
): boolean {
if (entityState.state === UNAVAILABLE) {
return false;

View File

@@ -1,6 +1,4 @@
import "@material/mwc-list/mwc-list-item";
import { ComboBoxLitRenderer } from "@vaadin/combo-box/lit";
import { HassEntity } from "home-assistant-js-websocket";
import { html, LitElement, PropertyValues, TemplateResult } from "lit";
import { customElement, property, query, state } from "lit/decorators";
import { classMap } from "lit/directives/class-map";
@@ -12,11 +10,10 @@ import {
createAreaRegistryEntry,
} from "../data/area_registry";
import {
DeviceEntityDisplayLookup,
DeviceEntityLookup,
DeviceRegistryEntry,
getDeviceEntityDisplayLookup,
} from "../data/device_registry";
import { EntityRegistryDisplayEntry } from "../data/entity_registry";
import { EntityRegistryEntry } from "../data/entity_registry";
import {
showAlertDialog,
showPromptDialog,
@@ -86,7 +83,7 @@ export class HaAreaPicker extends LitElement {
@property() public deviceFilter?: HaDevicePickerDeviceFilterFunc;
@property() public entityFilter?: (entity: HassEntity) => boolean;
@property() public entityFilter?: (entity: EntityRegistryEntry) => boolean;
@property({ type: Boolean }) public disabled?: boolean;
@@ -114,7 +111,7 @@ export class HaAreaPicker extends LitElement {
(
areas: AreaRegistryEntry[],
devices: DeviceRegistryEntry[],
entities: EntityRegistryDisplayEntry[],
entities: EntityRegistryEntry[],
includeDomains: this["includeDomains"],
excludeDomains: this["excludeDomains"],
includeDeviceClasses: this["includeDeviceClasses"],
@@ -134,107 +131,96 @@ export class HaAreaPicker extends LitElement {
];
}
let deviceEntityLookup: DeviceEntityDisplayLookup = {};
const deviceEntityLookup: DeviceEntityLookup = {};
let inputDevices: DeviceRegistryEntry[] | undefined;
let inputEntities: EntityRegistryDisplayEntry[] | undefined;
let inputEntities: EntityRegistryEntry[] | undefined;
if (
includeDomains ||
excludeDomains ||
includeDeviceClasses ||
deviceFilter ||
entityFilter
) {
deviceEntityLookup = getDeviceEntityDisplayLookup(entities);
if (includeDomains || excludeDomains || includeDeviceClasses) {
for (const entity of entities) {
if (!entity.device_id) {
continue;
}
if (!(entity.device_id in deviceEntityLookup)) {
deviceEntityLookup[entity.device_id] = [];
}
deviceEntityLookup[entity.device_id].push(entity);
}
inputDevices = devices;
inputEntities = entities.filter((entity) => entity.area_id);
} else {
if (deviceFilter) {
inputDevices = devices;
}
if (entityFilter) {
inputEntities = entities.filter((entity) => entity.area_id);
}
}
if (includeDomains) {
inputDevices = inputDevices!.filter((device) => {
const devEntities = deviceEntityLookup[device.id];
if (!devEntities || !devEntities.length) {
return false;
}
return deviceEntityLookup[device.id].some((entity) =>
includeDomains.includes(computeDomain(entity.entity_id))
);
});
inputEntities = inputEntities!.filter((entity) =>
if (includeDomains) {
inputDevices = inputDevices!.filter((device) => {
const devEntities = deviceEntityLookup[device.id];
if (!devEntities || !devEntities.length) {
return false;
}
return deviceEntityLookup[device.id].some((entity) =>
includeDomains.includes(computeDomain(entity.entity_id))
);
}
});
inputEntities = inputEntities!.filter((entity) =>
includeDomains.includes(computeDomain(entity.entity_id))
);
}
if (excludeDomains) {
inputDevices = inputDevices!.filter((device) => {
const devEntities = deviceEntityLookup[device.id];
if (!devEntities || !devEntities.length) {
return true;
}
return entities.every(
(entity) =>
!excludeDomains.includes(computeDomain(entity.entity_id))
);
});
inputEntities = inputEntities!.filter(
if (excludeDomains) {
inputDevices = inputDevices!.filter((device) => {
const devEntities = deviceEntityLookup[device.id];
if (!devEntities || !devEntities.length) {
return true;
}
return entities.every(
(entity) =>
!excludeDomains.includes(computeDomain(entity.entity_id))
);
}
});
inputEntities = inputEntities!.filter(
(entity) => !excludeDomains.includes(computeDomain(entity.entity_id))
);
}
if (includeDeviceClasses) {
inputDevices = inputDevices!.filter((device) => {
const devEntities = deviceEntityLookup[device.id];
if (!devEntities || !devEntities.length) {
if (includeDeviceClasses) {
inputDevices = inputDevices!.filter((device) => {
const devEntities = deviceEntityLookup[device.id];
if (!devEntities || !devEntities.length) {
return false;
}
return deviceEntityLookup[device.id].some((entity) => {
const stateObj = this.hass.states[entity.entity_id];
if (!stateObj) {
return false;
}
return deviceEntityLookup[device.id].some((entity) => {
const stateObj = this.hass.states[entity.entity_id];
if (!stateObj) {
return false;
}
return (
stateObj.attributes.device_class &&
includeDeviceClasses.includes(stateObj.attributes.device_class)
);
});
});
inputEntities = inputEntities!.filter((entity) => {
const stateObj = this.hass.states[entity.entity_id];
return (
stateObj.attributes.device_class &&
includeDeviceClasses.includes(stateObj.attributes.device_class)
);
});
}
if (deviceFilter) {
inputDevices = inputDevices!.filter((device) =>
deviceFilter!(device)
});
inputEntities = inputEntities!.filter((entity) => {
const stateObj = this.hass.states[entity.entity_id];
return (
stateObj.attributes.device_class &&
includeDeviceClasses.includes(stateObj.attributes.device_class)
);
}
});
}
if (entityFilter) {
inputDevices = inputDevices!.filter((device) => {
const devEntities = deviceEntityLookup[device.id];
if (!devEntities || !devEntities.length) {
return false;
}
return deviceEntityLookup[device.id].some((entity) => {
const stateObj = this.hass.states[entity.entity_id];
if (!stateObj) {
return false;
}
return entityFilter(stateObj);
});
});
inputEntities = inputEntities!.filter((entity) => {
const stateObj = this.hass.states[entity.entity_id];
if (!stateObj) {
return false;
}
return entityFilter!(stateObj);
});
}
if (deviceFilter) {
inputDevices = inputDevices!.filter((device) => deviceFilter!(device));
}
if (entityFilter) {
inputEntities = inputEntities!.filter((entity) =>
entityFilter!(entity)
);
}
let outputAreas = areas;

View File

@@ -1,7 +1,7 @@
import { HassEntity } from "home-assistant-js-websocket";
import { css, html, LitElement, TemplateResult } from "lit";
import { customElement, property } from "lit/decorators";
import { fireEvent } from "../common/dom/fire_event";
import type { EntityRegistryEntry } from "../data/entity_registry";
import { SubscribeMixin } from "../mixins/subscribe-mixin";
import type { HomeAssistant } from "../types";
import type { HaDevicePickerDeviceFilterFunc } from "./device/ha-device-picker";
@@ -48,7 +48,7 @@ export class HaAreasPicker extends SubscribeMixin(LitElement) {
@property() public deviceFilter?: HaDevicePickerDeviceFilterFunc;
@property() public entityFilter?: (entity: HassEntity) => boolean;
@property() public entityFilter?: (entity: EntityRegistryEntry) => boolean;
@property({ attribute: "picked-area-label" })
public pickedAreaLabel?: string;

View File

@@ -1,4 +1,4 @@
import { DIRECTION_ALL, Manager, Pan, Tap } from "@egjs/hammerjs";
import "hammerjs";
import {
css,
CSSResultGroup,
@@ -42,9 +42,9 @@ const getPercentageFromEvent = (e: HammerInput, vertical: boolean) => {
return Math.max(Math.min(1, (x - offset) / total), 0);
};
@customElement("ha-control-slider")
export class HaControlSlider extends LitElement {
@property({ type: Boolean, reflect: true })
@customElement("ha-bar-slider")
export class HaBarSlider extends LitElement {
@property({ type: Boolean })
public disabled = false;
@property()
@@ -131,18 +131,18 @@ export class HaControlSlider extends LitElement {
setupListeners() {
if (this.slider && !this._mc) {
this._mc = new Manager(this.slider, {
this._mc = new Hammer.Manager(this.slider, {
touchAction: this.vertical ? "pan-x" : "pan-y",
});
this._mc.add(
new Pan({
new Hammer.Pan({
threshold: 10,
direction: DIRECTION_ALL,
direction: Hammer.DIRECTION_ALL,
enable: true,
})
);
this._mc.add(new Tap({ event: "singletap" }));
this._mc.add(new Hammer.Tap({ event: "singletap" }));
let savedValue;
this._mc.on("panstart", () => {
@@ -245,16 +245,14 @@ export class HaControlSlider extends LitElement {
>
<div class="slider-track-background"></div>
${this.mode === "cursor"
? this.value != null
? html`
<div
class=${classMap({
"slider-track-cursor": true,
vertical: this.vertical,
})}
></div>
`
: null
? html`
<div
class=${classMap({
"slider-track-cursor": true,
vertical: this.vertical,
})}
></div>
`
: html`
<div
class=${classMap({
@@ -273,29 +271,28 @@ export class HaControlSlider extends LitElement {
return css`
:host {
display: block;
--control-slider-color: var(--primary-color);
--control-slider-background: var(--disabled-color);
--control-slider-background-opacity: 0.2;
--control-slider-thickness: 40px;
--control-slider-border-radius: 10px;
height: var(--control-slider-thickness);
--slider-bar-color: var(--primary-color);
--slider-bar-background: var(--disabled-color);
--slider-bar-background-opacity: 0.2;
--slider-bar-thickness: 40px;
--slider-bar-border-radius: 10px;
height: var(--slider-bar-thickness);
width: 100%;
border-radius: var(--control-slider-border-radius);
border-radius: var(--slider-bar-border-radius);
outline: none;
transition: box-shadow 180ms ease-in-out;
}
:host(:focus-visible) {
box-shadow: 0 0 0 2px var(--control-slider-color);
box-shadow: 0 0 0 2px var(--slider-bar-color);
}
:host([vertical]) {
width: var(--control-slider-thickness);
width: var(--slider-bar-thickness);
height: 100%;
}
.slider {
position: relative;
height: 100%;
width: 100%;
border-radius: var(--control-slider-border-radius);
border-radius: var(--slider-bar-border-radius);
transform: translateZ(0);
overflow: hidden;
cursor: pointer;
@@ -309,20 +306,19 @@ export class HaControlSlider extends LitElement {
left: 0;
height: 100%;
width: 100%;
background: var(--control-slider-background);
opacity: var(--control-slider-background-opacity);
background: var(--slider-bar-background);
opacity: var(--slider-bar-background-opacity);
}
.slider .slider-track-bar {
--border-radius: var(--control-slider-border-radius);
--border-radius: var(--slider-bar-border-radius);
--handle-size: 4px;
--handle-margin: calc(var(--control-slider-thickness) / 8);
--handle-margin: calc(var(--slider-bar-thickness) / 8);
--slider-size: 100%;
position: absolute;
height: 100%;
width: 100%;
background-color: var(--control-slider-color);
transition: transform 180ms ease-in-out,
background-color 180ms ease-in-out;
background-color: var(--slider-bar-color);
transition: transform 180ms ease-in-out;
}
.slider .slider-track-bar.show-handle {
--slider-size: calc(
@@ -416,7 +412,7 @@ export class HaControlSlider extends LitElement {
}
.slider .slider-track-cursor {
--cursor-size: calc(var(--control-slider-thickness) / 4);
--cursor-size: calc(var(--slider-bar-thickness) / 4);
--handle-size: 4px;
position: absolute;
background-color: white;
@@ -449,15 +445,12 @@ export class HaControlSlider extends LitElement {
:host([pressed]) .slider-track-cursor {
transition: none;
}
:host(:disabled) .slider {
cursor: not-allowed;
}
`;
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-control-slider": HaControlSlider;
"ha-bar-slider": HaBarSlider;
}
}

View File

@@ -1,10 +1,3 @@
import {
DIRECTION_HORIZONTAL,
DIRECTION_VERTICAL,
Manager,
Swipe,
Tap,
} from "@egjs/hammerjs";
import {
css,
CSSResultGroup,
@@ -13,13 +6,13 @@ import {
PropertyValues,
TemplateResult,
} from "lit";
import { customElement, property, query } from "lit/decorators";
import { customElement, property } from "lit/decorators";
import { fireEvent } from "../common/dom/fire_event";
import "./ha-svg-icon";
@customElement("ha-control-switch")
export class HaControlSwitch extends LitElement {
@property({ type: Boolean, reflect: true })
@customElement("ha-bar-switch")
export class HaBarSwitch extends LitElement {
@property({ type: Boolean, attribute: "disabled" })
public disabled = false;
@property({ type: Boolean })
@@ -37,11 +30,8 @@ export class HaControlSwitch extends LitElement {
// SVG icon path (if you need a non SVG icon instead, use the provided off icon slot to pass an <ha-icon slot="icon-off"> in)
@property({ type: String }) pathOff?: string;
private _mc?: HammerManager;
protected firstUpdated(changedProperties: PropertyValues): void {
super.firstUpdated(changedProperties);
this.setupListeners();
this.setAttribute("role", "switch");
if (!this.hasAttribute("tabindex")) {
this.setAttribute("tabindex", "0");
@@ -50,7 +40,7 @@ export class HaControlSwitch extends LitElement {
protected updated(changedProps: PropertyValues) {
super.updated(changedProps);
if (changedProps.has("checked")) {
if (changedProps.has("value")) {
this.setAttribute("aria-checked", this.checked ? "true" : "false");
}
}
@@ -63,70 +53,14 @@ export class HaControlSwitch extends LitElement {
connectedCallback(): void {
super.connectedCallback();
this.setupListeners();
this.addEventListener("keydown", this._keydown);
this.addEventListener("click", this._toggle);
}
disconnectedCallback(): void {
super.disconnectedCallback();
this.destroyListeners();
}
@query("#switch")
private switch!: HTMLDivElement;
setupListeners() {
if (this.switch && !this._mc) {
this._mc = new Manager(this.switch, {
touchAction: this.vertical ? "pan-x" : "pan-y",
});
this._mc.add(
new Swipe({
direction: this.vertical ? DIRECTION_VERTICAL : DIRECTION_HORIZONTAL,
})
);
this._mc.add(new Tap({ event: "singletap" }));
if (this.vertical) {
this._mc.on("swipeup", () => {
if (this.disabled) return;
this.checked = !!this.reversed;
fireEvent(this, "change");
});
this._mc.on("swipedown", () => {
if (this.disabled) return;
this.checked = !this.reversed;
fireEvent(this, "change");
});
} else {
this._mc.on("swiperight", () => {
if (this.disabled) return;
this.checked = !this.reversed;
fireEvent(this, "change");
});
this._mc.on("swipeleft", () => {
if (this.disabled) return;
this.checked = !!this.reversed;
fireEvent(this, "change");
});
}
this._mc.on("singletap", () => {
if (this.disabled) return;
this._toggle();
});
this.addEventListener("keydown", this._keydown);
}
}
destroyListeners() {
if (this._mc) {
this._mc.destroy();
this._mc = undefined;
}
this.removeEventListener("keydown", this._keydown);
this.removeEventListener("click", this._toggle);
}
private _keydown(ev: any) {
@@ -139,7 +73,7 @@ export class HaControlSwitch extends LitElement {
protected render(): TemplateResult {
return html`
<div id="switch" class="switch">
<div class="switch">
<div class="background"></div>
<div class="button" aria-hidden="true">
${this.checked
@@ -158,37 +92,35 @@ export class HaControlSwitch extends LitElement {
return css`
:host {
display: block;
--control-switch-on-color: var(--primary-color);
--control-switch-off-color: var(--disabled-color);
--control-switch-background-opacity: 0.2;
--control-switch-thickness: 40px;
--control-switch-border-radius: 12px;
--control-switch-padding: 4px;
--switch-bar-on-color: var(--primary-color);
--switch-bar-off-color: var(--disabled-color);
--switch-bar-background-opacity: 0.2;
--switch-bar-thickness: 40px;
--switch-bar-border-radius: 12px;
--switch-bar-padding: 4px;
--mdc-icon-size: 20px;
height: var(--control-switch-thickness);
height: var(--switch-bar-thickness);
width: 100%;
box-sizing: border-box;
user-select: none;
cursor: pointer;
border-radius: var(--control-switch-border-radius);
border-radius: var(--switch-bar-border-radius);
outline: none;
transition: box-shadow 180ms ease-in-out;
-webkit-tap-highlight-color: transparent;
}
:host(:focus-visible) {
box-shadow: 0 0 0 2px var(--control-switch-off-color);
box-shadow: 0 0 0 2px var(--switch-bar-off-color);
}
:host([checked]:focus-visible) {
box-shadow: 0 0 0 2px var(--control-switch-on-color);
box-shadow: 0 0 0 2px var(--switch-bar-on-color);
}
.switch {
box-sizing: border-box;
position: relative;
height: 100%;
width: 100%;
border-radius: var(--control-switch-border-radius);
border-radius: var(--switch-bar-border-radius);
overflow: hidden;
padding: var(--control-switch-padding);
padding: var(--switch-bar-padding);
display: flex;
}
.switch .background {
@@ -197,31 +129,31 @@ export class HaControlSwitch extends LitElement {
left: 0;
height: 100%;
width: 100%;
background-color: var(--control-switch-off-color);
background-color: var(--switch-bar-off-color);
transition: background-color 180ms ease-in-out;
opacity: var(--control-switch-background-opacity);
opacity: var(--switch-bar-background-opacity);
}
.switch .button {
width: 50%;
height: 100%;
background: lightgrey;
border-radius: calc(
var(--control-switch-border-radius) - var(--control-switch-padding)
var(--switch-bar-border-radius) - var(--switch-bar-padding)
);
transition: transform 180ms ease-in-out,
background-color 180ms ease-in-out;
background-color: var(--control-switch-off-color);
background-color: var(--switch-bar-off-color);
color: white;
display: flex;
align-items: center;
justify-content: center;
}
:host([checked]) .switch .background {
background-color: var(--control-switch-on-color);
background-color: var(--switch-bar-on-color);
}
:host([checked]) .switch .button {
transform: translateX(100%);
background-color: var(--control-switch-on-color);
background-color: var(--switch-bar-on-color);
}
:host([reversed]) .switch {
flex-direction: row-reverse;
@@ -230,7 +162,7 @@ export class HaControlSwitch extends LitElement {
transform: translateX(-100%);
}
:host([vertical]) {
width: var(--control-switch-thickness);
width: var(--switch-bar-thickness);
height: 100%;
}
:host([vertical][checked]) .switch .button {
@@ -256,6 +188,6 @@ export class HaControlSwitch extends LitElement {
declare global {
interface HTMLElementTagNameMap {
"ha-control-switch": HaControlSwitch;
"ha-bar-switch": HaBarSwitch;
}
}

View File

@@ -44,6 +44,7 @@ export class HaBar extends LitElement {
}
rect:last-child {
fill: var(--ha-bar-primary-color, var(--primary-color));
rx: var(--ha-bar-border-radius, 4px);
}
svg {
border-radius: var(--ha-bar-border-radius, 4px);

View File

@@ -1,63 +0,0 @@
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { customElement, property } from "lit/decorators";
@customElement("ha-control-button-group")
export class HaControlButtonGroup extends LitElement {
@property({ type: Boolean, reflect: true })
public vertical = false;
protected render(): TemplateResult {
return html`
<div class="container">
<slot></slot>
</div>
`;
}
static get styles(): CSSResultGroup {
return css`
:host {
--control-button-group-spacing: 12px;
--control-button-group-thickness: 40px;
height: var(--control-button-group-thickness);
width: auto;
display: block;
}
.container {
display: flex;
flex-direction: row;
width: 100%;
height: 100%;
}
::slotted(*) {
flex: 1;
height: 100%;
width: 100%;
}
::slotted(*:not(:last-child)) {
margin-right: var(--control-button-group-spacing);
margin-inline-end: var(--control-button-group-spacing);
margin-inline-start: initial;
direction: var(--direction);
}
:host([vertical]) {
width: var(--control-button-group-thickness);
height: auto;
}
:host([vertical]) .container {
flex-direction: column;
}
:host([vertical]) ::slotted(ha-control-button:not(:last-child)) {
margin-right: initial;
margin-inline-end: initial;
margin-bottom: var(--control-button-group-spacing);
}
`;
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-control-button-group": HaControlButtonGroup;
}
}

View File

@@ -40,22 +40,6 @@ export class HaDialog extends DialogBase {
this.suppressDefaultPressSelector,
SUPPRESS_DEFAULT_PRESS_SELECTOR,
].join(", ");
this._updateScrolledAttribute();
this.contentElement?.addEventListener("scroll", this._onScroll);
}
disconnectedCallback(): void {
super.disconnectedCallback();
this.contentElement.removeEventListener("scroll", this._onScroll);
}
private _onScroll = () => {
this._updateScrolledAttribute();
};
private _updateScrolledAttribute() {
if (!this.contentElement) return;
this.toggleAttribute("scrolled", this.contentElement.scrollTop !== 0);
}
static override styles = [

View File

@@ -26,9 +26,6 @@ export class Gauge extends LitElement {
@property({ type: Number }) public value = 0;
@property({ attribute: false })
public formatOptions?: Intl.NumberFormatOptions;
@property({ type: String }) public valueText?: string;
@property() public locale!: FrontendLocaleData;
@@ -135,8 +132,7 @@ export class Gauge extends LitElement {
${
this._segment_label
? this._segment_label
: this.valueText ||
formatNumber(this.value, this.locale, this.formatOptions)
: this.valueText || formatNumber(this.value, this.locale)
}${
this._segment_label
? ""

View File

@@ -36,12 +36,6 @@ export class HaHeaderBar extends LitElement {
position: static;
color: var(--mdc-theme-on-primary, #fff);
}
.mdc-top-app-bar__section.mdc-top-app-bar__section--align-start {
flex: 1;
}
.mdc-top-app-bar__section.mdc-top-app-bar__section--align-end {
flex: none;
}
`,
];
}

View File

@@ -1,8 +1,6 @@
import { mdiChevronLeft, mdiChevronRight } from "@mdi/js";
import { customElement } from "lit/decorators";
import { HaSvgIcon } from "./ha-svg-icon";
@customElement("ha-icon-next")
export class HaIconNext extends HaSvgIcon {
public connectedCallback() {
super.connectedCallback();
@@ -22,3 +20,5 @@ declare global {
"ha-icon-next": HaIconNext;
}
}
customElements.define("ha-icon-next", HaIconNext);

View File

@@ -1,8 +1,6 @@
import { mdiChevronLeft, mdiChevronRight } from "@mdi/js";
import { customElement } from "lit/decorators";
import { HaSvgIcon } from "./ha-svg-icon";
@customElement("ha-icon-prev")
export class HaIconPrev extends HaSvgIcon {
public connectedCallback() {
super.connectedCallback();
@@ -22,3 +20,5 @@ declare global {
"ha-icon-prev": HaIconPrev;
}
}
customElements.define("ha-icon-prev", HaIconPrev);

View File

@@ -6,10 +6,9 @@ import {
PropertyValues,
TemplateResult,
} from "lit";
import { customElement, property } from "lit/decorators";
import { property } from "lit/decorators";
import { classMap } from "lit/directives/class-map";
@customElement("ha-label-badge")
class HaLabelBadge extends LitElement {
@property() public label?: string;
@@ -62,7 +61,7 @@ class HaLabelBadge extends LitElement {
height: var(--ha-label-badge-size, 2.5em);
line-height: var(--ha-label-badge-size, 2.5em);
font-size: var(--ha-label-badge-font-size, 1.5em);
border-radius: var(--ha-label-badge-border-radius, 50%);
border-radius: 50%;
border: 0.1em solid var(--ha-label-badge-color, var(--primary-color));
color: var(--label-badge-text-color, rgb(76, 76, 76));
@@ -133,3 +132,5 @@ declare global {
"ha-label-badge": HaLabelBadge;
}
}
customElements.define("ha-label-badge", HaLabelBadge);

View File

@@ -30,30 +30,6 @@ export class HaListItem extends ListItemBase {
margin-inline-end: 0px !important;
direction: var(--direction);
}
:host([multiline-secondary]) {
height: auto;
}
:host([multiline-secondary]) .mdc-deprecated-list-item__text {
padding: 8px 0;
}
:host([multiline-secondary]) .mdc-deprecated-list-item__secondary-text {
text-overflow: initial;
white-space: normal;
overflow: auto;
display: inline-block;
margin-top: 10px;
}
:host([multiline-secondary]) .mdc-deprecated-list-item__primary-text {
margin-top: 10px;
}
:host([multiline-secondary])
.mdc-deprecated-list-item__secondary-text::before {
display: none;
}
:host([multiline-secondary])
.mdc-deprecated-list-item__primary-text::before {
display: none;
}
`,
];
}

View File

@@ -282,9 +282,7 @@ export class HaRelatedItems extends SubscribeMixin(LitElement) {
private async _navigateAwayClose() {
// allow new page to open before closing dialog
await new Promise((resolve) => {
setTimeout(resolve, 0);
});
await new Promise((resolve) => setTimeout(resolve, 0));
fireEvent(this, "close-dialog");
}

View File

@@ -1,10 +1,13 @@
import { HassEntity } from "home-assistant-js-websocket";
import { HassEntity, UnsubscribeFunc } from "home-assistant-js-websocket";
import { html, LitElement, PropertyValues, TemplateResult } from "lit";
import { customElement, property, state } from "lit/decorators";
import memoizeOne from "memoize-one";
import { ensureArray } from "../../common/array/ensure-array";
import type { DeviceRegistryEntry } from "../../data/device_registry";
import { getDeviceIntegrationLookup } from "../../data/device_registry";
import {
EntityRegistryEntry,
subscribeEntityRegistry,
} from "../../data/entity_registry";
import {
EntitySources,
fetchEntitySourcesWithCache,
@@ -14,12 +17,13 @@ import {
filterSelectorDevices,
filterSelectorEntities,
} from "../../data/selector";
import { SubscribeMixin } from "../../mixins/subscribe-mixin";
import { HomeAssistant } from "../../types";
import "../ha-area-picker";
import "../ha-areas-picker";
@customElement("ha-selector-area")
export class HaAreaSelector extends LitElement {
export class HaAreaSelector extends SubscribeMixin(LitElement) {
@property() public hass!: HomeAssistant;
@property() public selector!: AreaSelector;
@@ -36,23 +40,23 @@ export class HaAreaSelector extends LitElement {
@state() private _entitySources?: EntitySources;
@state() private _entities?: EntityRegistryEntry[];
private _deviceIntegrationLookup = memoizeOne(getDeviceIntegrationLookup);
private _hasIntegration(selector: AreaSelector) {
return (
(selector.area?.entity &&
ensureArray(selector.area.entity).some(
(filter) => filter.integration
)) ||
(selector.area?.device &&
ensureArray(selector.area.device).some((device) => device.integration))
);
public hassSubscribe(): UnsubscribeFunc[] {
return [
subscribeEntityRegistry(this.hass.connection!, (entities) => {
this._entities = entities.filter((entity) => entity.device_id !== null);
}),
];
}
protected updated(changedProperties: PropertyValues): void {
if (
changedProperties.has("selector") &&
this._hasIntegration(this.selector) &&
(this.selector.area?.device?.integration ||
this.selector.area?.entity?.integration) &&
!this._entitySources
) {
fetchEntitySourcesWithCache(this.hass).then((sources) => {
@@ -62,7 +66,11 @@ export class HaAreaSelector extends LitElement {
}
protected render(): TemplateResult {
if (this._hasIntegration(this.selector) && !this._entitySources) {
if (
(this.selector.area?.device?.integration ||
this.selector.area?.entity?.integration) &&
!this._entitySources
) {
return html``;
}
@@ -102,8 +110,10 @@ export class HaAreaSelector extends LitElement {
return true;
}
return ensureArray(this.selector.area.entity).some((filter) =>
filterSelectorEntities(filter, entity, this._entitySources)
return filterSelectorEntities(
this.selector.area.entity,
entity,
this._entitySources
);
};
@@ -112,15 +122,15 @@ export class HaAreaSelector extends LitElement {
return true;
}
const deviceIntegrations = this._entitySources
? this._deviceIntegrationLookup(
this._entitySources,
Object.values(this.hass.entities)
)
: undefined;
const deviceIntegrations =
this._entitySources && this._entities
? this._deviceIntegrationLookup(this._entitySources, this._entities)
: undefined;
return ensureArray(this.selector.area.device).some((filter) =>
filterSelectorDevices(filter, device, deviceIntegrations)
return filterSelectorDevices(
this.selector.area.device,
device,
deviceIntegrations
);
};
}

View File

@@ -2,11 +2,12 @@ import { html, LitElement, PropertyValues } from "lit";
import { customElement, property } from "lit/decorators";
import { fireEvent } from "../../common/dom/fire_event";
import { AttributeSelector } from "../../data/selector";
import { SubscribeMixin } from "../../mixins/subscribe-mixin";
import { HomeAssistant } from "../../types";
import "../entity/ha-entity-attribute-picker";
@customElement("ha-selector-attribute")
export class HaSelectorAttribute extends LitElement {
export class HaSelectorAttribute extends SubscribeMixin(LitElement) {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public selector!: AttributeSelector;

View File

@@ -1,31 +1,34 @@
import { HassEntity } from "home-assistant-js-websocket";
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 { ensureArray } from "../../common/array/ensure-array";
import type { DeviceRegistryEntry } from "../../data/device_registry";
import { getDeviceIntegrationLookup } from "../../data/device_registry";
import {
EntityRegistryEntry,
subscribeEntityRegistry,
} from "../../data/entity_registry";
import {
EntitySources,
fetchEntitySourcesWithCache,
} from "../../data/entity_sources";
import type { DeviceSelector } from "../../data/selector";
import {
filterSelectorDevices,
filterSelectorEntities,
} from "../../data/selector";
import { filterSelectorDevices } from "../../data/selector";
import { SubscribeMixin } from "../../mixins/subscribe-mixin";
import type { HomeAssistant } from "../../types";
import "../device/ha-device-picker";
import "../device/ha-devices-picker";
@customElement("ha-selector-device")
export class HaDeviceSelector extends LitElement {
export class HaDeviceSelector extends SubscribeMixin(LitElement) {
@property() public hass!: HomeAssistant;
@property() public selector!: DeviceSelector;
@state() private _entitySources?: EntitySources;
@state() private _entities?: EntityRegistryEntry[];
@property() public value?: any;
@property() public label?: string;
@@ -38,24 +41,19 @@ export class HaDeviceSelector extends LitElement {
private _deviceIntegrationLookup = memoizeOne(getDeviceIntegrationLookup);
private _hasIntegration(selector: DeviceSelector) {
return (
(selector.device?.filter &&
ensureArray(selector.device.filter).some(
(filter) => filter.integration
)) ||
(selector.device?.entity &&
ensureArray(selector.device.entity).some(
(device) => device.integration
))
);
public hassSubscribe(): UnsubscribeFunc[] {
return [
subscribeEntityRegistry(this.hass.connection!, (entities) => {
this._entities = entities.filter((entity) => entity.device_id !== null);
}),
];
}
protected updated(changedProperties): void {
super.updated(changedProperties);
if (
changedProperties.has("selector") &&
this._hasIntegration(this.selector) &&
this.selector.device?.integration &&
!this._entitySources
) {
fetchEntitySourcesWithCache(this.hass).then((sources) => {
@@ -65,7 +63,7 @@ export class HaDeviceSelector extends LitElement {
}
protected render() {
if (this._hasIntegration(this.selector) && !this._entitySources) {
if (this.selector.device?.integration && !this._entitySources) {
return html``;
}
@@ -77,7 +75,12 @@ export class HaDeviceSelector extends LitElement {
.label=${this.label}
.helper=${this.helper}
.deviceFilter=${this._filterDevices}
.entityFilter=${this._filterEntities}
.includeDeviceClasses=${this.selector.device?.entity?.device_class
? [this.selector.device.entity.device_class]
: undefined}
.includeDomains=${this.selector.device?.entity?.domain
? [this.selector.device.entity.domain]
: undefined}
.disabled=${this.disabled}
.required=${this.required}
allow-custom-entity
@@ -92,7 +95,12 @@ export class HaDeviceSelector extends LitElement {
.value=${this.value}
.helper=${this.helper}
.deviceFilter=${this._filterDevices}
.entityFilter=${this._filterEntities}
.includeDeviceClasses=${this.selector.device.entity?.device_class
? [this.selector.device.entity.device_class]
: undefined}
.includeDomains=${this.selector.device.entity?.domain
? [this.selector.device.entity.domain]
: undefined}
.disabled=${this.disabled}
.required=${this.required}
></ha-devices-picker>
@@ -100,27 +108,18 @@ export class HaDeviceSelector extends LitElement {
}
private _filterDevices = (device: DeviceRegistryEntry): boolean => {
if (!this.selector.device?.filter) {
const deviceIntegrations =
this._entitySources && this._entities
? this._deviceIntegrationLookup(this._entitySources, this._entities)
: undefined;
if (!this.selector.device) {
return true;
}
const deviceIntegrations = this._entitySources
? this._deviceIntegrationLookup(
this._entitySources,
Object.values(this.hass.entities)
)
: undefined;
return ensureArray(this.selector.device.filter).some((filter) =>
filterSelectorDevices(filter, device, deviceIntegrations)
);
};
private _filterEntities = (entity: HassEntity): boolean => {
if (!this.selector.device?.entity) {
return true;
}
return ensureArray(this.selector.device.entity).some((filter) =>
filterSelectorEntities(filter, entity, this._entitySources)
return filterSelectorDevices(
this.selector.device,
device,
deviceIntegrations
);
};
}

View File

@@ -1,7 +1,6 @@
import { HassEntity } from "home-assistant-js-websocket";
import { html, LitElement, PropertyValues } from "lit";
import { customElement, property, state } from "lit/decorators";
import { ensureArray } from "../../common/array/ensure-array";
import {
EntitySources,
fetchEntitySourcesWithCache,
@@ -30,18 +29,7 @@ export class HaEntitySelector extends LitElement {
@property({ type: Boolean }) public required = true;
private _hasIntegration(selector: EntitySelector) {
return (
selector.entity?.filter &&
ensureArray(selector.entity.filter).some((filter) => filter.integration)
);
}
protected render() {
if (this._hasIntegration(this.selector) && !this._entitySources) {
return html``;
}
if (!this.selector.entity?.multiple) {
return html`<ha-entity-picker
.hass=${this.hass}
@@ -76,7 +64,7 @@ export class HaEntitySelector extends LitElement {
super.updated(changedProps);
if (
changedProps.has("selector") &&
this._hasIntegration(this.selector) &&
this.selector.entity?.integration &&
!this._entitySources
) {
fetchEntitySourcesWithCache(this.hass).then((sources) => {
@@ -86,11 +74,13 @@ export class HaEntitySelector extends LitElement {
}
private _filterEntities = (entity: HassEntity): boolean => {
if (!this.selector?.entity?.filter) {
if (!this.selector?.entity) {
return true;
}
return ensureArray(this.selector.entity.filter).some((filter) =>
filterSelectorEntities(filter, entity, this._entitySources)
return filterSelectorEntities(
this.selector.entity,
entity,
this._entitySources
);
};
}

View File

@@ -14,6 +14,7 @@ import {
DeviceRegistryEntry,
getDeviceIntegrationLookup,
} from "../../data/device_registry";
import { EntityRegistryEntry } from "../../data/entity_registry";
import {
EntitySources,
fetchEntitySourcesWithCache,
@@ -44,24 +45,12 @@ export class HaTargetSelector extends LitElement {
private _deviceIntegrationLookup = memoizeOne(getDeviceIntegrationLookup);
private _hasIntegration(selector: TargetSelector) {
return (
(selector.target?.entity &&
ensureArray(selector.target.entity).some(
(filter) => filter.integration
)) ||
(selector.target?.device &&
ensureArray(selector.target.device).some(
(device) => device.integration
))
);
}
protected updated(changedProperties: PropertyValues): void {
super.updated(changedProperties);
if (
changedProperties.has("selector") &&
this._hasIntegration(this.selector) &&
(this.selector.target?.device?.integration ||
this.selector.target?.entity?.integration) &&
!this._entitySources
) {
fetchEntitySourcesWithCache(this.hass).then((sources) => {
@@ -71,7 +60,11 @@ export class HaTargetSelector extends LitElement {
}
protected render(): TemplateResult {
if (this._hasIntegration(this.selector) && !this._entitySources) {
if (
(this.selector.target?.device?.integration ||
this.selector.target?.entity?.integration) &&
!this._entitySources
) {
return html``;
}
@@ -80,21 +73,39 @@ export class HaTargetSelector extends LitElement {
.value=${this.value}
.helper=${this.helper}
.deviceFilter=${this._filterDevices}
.entityFilter=${this._filterEntities}
.entityFilter=${this._filterStates}
.entityRegFilter=${this._filterRegEntities}
.includeDeviceClasses=${this.selector.target?.entity?.device_class
? [this.selector.target?.entity.device_class]
: undefined}
.includeDomains=${this.selector.target?.entity?.domain
? ensureArray(this.selector.target.entity.domain as string | string[])
: undefined}
.disabled=${this.disabled}
></ha-target-picker>`;
}
private _filterEntities = (entity: HassEntity): boolean => {
private _filterStates = (entity: HassEntity): boolean => {
if (!this.selector.target?.entity) {
return true;
}
return ensureArray(this.selector.target.entity).some((filter) =>
filterSelectorEntities(filter, entity, this._entitySources)
return filterSelectorEntities(
this.selector.target.entity,
entity,
this._entitySources
);
};
private _filterRegEntities = (entity: EntityRegistryEntry): boolean => {
if (this.selector.target?.entity?.integration) {
if (entity.platform !== this.selector.target.entity.integration) {
return false;
}
}
return true;
};
private _filterDevices = (device: DeviceRegistryEntry): boolean => {
if (!this.selector.target?.device) {
return true;
@@ -107,8 +118,10 @@ export class HaTargetSelector extends LitElement {
)
: undefined;
return ensureArray(this.selector.target.device).some((filter) =>
filterSelectorDevices(filter, device, deviceIntegrations)
return filterSelectorDevices(
this.selector.target.device,
device,
deviceIntegrations
);
};

View File

@@ -1,12 +1,7 @@
import { html, LitElement, PropertyValues } from "lit";
import { customElement, property } from "lit/decorators";
import memoizeOne from "memoize-one";
import { dynamicElement } from "../../common/dom/dynamic-element-directive";
import {
Selector,
handleLegacyEntitySelector,
handleLegacyDeviceSelector,
} from "../../data/selector";
import type { Selector } from "../../data/selector";
import type { HomeAssistant } from "../../types";
const LOAD_ELEMENTS = {
@@ -80,22 +75,12 @@ export class HaSelector extends LitElement {
}
}
private _handleLegacySelector = memoizeOne((selector: Selector) => {
if ("entity" in selector) {
return handleLegacyEntitySelector(selector);
}
if ("device" in selector) {
return handleLegacyDeviceSelector(selector);
}
return selector;
});
protected render() {
return html`
${dynamicElement(`ha-selector-${this._type}`, {
hass: this.hass,
name: this.name,
selector: this._handleLegacySelector(this.selector),
selector: this.selector,
value: this.value,
label: this.label,
placeholder: this.placeholder,

View File

@@ -1,6 +1,6 @@
import { html, LitElement } from "lit";
import { ComboBoxLitRenderer } from "@vaadin/combo-box/lit";
import { customElement, property, state } from "lit/decorators";
import { property, state } from "lit/decorators";
import memoizeOne from "memoize-one";
import { fireEvent } from "../common/dom/fire_event";
import { LocalizeFunc } from "../common/translations/localize";
@@ -17,7 +17,6 @@ const rowRenderer: ComboBoxLitRenderer<{ service: string; name: string }> = (
>
</mwc-list-item>`;
@customElement("ha-service-picker")
class HaServicePicker extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@@ -114,6 +113,8 @@ class HaServicePicker extends LitElement {
}
}
customElements.define("ha-service-picker", HaServicePicker);
declare global {
interface HTMLElementTagNameMap {
"ha-service-picker": HaServicePicker;

View File

@@ -1,7 +1,6 @@
// @ts-ignore
import chipStyles from "@material/chips/dist/mdc.chips.min.css";
import "@material/mwc-button/mwc-button";
import "@material/mwc-menu/mwc-menu-surface";
import {
mdiClose,
mdiDevices,
@@ -10,22 +9,32 @@ import {
mdiUnfoldMoreVertical,
} from "@mdi/js";
import "@polymer/paper-tooltip/paper-tooltip";
import { ComboBoxLightOpenedChangedEvent } from "@vaadin/combo-box/vaadin-combo-box-light";
import { HassEntity, HassServiceTarget } from "home-assistant-js-websocket";
import {
HassEntity,
HassServiceTarget,
UnsubscribeFunc,
} from "home-assistant-js-websocket";
import { css, CSSResultGroup, html, LitElement, unsafeCSS } from "lit";
import { customElement, property, query, state } from "lit/decorators";
import { classMap } from "lit/directives/class-map";
import { ensureArray } from "../common/array/ensure-array";
import { fireEvent } from "../common/dom/fire_event";
import { stopPropagation } from "../common/dom/stop_propagation";
import { ensureArray } from "../common/array/ensure-array";
import { computeDomain } from "../common/entity/compute_domain";
import { computeStateName } from "../common/entity/compute_state_name";
import { isValidEntityId } from "../common/entity/valid_entity_id";
import {
AreaRegistryEntry,
subscribeAreaRegistry,
} from "../data/area_registry";
import {
computeDeviceName,
DeviceRegistryEntry,
subscribeDeviceRegistry,
} from "../data/device_registry";
import { EntityRegistryDisplayEntry } from "../data/entity_registry";
import {
EntityRegistryEntry,
subscribeEntityRegistry,
} from "../data/entity_registry";
import { SubscribeMixin } from "../mixins/subscribe-mixin";
import { HomeAssistant } from "../types";
import "./device/ha-device-picker";
import type { HaDevicePickerDeviceFilterFunc } from "./device/ha-device-picker";
@@ -37,7 +46,7 @@ import "./ha-input-helper-text";
import "./ha-svg-icon";
@customElement("ha-target-picker")
export class HaTargetPicker extends LitElement {
export class HaTargetPicker extends SubscribeMixin(LitElement) {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public value?: HassServiceTarget;
@@ -64,25 +73,67 @@ export class HaTargetPicker extends LitElement {
@property() public deviceFilter?: HaDevicePickerDeviceFilterFunc;
@property() public entityRegFilter?: (entity: EntityRegistryEntry) => boolean;
@property() public entityFilter?: HaEntityPickerEntityFilterFunc;
@property({ type: Boolean, reflect: true }) public disabled = false;
@property({ type: Boolean }) public addOnTop = false;
@property({ type: Boolean }) public horizontal = false;
@state() private _areas?: { [areaId: string]: AreaRegistryEntry };
@state() private _devices?: {
[deviceId: string]: DeviceRegistryEntry;
};
@state() private _entities?: EntityRegistryEntry[];
@state() private _addMode?: "area_id" | "entity_id" | "device_id";
@query("#input") private _inputElement?;
@query(".add-container", true) private _addContainer?: HTMLDivElement;
private _opened = false;
public hassSubscribe(): UnsubscribeFunc[] {
return [
subscribeAreaRegistry(this.hass.connection!, (areas) => {
const areaLookup: { [areaId: string]: AreaRegistryEntry } = {};
for (const area of areas) {
areaLookup[area.area_id] = area;
}
this._areas = areaLookup;
}),
subscribeDeviceRegistry(this.hass.connection!, (devices) => {
const deviceLookup: { [deviceId: string]: DeviceRegistryEntry } = {};
for (const device of devices) {
deviceLookup[device.id] = device;
}
this._devices = deviceLookup;
}),
subscribeEntityRegistry(this.hass.connection!, (entities) => {
this._entities = entities;
}),
];
}
protected render() {
if (this.addOnTop) {
return html` ${this._renderChips()} ${this._renderItems()} `;
if (!this._areas || !this._devices || !this._entities) {
return html``;
}
return html` ${this._renderItems()} ${this._renderChips()} `;
return html`
${this.horizontal
? html`
<div class="horizontal-container">
${this._renderChips()} ${this._renderPicker()}
</div>
${this._renderItems()}
`
: html`
<div>
${this._renderItems()} ${this._renderPicker()}
${this._renderChips()}
</div>
`}
`;
}
private _renderItems() {
@@ -90,7 +141,7 @@ export class HaTargetPicker extends LitElement {
<div class="mdc-chip-set items">
${this.value?.area_id
? ensureArray(this.value.area_id).map((area_id) => {
const area = this.hass.areas![area_id];
const area = this._areas![area_id];
return this._renderChip(
"area_id",
area_id,
@@ -102,7 +153,7 @@ export class HaTargetPicker extends LitElement {
: ""}
${this.value?.device_id
? ensureArray(this.value.device_id).map((device_id) => {
const device = this.hass.devices![device_id];
const device = this._devices![device_id];
return this._renderChip(
"device_id",
device_id,
@@ -129,7 +180,7 @@ export class HaTargetPicker extends LitElement {
private _renderChips() {
return html`
<div class="mdc-chip-set add-container">
<div class="mdc-chip-set">
<div
class="mdc-chip area_id add"
.type=${"area_id"}
@@ -190,7 +241,6 @@ export class HaTargetPicker extends LitElement {
</span>
</span>
</div>
${this._renderPicker()}
</div>
${this.helper
? html`<ha-input-helper-text>${this.helper}</ha-input-helper-text>`
@@ -198,8 +248,11 @@ export class HaTargetPicker extends LitElement {
`;
}
private _showPicker(ev) {
private async _showPicker(ev) {
this._addMode = ev.currentTarget.type;
await this.updateComplete;
await this._inputElement?.focus();
await this._inputElement?.open();
}
private _renderChip(
@@ -234,7 +287,7 @@ export class HaTargetPicker extends LitElement {
</span>
${type === "entity_id"
? ""
: html`<span role="gridcell">
: html` <span role="gridcell">
<ha-icon-button
class="expand-btn mdc-chip__icon mdc-chip__icon--trailing"
tabindex="-1"
@@ -277,72 +330,60 @@ export class HaTargetPicker extends LitElement {
}
private _renderPicker() {
if (!this._addMode) {
return html``;
switch (this._addMode) {
case "area_id":
return html`
<ha-area-picker
.hass=${this.hass}
id="input"
.type=${"area_id"}
.label=${this.hass.localize(
"ui.components.target-picker.add_area_id"
)}
no-add
.deviceFilter=${this.deviceFilter}
.entityFilter=${this.entityRegFilter}
.includeDeviceClasses=${this.includeDeviceClasses}
.includeDomains=${this.includeDomains}
.excludeAreas=${ensureArray(this.value?.area_id)}
@value-changed=${this._targetPicked}
></ha-area-picker>
`;
case "device_id":
return html`
<ha-device-picker
.hass=${this.hass}
id="input"
.type=${"device_id"}
.label=${this.hass.localize(
"ui.components.target-picker.add_device_id"
)}
.deviceFilter=${this.deviceFilter}
.includeDeviceClasses=${this.includeDeviceClasses}
.includeDomains=${this.includeDomains}
.excludeDevices=${ensureArray(this.value?.device_id)}
@value-changed=${this._targetPicked}
></ha-device-picker>
`;
case "entity_id":
return html`
<ha-entity-picker
.hass=${this.hass}
id="input"
.type=${"entity_id"}
.label=${this.hass.localize(
"ui.components.target-picker.add_entity_id"
)}
.entityFilter=${this.entityFilter}
.includeDeviceClasses=${this.includeDeviceClasses}
.includeDomains=${this.includeDomains}
.excludeEntities=${ensureArray(this.value?.entity_id)}
@value-changed=${this._targetPicked}
allow-custom-entity
></ha-entity-picker>
`;
}
return html`<mwc-menu-surface
open
.anchor=${this._addContainer}
.corner=${"BOTTOM_START"}
@closed=${this._onClosed}
@opened=${this._onOpened}
@opened-changed=${this._openedChanged}
@input=${stopPropagation}
>${this._addMode === "area_id"
? html`
<ha-area-picker
.hass=${this.hass}
id="input"
.type=${"area_id"}
.label=${this.hass.localize(
"ui.components.target-picker.add_area_id"
)}
no-add
.deviceFilter=${this.deviceFilter}
.entityFilter=${this.entityFilter}
.includeDeviceClasses=${this.includeDeviceClasses}
.includeDomains=${this.includeDomains}
.excludeAreas=${ensureArray(this.value?.area_id)}
@value-changed=${this._targetPicked}
@click=${this._preventDefault}
></ha-area-picker>
`
: this._addMode === "device_id"
? html`
<ha-device-picker
.hass=${this.hass}
id="input"
.type=${"device_id"}
.label=${this.hass.localize(
"ui.components.target-picker.add_device_id"
)}
.deviceFilter=${this.deviceFilter}
.entityFilter=${this.entityFilter}
.includeDeviceClasses=${this.includeDeviceClasses}
.includeDomains=${this.includeDomains}
.excludeDevices=${ensureArray(this.value?.device_id)}
@value-changed=${this._targetPicked}
@click=${this._preventDefault}
></ha-device-picker>
`
: html`
<ha-entity-picker
.hass=${this.hass}
id="input"
.type=${"entity_id"}
.label=${this.hass.localize(
"ui.components.target-picker.add_entity_id"
)}
.entityFilter=${this.entityFilter}
.includeDeviceClasses=${this.includeDeviceClasses}
.includeDomains=${this.includeDomains}
.excludeEntities=${ensureArray(this.value?.entity_id)}
@value-changed=${this._targetPicked}
@click=${this._preventDefault}
allow-custom-entity
></ha-entity-picker>
`}</mwc-menu-surface
>`;
return html``;
}
private _targetPicked(ev) {
@@ -352,12 +393,8 @@ export class HaTargetPicker extends LitElement {
}
const value = ev.detail.value;
const target = ev.currentTarget;
if (target.type === "entity_id" && !isValidEntityId(value)) {
return;
}
target.value = "";
this._addMode = undefined;
if (
this.value &&
this.value[target.type] &&
@@ -382,7 +419,7 @@ export class HaTargetPicker extends LitElement {
const newDevices: string[] = [];
const newEntities: string[] = [];
if (target.type === "area_id") {
Object.values(this.hass.devices).forEach((device) => {
Object.values(this._devices!).forEach((device) => {
if (
device.area_id === target.id &&
!this.value!.device_id?.includes(device.id) &&
@@ -391,7 +428,7 @@ export class HaTargetPicker extends LitElement {
newDevices.push(device.id);
}
});
Object.values(this.hass.entities).forEach((entity) => {
this._entities!.forEach((entity) => {
if (
entity.area_id === target.id &&
!this.value!.entity_id?.includes(entity.entity_id) &&
@@ -401,7 +438,7 @@ export class HaTargetPicker extends LitElement {
}
});
} else if (target.type === "device_id") {
Object.values(this.hass.entities).forEach((entity) => {
this._entities!.forEach((entity) => {
if (
entity.device_id === target.id &&
!this.value!.entity_id?.includes(entity.entity_id) &&
@@ -464,36 +501,10 @@ export class HaTargetPicker extends LitElement {
return undefined;
}
private _onClosed(ev) {
ev.stopPropagation();
ev.target.open = true;
}
private async _onOpened() {
if (!this._addMode) {
return;
}
await this._inputElement?.focus();
await this._inputElement?.open();
this._opened = true;
}
private _openedChanged(ev: ComboBoxLightOpenedChangedEvent) {
if (this._opened && !ev.detail.value) {
this._opened = false;
this._addMode = undefined;
}
}
private _preventDefault(ev: Event) {
ev.preventDefault();
}
private _deviceMeetsFilter(device: DeviceRegistryEntry): boolean {
const devEntities = Object.values(this.hass.entities).filter(
const devEntities = this._entities?.filter(
(entity) => entity.device_id === device.id
);
if (this.includeDomains) {
if (!devEntities || !devEntities.length) {
return false;
@@ -530,32 +541,15 @@ export class HaTargetPicker extends LitElement {
}
if (this.deviceFilter) {
if (!this.deviceFilter(device)) {
return false;
}
}
if (this.entityFilter) {
if (
!devEntities.some((entity) => {
const stateObj = this.hass.states[entity.entity_id];
if (!stateObj) {
return false;
}
return this.entityFilter!(stateObj);
})
) {
return false;
}
return this.deviceFilter(device);
}
return true;
}
private _entityRegMeetsFilter(entity: EntityRegistryDisplayEntry): boolean {
private _entityRegMeetsFilter(entity: EntityRegistryEntry): boolean {
if (entity.entity_category) {
return false;
}
if (
this.includeDomains &&
!this.includeDomains.includes(computeDomain(entity.entity_id))
@@ -574,15 +568,8 @@ export class HaTargetPicker extends LitElement {
return false;
}
}
if (this.entityFilter) {
const stateObj = this.hass.states[entity.entity_id];
if (!stateObj) {
return false;
}
if (!this.entityFilter!(stateObj)) {
return false;
}
if (this.entityRegFilter) {
return this.entityRegFilter(entity);
}
return true;
}
@@ -590,6 +577,12 @@ export class HaTargetPicker extends LitElement {
static get styles(): CSSResultGroup {
return css`
${unsafeCSS(chipStyles)}
.horizontal-container {
display: flex;
flex-wrap: wrap;
min-height: 56px;
align-items: center;
}
.mdc-chip {
color: var(--primary-text-color);
}
@@ -602,10 +595,6 @@ export class HaTargetPicker extends LitElement {
.mdc-chip.add {
color: rgba(0, 0, 0, 0.87);
}
.add-container {
position: relative;
display: inline-flex;
}
.mdc-chip:not(.add) {
cursor: default;
}
@@ -677,15 +666,6 @@ export class HaTargetPicker extends LitElement {
opacity: var(--light-disabled-opacity);
pointer-events: none;
}
mwc-menu-surface {
--mdc-menu-min-width: 100%;
}
ha-entity-picker,
ha-device-picker,
ha-area-picker {
display: block;
width: 100%;
}
`;
}
}

View File

@@ -1,24 +1,15 @@
import { mdiLightbulbOutline } from "@mdi/js";
import { css, html, LitElement, TemplateResult } from "lit";
import { property, customElement } from "lit/decorators";
import { HomeAssistant } from "../types";
import { css, html, LitElement } from "lit";
import { customElement } from "lit/decorators";
import "./ha-svg-icon";
@customElement("ha-tip")
class HaTip extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
public render(): TemplateResult {
if (!this.hass) {
return html``;
}
public render() {
return html`
<ha-svg-icon .path=${mdiLightbulbOutline}></ha-svg-icon>
<span class="prefix"
>${this.hass.localize("ui.panel.config.tips.tip")}</span
>
<span class="prefix">Tip!</span>
<span class="text"><slot></slot></span>
`;
}
@@ -30,10 +21,7 @@ class HaTip extends LitElement {
}
.text {
direction: var(--direction);
margin-left: 2px;
margin-inline-start: 2px;
margin-inline-end: initial;
color: var(--secondary-text-color);
}

View File

@@ -1,13 +1,11 @@
import "@polymer/paper-toast/paper-toast";
import type { PaperToastElement } from "@polymer/paper-toast/paper-toast";
import { customElement } from "lit/decorators";
import type { Constructor } from "../types";
const PaperToast = customElements.get(
"paper-toast"
) as Constructor<PaperToastElement>;
@customElement("ha-toast")
export class HaToast extends PaperToast {
private _resizeListener?: (obj: { matches: boolean }) => unknown;
@@ -36,3 +34,5 @@ declare global {
"ha-toast": HaToast;
}
}
customElements.define("ha-toast", HaToast);

View File

@@ -9,9 +9,11 @@ import {
state,
} from "lit/decorators";
import { ifDefined } from "lit/directives/if-defined";
import "../ha-icon";
import "../ha-svg-icon";
@customElement("ha-control-button")
export class HaControlButton extends LitElement {
@customElement("ha-tile-button")
export class HaTileButton extends LitElement {
@property({ type: Boolean, reflect: true }) disabled = false;
@property() public label?: string;
@@ -26,7 +28,7 @@ export class HaControlButton extends LitElement {
type="button"
class="button"
aria-label=${ifDefined(this.label)}
title=${ifDefined(this.label)}
.title=${this.label}
.disabled=${Boolean(this.disabled)}
@focus=${this.handleRippleFocus}
@blur=${this.handleRippleBlur}
@@ -79,12 +81,9 @@ export class HaControlButton extends LitElement {
static get styles(): CSSResultGroup {
return css`
:host {
display: block;
--control-button-icon-color: var(--primary-text-color);
--control-button-background-color: var(--disabled-color);
--control-button-background-opacity: 0.2;
--control-button-border-radius: 10px;
--mdc-icon-size: 20px;
--tile-button-icon-color: var(--primary-text-color);
--tile-button-background-color: var(--disabled-color);
--tile-button-background-opacity: 0.2;
width: 40px;
height: 40px;
-webkit-tap-highlight-color: transparent;
@@ -98,7 +97,7 @@ export class HaControlButton extends LitElement {
justify-content: center;
width: 100%;
height: 100%;
border-radius: var(--control-button-border-radius);
border-radius: 10px;
border: none;
margin: 0;
padding: 0;
@@ -107,8 +106,7 @@ export class HaControlButton extends LitElement {
outline: none;
overflow: hidden;
background: none;
z-index: 1;
--mdc-ripple-color: var(--control-button-background-color);
--mdc-ripple-color: var(--tile-button-background-color);
}
.button::before {
content: "";
@@ -117,21 +115,22 @@ export class HaControlButton extends LitElement {
left: 0;
height: 100%;
width: 100%;
background-color: var(--control-button-background-color);
background-color: var(--tile-button-background-color);
transition: background-color 180ms ease-in-out,
opacity 180ms ease-in-out;
opacity: var(--control-button-background-opacity);
opacity: var(--tile-button-background-opacity);
}
.button ::slotted(*) {
--mdc-icon-size: 20px;
transition: color 180ms ease-in-out;
color: var(--control-button-icon-color);
color: var(--tile-button-icon-color);
pointer-events: none;
}
.button:disabled {
cursor: not-allowed;
--control-button-background-color: var(--disabled-color);
--control-button-icon-color: var(--disabled-text-color);
--control-button-background-opacity: 0.2;
--tile-button-background-color: var(--disabled-color);
--tile-button-icon-color: var(--disabled-text-color);
--tile-button-background-opacity: 0.2;
}
`;
}
@@ -139,6 +138,6 @@ export class HaControlButton extends LitElement {
declare global {
interface HTMLElementTagNameMap {
"ha-control-button": HaControlButton;
"ha-tile-button": HaTileButton;
}
}

View File

@@ -1,7 +1,7 @@
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { customElement, property } from "lit/decorators";
import { ifDefined } from "lit/directives/if-defined";
import "../ha-control-slider";
import "../ha-bar-slider";
@customElement("ha-tile-slider")
export class HaTileSlider extends LitElement {
@@ -30,7 +30,7 @@ export class HaTileSlider extends LitElement {
protected render(): TemplateResult {
return html`
<ha-control-slider
<ha-bar-slider
.disabled=${this.disabled}
.mode=${this.mode}
.value=${this.value}
@@ -40,24 +40,24 @@ export class HaTileSlider extends LitElement {
aria-label=${ifDefined(this.label)}
.showHandle=${this.showHandle}
>
</ha-control-slider>
</ha-bar-slider>
`;
}
static get styles(): CSSResultGroup {
return css`
ha-control-slider {
--control-slider-color: var(--tile-slider-color, var(--primary-color));
--control-slider-background: var(
ha-bar-slider {
--slider-bar-color: var(--tile-slider-color, var(--primary-color));
--slider-bar-background: var(
--tile-slider-background,
var(--disabled-color)
);
--control-slider-background-opacity: var(
--slider-bar-background-opacity: var(
--tile-slider-background-opacity,
0.2
);
--control-slider-thickness: 40px;
--control-slider-border-radius: 10px;
--slider-bar-thickness: 40px;
--slider-bar-border-radius: 10px;
}
`;
}

View File

@@ -6,7 +6,6 @@ import {
mdiProgressWrench,
mdiRecordCircleOutline,
} from "@mdi/js";
import { UnsubscribeFunc } from "home-assistant-js-websocket";
import {
css,
CSSResultGroup,
@@ -15,16 +14,12 @@ import {
PropertyValues,
TemplateResult,
} from "lit";
import { customElement, property, state } from "lit/decorators";
import { customElement, property } from "lit/decorators";
import { ifDefined } from "lit/directives/if-defined";
import { formatDateTimeWithSeconds } from "../../common/datetime/format_date_time";
import { relativeTime } from "../../common/datetime/relative_time";
import { fireEvent } from "../../common/dom/fire_event";
import { toggleAttribute } from "../../common/dom/toggle_attribute";
import {
EntityRegistryEntry,
subscribeEntityRegistry,
} from "../../data/entity_registry";
import { LogbookEntry } from "../../data/logbook";
import {
ChooseAction,
@@ -198,7 +193,6 @@ class ActionRenderer {
constructor(
private hass: HomeAssistant,
private entityReg: EntityRegistryEntry[],
private entries: TemplateResult[],
private trace: AutomationTraceExtended,
private logbookRenderer: LogbookRenderer,
@@ -304,7 +298,7 @@ class ActionRenderer {
this._renderEntry(
path,
describeAction(this.hass, this.entityReg, data, actionType),
describeAction(this.hass, data, actionType),
undefined,
data.enabled === false
);
@@ -447,9 +441,7 @@ class ActionRenderer {
) as RepeatAction;
const disabled = repeatConfig.enabled === false;
const name =
repeatConfig.alias ||
describeAction(this.hass, this.entityReg, repeatConfig);
const name = repeatConfig.alias || describeAction(this.hass, repeatConfig);
this._renderEntry(repeatPath, name, undefined, disabled);
@@ -585,16 +577,6 @@ export class HaAutomationTracer extends LitElement {
@property({ type: Boolean }) public allowPick = false;
@state() private _entityReg: EntityRegistryEntry[] = [];
public hassSubscribe(): UnsubscribeFunc[] {
return [
subscribeEntityRegistry(this.hass.connection!, (entities) => {
this._entityReg = entities;
}),
];
}
protected render(): TemplateResult {
if (!this.trace) {
return html``;
@@ -610,7 +592,6 @@ export class HaAutomationTracer extends LitElement {
);
const actionRenderer = new ActionRenderer(
this.hass,
this._entityReg,
entries,
this.trace,
logbookRenderer,

View File

@@ -3,15 +3,6 @@ import { HomeAssistant } from "../types";
export const FORMAT_TEXT = "text";
export const FORMAT_NUMBER = "number";
export const enum AlarmControlPanelEntityFeature {
ARM_HOME = 1,
ARM_AWAY = 2,
ARM_NIGHT = 4,
TRIGGER = 8,
ARM_CUSTOM_BYPASS = 16,
ARM_VACATION = 32,
}
export const callAlarmAction = (
hass: HomeAssistant,
entity: string,

View File

@@ -146,7 +146,6 @@ export interface TimeTrigger extends BaseTrigger {
export interface TemplateTrigger extends BaseTrigger {
platform: "template";
value_template: string;
for?: string | number | ForDict;
}
export interface EventTrigger extends BaseTrigger {

View File

@@ -3,7 +3,7 @@ import secondsToDuration from "../common/datetime/seconds_to_duration";
import { ensureArray } from "../common/array/ensure-array";
import { computeStateName } from "../common/entity/compute_state_name";
import type { HomeAssistant } from "../types";
import { Condition, Trigger, ForDict } from "./automation";
import { Condition, Trigger } from "./automation";
import {
DeviceCondition,
DeviceTrigger,
@@ -12,18 +12,6 @@ import {
} from "./device_automation";
import { formatAttributeName } from "./entity_attributes";
const describeDuration = (forTime: number | string | ForDict) => {
let duration: string | null;
if (typeof forTime === "number") {
duration = secondsToDuration(forTime);
} else if (typeof forTime === "string") {
duration = forTime;
} else {
duration = formatDuration(forTime);
}
return duration;
};
export const describeTrigger = (
trigger: Trigger,
hass: HomeAssistant,
@@ -85,7 +73,14 @@ export const describeTrigger = (
}
if (trigger.for) {
const duration = describeDuration(trigger.for);
let duration: string | null;
if (typeof trigger.for === "number") {
duration = secondsToDuration(trigger.for);
} else if (typeof trigger.for === "string") {
duration = trigger.for;
} else {
duration = formatDuration(trigger.for);
}
if (duration) {
base += ` for ${duration}`;
}
@@ -161,7 +156,15 @@ export const describeTrigger = (
}
if (trigger.for) {
const duration = describeDuration(trigger.for);
let duration: string | null;
if (typeof trigger.for === "number") {
duration = secondsToDuration(trigger.for);
} else if (typeof trigger.for === "string") {
duration = trigger.for;
} else {
duration = formatDuration(trigger.for);
}
if (duration) {
base += ` for ${duration}`;
}
@@ -316,14 +319,7 @@ export const describeTrigger = (
// Template Trigger
if (trigger.platform === "template") {
let base = "When a template triggers";
if (trigger.for) {
const duration = describeDuration(trigger.for);
if (duration) {
base += ` for ${duration}`;
}
}
return base;
return "When a template triggers";
}
// Webhook Trigger
@@ -444,7 +440,14 @@ export const describeCondition = (
base += ` ${entity} is ${states}`;
if (condition.for) {
const duration = describeDuration(condition.for);
let duration: string | null;
if (typeof condition.for === "number") {
duration = secondsToDuration(condition.for);
} else if (typeof condition.for === "string") {
duration = condition.for;
} else {
duration = formatDuration(condition.for);
}
if (duration) {
base += ` for ${duration}`;
}

View File

@@ -4,10 +4,7 @@ import { computeStateName } from "../common/entity/compute_state_name";
import { caseInsensitiveStringCompare } from "../common/string/compare";
import { debounce } from "../common/util/debounce";
import type { HomeAssistant } from "../types";
import type {
EntityRegistryDisplayEntry,
EntityRegistryEntry,
} from "./entity_registry";
import type { EntityRegistryEntry } from "./entity_registry";
import type { EntitySources } from "./entity_sources";
export interface DeviceRegistryEntry {
@@ -28,10 +25,6 @@ export interface DeviceRegistryEntry {
configuration_url: string | null;
}
export interface DeviceEntityDisplayLookup {
[deviceId: string]: EntityRegistryDisplayEntry[];
}
export interface DeviceEntityLookup {
[deviceId: string]: EntityRegistryEntry[];
}
@@ -154,25 +147,9 @@ export const getDeviceEntityLookup = (
return deviceEntityLookup;
};
export const getDeviceEntityDisplayLookup = (
entities: EntityRegistryDisplayEntry[]
): DeviceEntityDisplayLookup => {
const deviceEntityLookup: DeviceEntityDisplayLookup = {};
for (const entity of entities) {
if (!entity.device_id) {
continue;
}
if (!(entity.device_id in deviceEntityLookup)) {
deviceEntityLookup[entity.device_id] = [];
}
deviceEntityLookup[entity.device_id].push(entity);
}
return deviceEntityLookup;
};
export const getDeviceIntegrationLookup = (
entitySources: EntitySources,
entities: EntityRegistryDisplayEntry[]
entities: EntityRegistryEntry[]
): Record<string, string[]> => {
const deviceIntegrations: Record<string, string[]> = {};

View File

@@ -6,35 +6,6 @@ import { caseInsensitiveStringCompare } from "../common/string/compare";
import { debounce } from "../common/util/debounce";
import { HomeAssistant } from "../types";
type entityCategory = "config" | "diagnostic";
export interface EntityRegistryDisplayEntry {
entity_id: string;
name?: string;
device_id?: string;
area_id?: string;
hidden?: boolean;
entity_category?: entityCategory;
translation_key?: string;
platform?: string;
display_precision?: number;
}
interface EntityRegistryDisplayEntryResponse {
entities: {
ei: string;
di?: string;
ai?: string;
ec?: number;
en?: string;
pl?: string;
tk?: string;
hb?: boolean;
dp?: number;
}[];
entity_categories: Record<number, entityCategory>;
}
export interface EntityRegistryEntry {
id: string;
entity_id: string;
@@ -46,7 +17,7 @@ export interface EntityRegistryEntry {
area_id: string | null;
disabled_by: "user" | "device" | "integration" | "config_entry" | null;
hidden_by: Exclude<EntityRegistryEntry["disabled_by"], "config_entry">;
entity_category: entityCategory | null;
entity_category: "config" | "diagnostic" | null;
has_entity_name: boolean;
original_name?: string;
unique_id: string;
@@ -183,11 +154,6 @@ export const fetchEntityRegistry = (conn: Connection) =>
type: "config/entity_registry/list",
});
export const fetchEntityRegistryDisplay = (conn: Connection) =>
conn.sendMessagePromise<EntityRegistryDisplayEntryResponse>({
type: "config/entity_registry/list_for_display",
});
const subscribeEntityRegistryUpdates = (
conn: Connection,
store: Store<EntityRegistryEntry[]>
@@ -216,34 +182,6 @@ export const subscribeEntityRegistry = (
onChange
);
const subscribeEntityRegistryDisplayUpdates = (
conn: Connection,
store: Store<EntityRegistryDisplayEntryResponse>
) =>
conn.subscribeEvents(
debounce(
() =>
fetchEntityRegistryDisplay(conn).then((entities) =>
store.setState(entities, true)
),
500,
true
),
"entity_registry_updated"
);
export const subscribeEntityRegistryDisplay = (
conn: Connection,
onChange: (entities: EntityRegistryDisplayEntryResponse) => void
) =>
createCollection<EntityRegistryDisplayEntryResponse>(
"_entityRegistryDisplay",
fetchEntityRegistryDisplay,
subscribeEntityRegistryDisplayUpdates,
conn,
onChange
);
export const sortEntityRegistryByName = (
entries: EntityRegistryEntry[],
language: string
@@ -252,20 +190,10 @@ export const sortEntityRegistryByName = (
caseInsensitiveStringCompare(entry1.name || "", entry2.name || "", language)
);
export const entityRegistryByEntityId = memoizeOne(
(entries: EntityRegistryEntry[]) => {
const entities: Record<string, EntityRegistryEntry> = {};
for (const entity of entries) {
entities[entity.entity_id] = entity;
}
return entities;
}
);
export const entityRegistryById = memoizeOne(
(entries: EntityRegistryEntry[]) => {
const entities: Record<string, EntityRegistryEntry> = {};
for (const entity of entries) {
(entries: HomeAssistant["entities"]) => {
const entities: HomeAssistant["entities"] = {};
for (const entity of Object.values(entries)) {
entities[entity.id] = entity;
}
return entities;

View File

@@ -2,7 +2,6 @@ import {
HassEntityAttributeBase,
HassEntityBase,
} from "home-assistant-js-websocket";
import { computeDomain } from "../common/entity/compute_domain";
interface GroupEntityAttributes extends HassEntityAttributeBase {
entity_id: string[];
@@ -14,13 +13,3 @@ interface GroupEntityAttributes extends HassEntityAttributeBase {
export interface GroupEntity extends HassEntityBase {
attributes: GroupEntityAttributes;
}
export const computeGroupDomain = (
stateObj: GroupEntity
): string | undefined => {
const entityIds = stateObj.attributes.entity_id || [];
const uniqueDomains = [
...new Set(entityIds.map((entityId) => computeDomain(entityId))),
];
return uniqueDomains.length === 1 ? uniqueDomains[0] : undefined;
};

View File

@@ -1,5 +1,3 @@
import type { HassEntity } from "home-assistant-js-websocket";
export interface CustomCardEntry {
type: string;
name?: string;
@@ -8,16 +6,8 @@ export interface CustomCardEntry {
documentationURL?: string;
}
export interface CustomTileFeatureEntry {
type: string;
name?: string;
supported?: (stateObj: HassEntity) => boolean;
configurable?: boolean;
}
export interface CustomCardsWindow {
customCards?: CustomCardEntry[];
customTileFeatures?: CustomTileFeatureEntry[];
}
export const CUSTOM_TYPE_PREFIX = "custom:";
@@ -27,18 +17,8 @@ const customCardsWindow = window as CustomCardsWindow;
if (!("customCards" in customCardsWindow)) {
customCardsWindow.customCards = [];
}
if (!("customTileFeatures" in customCardsWindow)) {
customCardsWindow.customTileFeatures = [];
}
export const customCards = customCardsWindow.customCards!;
export const customTileFeatures = customCardsWindow.customTileFeatures!;
export const getCustomCardEntry = (type: string) =>
customCards.find((card) => card.type === type);
export const isCustomType = (type: string) =>
type.startsWith(CUSTOM_TYPE_PREFIX);
export const stripCustomPrefix = (type: string) =>
type.slice(CUSTOM_TYPE_PREFIX.length);

View File

@@ -73,9 +73,7 @@ export interface MediaPlayerEntity extends HassEntityBase {
| "off"
| "on"
| "unavailable"
| "unknown"
| "standby"
| "buffering";
| "unknown";
}
export const enum MediaPlayerEntityFeature {

View File

@@ -11,7 +11,6 @@ import { computeDeviceName } from "./device_registry";
import {
computeEntityRegistryName,
entityRegistryById,
EntityRegistryEntry,
} from "./entity_registry";
import { domainToName } from "./integration";
import {
@@ -34,7 +33,6 @@ import {
export const describeAction = <T extends ActionType>(
hass: HomeAssistant,
entityRegistry: EntityRegistryEntry[],
action: ActionTypes[T],
actionType?: T,
ignoreAlias = false
@@ -93,7 +91,7 @@ export const describeAction = <T extends ActionType>(
targets.push(targetThing);
}
} else {
const entityReg = entityRegistryById(entityRegistry)[targetThing];
const entityReg = entityRegistryById(hass.entities)[targetThing];
if (entityReg) {
targets.push(
computeEntityRegistryName(hass, entityReg) || targetThing

View File

@@ -16,10 +16,8 @@ export type Selector =
| DateSelector
| DateTimeSelector
| DeviceSelector
| LegacyDeviceSelector
| DurationSelector
| EntitySelector
| LegacyEntitySelector
| FileSelector
| IconSelector
| LocationSelector
@@ -50,10 +48,22 @@ export interface AddonSelector {
} | null;
}
export interface SelectorDevice {
integration?: NonNullable<DeviceSelector["device"]>["integration"];
manufacturer?: NonNullable<DeviceSelector["device"]>["manufacturer"];
model?: NonNullable<DeviceSelector["device"]>["model"];
}
export interface SelectorEntity {
integration?: NonNullable<EntitySelector["entity"]>["integration"];
domain?: NonNullable<EntitySelector["entity"]>["domain"];
device_class?: NonNullable<EntitySelector["entity"]>["device_class"];
}
export interface AreaSelector {
area: {
entity?: EntitySelectorFilter | readonly EntitySelectorFilter[];
device?: DeviceSelectorFilter | readonly DeviceSelectorFilter[];
entity?: SelectorEntity;
device?: SelectorDevice;
multiple?: boolean;
} | null;
}
@@ -98,77 +108,33 @@ export interface DateTimeSelector {
datetime: {} | null;
}
interface DeviceSelectorFilter {
integration?: string;
manufacturer?: string;
model?: string;
}
export interface DeviceSelector {
device: {
filter?: DeviceSelectorFilter | readonly DeviceSelectorFilter[];
entity?: EntitySelectorFilter | readonly EntitySelectorFilter[];
integration?: string;
manufacturer?: string;
model?: string;
entity?: SelectorEntity;
multiple?: boolean;
} | null;
}
export interface LegacyDeviceSelector {
device:
| DeviceSelector["device"] & {
/**
* @deprecated Use filter instead
*/
integration?: DeviceSelectorFilter["integration"];
/**
* @deprecated Use filter instead
*/
manufacturer?: DeviceSelectorFilter["manufacturer"];
/**
* @deprecated Use filter instead
*/
model?: DeviceSelectorFilter["model"];
};
}
export interface DurationSelector {
duration: {
enable_day?: boolean;
} | null;
}
interface EntitySelectorFilter {
integration?: string;
domain?: string | readonly string[];
device_class?: string | readonly string[];
}
export interface EntitySelector {
entity: {
integration?: string;
domain?: string | readonly string[];
device_class?: string;
multiple?: boolean;
include_entities?: string[];
exclude_entities?: string[];
filter?: EntitySelectorFilter | readonly EntitySelectorFilter[];
} | null;
}
export interface LegacyEntitySelector {
entity:
| EntitySelector["entity"] & {
/**
* @deprecated Use filter instead
*/
integration?: EntitySelectorFilter["integration"];
/**
* @deprecated Use filter instead
*/
domain?: EntitySelectorFilter["domain"];
/**
* @deprecated Use filter instead
*/
device_class?: EntitySelectorFilter["device_class"];
};
}
export interface StatisticSelector {
statistic: {
device_class?: string;
@@ -284,8 +250,8 @@ export interface StringSelector {
export interface TargetSelector {
target: {
entity?: EntitySelectorFilter | readonly EntitySelectorFilter[];
device?: DeviceSelectorFilter | readonly DeviceSelectorFilter[];
entity?: SelectorEntity;
device?: SelectorDevice;
} | null;
}
@@ -315,7 +281,7 @@ export interface UiColorSelector {
}
export const filterSelectorDevices = (
filterDevice: DeviceSelectorFilter,
filterDevice: SelectorDevice,
device: DeviceRegistryEntry,
deviceIntegrationLookup: Record<string, string[]> | undefined
): boolean => {
@@ -342,7 +308,7 @@ export const filterSelectorDevices = (
};
export const filterSelectorEntities = (
filterEntity: EntitySelectorFilter,
filterEntity: SelectorEntity,
entity: HassEntity,
entitySources?: EntitySources
): boolean => {
@@ -363,15 +329,11 @@ export const filterSelectorEntities = (
}
}
if (filterDeviceClass) {
const entityDeviceClass = entity.attributes.device_class;
if (
entityDeviceClass && Array.isArray(filterDeviceClass)
? !filterDeviceClass.includes(entityDeviceClass)
: entityDeviceClass !== filterDeviceClass
) {
return false;
}
if (
filterDeviceClass &&
entity.attributes.device_class !== filterDeviceClass
) {
return false;
}
if (
@@ -383,59 +345,3 @@ export const filterSelectorEntities = (
return true;
};
export const handleLegacyEntitySelector = (
selector: LegacyEntitySelector | EntitySelector
): EntitySelector => {
if (!selector.entity) return { entity: null };
if ("filter" in selector.entity) return selector;
const { domain, integration, device_class, ...rest } = (
selector as LegacyEntitySelector
).entity!;
if (domain || integration || device_class) {
return {
entity: {
...rest,
filter: {
domain,
integration,
device_class,
},
},
};
}
return {
entity: rest,
};
};
export const handleLegacyDeviceSelector = (
selector: LegacyDeviceSelector | DeviceSelector
): DeviceSelector => {
if (!selector.device) return { device: null };
if ("filter" in selector.device) return selector;
const { integration, manufacturer, model, ...rest } = (
selector as LegacyDeviceSelector
).device!;
if (integration || manufacturer || model) {
return {
device: {
...rest,
filter: {
integration,
manufacturer,
model,
},
},
};
}
return {
device: rest,
};
};

View File

@@ -1,89 +0,0 @@
import { HomeAssistant } from "../types";
export interface ThreadRouter {
brand: "google" | "apple" | "homeassistant";
server: string;
extended_pan_id: string;
model_name: string | null;
network_name: string;
vendor_name: string;
}
export interface ThreadDataSet {
created: string;
dataset_id: string;
preferred: boolean;
source: string;
network_name: string;
extended_pan_id?: string;
pan_id?: string;
}
export interface ThreadRouterDiscoveryEvent {
key: string;
type: "router_discovered" | "router_removed";
data: ThreadRouter;
}
class DiscoveryStream {
routers: { [key: string]: ThreadRouter };
constructor() {
this.routers = {};
}
processEvent(streamMessage: ThreadRouterDiscoveryEvent): ThreadRouter[] {
if (streamMessage.type === "router_discovered") {
this.routers[streamMessage.key] = streamMessage.data;
} else if (streamMessage.type === "router_removed") {
delete this.routers[streamMessage.key];
}
return Object.values(this.routers);
}
}
export const subscribeDiscoverThreadRouters = (
hass: HomeAssistant,
callbackFunction: (routers: ThreadRouter[]) => void
) => {
const stream = new DiscoveryStream();
return hass.connection.subscribeMessage<ThreadRouterDiscoveryEvent>(
(message) => callbackFunction(stream.processEvent(message)),
{
type: "thread/discover_routers",
}
);
};
export const listThreadDataSets = (
hass: HomeAssistant
): Promise<{ datasets: ThreadDataSet[] }> =>
hass.callWS({
type: "thread/list_datasets",
});
export const getThreadDataSetTLV = (
hass: HomeAssistant,
dataset_id: string
): Promise<{ tlv: string }> =>
hass.callWS({ type: "thread/get_dataset_tlv", dataset_id });
export const addThreadDataSet = (
hass: HomeAssistant,
source: string,
tlv: string
): Promise<void> =>
hass.callWS({
type: "thread/add_dataset_tlv",
source,
tlv,
});
export const removeThreadDataSet = (
hass: HomeAssistant,
dataset_id: string
): Promise<void> =>
hass.callWS({
type: "thread/delete_dataset",
dataset_id,
});

View File

@@ -150,9 +150,7 @@ export const checkForEntityUpdates = async (
});
// there is no reliable way to know if all the updates are done updating, so we just wait a bit for now...
await new Promise((r) => {
setTimeout(r, 10000);
});
await new Promise((r) => setTimeout(r, 10000));
unsubscribeEvents();

View File

@@ -92,23 +92,7 @@ enum NodeType {
"End Node" = 1,
}
enum RFRegion {
"Europe" = 0x00,
"USA" = 0x01,
"Australia/New Zealand" = 0x02,
"Hong Kong" = 0x03,
"India" = 0x05,
"Israel" = 0x06,
"Russia" = 0x07,
"China" = 0x08,
"USA (Long Range)" = 0x09,
"Japan" = 0x20,
"Korea" = 0x21,
"Unknown" = 0xfe,
"Default (EU)" = 0xff,
}
export enum NodeFirmwareUpdateStatus {
export enum FirmwareUpdateStatus {
Error_Timeout = -1,
Error_Checksum = 0,
Error_TransmissionFailed = 1,
@@ -124,19 +108,6 @@ export enum NodeFirmwareUpdateStatus {
OK_RestartPending = 0xff,
}
export enum ControllerFirmwareUpdateStatus {
// An expected response was not received from the controller in time
Error_Timeout = 0,
/** The maximum number of retry attempts for a firmware fragments were reached */
Error_RetryLimitReached,
/** The update was aborted by the bootloader */
Error_Aborted,
/** This controller does not support firmware updates */
Error_NotSupported,
OK = 0xff,
}
export interface QRProvisioningInformation {
version: QRCodeVersion;
securityClasses: SecurityClass[];
@@ -178,7 +149,6 @@ export interface ZWaveJSController {
sdk_version: string;
type: number;
own_node_id: number;
rf_region: RFRegion | null;
is_primary: boolean;
is_using_home_id_from_other_network: boolean;
is_sis_present: boolean;
@@ -206,7 +176,6 @@ export interface ZWaveJSNodeStatus {
zwave_plus_version: number | null;
highest_security_class: SecurityClass | null;
is_controller_node: boolean;
has_firmware_update_cc: boolean;
}
export interface ZwaveJSNodeMetadata {
@@ -335,7 +304,7 @@ export interface ZWaveJSNodeStatusUpdatedMessage {
status: NodeStatus;
}
export interface ZWaveJSFirmwareUpdateProgressMessage {
export interface ZWaveJSNodeFirmwareUpdateProgressMessage {
event: "firmware update progress";
current_file: number;
total_files: number;
@@ -346,18 +315,12 @@ export interface ZWaveJSFirmwareUpdateProgressMessage {
export interface ZWaveJSNodeFirmwareUpdateFinishedMessage {
event: "firmware update finished";
status: NodeFirmwareUpdateStatus;
status: FirmwareUpdateStatus;
success: boolean;
wait_time?: number;
reinterview: boolean;
}
export interface ZWaveJSControllerFirmwareUpdateFinishedMessage {
event: "firmware update finished";
status: ControllerFirmwareUpdateStatus;
success: boolean;
}
export type ZWaveJSNodeFirmwareUpdateCapabilities =
| { firmware_upgradable: false }
| {
@@ -459,8 +422,7 @@ export const subscribeAddZwaveNode = (
inclusion_strategy: InclusionStrategy = InclusionStrategy.Default,
qr_provisioning_information?: QRProvisioningInformation,
qr_code_string?: string,
planned_provisioning_entry?: PlannedProvisioningEntry,
dsk?: string
planned_provisioning_entry?: PlannedProvisioningEntry
): Promise<UnsubscribeFunc> =>
hass.connection.subscribeMessage((message) => callbackFunction(message), {
type: "zwave_js/add_node",
@@ -469,7 +431,6 @@ export const subscribeAddZwaveNode = (
qr_code_string,
qr_provisioning_information,
planned_provisioning_entry,
dsk,
});
export const stopZwaveInclusion = (hass: HomeAssistant, entry_id: string) =>
@@ -497,17 +458,6 @@ export const zwaveGrantSecurityClasses = (
client_side_auth,
});
export const zwaveTryParseDskFromQrCode = (
hass: HomeAssistant,
entry_id: string,
qr_code_string: string
) =>
hass.callWS<string | null>({
type: "zwave_js/try_parse_dsk_from_qr_code_string",
entry_id,
qr_code_string,
});
export const zwaveValidateDskAndEnterPin = (
hass: HomeAssistant,
entry_id: string,
@@ -757,14 +707,10 @@ export const fetchZwaveNodeFirmwareUpdateCapabilities = (
export const uploadFirmwareAndBeginUpdate = async (
hass: HomeAssistant,
device_id: string,
file: File,
target?: number
file: File
) => {
const fd = new FormData();
fd.append("file", file);
if (target !== undefined) {
fd.append("target", target.toString());
}
const resp = await hass.fetchWithAuth(
`/api/zwave_js/firmware/upload/${device_id}`,
{
@@ -783,9 +729,8 @@ export const subscribeZwaveNodeFirmwareUpdate = (
device_id: string,
callbackFunction: (
message:
| ZWaveJSFirmwareUpdateProgressMessage
| ZWaveJSControllerFirmwareUpdateFinishedMessage
| ZWaveJSNodeFirmwareUpdateFinishedMessage
| ZWaveJSNodeFirmwareUpdateProgressMessage
) => void
): Promise<UnsubscribeFunc> =>
hass.connection.subscribeMessage(

View File

@@ -101,19 +101,6 @@ export const showConfigFlowDialog = (
return hass.localize(`component.${step.handler}.selector.${key}`);
},
renderShowFormStepSubmitButton(hass, step) {
return (
hass.localize(
`component.${step.handler}.config.step.${step.step_id}.submit`
) ||
hass.localize(
`ui.panel.config.integrations.config_flow.${
step.last_step === false ? "next" : "submit"
}`
)
);
},
renderExternalStepHeader(hass, step) {
return (
hass.localize(

View File

@@ -67,11 +67,6 @@ export interface FlowConfig {
key: string
): string;
renderShowFormStepSubmitButton(
hass: HomeAssistant,
step: DataEntryFlowStepForm
): string;
renderExternalStepHeader(
hass: HomeAssistant,
step: DataEntryFlowStepExternal

View File

@@ -115,19 +115,6 @@ export const showOptionsFlowDialog = (
return hass.localize(`component.${configEntry.domain}.selector.${key}`);
},
renderShowFormStepSubmitButton(hass, step) {
return (
hass.localize(
`component.${configEntry.domain}.options.step.${step.step_id}.submit`
) ||
hass.localize(
`ui.panel.config.integrations.config_flow.${
step.last_step === false ? "next" : "submit"
}`
)
);
},
renderExternalStepHeader(_hass, _step) {
return "";
},

View File

@@ -70,9 +70,10 @@ class StepFlowForm extends LitElement {
: html`
<div>
<mwc-button @click=${this._submitStep}>
${this.flowConfig.renderShowFormStepSubmitButton(
this.hass,
this.step
${this.hass.localize(
`ui.panel.config.integrations.config_flow.${
this.step.last_step === false ? "next" : "submit"
}`
)}
</mwc-button>
</div>

View File

@@ -1,24 +0,0 @@
import { css } from "lit";
export const moreInfoControlStyle = css`
:host {
display: flex;
flex-direction: column;
flex: 1;
justify-content: space-between;
}
.controls {
display: flex;
flex-direction: column;
align-items: center;
}
.controls > *:not(:last-child) {
margin-bottom: 24px;
}
ha-attributes {
width: 100%;
}
`;

View File

@@ -1,85 +0,0 @@
import { HassEntity } from "home-assistant-js-websocket";
import { html, LitElement, TemplateResult, css, CSSResultGroup } from "lit";
import { customElement, property } from "lit/decorators";
import { computeStateDisplay } from "../../../common/entity/compute_state_display";
import { isUnavailableState } from "../../../data/entity";
import { LightEntity } from "../../../data/light";
import { SENSOR_DEVICE_CLASS_TIMESTAMP } from "../../../data/sensor";
import "../../../panels/lovelace/components/hui-timestamp-display";
import { HomeAssistant } from "../../../types";
@customElement("ha-more-info-state-header")
export class HaMoreInfoStateHeader extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public stateObj!: LightEntity;
@property({ attribute: false }) public stateOverride?: string;
private _computeStateDisplay(stateObj: HassEntity): TemplateResult | string {
if (
stateObj.attributes.device_class === SENSOR_DEVICE_CLASS_TIMESTAMP &&
!isUnavailableState(stateObj.state)
) {
return html`
<hui-timestamp-display
.hass=${this.hass}
.ts=${new Date(stateObj.state)}
format="relative"
capitalize
></hui-timestamp-display>
`;
}
const stateDisplay = computeStateDisplay(
this.hass!.localize,
stateObj,
this.hass!.locale,
this.hass!.entities
);
return stateDisplay;
}
protected render(): TemplateResult {
const name = this.stateObj.attributes.friendly_name;
const stateDisplay =
this.stateOverride ?? this._computeStateDisplay(this.stateObj);
return html`
<p class="name">${name}</p>
<p class="state">${stateDisplay}</p>
`;
}
static get styles(): CSSResultGroup {
return css`
p {
text-align: center;
margin: 0;
}
.name {
font-style: normal;
font-weight: 400;
font-size: 28px;
line-height: 36px;
margin-bottom: 4px;
}
.state {
font-style: normal;
font-weight: 500;
font-size: 16px;
line-height: 24px;
letter-spacing: 0.1px;
margin-bottom: 24px;
}
`;
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-more-info-state-header": HaMoreInfoStateHeader;
}
}

View File

@@ -1,167 +0,0 @@
import { mdiFlash, mdiFlashOff } from "@mdi/js";
import { HassEntity } from "home-assistant-js-websocket";
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { customElement, property } from "lit/decorators";
import { classMap } from "lit/directives/class-map";
import { styleMap } from "lit/directives/style-map";
import { computeDomain } from "../../../common/entity/compute_domain";
import { stateActive } from "../../../common/entity/state_active";
import { stateColorCss } from "../../../common/entity/state_color";
import "../../../components/ha-control-button";
import "../../../components/ha-control-switch";
import { UNAVAILABLE, UNKNOWN } from "../../../data/entity";
import { forwardHaptic } from "../../../data/haptics";
import { HomeAssistant } from "../../../types";
@customElement("ha-more-info-toggle")
export class HaMoreInfoToggle extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public stateObj!: HassEntity;
@property({ attribute: false }) public iconPathOn?: string;
@property({ attribute: false }) public iconPathOff?: string;
private _valueChanged(ev) {
const checked = ev.target.checked as boolean;
if (checked) {
this._turnOn();
} else {
this._turnOff();
}
}
private _turnOn() {
this._callService(true);
}
private _turnOff() {
this._callService(false);
}
private async _callService(turnOn): Promise<void> {
if (!this.hass || !this.stateObj) {
return;
}
forwardHaptic("light");
const stateDomain = computeDomain(this.stateObj.entity_id);
let serviceDomain;
let service;
if (stateDomain === "group") {
serviceDomain = "homeassistant";
service = turnOn ? "turn_on" : "turn_off";
} else {
serviceDomain = stateDomain;
service = turnOn ? "turn_on" : "turn_off";
}
await this.hass.callService(serviceDomain, service, {
entity_id: this.stateObj.entity_id,
});
}
protected render(): TemplateResult {
const color = stateColorCss(this.stateObj);
const isOn = this.stateObj.state === "on";
const isOff = this.stateObj.state === "off";
if (
this.stateObj.attributes.assumed_state ||
this.stateObj.state === UNKNOWN
) {
return html`
<div class="buttons">
<ha-control-button
.label=${this.hass.localize("ui.dialogs.more_info_control.turn_on")}
@click=${this._turnOn}
.disabled=${this.stateObj.state === UNAVAILABLE}
class=${classMap({
active: isOn,
})}
style=${styleMap({
"--color": color,
})}
>
<ha-svg-icon .path=${this.iconPathOn || mdiFlash}></ha-svg-icon>
</ha-control-button>
<ha-control-button
.label=${this.hass.localize(
"ui.dialogs.more_info_control.turn_off"
)}
@click=${this._turnOff}
.disabled=${this.stateObj.state === UNAVAILABLE}
class=${classMap({
active: isOff,
})}
style=${styleMap({
"--color": color,
})}
>
<ha-svg-icon .path=${this.iconPathOff || mdiFlashOff}></ha-svg-icon>
</ha-control-button>
</div>
`;
}
return html`
<ha-control-switch
.pathOn=${this.iconPathOn || mdiFlash}
.pathOff=${this.iconPathOff || mdiFlashOff}
vertical
reversed
.checked=${isOn}
.showHandle=${stateActive(this.stateObj)}
@change=${this._valueChanged}
.ariaLabel=${this.hass.localize("ui.dialogs.more_info_control.toggle")}
style=${styleMap({
"--control-switch-on-color": color,
})}
.disabled=${this.stateObj.state === UNAVAILABLE}
>
</ha-control-switch>
`;
}
static get styles(): CSSResultGroup {
return css`
ha-control-switch {
height: 320px;
--control-switch-thickness: 100px;
--control-switch-border-radius: 24px;
--control-switch-padding: 6px;
--mdc-icon-size: 24px;
}
.buttons {
display: flex;
flex-direction: column;
width: 100px;
height: 320px;
padding: 6px;
box-sizing: border-box;
}
ha-control-button {
flex: 1;
width: 100%;
--control-button-border-radius: 18px;
--mdc-icon-size: 24px;
}
ha-control-button.active {
--control-button-icon-color: white;
--control-button-background-color: var(--color);
--control-button-background-opacity: 1;
}
ha-control-button:not(:last-child) {
margin-bottom: 6px;
}
`;
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-more-info-toggle": HaMoreInfoToggle;
}
}

View File

@@ -1,102 +0,0 @@
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { customElement, property, state } from "lit/decorators";
import { styleMap } from "lit/directives/style-map";
import {
hsv2rgb,
rgb2hex,
rgb2hsv,
} from "../../../../common/color/convert-color";
import { stateActive } from "../../../../common/entity/state_active";
import { stateColorCss } from "../../../../common/entity/state_color";
import "../../../../components/ha-control-slider";
import { UNAVAILABLE } from "../../../../data/entity";
import { LightEntity } from "../../../../data/light";
import { HomeAssistant } from "../../../../types";
@customElement("ha-more-info-light-brightness")
export class HaMoreInfoLightBrightness extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public stateObj!: LightEntity;
@state() value?: number;
protected updated(changedProp: Map<string | number | symbol, unknown>): void {
if (changedProp.has("stateObj")) {
this.value =
this.stateObj.attributes.brightness != null
? Math.max(
Math.round((this.stateObj.attributes.brightness * 100) / 255),
1
)
: undefined;
}
}
private _valueChanged(ev: CustomEvent) {
const value = (ev.detail as any).value;
if (isNaN(value)) return;
this.hass.callService("light", "turn_on", {
entity_id: this.stateObj!.entity_id,
brightness_pct: value,
});
}
protected render(): TemplateResult {
let color = stateColorCss(this.stateObj);
if (this.stateObj.attributes.rgb_color) {
const hsvColor = rgb2hsv(this.stateObj.attributes.rgb_color);
// Modify the real rgb color for better contrast
if (hsvColor[1] < 0.4) {
// Special case for very light color (e.g: white)
if (hsvColor[1] < 0.1) {
hsvColor[2] = 225;
} else {
hsvColor[1] = 0.4;
}
}
color = rgb2hex(hsv2rgb(hsvColor));
}
return html`
<ha-control-slider
vertical
.value=${this.value}
min="1"
max="100"
.showHandle=${stateActive(this.stateObj)}
@value-changed=${this._valueChanged}
.ariaLabel=${this.hass.localize(
"ui.dialogs.more_info_control.light.brightness"
)}
style=${styleMap({
"--control-slider-color": color,
})}
.disabled=${this.stateObj.state === UNAVAILABLE}
>
</ha-control-slider>
`;
}
static get styles(): CSSResultGroup {
return css`
ha-control-slider {
height: 320px;
--control-slider-thickness: 100px;
--control-slider-border-radius: 24px;
--control-slider-color: var(--primary-color);
--control-slider-background: var(--disabled-color);
--control-slider-background-opacity: 0.2;
}
`;
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-more-info-light-brightness": HaMoreInfoLightBrightness;
}
}

View File

@@ -1,553 +0,0 @@
import "@material/mwc-button";
import "@material/mwc-tab-bar/mwc-tab-bar";
import "@material/mwc-tab/mwc-tab";
import { mdiPalette } from "@mdi/js";
import {
css,
CSSResultGroup,
html,
LitElement,
PropertyValues,
TemplateResult,
} from "lit";
import { customElement, property, state } from "lit/decorators";
import "../../../../components/ha-button-toggle-group";
import "../../../../components/ha-color-picker";
import "../../../../components/ha-control-slider";
import "../../../../components/ha-icon-button-prev";
import "../../../../components/ha-labeled-slider";
import {
getLightCurrentModeRgbColor,
LightColorMode,
LightEntity,
lightSupportsColor,
lightSupportsColorMode,
} from "../../../../data/light";
import { HomeAssistant } from "../../../../types";
import { LightColorPickerViewParams } from "./show-view-light-color-picker";
type Mode = "color_temp" | "color";
@customElement("ha-more-info-view-light-color-picker")
class MoreInfoViewLightColorPicker extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property() public params?: LightColorPickerViewParams;
@state() private _ctSliderValue?: number;
@state() private _cwSliderValue?: number;
@state() private _wwSliderValue?: number;
@state() private _wvSliderValue?: number;
@state() private _colorBrightnessSliderValue?: number;
@state() private _brightnessAdjusted?: number;
@state() private _hueSegments = 24;
@state() private _saturationSegments = 8;
@state() private _colorPickerColor?: [number, number, number];
@state() private _mode?: Mode;
@state() private _modes: Mode[] = [];
get stateObj() {
return this.params
? (this.hass.states[this.params.entityId] as LightEntity)
: undefined;
}
protected render(): TemplateResult {
if (!this.params || !this.stateObj) {
return html``;
}
const supportsRgbww = lightSupportsColorMode(
this.stateObj,
LightColorMode.RGBWW
);
const supportsRgbw =
!supportsRgbww &&
lightSupportsColorMode(this.stateObj, LightColorMode.RGBW);
return html`
${this._modes.length > 1
? html`
<mwc-tab-bar
.activeIndex=${this._mode ? this._modes.indexOf(this._mode) : 0}
@MDCTabBar:activated=${this._handleTabChanged}
>
${this._modes.map(
(value) =>
html`<mwc-tab
.label=${this.hass.localize(
`ui.dialogs.more_info_control.light.color_picker.mode.${value}`
)}
></mwc-tab>`
)}
</mwc-tab-bar>
`
: ""}
<div class="content">
${this._mode === LightColorMode.COLOR_TEMP
? html`
<ha-control-slider
vertical
class="color_temp"
label=${this.hass.localize("ui.card.light.color_temperature")}
min="1"
max="100"
mode="cursor"
.value=${this._ctSliderValue}
@value-changed=${this._ctSliderChanged}
.min=${this.stateObj.attributes.min_color_temp_kelvin!}
.max=${this.stateObj.attributes.max_color_temp_kelvin!}
>
</ha-control-slider>
`
: ""}
${this._mode === "color"
? html`
<div class="segmentationContainer">
<ha-color-picker
class="color"
@colorselected=${this._colorPicked}
.desiredRgbColor=${this._colorPickerColor}
throttle="500"
.hueSegments=${this._hueSegments}
.saturationSegments=${this._saturationSegments}
>
</ha-color-picker>
<ha-icon-button
.path=${mdiPalette}
@click=${this._segmentClick}
class="segmentationButton"
></ha-icon-button>
</div>
${supportsRgbw || supportsRgbww
? html`<ha-labeled-slider
.caption=${this.hass.localize(
"ui.card.light.color_brightness"
)}
icon="hass:brightness-7"
max="100"
.value=${this._colorBrightnessSliderValue}
@change=${this._colorBrightnessSliderChanged}
pin
></ha-labeled-slider>`
: ""}
${supportsRgbw
? html`
<ha-labeled-slider
.caption=${this.hass.localize(
"ui.card.light.white_value"
)}
icon="hass:file-word-box"
max="100"
.name=${"wv"}
.value=${this._wvSliderValue}
@change=${this._wvSliderChanged}
pin
></ha-labeled-slider>
`
: ""}
${supportsRgbww
? html`
<ha-labeled-slider
.caption=${this.hass.localize(
"ui.card.light.cold_white_value"
)}
icon="hass:file-word-box-outline"
max="100"
.name=${"cw"}
.value=${this._cwSliderValue}
@change=${this._wvSliderChanged}
pin
></ha-labeled-slider>
<ha-labeled-slider
.caption=${this.hass.localize(
"ui.card.light.warm_white_value"
)}
icon="hass:file-word-box"
max="100"
.name=${"ww"}
.value=${this._wwSliderValue}
@change=${this._wvSliderChanged}
pin
></ha-labeled-slider>
`
: ""}
`
: ""}
</div>
`;
}
public _updateSliderValues() {
const stateObj = this.stateObj;
if (stateObj?.state === "on") {
this._brightnessAdjusted = undefined;
if (
stateObj.attributes.color_mode === LightColorMode.RGB &&
!lightSupportsColorMode(stateObj, LightColorMode.RGBWW) &&
!lightSupportsColorMode(stateObj, LightColorMode.RGBW)
) {
const maxVal = Math.max(...stateObj.attributes.rgb_color!);
if (maxVal < 255) {
this._brightnessAdjusted = maxVal;
}
}
this._ctSliderValue = stateObj.attributes.color_temp_kelvin;
this._wvSliderValue =
stateObj.attributes.color_mode === LightColorMode.RGBW
? Math.round((stateObj.attributes.rgbw_color![3] * 100) / 255)
: undefined;
this._cwSliderValue =
stateObj.attributes.color_mode === LightColorMode.RGBWW
? Math.round((stateObj.attributes.rgbww_color![3] * 100) / 255)
: undefined;
this._wwSliderValue =
stateObj.attributes.color_mode === LightColorMode.RGBWW
? Math.round((stateObj.attributes.rgbww_color![4] * 100) / 255)
: undefined;
const currentRgbColor = getLightCurrentModeRgbColor(stateObj);
this._colorBrightnessSliderValue = currentRgbColor
? Math.round((Math.max(...currentRgbColor.slice(0, 3)) * 100) / 255)
: undefined;
this._colorPickerColor = currentRgbColor?.slice(0, 3) as [
number,
number,
number
];
} else {
this._colorPickerColor = [0, 0, 0];
this._ctSliderValue = undefined;
this._wvSliderValue = undefined;
this._cwSliderValue = undefined;
this._wwSliderValue = undefined;
}
}
public willUpdate(changedProps: PropertyValues) {
super.willUpdate(changedProps);
if (!changedProps.has("params") || !changedProps.has("hass")) {
return;
}
if (changedProps.has("params")) {
const supportsTemp = lightSupportsColorMode(
this.stateObj!,
LightColorMode.COLOR_TEMP
);
const supportsColor = lightSupportsColor(this.stateObj!);
const modes: Mode[] = [];
if (supportsColor) {
modes.push("color");
}
if (supportsTemp) {
modes.push("color_temp");
}
this._modes = modes;
this._mode =
this.stateObj!.attributes.color_mode === LightColorMode.COLOR_TEMP
? LightColorMode.COLOR_TEMP
: "color";
}
this._updateSliderValues();
}
private _handleTabChanged(ev: CustomEvent): void {
const newMode = this._modes[ev.detail.index];
if (newMode === this._mode) {
return;
}
this._mode = newMode;
}
private _ctSliderChanged(ev: CustomEvent) {
const ct = ev.detail.value;
if (isNaN(ct)) {
return;
}
this._ctSliderValue = ct;
this.hass.callService("light", "turn_on", {
entity_id: this.stateObj!.entity_id,
color_temp_kelvin: ct,
});
}
private _wvSliderChanged(ev: CustomEvent) {
const target = ev.target as any;
let wv = Number(target.value);
const name = target.name;
if (isNaN(wv)) {
return;
}
if (name === "wv") {
this._wvSliderValue = wv;
} else if (name === "cw") {
this._cwSliderValue = wv;
} else if (name === "ww") {
this._wwSliderValue = wv;
}
wv = Math.min(255, Math.round((wv * 255) / 100));
const rgb = getLightCurrentModeRgbColor(this.stateObj!);
if (name === "wv") {
const rgbw_color = rgb || [0, 0, 0, 0];
rgbw_color[3] = wv;
this.hass.callService("light", "turn_on", {
entity_id: this.stateObj!.entity_id,
rgbw_color,
});
return;
}
const rgbww_color = rgb || [0, 0, 0, 0, 0];
while (rgbww_color.length < 5) {
rgbww_color.push(0);
}
rgbww_color[name === "cw" ? 3 : 4] = wv;
this.hass.callService("light", "turn_on", {
entity_id: this.stateObj!.entity_id,
rgbww_color,
});
}
private _colorBrightnessSliderChanged(ev: CustomEvent) {
const target = ev.target as any;
let value = Number(target.value);
if (isNaN(value)) {
return;
}
const oldValue = this._colorBrightnessSliderValue;
this._colorBrightnessSliderValue = value;
value = (value * 255) / 100;
const rgb = (getLightCurrentModeRgbColor(this.stateObj!)?.slice(0, 3) || [
255, 255, 255,
]) as [number, number, number];
this._setRgbWColor(
this._adjustColorBrightness(
// first normalize the value
oldValue
? this._adjustColorBrightness(rgb, (oldValue * 255) / 100, true)
: rgb,
value
)
);
}
private _segmentClick() {
if (this._hueSegments === 24 && this._saturationSegments === 8) {
this._hueSegments = 0;
this._saturationSegments = 0;
} else {
this._hueSegments = 24;
this._saturationSegments = 8;
}
}
private _adjustColorBrightness(
rgbColor: [number, number, number],
value?: number,
invert = false
) {
if (value !== undefined && value !== 255) {
let ratio = value / 255;
if (invert) {
ratio = 1 / ratio;
}
rgbColor[0] = Math.min(255, Math.round(rgbColor[0] * ratio));
rgbColor[1] = Math.min(255, Math.round(rgbColor[1] * ratio));
rgbColor[2] = Math.min(255, Math.round(rgbColor[2] * ratio));
}
return rgbColor;
}
private _setRgbWColor(rgbColor: [number, number, number]) {
if (lightSupportsColorMode(this.stateObj!, LightColorMode.RGBWW)) {
const rgbww_color: [number, number, number, number, number] = this
.stateObj!.attributes.rgbww_color
? [...this.stateObj!.attributes.rgbww_color]
: [0, 0, 0, 0, 0];
this.hass.callService("light", "turn_on", {
entity_id: this.stateObj!.entity_id,
rgbww_color: rgbColor.concat(rgbww_color.slice(3)),
});
} else if (lightSupportsColorMode(this.stateObj!, LightColorMode.RGBW)) {
const rgbw_color: [number, number, number, number] = this.stateObj!
.attributes.rgbw_color
? [...this.stateObj!.attributes.rgbw_color]
: [0, 0, 0, 0];
this.hass.callService("light", "turn_on", {
entity_id: this.stateObj!.entity_id,
rgbw_color: rgbColor.concat(rgbw_color.slice(3)),
});
}
}
/**
* Called when a new color has been picked.
* should be throttled with the 'throttle=' attribute of the color picker
*/
private _colorPicked(
ev: CustomEvent<{
hs: { h: number; s: number };
rgb: { r: number; g: number; b: number };
}>
) {
this._colorPickerColor = [
ev.detail.rgb.r,
ev.detail.rgb.g,
ev.detail.rgb.b,
];
if (
lightSupportsColorMode(this.stateObj!, LightColorMode.RGBWW) ||
lightSupportsColorMode(this.stateObj!, LightColorMode.RGBW)
) {
this._setRgbWColor(
this._colorBrightnessSliderValue
? this._adjustColorBrightness(
[ev.detail.rgb.r, ev.detail.rgb.g, ev.detail.rgb.b],
(this._colorBrightnessSliderValue * 255) / 100
)
: [ev.detail.rgb.r, ev.detail.rgb.g, ev.detail.rgb.b]
);
} else if (lightSupportsColorMode(this.stateObj!, LightColorMode.RGB)) {
const rgb_color: [number, number, number] = [
ev.detail.rgb.r,
ev.detail.rgb.g,
ev.detail.rgb.b,
];
if (this._brightnessAdjusted) {
const brightnessAdjust = (this._brightnessAdjusted / 255) * 100;
const brightnessPercentage = Math.round(
((this.stateObj!.attributes.brightness || 0) * brightnessAdjust) / 255
);
this.hass.callService("light", "turn_on", {
entity_id: this.stateObj!.entity_id,
brightness_pct: brightnessPercentage,
rgb_color: this._adjustColorBrightness(
rgb_color,
this._brightnessAdjusted,
true
),
});
} else {
this.hass.callService("light", "turn_on", {
entity_id: this.stateObj!.entity_id,
rgb_color,
});
}
} else {
this.hass.callService("light", "turn_on", {
entity_id: this.stateObj!.entity_id,
hs_color: [ev.detail.hs.h, ev.detail.hs.s * 100],
});
}
}
static get styles(): CSSResultGroup {
return [
css`
:host {
display: flex;
flex-direction: column;
}
.content {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 24px;
flex: 1;
}
.segmentationContainer {
position: relative;
max-height: 500px;
display: flex;
justify-content: center;
}
.segmentationButton {
position: absolute;
top: 5%;
left: 0;
color: var(--secondary-text-color);
}
ha-color-picker {
--ha-color-picker-wheel-borderwidth: 5;
--ha-color-picker-wheel-bordercolor: white;
--ha-color-picker-wheel-shadow: none;
--ha-color-picker-marker-borderwidth: 2;
--ha-color-picker-marker-bordercolor: white;
}
ha-control-slider {
height: 320px;
margin: 20px 0;
}
ha-labeled-slider {
width: 100%;
}
.color_temp {
--control-slider-thickness: 100px;
--control-slider-border-radius: 24px;
--control-slider-background: -webkit-linear-gradient(
top,
rgb(166, 209, 255) 0%,
white 50%,
rgb(255, 160, 0) 100%
);
--control-slider-background-opacity: 1;
}
hr {
border-color: var(--divider-color);
border-bottom: none;
margin: 16px 0;
}
`,
];
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-more-info-view-light-color-picker": MoreInfoViewLightColorPicker;
}
}

View File

@@ -1,21 +0,0 @@
import { fireEvent } from "../../../../common/dom/fire_event";
export interface LightColorPickerViewParams {
entityId: string;
}
export const loadLightColorPickerView = () =>
import("./ha-more-info-view-light-color-picker");
export const showLightColorPickerView = (
element: HTMLElement,
title: string,
params: LightColorPickerViewParams
): void => {
fireEvent(element, "show-child-view", {
viewTag: "ha-more-info-view-light-color-picker",
viewImport: loadLightColorPickerView,
viewTitle: title,
viewParams: params,
});
};

View File

@@ -1,7 +1,5 @@
import { HassEntity } from "home-assistant-js-websocket";
import { isComponentLoaded } from "../../common/config/is_component_loaded";
import { computeDomain } from "../../common/entity/compute_domain";
import { computeGroupDomain, GroupEntity } from "../../data/group";
import { CONTINUOUS_DOMAINS } from "../../data/logbook";
import { HomeAssistant } from "../../types";
@@ -15,8 +13,7 @@ export const EDITABLE_DOMAINS_WITH_ID = ["scene", "automation"];
* Entity Domains that should always be editable; {@see shouldShowEditIcon}.
* */
export const EDITABLE_DOMAINS_WITH_UNIQUE_ID = ["script"];
/** Domains with with new more info design. */
export const DOMAINS_WITH_NEW_MORE_INFO = ["light", "siren", "switch"];
/** Domains with separate more info dialog. */
export const DOMAINS_WITH_MORE_INFO = [
"alarm_control_panel",
@@ -37,9 +34,7 @@ export const DOMAINS_WITH_MORE_INFO = [
"remote",
"script",
"scene",
"siren",
"sun",
"switch",
"timer",
"update",
"vacuum",
@@ -93,16 +88,3 @@ export const computeShowLogBookComponent = (
return true;
};
export const computeShowNewMoreInfo = (stateObj: HassEntity): boolean => {
const domain = computeDomain(stateObj.entity_id);
if (domain === "group") {
const groupDomain = computeGroupDomain(stateObj as GroupEntity);
return (
groupDomain != null &&
groupDomain !== "group" &&
DOMAINS_WITH_NEW_MORE_INFO.includes(groupDomain)
);
}
return DOMAINS_WITH_NEW_MORE_INFO.includes(domain);
};

View File

@@ -1,19 +1,18 @@
import "@material/mwc-button";
import type { HassEntity } from "home-assistant-js-websocket";
import { css, html, LitElement, PropertyValues, TemplateResult } from "lit";
import { customElement, property, state, query } from "lit/decorators";
import { css, html, LitElement, TemplateResult } from "lit";
import { customElement, property, query } from "lit/decorators";
import { classMap } from "lit/directives/class-map";
import "../../../components/ha-textfield";
import { supportsFeature } from "../../../common/entity/supports-feature";
import type { HaTextField } from "../../../components/ha-textfield";
import {
callAlarmAction,
FORMAT_NUMBER,
AlarmControlPanelEntityFeature,
} from "../../../data/alarm_control_panel";
import type { HomeAssistant } from "../../../types";
const BUTTONS = ["1", "2", "3", "4", "5", "6", "7", "8", "9", "", "0", "clear"];
const ARM_ACTIONS = ["arm_home", "arm_away"];
const DISARM_ACTIONS = ["disarm"];
@customElement("more-info-alarm_control_panel")
@@ -22,51 +21,8 @@ export class MoreInfoAlarmControlPanel extends LitElement {
@property({ attribute: false }) public stateObj?: HassEntity;
@state() private _armActions: string[] = [];
@query("#alarmCode") private _input?: HaTextField;
public willUpdate(changedProps: PropertyValues<this>) {
super.willUpdate(changedProps);
if (!this.stateObj || !changedProps.has("stateObj")) {
return;
}
this._armActions = [];
if (
supportsFeature(this.stateObj, AlarmControlPanelEntityFeature.ARM_HOME)
) {
this._armActions.push("arm_home");
}
if (
supportsFeature(this.stateObj, AlarmControlPanelEntityFeature.ARM_AWAY)
) {
this._armActions.push("arm_away");
}
if (
supportsFeature(this.stateObj, AlarmControlPanelEntityFeature.ARM_NIGHT)
) {
this._armActions.push("arm_night");
}
if (
supportsFeature(
this.stateObj,
AlarmControlPanelEntityFeature.ARM_CUSTOM_BYPASS
)
) {
this._armActions.push("arm_custom_bypass");
}
if (
supportsFeature(
this.stateObj,
AlarmControlPanelEntityFeature.ARM_VACATION
)
) {
this._armActions.push("arm_vacation");
}
}
protected render(): TemplateResult {
if (!this.hass || !this.stateObj) {
return html``;
@@ -116,7 +72,7 @@ export class MoreInfoAlarmControlPanel extends LitElement {
`}
<div class="actions">
${(this.stateObj.state === "disarmed"
? this._armActions
? ARM_ACTIONS
: DISARM_ACTIONS
).map(
(stateAction) => html`

View File

@@ -467,9 +467,7 @@ class MoreInfoClimate extends LitElement {
// We reset stateObj to re-sync the inputs with the state. It will be out
// of sync if our service call did not result in the entity to be turned
// on. Since the state is not changing, the resync is not called automatic.
await new Promise((resolve) => {
setTimeout(resolve, 2000);
});
await new Promise((resolve) => setTimeout(resolve, 2000));
// No need to resync if we received a new state.
if (this.stateObj !== curState) {

View File

@@ -9,10 +9,10 @@ import {
} from "lit";
import { property, state } from "lit/decorators";
import { dynamicElement } from "../../../common/dom/dynamic-element-directive";
import { computeGroupDomain, GroupEntity } from "../../../data/group";
import { computeStateDomain } from "../../../common/entity/compute_state_domain";
import { GroupEntity } from "../../../data/group";
import "../../../state-summary/state-card-content";
import { HomeAssistant } from "../../../types";
import { moreInfoControlStyle } from "../components/ha-more-info-control-style";
import {
domainMoreInfoType,
importMoreInfoControl,
@@ -47,19 +47,20 @@ class MoreInfoGroup extends LitElement {
}
const baseStateObj = states.find((s) => s.state === "on") || states[0];
const groupDomain = computeGroupDomain(this.stateObj);
const groupDomain = computeStateDomain(baseStateObj);
// Groups need to be filtered out or we'll show content of
// first child above the children of the current group
if (groupDomain && groupDomain !== "group") {
if (
groupDomain !== "group" &&
states.every(
(entityState) => groupDomain === computeStateDomain(entityState)
)
) {
this._groupDomainStateObj = {
...baseStateObj,
entity_id: this.stateObj.entity_id,
attributes: {
...baseStateObj.attributes,
friendly_name: this.stateObj.attributes.friendly_name,
},
attributes: { ...baseStateObj.attributes },
};
const type = domainMoreInfoType(groupDomain);
importMoreInfoControl(type);
@@ -95,15 +96,12 @@ class MoreInfoGroup extends LitElement {
}
static get styles(): CSSResultGroup {
return [
moreInfoControlStyle,
css`
state-card-content {
display: block;
margin-top: 8px;
}
`,
];
return css`
state-card-content {
display: block;
margin-top: 8px;
}
`;
}
}

View File

@@ -147,9 +147,7 @@ class MoreInfoHumidifier extends LitElement {
// We reset stateObj to re-sync the inputs with the state. It will be out
// of sync if our service call did not result in the entity to be turned
// on. Since the state is not changing, the resync is not called automatic.
await new Promise((resolve) => {
setTimeout(resolve, 2000);
});
await new Promise((resolve) => setTimeout(resolve, 2000));
// No need to resync if we received a new state.
if (this.stateObj !== curState) {

View File

@@ -1,12 +1,5 @@
import "@material/mwc-list/mwc-list-item";
import "@material/web/iconbutton/outlined-icon-button";
import {
mdiCreation,
mdiLightbulb,
mdiLightbulbOff,
mdiPalette,
mdiPower,
} from "@mdi/js";
import { mdiPalette } from "@mdi/js";
import {
css,
CSSResultGroup,
@@ -16,28 +9,26 @@ import {
TemplateResult,
} from "lit";
import { customElement, property, state } from "lit/decorators";
import memoizeOne from "memoize-one";
import { stopPropagation } from "../../../common/dom/stop_propagation";
import { supportsFeature } from "../../../common/entity/supports-feature";
import { blankBeforePercent } from "../../../common/translations/blank_before_percent";
import "../../../components/ha-attributes";
import "../../../components/ha-button-menu";
import "../../../components/ha-button-toggle-group";
import "../../../components/ha-color-picker";
import "../../../components/ha-icon-button";
import "../../../components/ha-labeled-slider";
import "../../../components/ha-select";
import { UNAVAILABLE } from "../../../data/entity";
import { forwardHaptic } from "../../../data/haptics";
import {
getLightCurrentModeRgbColor,
LightColorMode,
LightEntity,
LightEntityFeature,
lightSupportsBrightness,
lightIsInColorMode,
lightSupportsColor,
lightSupportsColorMode,
lightSupportsBrightness,
} from "../../../data/light";
import type { HomeAssistant } from "../../../types";
import { moreInfoControlStyle } from "../components/ha-more-info-control-style";
import "../components/ha-more-info-state-header";
import "../components/ha-more-info-toggle";
import "../components/lights/ha-more-info-light-brightness";
import { showLightColorPickerView } from "../components/lights/show-view-light-color-picker";
@customElement("more-info-light")
class MoreInfoLight extends LitElement {
@@ -45,218 +36,591 @@ class MoreInfoLight extends LitElement {
@property({ attribute: false }) public stateObj?: LightEntity;
@state() private _selectedBrightness?: number;
@state() private _brightnessSliderValue = 0;
private _brightnessChanged(ev) {
const value = (ev.detail as any).value;
if (isNaN(value)) return;
this._selectedBrightness = value;
}
@state() private _ctSliderValue?: number;
protected updated(changedProps: PropertyValues): void {
if (changedProps.has("stateObj")) {
this._selectedBrightness = this.stateObj?.attributes.brightness
? Math.round((this.stateObj.attributes.brightness * 100) / 255)
: undefined;
}
}
@state() private _cwSliderValue?: number;
protected render(): TemplateResult | null {
@state() private _wwSliderValue?: number;
@state() private _wvSliderValue?: number;
@state() private _colorBrightnessSliderValue?: number;
@state() private _brightnessAdjusted?: number;
@state() private _hueSegments = 24;
@state() private _saturationSegments = 8;
@state() private _colorPickerColor?: [number, number, number];
@state() private _mode?: "color" | LightColorMode;
protected render(): TemplateResult {
if (!this.hass || !this.stateObj) {
return null;
return html``;
}
const supportsColorTemp = lightSupportsColorMode(
const supportsTemp = lightSupportsColorMode(
this.stateObj,
LightColorMode.COLOR_TEMP
);
const supportsColor = lightSupportsColor(this.stateObj);
const supportsBrightness = lightSupportsBrightness(this.stateObj);
const supportsEffects = supportsFeature(
const supportsWhite = lightSupportsColorMode(
this.stateObj,
LightEntityFeature.EFFECT
LightColorMode.WHITE
);
const stateOverride = this._selectedBrightness
? `${Math.round(this._selectedBrightness)}${blankBeforePercent(
this.hass!.locale
)}%`
: undefined;
const supportsRgbww = lightSupportsColorMode(
this.stateObj,
LightColorMode.RGBWW
);
const supportsRgbw =
!supportsRgbww &&
lightSupportsColorMode(this.stateObj, LightColorMode.RGBW);
const supportsColor =
supportsRgbww || supportsRgbw || lightSupportsColor(this.stateObj);
return html`
<ha-more-info-state-header
.hass=${this.hass}
.stateObj=${this.stateObj}
.stateOverride=${stateOverride}
></ha-more-info-state-header>
<div class="controls">
${supportsBrightness
<div class="content">
${lightSupportsBrightness(this.stateObj)
? html`
<ha-more-info-light-brightness
.stateObj=${this.stateObj}
.hass=${this.hass}
@slider-moved=${this._brightnessChanged}
>
</ha-more-info-light-brightness>
<ha-labeled-slider
caption=${this.hass.localize("ui.card.light.brightness")}
icon="hass:brightness-5"
min="1"
max="100"
value=${this._brightnessSliderValue}
@change=${this._brightnessSliderChanged}
pin
></ha-labeled-slider>
`
: html`
<ha-more-info-toggle
.stateObj=${this.stateObj}
.hass=${this.hass}
.iconPathOn=${mdiLightbulb}
.iconPathOff=${mdiLightbulbOff}
></ha-more-info-toggle>
`}
${supportsColorTemp ||
supportsColor ||
supportsEffects ||
supportsBrightness
: ""}
${this.stateObj.state === "on"
? html`
<div class="buttons">
${supportsBrightness
? html`
<md-outlined-icon-button
.disabled=${this.stateObj.state === UNAVAILABLE}
.title=${this.hass.localize(
"ui.dialogs.more_info_control.light.toggle"
)}
.ariaLabel=${this.hass.localize(
"ui.dialogs.more_info_control.light.toggle"
)}
@click=${this._toggle}
${supportsTemp || supportsColor ? html`<hr />` : ""}
${supportsColor && (supportsTemp || supportsWhite)
? html`<ha-button-toggle-group
fullWidth
.buttons=${this._toggleButtons(supportsTemp, supportsWhite)}
.active=${this._mode}
@value-changed=${this._modeChanged}
></ha-button-toggle-group>`
: ""}
${supportsTemp &&
((!supportsColor && !supportsWhite) ||
this._mode === LightColorMode.COLOR_TEMP)
? html`
<ha-labeled-slider
class="color_temp"
caption=${this.hass.localize(
"ui.card.light.color_temperature"
)}
icon="hass:thermometer"
.min=${this.stateObj.attributes.min_color_temp_kelvin}
.max=${this.stateObj.attributes.max_color_temp_kelvin}
.value=${this._ctSliderValue}
@change=${this._ctSliderChanged}
pin
></ha-labeled-slider>
`
: ""}
${supportsColor &&
((!supportsTemp && !supportsWhite) || this._mode === "color")
? html`
<div class="segmentationContainer">
<ha-color-picker
class="color"
@colorselected=${this._colorPicked}
.desiredRgbColor=${this._colorPickerColor}
throttle="500"
.hueSegments=${this._hueSegments}
.saturationSegments=${this._saturationSegments}
>
<ha-svg-icon .path=${mdiPower}></ha-svg-icon>
</md-outlined-icon-button>
`
: null}
${supportsColorTemp || supportsColor
? html`
<md-outlined-icon-button
.disabled=${this.stateObj.state === UNAVAILABLE}
.title=${this.hass.localize(
"ui.dialogs.more_info_control.light.change_color"
)}
.ariaLabel=${this.hass.localize(
"ui.dialogs.more_info_control.light.change_color"
)}
@click=${this._showLightColorPickerView}
>
<ha-svg-icon .path=${mdiPalette}></ha-svg-icon>
</md-outlined-icon-button>
`
: null}
${supportsEffects
? html`
<ha-button-menu
corner="BOTTOM_START"
@action=${this._handleEffectButton}
@closed=${stopPropagation}
fixed
.disabled=${this.stateObj.state === UNAVAILABLE}
>
<md-outlined-icon-button
.disabled=${this.stateObj.state === UNAVAILABLE}
slot="trigger"
.title=${this.hass.localize(
"ui.dialogs.more_info_control.light.select_effect"
)}
.ariaLabel=${this.hass.localize(
"ui.dialogs.more_info_control.light.select_effect"
)}
>
<ha-svg-icon .path=${mdiCreation}></ha-svg-icon>
</md-outlined-icon-button>
${this.stateObj.attributes.effect_list!.map(
(effect: string) => html`
<mwc-list-item
.value=${effect}
.activated=${this.stateObj!.attributes.effect ===
effect}
>
${effect}
</mwc-list-item>
`
)}
</ha-button-menu>
`
: null}
</div>
`
: null}
</div>
</ha-color-picker>
<ha-icon-button
.path=${mdiPalette}
@click=${this._segmentClick}
class="segmentationButton"
></ha-icon-button>
</div>
<ha-attributes
.hass=${this.hass}
.stateObj=${this.stateObj}
extra-filters="brightness,color_temp,color_temp_kelvin,white_value,effect_list,effect,hs_color,rgb_color,rgbw_color,rgbww_color,xy_color,min_mireds,max_mireds,min_color_temp_kelvin,max_color_temp_kelvin,entity_id,supported_color_modes,color_mode"
></ha-attributes>
${supportsRgbw || supportsRgbww
? html`<ha-labeled-slider
.caption=${this.hass.localize(
"ui.card.light.color_brightness"
)}
icon="hass:brightness-7"
max="100"
.value=${this._colorBrightnessSliderValue}
@change=${this._colorBrightnessSliderChanged}
pin
></ha-labeled-slider>`
: ""}
${supportsRgbw
? html`
<ha-labeled-slider
.caption=${this.hass.localize(
"ui.card.light.white_value"
)}
icon="hass:file-word-box"
max="100"
.name=${"wv"}
.value=${this._wvSliderValue}
@change=${this._wvSliderChanged}
pin
></ha-labeled-slider>
`
: ""}
${supportsRgbww
? html`
<ha-labeled-slider
.caption=${this.hass.localize(
"ui.card.light.cold_white_value"
)}
icon="hass:file-word-box-outline"
max="100"
.name=${"cw"}
.value=${this._cwSliderValue}
@change=${this._wvSliderChanged}
pin
></ha-labeled-slider>
<ha-labeled-slider
.caption=${this.hass.localize(
"ui.card.light.warm_white_value"
)}
icon="hass:file-word-box"
max="100"
.name=${"ww"}
.value=${this._wwSliderValue}
@change=${this._wvSliderChanged}
pin
></ha-labeled-slider>
`
: ""}
`
: ""}
${supportsFeature(this.stateObj, LightEntityFeature.EFFECT) &&
this.stateObj!.attributes.effect_list?.length
? html`
<hr />
<ha-select
.label=${this.hass.localize("ui.card.light.effect")}
.value=${this.stateObj.attributes.effect || ""}
fixedMenuPosition
naturalMenuWidth
@selected=${this._effectChanged}
@closed=${stopPropagation}
>
${this.stateObj.attributes.effect_list.map(
(effect: string) => html`
<mwc-list-item .value=${effect}>
${effect}
</mwc-list-item>
`
)}
</ha-select>
`
: ""}
`
: ""}
<ha-attributes
.hass=${this.hass}
.stateObj=${this.stateObj}
extra-filters="brightness,color_temp,color_temp_kelvin,white_value,effect_list,effect,hs_color,rgb_color,rgbw_color,rgbww_color,xy_color,min_mireds,max_mireds,min_color_temp_kelvin,max_color_temp_kelvin,entity_id,supported_color_modes,color_mode"
></ha-attributes>
</div>
`;
}
private _toggle = () => {
const service = this.stateObj?.state === "on" ? "turn_off" : "turn_on";
forwardHaptic("light");
this.hass.callService("light", service, {
entity_id: this.stateObj!.entity_id,
});
};
public willUpdate(changedProps: PropertyValues<this>) {
super.willUpdate(changedProps);
private _showLightColorPickerView = () => {
showLightColorPickerView(
this,
this.hass.localize(
"ui.dialogs.more_info_control.light.color_picker.title"
),
{
entityId: this.stateObj!.entity_id,
if (!changedProps.has("stateObj")) {
return;
}
const stateObj = this.stateObj! as LightEntity;
const oldStateObj = changedProps.get("stateObj") as LightEntity | undefined;
if (stateObj.state === "on") {
// Don't change tab when the color mode changes
if (
oldStateObj?.entity_id !== stateObj.entity_id ||
oldStateObj?.state !== stateObj.state
) {
this._mode = lightIsInColorMode(this.stateObj!)
? "color"
: this.stateObj!.attributes.color_mode;
}
);
};
private _handleEffectButton(ev) {
ev.stopPropagation();
ev.preventDefault();
let brightnessAdjust = 100;
this._brightnessAdjusted = undefined;
if (
stateObj.attributes.color_mode === LightColorMode.RGB &&
!lightSupportsColorMode(stateObj, LightColorMode.RGBWW) &&
!lightSupportsColorMode(stateObj, LightColorMode.RGBW)
) {
const maxVal = Math.max(...stateObj.attributes.rgb_color!);
if (maxVal < 255) {
this._brightnessAdjusted = maxVal;
brightnessAdjust = (this._brightnessAdjusted / 255) * 100;
}
}
this._brightnessSliderValue = Math.round(
((stateObj.attributes.brightness || 0) * brightnessAdjust) / 255
);
this._ctSliderValue = stateObj.attributes.color_temp_kelvin;
this._wvSliderValue =
stateObj.attributes.color_mode === LightColorMode.RGBW
? Math.round((stateObj.attributes.rgbw_color![3] * 100) / 255)
: undefined;
this._cwSliderValue =
stateObj.attributes.color_mode === LightColorMode.RGBWW
? Math.round((stateObj.attributes.rgbww_color![3] * 100) / 255)
: undefined;
this._wwSliderValue =
stateObj.attributes.color_mode === LightColorMode.RGBWW
? Math.round((stateObj.attributes.rgbww_color![4] * 100) / 255)
: undefined;
const index = ev.detail.index;
const effect = this.stateObj!.attributes.effect_list![index];
const currentRgbColor = getLightCurrentModeRgbColor(stateObj);
if (!effect || this.stateObj!.attributes.effect === effect) {
this._colorBrightnessSliderValue = currentRgbColor
? Math.round((Math.max(...currentRgbColor.slice(0, 3)) * 100) / 255)
: undefined;
this._colorPickerColor = currentRgbColor?.slice(0, 3) as [
number,
number,
number
];
} else {
this._brightnessSliderValue = 0;
}
}
private _toggleButtons = memoizeOne(
(supportsTemp: boolean, supportsWhite: boolean) => {
const modes = [{ label: "Color", value: "color" }];
if (supportsTemp) {
modes.push({ label: "Temperature", value: LightColorMode.COLOR_TEMP });
}
if (supportsWhite) {
modes.push({ label: "White", value: LightColorMode.WHITE });
}
return modes;
}
);
private _modeChanged(ev: CustomEvent) {
this._mode = ev.detail.value;
}
private _effectChanged(ev) {
const newVal = ev.target.value;
if (!newVal || this.stateObj!.attributes.effect === newVal) {
return;
}
this.hass.callService("light", "turn_on", {
entity_id: this.stateObj!.entity_id,
effect,
effect: newVal,
});
}
static get styles(): CSSResultGroup {
return [
moreInfoControlStyle,
css`
.buttons {
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 12px;
}
.buttons > * {
margin: 4px;
}
private _brightnessSliderChanged(ev: CustomEvent) {
const bri = Number((ev.target as any).value);
md-outlined-icon-button-toggle,
md-outlined-icon-button {
--ha-icon-display: block;
--md-sys-color-on-surface: var(--secondary-text-color);
--md-sys-color-on-surface-variant: var(--secondary-text-color);
--md-sys-color-on-surface-rgb: var(--rgb-secondary-text-color);
--md-sys-color-outline: var(--secondary-text-color);
}
`,
if (isNaN(bri)) {
return;
}
this._brightnessSliderValue = bri;
if (this._mode === LightColorMode.WHITE) {
this.hass.callService("light", "turn_on", {
entity_id: this.stateObj!.entity_id,
white: Math.min(255, Math.round((bri * 255) / 100)),
});
return;
}
if (this._brightnessAdjusted) {
const rgb =
this.stateObj!.attributes.rgb_color ||
([0, 0, 0] as [number, number, number]);
this.hass.callService("light", "turn_on", {
entity_id: this.stateObj!.entity_id,
brightness_pct: bri,
rgb_color: this._adjustColorBrightness(
rgb,
this._brightnessAdjusted,
true
),
});
return;
}
this.hass.callService("light", "turn_on", {
entity_id: this.stateObj!.entity_id,
brightness_pct: bri,
});
}
private _ctSliderChanged(ev: CustomEvent) {
const ct = Number((ev.target as any).value);
if (isNaN(ct)) {
return;
}
this._ctSliderValue = ct;
this.hass.callService("light", "turn_on", {
entity_id: this.stateObj!.entity_id,
color_temp_kelvin: ct,
});
}
private _wvSliderChanged(ev: CustomEvent) {
const target = ev.target as any;
let wv = Number(target.value);
const name = target.name;
if (isNaN(wv)) {
return;
}
if (name === "wv") {
this._wvSliderValue = wv;
} else if (name === "cw") {
this._cwSliderValue = wv;
} else if (name === "ww") {
this._wwSliderValue = wv;
}
wv = Math.min(255, Math.round((wv * 255) / 100));
const rgb = getLightCurrentModeRgbColor(this.stateObj!);
if (name === "wv") {
const rgbw_color = rgb || [0, 0, 0, 0];
rgbw_color[3] = wv;
this.hass.callService("light", "turn_on", {
entity_id: this.stateObj!.entity_id,
rgbw_color,
});
return;
}
const rgbww_color = rgb || [0, 0, 0, 0, 0];
while (rgbww_color.length < 5) {
rgbww_color.push(0);
}
rgbww_color[name === "cw" ? 3 : 4] = wv;
this.hass.callService("light", "turn_on", {
entity_id: this.stateObj!.entity_id,
rgbww_color,
});
}
private _colorBrightnessSliderChanged(ev: CustomEvent) {
const target = ev.target as any;
let value = Number(target.value);
if (isNaN(value)) {
return;
}
const oldValue = this._colorBrightnessSliderValue;
this._colorBrightnessSliderValue = value;
value = (value * 255) / 100;
const rgb = (getLightCurrentModeRgbColor(this.stateObj!)?.slice(0, 3) || [
255, 255, 255,
]) as [number, number, number];
this._setRgbWColor(
this._adjustColorBrightness(
// first normalize the value
oldValue
? this._adjustColorBrightness(rgb, (oldValue * 255) / 100, true)
: rgb,
value
)
);
}
private _segmentClick() {
if (this._hueSegments === 24 && this._saturationSegments === 8) {
this._hueSegments = 0;
this._saturationSegments = 0;
} else {
this._hueSegments = 24;
this._saturationSegments = 8;
}
}
private _adjustColorBrightness(
rgbColor: [number, number, number],
value?: number,
invert = false
) {
if (value !== undefined && value !== 255) {
let ratio = value / 255;
if (invert) {
ratio = 1 / ratio;
}
rgbColor[0] = Math.min(255, Math.round(rgbColor[0] * ratio));
rgbColor[1] = Math.min(255, Math.round(rgbColor[1] * ratio));
rgbColor[2] = Math.min(255, Math.round(rgbColor[2] * ratio));
}
return rgbColor;
}
private _setRgbWColor(rgbColor: [number, number, number]) {
if (lightSupportsColorMode(this.stateObj!, LightColorMode.RGBWW)) {
const rgbww_color: [number, number, number, number, number] = this
.stateObj!.attributes.rgbww_color
? [...this.stateObj!.attributes.rgbww_color]
: [0, 0, 0, 0, 0];
this.hass.callService("light", "turn_on", {
entity_id: this.stateObj!.entity_id,
rgbww_color: rgbColor.concat(rgbww_color.slice(3)),
});
} else if (lightSupportsColorMode(this.stateObj!, LightColorMode.RGBW)) {
const rgbw_color: [number, number, number, number] = this.stateObj!
.attributes.rgbw_color
? [...this.stateObj!.attributes.rgbw_color]
: [0, 0, 0, 0];
this.hass.callService("light", "turn_on", {
entity_id: this.stateObj!.entity_id,
rgbw_color: rgbColor.concat(rgbw_color.slice(3)),
});
}
}
/**
* Called when a new color has been picked.
* should be throttled with the 'throttle=' attribute of the color picker
*/
private _colorPicked(
ev: CustomEvent<{
hs: { h: number; s: number };
rgb: { r: number; g: number; b: number };
}>
) {
this._colorPickerColor = [
ev.detail.rgb.r,
ev.detail.rgb.g,
ev.detail.rgb.b,
];
if (
lightSupportsColorMode(this.stateObj!, LightColorMode.RGBWW) ||
lightSupportsColorMode(this.stateObj!, LightColorMode.RGBW)
) {
this._setRgbWColor(
this._colorBrightnessSliderValue
? this._adjustColorBrightness(
[ev.detail.rgb.r, ev.detail.rgb.g, ev.detail.rgb.b],
(this._colorBrightnessSliderValue * 255) / 100
)
: [ev.detail.rgb.r, ev.detail.rgb.g, ev.detail.rgb.b]
);
} else if (lightSupportsColorMode(this.stateObj!, LightColorMode.RGB)) {
const rgb_color: [number, number, number] = [
ev.detail.rgb.r,
ev.detail.rgb.g,
ev.detail.rgb.b,
];
if (this._brightnessAdjusted) {
this.hass.callService("light", "turn_on", {
entity_id: this.stateObj!.entity_id,
brightness_pct: this._brightnessSliderValue,
rgb_color: this._adjustColorBrightness(
rgb_color,
this._brightnessAdjusted,
true
),
});
} else {
this.hass.callService("light", "turn_on", {
entity_id: this.stateObj!.entity_id,
rgb_color,
});
}
} else {
this.hass.callService("light", "turn_on", {
entity_id: this.stateObj!.entity_id,
hs_color: [ev.detail.hs.h, ev.detail.hs.s * 100],
});
}
}
static get styles(): CSSResultGroup {
return css`
.content {
display: flex;
flex-direction: column;
align-items: center;
}
.content > * {
width: 100%;
}
.color_temp {
--ha-slider-background: -webkit-linear-gradient(
var(--float-end),
rgb(166, 209, 255) 0%,
white 50%,
rgb(255, 160, 0) 100%
);
/* The color temp minimum value shouldn't be rendered differently. It's not "off". */
--paper-slider-knob-start-border-color: var(--primary-color);
margin-bottom: 4px;
}
.segmentationContainer {
position: relative;
max-height: 500px;
display: flex;
justify-content: center;
}
ha-button-toggle-group {
margin-bottom: 8px;
}
ha-color-picker {
--ha-color-picker-wheel-borderwidth: 5;
--ha-color-picker-wheel-bordercolor: white;
--ha-color-picker-wheel-shadow: none;
--ha-color-picker-marker-borderwidth: 2;
--ha-color-picker-marker-bordercolor: white;
}
.segmentationButton {
position: absolute;
top: 5%;
left: 0;
color: var(--secondary-text-color);
}
hr {
border-color: var(--divider-color);
border-bottom: none;
margin: 16px 0;
}
`;
}
}

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