mirror of
https://github.com/home-assistant/frontend.git
synced 2026-01-07 15:57:28 +00:00
Compare commits
135 Commits
dependabot
...
compress_w
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7128872bcc | ||
|
|
60f3fc85ae | ||
|
|
3f9df79849 | ||
|
|
3ce4e67083 | ||
|
|
77e05af465 | ||
|
|
97b6fc03b3 | ||
|
|
cf142cee83 | ||
|
|
6347889fcb | ||
|
|
7f216699ac | ||
|
|
2639cae21e | ||
|
|
e6f33fb62f | ||
|
|
38cab70e8b | ||
|
|
09d10230a7 | ||
|
|
8174fbbc3e | ||
|
|
84b4daf4bf | ||
|
|
2d699c8f3f | ||
|
|
50e6a18eee | ||
|
|
58ecabd351 | ||
|
|
23e9f265ee | ||
|
|
4c9fc516ce | ||
|
|
d369ad0a37 | ||
|
|
f323c93a96 | ||
|
|
3ae49bb336 | ||
|
|
1ae950d17a | ||
|
|
5eafecf95d | ||
|
|
7f644530e4 | ||
|
|
3aea416175 | ||
|
|
868c414759 | ||
|
|
f4848964c3 | ||
|
|
b10c95e803 | ||
|
|
25cf879793 | ||
|
|
ba3b265b9a | ||
|
|
7568ae5964 | ||
|
|
d4ace99de3 | ||
|
|
087bda22d4 | ||
|
|
83ffc754c9 | ||
|
|
7a8ea4a9f9 | ||
|
|
fa7f5eccdb | ||
|
|
f75d17e10c | ||
|
|
650f32ba6d | ||
|
|
01ec9aaf96 | ||
|
|
682f383c9e | ||
|
|
7724fa02d7 | ||
|
|
0a49bf2d4c | ||
|
|
12478a2177 | ||
|
|
96a261d831 | ||
|
|
1095088d42 | ||
|
|
43541f9754 | ||
|
|
f69ae84cc6 | ||
|
|
7173b30716 | ||
|
|
f92deb3225 | ||
|
|
2e86d739fc | ||
|
|
7e0c80ae24 | ||
|
|
3b5fe7fd9f | ||
|
|
03e3f161f7 | ||
|
|
ab231eec4f | ||
|
|
7041d322d6 | ||
|
|
f786539f15 | ||
|
|
19d721f193 | ||
|
|
5629346fc3 | ||
|
|
ca6ade4858 | ||
|
|
6b9e0405d0 | ||
|
|
e46803cb4e | ||
|
|
e697a09e53 | ||
|
|
3158e6043d | ||
|
|
2c61c960bc | ||
|
|
91493e109e | ||
|
|
2bd6d9d202 | ||
|
|
dfb74fd576 | ||
|
|
fef74b0b8a | ||
|
|
0fc36823da | ||
|
|
25d8550dd7 | ||
|
|
e44ccb0574 | ||
|
|
d00467a39c | ||
|
|
4e841c4a06 | ||
|
|
cf304d91c7 | ||
|
|
3066a9d10d | ||
|
|
3e72b1cb5d | ||
|
|
a03c335331 | ||
|
|
6e153cb307 | ||
|
|
8ed4914232 | ||
|
|
88205a94d6 | ||
|
|
0b54e60b91 | ||
|
|
a469a92601 | ||
|
|
77f7a5647d | ||
|
|
850609b1d0 | ||
|
|
c95a600fbb | ||
|
|
1cffe4f9bf | ||
|
|
2cae0cd54f | ||
|
|
d56f35e6fd | ||
|
|
be8cb8fb3f | ||
|
|
74bc6eeda8 | ||
|
|
fbf084bf00 | ||
|
|
76f90e1449 | ||
|
|
404199bb19 | ||
|
|
dfbe32018a | ||
|
|
48dcd98634 | ||
|
|
658ce80801 | ||
|
|
7adb49c772 | ||
|
|
05f2fb896c | ||
|
|
e21816be52 | ||
|
|
bef586efb3 | ||
|
|
5f9f51f92d | ||
|
|
cabbbcf9f3 | ||
|
|
d5fb924cb4 | ||
|
|
8754947133 | ||
|
|
9c42eb5130 | ||
|
|
1ab1cf0fab | ||
|
|
e91a477b8b | ||
|
|
c4160e8368 | ||
|
|
cf377558ae | ||
|
|
6986c1c8b7 | ||
|
|
6c9d6755f1 | ||
|
|
747b9e2ae1 | ||
|
|
8f22b3c1fa | ||
|
|
054e4165f8 | ||
|
|
82b2c6aa06 | ||
|
|
07d84a32fd | ||
|
|
d37e53b4a5 | ||
|
|
252e58d63b | ||
|
|
d98a26146b | ||
|
|
8631139133 | ||
|
|
d5578c236f | ||
|
|
de5199e34c | ||
|
|
980758528a | ||
|
|
0fdb012e24 | ||
|
|
3b1bc37f53 | ||
|
|
cb85bc054a | ||
|
|
94dd3c95ff | ||
|
|
6e55077cd3 | ||
|
|
ff47d7d408 | ||
|
|
a9180e062c | ||
|
|
9d918b727c | ||
|
|
7c65bb9791 | ||
|
|
1e2c21d49e |
54
.github/workflows/dedupe.yaml
vendored
54
.github/workflows/dedupe.yaml
vendored
@@ -1,54 +0,0 @@
|
||||
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}"
|
||||
@@ -67,7 +67,7 @@ module.exports.babelOptions = ({ latestBuild }) => ({
|
||||
"@babel/preset-env",
|
||||
{
|
||||
useBuiltIns: "entry",
|
||||
corejs: { version: "3.27", proposals: true },
|
||||
corejs: { version: "3.28", proposals: true },
|
||||
bugfixes: true,
|
||||
},
|
||||
],
|
||||
|
||||
@@ -2,6 +2,7 @@ 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");
|
||||
@@ -75,6 +76,7 @@ const createWebpackConfig = ({
|
||||
chunkIds: isProdBuild && !isStatsBuild ? "deterministic" : "named",
|
||||
},
|
||||
plugins: [
|
||||
new CompressionWebpackPlugin(),
|
||||
!isStatsBuild && new WebpackBar({ fancy: !isProdBuild }),
|
||||
new WebpackManifestPlugin({
|
||||
// Only include the JS of entrypoints
|
||||
|
||||
@@ -181,7 +181,7 @@ class HcCast extends LitElement {
|
||||
private async _handlePickView(ev: Event) {
|
||||
const path = (ev.currentTarget as any).getAttribute("data-path");
|
||||
await ensureConnectedCastSession(this.castManager!, this.auth!);
|
||||
castSendShowLovelaceView(this.castManager, path, this.auth.data.hassUrl);
|
||||
castSendShowLovelaceView(this.castManager, this.auth.data.hassUrl, path);
|
||||
}
|
||||
|
||||
private async _handleLogout() {
|
||||
|
||||
@@ -252,6 +252,22 @@ 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;
|
||||
|
||||
@@ -6,6 +6,9 @@ set -e
|
||||
|
||||
cd "$(dirname "$0")/.."
|
||||
|
||||
STATS=1 NODE_ENV=production ../node_modules/.bin/webpack --profile --json > compilation-stats.json
|
||||
npx webpack-bundle-analyzer compilation-stats.json dist/frontend_latest
|
||||
rm compilation-stats.json
|
||||
export STATS=1
|
||||
statsfile="compilation-stats-demo.json"
|
||||
|
||||
./node_modules/.bin/webpack-cli --profile --node-env=production --json=$statsfile
|
||||
npx webpack-bundle-analyzer $statsfile dist/frontend_latest
|
||||
rm -f $statsfile
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import "@material/mwc-button";
|
||||
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
|
||||
import { property, state } from "lit/decorators";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { until } from "lit/directives/until";
|
||||
import "../../../src/components/ha-card";
|
||||
import "../../../src/components/ha-circular-progress";
|
||||
@@ -14,6 +14,7 @@ import {
|
||||
setDemoConfig,
|
||||
} from "../configs/demo-configs";
|
||||
|
||||
@customElement("ha-demo-card")
|
||||
export class HADemoCard extends LitElement implements LovelaceCard {
|
||||
@property({ attribute: false }) public lovelace?: Lovelace;
|
||||
|
||||
@@ -154,5 +155,3 @@ declare global {
|
||||
"ha-demo-card": HADemoCard;
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define("ha-demo-card", HADemoCard);
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
// Compat needs to be first import
|
||||
import "../../src/resources/compatibility";
|
||||
import { customElement } from "lit/decorators";
|
||||
import { isNavigationClick } from "../../src/common/dom/is-navigation-click";
|
||||
import { navigate } from "../../src/common/navigate";
|
||||
import {
|
||||
@@ -26,7 +27,8 @@ import { mockSystemLog } from "./stubs/system_log";
|
||||
import { mockTemplate } from "./stubs/template";
|
||||
import { mockTranslations } from "./stubs/translations";
|
||||
|
||||
class HaDemo extends HomeAssistantAppEl {
|
||||
@customElement("ha-demo")
|
||||
export class HaDemo extends HomeAssistantAppEl {
|
||||
protected async _initializeHass() {
|
||||
const initial: Partial<MockHomeAssistant> = {
|
||||
panelUrl: (this as any)._panelUrl,
|
||||
@@ -123,8 +125,6 @@ class HaDemo extends HomeAssistantAppEl {
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define("ha-demo", HaDemo);
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-demo": HaDemo;
|
||||
|
||||
@@ -66,7 +66,7 @@ const incrementalUnits = ["clients", "queries", "ads"];
|
||||
|
||||
export const mockHistory = (mockHass: MockHomeAssistant) => {
|
||||
mockHass.mockAPI(
|
||||
new RegExp("history/period/.+"),
|
||||
/history\/period\/.+/,
|
||||
(hass, _method, path, _parameters) => {
|
||||
const params = parseQuery<HistoryQueryParams>(path.split("?")[1]);
|
||||
const entities = params.filter_entity_id.split(",");
|
||||
|
||||
@@ -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>
|
||||
`
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
---
|
||||
title: Bar Slider
|
||||
---
|
||||
@@ -1,3 +0,0 @@
|
||||
---
|
||||
title: Bar Switch
|
||||
---
|
||||
3
gallery/src/pages/components/ha-control-button.markdown
Normal file
3
gallery/src/pages/components/ha-control-button.markdown
Normal file
@@ -0,0 +1,3 @@
|
||||
---
|
||||
title: Control Button
|
||||
---
|
||||
192
gallery/src/pages/components/ha-control-button.ts
Normal file
192
gallery/src/pages/components/ha-control-button.ts
Normal file
@@ -0,0 +1,192 @@
|
||||
import {
|
||||
mdiFanSpeed1,
|
||||
mdiFanSpeed2,
|
||||
mdiFanSpeed3,
|
||||
mdiLightbulb,
|
||||
} from "@mdi/js";
|
||||
import { css, html, LitElement, TemplateResult } from "lit";
|
||||
import { customElement } from "lit/decorators";
|
||||
import { ifDefined } from "lit/directives/if-defined";
|
||||
import { repeat } from "lit/directives/repeat";
|
||||
import "../../../../src/components/ha-control-button";
|
||||
import "../../../../src/components/ha-card";
|
||||
import "../../../../src/components/ha-svg-icon";
|
||||
import "../../../../src/components/ha-control-button-group";
|
||||
|
||||
type Button = {
|
||||
label: string;
|
||||
icon?: string;
|
||||
class?: string;
|
||||
disabled?: boolean;
|
||||
};
|
||||
|
||||
const buttons: Button[] = [
|
||||
{
|
||||
label: "Button",
|
||||
},
|
||||
{
|
||||
label: "Button and custom style",
|
||||
class: "custom",
|
||||
},
|
||||
{
|
||||
label: "Disabled Button",
|
||||
disabled: true,
|
||||
},
|
||||
];
|
||||
|
||||
type ButtonGroup = {
|
||||
vertical?: boolean;
|
||||
class?: string;
|
||||
};
|
||||
|
||||
const buttonGroups: ButtonGroup[] = [
|
||||
{},
|
||||
{
|
||||
class: "custom-group",
|
||||
},
|
||||
];
|
||||
|
||||
@customElement("demo-components-ha-control-button")
|
||||
export class DemoHaBarButton extends LitElement {
|
||||
protected render(): TemplateResult {
|
||||
return html`
|
||||
<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;
|
||||
}
|
||||
}
|
||||
3
gallery/src/pages/components/ha-control-slider.markdown
Normal file
3
gallery/src/pages/components/ha-control-slider.markdown
Normal file
@@ -0,0 +1,3 @@
|
||||
---
|
||||
title: Control Slider
|
||||
---
|
||||
@@ -2,7 +2,7 @@ import { css, html, LitElement, TemplateResult } from "lit";
|
||||
import { customElement, state } from "lit/decorators";
|
||||
import { ifDefined } from "lit/directives/if-defined";
|
||||
import { repeat } from "lit/directives/repeat";
|
||||
import "../../../../src/components/ha-bar-slider";
|
||||
import "../../../../src/components/ha-control-slider";
|
||||
import "../../../../src/components/ha-card";
|
||||
|
||||
const sliders: {
|
||||
@@ -46,7 +46,7 @@ const sliders: {
|
||||
},
|
||||
];
|
||||
|
||||
@customElement("demo-components-ha-bar-slider")
|
||||
@customElement("demo-components-ha-control-slider")
|
||||
export class DemoHaBarSlider extends LitElement {
|
||||
@state() private value = 50;
|
||||
|
||||
@@ -86,7 +86,7 @@ export class DemoHaBarSlider extends LitElement {
|
||||
<div class="card-content">
|
||||
<label id=${id}>${label}</label>
|
||||
<pre>Config: ${JSON.stringify(config)}</pre>
|
||||
<ha-bar-slider
|
||||
<ha-control-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-bar-slider>
|
||||
</ha-control-slider>
|
||||
</div>
|
||||
</ha-card>
|
||||
`;
|
||||
@@ -106,7 +106,7 @@ export class DemoHaBarSlider extends LitElement {
|
||||
${repeat(sliders, (slider) => {
|
||||
const { id, label, ...config } = slider;
|
||||
return html`
|
||||
<ha-bar-slider
|
||||
<ha-control-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-bar-slider>
|
||||
</ha-control-slider>
|
||||
`;
|
||||
})}
|
||||
</div>
|
||||
@@ -141,11 +141,11 @@ export class DemoHaBarSlider extends LitElement {
|
||||
font-weight: 600;
|
||||
}
|
||||
.custom {
|
||||
--slider-bar-color: #ffcf4c;
|
||||
--slider-bar-background: #ffcf4c;
|
||||
--slider-bar-background-opacity: 0.2;
|
||||
--slider-bar-thickness: 100px;
|
||||
--slider-bar-border-radius: 24px;
|
||||
--control-slider-color: #ffcf4c;
|
||||
--control-slider-background: #ffcf4c;
|
||||
--control-slider-background-opacity: 0.2;
|
||||
--control-slider-thickness: 100px;
|
||||
--control-slider-border-radius: 24px;
|
||||
}
|
||||
.vertical-sliders {
|
||||
height: 300px;
|
||||
@@ -165,6 +165,6 @@ export class DemoHaBarSlider extends LitElement {
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"demo-components-ha-bar-slider": DemoHaBarSlider;
|
||||
"demo-components-ha-control-slider": DemoHaBarSlider;
|
||||
}
|
||||
}
|
||||
3
gallery/src/pages/components/ha-control-switch.markdown
Normal file
3
gallery/src/pages/components/ha-control-switch.markdown
Normal file
@@ -0,0 +1,3 @@
|
||||
---
|
||||
title: Control Switch
|
||||
---
|
||||
@@ -8,7 +8,7 @@ import { css, html, LitElement, TemplateResult } from "lit";
|
||||
import { customElement, state } from "lit/decorators";
|
||||
import { ifDefined } from "lit/directives/if-defined";
|
||||
import { repeat } from "lit/directives/repeat";
|
||||
import "../../../../src/components/ha-bar-switch";
|
||||
import "../../../../src/components/ha-control-switch";
|
||||
import "../../../../src/components/ha-card";
|
||||
|
||||
const switches: {
|
||||
@@ -39,8 +39,8 @@ const switches: {
|
||||
},
|
||||
];
|
||||
|
||||
@customElement("demo-components-ha-bar-switch")
|
||||
export class DemoHaBarSwitch extends LitElement {
|
||||
@customElement("demo-components-ha-control-switch")
|
||||
export class DemoHaControlSwitch extends LitElement {
|
||||
@state() private checked = false;
|
||||
|
||||
handleValueChanged(e: any) {
|
||||
@@ -56,7 +56,7 @@ export class DemoHaBarSwitch extends LitElement {
|
||||
<div class="card-content">
|
||||
<label id=${id}>${label}</label>
|
||||
<pre>Config: ${JSON.stringify(config)}</pre>
|
||||
<ha-bar-switch
|
||||
<ha-control-switch
|
||||
.checked=${this.checked}
|
||||
class=${ifDefined(config.class)}
|
||||
@change=${this.handleValueChanged}
|
||||
@@ -66,7 +66,7 @@ export class DemoHaBarSwitch extends LitElement {
|
||||
disabled=${ifDefined(config.disabled)}
|
||||
reversed=${ifDefined(config.reversed)}
|
||||
>
|
||||
</ha-bar-switch>
|
||||
</ha-control-switch>
|
||||
</div>
|
||||
</ha-card>
|
||||
`;
|
||||
@@ -78,7 +78,7 @@ export class DemoHaBarSwitch extends LitElement {
|
||||
${repeat(switches, (sw) => {
|
||||
const { id, label, ...config } = sw;
|
||||
return html`
|
||||
<ha-bar-switch
|
||||
<ha-control-switch
|
||||
.checked=${this.checked}
|
||||
vertical
|
||||
class=${ifDefined(config.class)}
|
||||
@@ -89,7 +89,7 @@ export class DemoHaBarSwitch extends LitElement {
|
||||
disabled=${ifDefined(config.disabled)}
|
||||
reversed=${ifDefined(config.reversed)}
|
||||
>
|
||||
</ha-bar-switch>
|
||||
</ha-control-switch>
|
||||
`;
|
||||
})}
|
||||
</div>
|
||||
@@ -115,11 +115,11 @@ export class DemoHaBarSwitch extends LitElement {
|
||||
font-weight: 600;
|
||||
}
|
||||
.custom {
|
||||
--switch-bar-on-color: var(--green-color);
|
||||
--switch-bar-off-color: var(--red-color);
|
||||
--switch-bar-thickness: 100px;
|
||||
--switch-bar-border-radius: 24px;
|
||||
--switch-bar-padding: 6px;
|
||||
--control-switch-on-color: var(--green-color);
|
||||
--control-switch-off-color: var(--red-color);
|
||||
--control-switch-thickness: 100px;
|
||||
--control-switch-border-radius: 24px;
|
||||
--control-switch-padding: 6px;
|
||||
--mdc-icon-size: 24px;
|
||||
}
|
||||
.vertical-switches {
|
||||
@@ -140,6 +140,6 @@ export class DemoHaBarSwitch extends LitElement {
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"demo-components-ha-bar-switch": DemoHaBarSwitch;
|
||||
"demo-components-ha-control-switch": DemoHaControlSwitch;
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,7 @@ import { customElement } from "lit/decorators";
|
||||
import "../../../../src/components/ha-tip";
|
||||
import "../../../../src/components/ha-card";
|
||||
import { applyThemesOnElement } from "../../../../src/common/dom/apply_themes_on_element";
|
||||
import { provideHass } from "../../../../src/fake_data/provide_hass";
|
||||
|
||||
const tips: (string | TemplateResult)[] = [
|
||||
"Test tip",
|
||||
@@ -18,7 +19,11 @@ export class DemoHaTip extends LitElement {
|
||||
<div class=${mode}>
|
||||
<ha-card header="ha-tip ${mode} demo">
|
||||
<div class="card-content">
|
||||
${tips.map((tip) => html`<ha-tip>${tip}</ha-tip>`)}
|
||||
${tips.map(
|
||||
(tip) => html`<ha-tip .hass=${provideHass(this)}
|
||||
>${tip}</ha-tip
|
||||
>`
|
||||
)}
|
||||
</div>
|
||||
</ha-card>
|
||||
</div>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { mdiArrowUpBoldCircle, mdiPuzzle } from "@mdi/js";
|
||||
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
|
||||
import { property } from "lit/decorators";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { atLeastVersion } from "../../../src/common/config/version";
|
||||
import { navigate } from "../../../src/common/navigate";
|
||||
@@ -14,7 +14,8 @@ import "../components/hassio-card-content";
|
||||
import { filterAndSort } from "../components/hassio-filter-addons";
|
||||
import { hassioStyle } from "../resources/hassio-style";
|
||||
|
||||
class HassioAddonRepositoryEl extends LitElement {
|
||||
@customElement("hassio-addon-repository")
|
||||
export class HassioAddonRepositoryEl extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public supervisor!: Supervisor;
|
||||
@@ -140,5 +141,3 @@ class HassioAddonRepositoryEl extends LitElement {
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define("hassio-addon-repository", HassioAddonRepositoryEl);
|
||||
|
||||
@@ -9,7 +9,7 @@ import {
|
||||
PropertyValues,
|
||||
TemplateResult,
|
||||
} from "lit";
|
||||
import { property, state } from "lit/decorators";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { atLeastVersion } from "../../../src/common/config/version";
|
||||
import { fireEvent } from "../../../src/common/dom/fire_event";
|
||||
@@ -49,7 +49,8 @@ const sortRepos = (a: HassioAddonRepository, b: HassioAddonRepository) => {
|
||||
return a.name.toUpperCase() < b.name.toUpperCase() ? -1 : 1;
|
||||
};
|
||||
|
||||
class HassioAddonStore extends LitElement {
|
||||
@customElement("hassio-addon-store")
|
||||
export class HassioAddonStore extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public supervisor!: Supervisor;
|
||||
@@ -250,5 +251,3 @@ class HassioAddonStore extends LitElement {
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define("hassio-addon-store", HassioAddonStore);
|
||||
|
||||
@@ -17,7 +17,6 @@ import "../../../../src/components/ha-formfield";
|
||||
import "../../../../src/components/ha-header-bar";
|
||||
import "../../../../src/components/ha-icon-button";
|
||||
import "../../../../src/components/ha-radio";
|
||||
import "../../../../src/components/ha-related-items";
|
||||
import { extractApiErrorMessage } from "../../../../src/data/hassio/common";
|
||||
import {
|
||||
AccessPoints,
|
||||
|
||||
79
package.json
79
package.json
@@ -25,19 +25,20 @@
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@braintree/sanitize-url": "^6.0.2",
|
||||
"@codemirror/autocomplete": "^6.4.0",
|
||||
"@codemirror/commands": "^6.2.0",
|
||||
"@codemirror/language": "^6.4.0",
|
||||
"@codemirror/autocomplete": "^6.4.2",
|
||||
"@codemirror/commands": "^6.2.1",
|
||||
"@codemirror/language": "^6.6.0",
|
||||
"@codemirror/legacy-modes": "^6.3.1",
|
||||
"@codemirror/search": "^6.2.3",
|
||||
"@codemirror/state": "^6.2.0",
|
||||
"@codemirror/view": "^6.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",
|
||||
"@codemirror/view": "^6.9.1",
|
||||
"@egjs/hammerjs": "^2.0.17",
|
||||
"@formatjs/intl-datetimeformat": "^6.5.1",
|
||||
"@formatjs/intl-getcanonicallocales": "^2.1.0",
|
||||
"@formatjs/intl-locale": "^3.1.1",
|
||||
"@formatjs/intl-numberformat": "^8.3.5",
|
||||
"@formatjs/intl-pluralrules": "^5.1.10",
|
||||
"@formatjs/intl-relativetimeformat": "^11.1.10",
|
||||
"@fullcalendar/core": "^6.1.4",
|
||||
"@fullcalendar/daygrid": "^6.1.4",
|
||||
"@fullcalendar/interaction": "^6.1.4",
|
||||
@@ -70,6 +71,7 @@
|
||||
"@material/mwc-textfield": "^0.27.0",
|
||||
"@material/mwc-top-app-bar-fixed": "^0.27.0",
|
||||
"@material/top-app-bar": "=14.0.0-canary.53b3cad2f.0",
|
||||
"@material/web": "=1.0.0-pre.3",
|
||||
"@mdi/js": "7.1.96",
|
||||
"@mdi/svg": "7.1.96",
|
||||
"@polymer/app-layout": "^3.1.0",
|
||||
@@ -87,8 +89,8 @@
|
||||
"@polymer/paper-tooltip": "^3.0.1",
|
||||
"@polymer/polymer": "3.4.1",
|
||||
"@thomasloven/round-slider": "0.6.0",
|
||||
"@vaadin/combo-box": "^23.3.6",
|
||||
"@vaadin/vaadin-themable-mixin": "^23.3.6",
|
||||
"@vaadin/combo-box": "^23.3.7",
|
||||
"@vaadin/vaadin-themable-mixin": "^23.3.7",
|
||||
"@vibrant/color": "^3.2.1-alpha.1",
|
||||
"@vibrant/core": "^3.2.1-alpha.1",
|
||||
"@vibrant/quantizer-mmcq": "^3.2.1-alpha.1",
|
||||
@@ -98,7 +100,8 @@
|
||||
"app-datepicker": "^5.1.0",
|
||||
"chart.js": "^3.3.2",
|
||||
"comlink": "^4.4.1",
|
||||
"core-js": "^3.27.2",
|
||||
"compression-webpack-plugin": "^10.0.0",
|
||||
"core-js": "^3.28.0",
|
||||
"cropperjs": "^1.5.13",
|
||||
"date-fns": "^2.29.3",
|
||||
"date-fns-tz": "^2.0.0",
|
||||
@@ -106,11 +109,10 @@
|
||||
"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.0",
|
||||
"intl-messageformat": "^10.3.1",
|
||||
"js-yaml": "^4.1.0",
|
||||
"leaflet": "^1.9.3",
|
||||
"leaflet-draw": "^1.0.4",
|
||||
@@ -130,13 +132,13 @@
|
||||
"superstruct": "^1.0.3",
|
||||
"tinykeys": "^1.4.0",
|
||||
"tsparticles-engine": "^2.9.3",
|
||||
"tsparticles-preset-links": "^2.8.0",
|
||||
"tsparticles-preset-links": "^2.9.3",
|
||||
"unfetch": "^5.0.0",
|
||||
"vis-data": "^7.1.4",
|
||||
"vis-network": "^8.5.4",
|
||||
"vue": "^2.6.12",
|
||||
"vue2-daterange-picker": "^0.5.1",
|
||||
"weekstart": "^1.1.0",
|
||||
"vis-network": "^9.1.4",
|
||||
"vue": "^2.7.14",
|
||||
"vue2-daterange-picker": "^0.6.8",
|
||||
"weekstart": "^2.0.0",
|
||||
"workbox-cacheable-response": "^6.5.4",
|
||||
"workbox-core": "^6.5.4",
|
||||
"workbox-expiration": "^6.5.4",
|
||||
@@ -146,18 +148,18 @@
|
||||
"xss": "^1.0.14"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.20.12",
|
||||
"@babel/core": "^7.21.0",
|
||||
"@babel/plugin-external-helpers": "^7.18.6",
|
||||
"@babel/plugin-proposal-class-properties": "^7.18.6",
|
||||
"@babel/plugin-proposal-decorators": "^7.20.13",
|
||||
"@babel/plugin-proposal-decorators": "^7.21.0",
|
||||
"@babel/plugin-proposal-nullish-coalescing-operator": "^7.18.6",
|
||||
"@babel/plugin-proposal-object-rest-spread": "^7.20.7",
|
||||
"@babel/plugin-proposal-optional-chaining": "^7.20.7",
|
||||
"@babel/plugin-proposal-optional-chaining": "^7.21.0",
|
||||
"@babel/plugin-syntax-dynamic-import": "^7.8.3",
|
||||
"@babel/plugin-syntax-import-meta": "^7.10.4",
|
||||
"@babel/plugin-syntax-top-level-await": "^7.14.5",
|
||||
"@babel/preset-env": "^7.20.2",
|
||||
"@babel/preset-typescript": "^7.18.6",
|
||||
"@babel/preset-typescript": "^7.21.0",
|
||||
"@koa/cors": "^4.0.0",
|
||||
"@octokit/auth-oauth-device": "^4.0.4",
|
||||
"@octokit/rest": "^19.0.7",
|
||||
@@ -169,26 +171,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": "^8",
|
||||
"@types/mocha": "^10",
|
||||
"@types/qrcode": "^1.5.0",
|
||||
"@types/sortablejs": "^1",
|
||||
"@types/tar": "^6",
|
||||
"@types/webspeechapi": "^0.0.29",
|
||||
"@typescript-eslint/eslint-plugin": "^5.51.0",
|
||||
"@typescript-eslint/parser": "^5.52.0",
|
||||
"@web/dev-server": "^0.0.24",
|
||||
"@typescript-eslint/eslint-plugin": "^5.53.0",
|
||||
"@typescript-eslint/parser": "^5.53.0",
|
||||
"@web/dev-server": "^0.1.35",
|
||||
"@web/dev-server-rollup": "^0.2.11",
|
||||
"babel-loader": "^9.1.2",
|
||||
"chai": "^4.3.7",
|
||||
"del": "^7.0.0",
|
||||
"eslint": "^7.32.0",
|
||||
"eslint-config-airbnb-base": "^14.2.1",
|
||||
"eslint": "^8.35.0",
|
||||
"eslint-config-airbnb-base": "^15.0.0",
|
||||
"eslint-config-airbnb-typescript": "^17.0.0",
|
||||
"eslint-config-prettier": "^8.6.0",
|
||||
"eslint-import-resolver-webpack": "^0.13.2",
|
||||
@@ -196,8 +198,9 @@
|
||||
"eslint-plugin-import": "^2.27.5",
|
||||
"eslint-plugin-lit": "^1.8.2",
|
||||
"eslint-plugin-lit-a11y": "^2.3.0",
|
||||
"eslint-plugin-unused-imports": "^1.1.5",
|
||||
"eslint-plugin-unused-imports": "^2.0.0",
|
||||
"eslint-plugin-wc": "^1.4.0",
|
||||
"esprima": "^4.0.1",
|
||||
"fancy-log": "^2.0.0",
|
||||
"fs-extra": "^11.1.0",
|
||||
"glob": "^8.1.0",
|
||||
@@ -211,15 +214,15 @@
|
||||
"husky": "^8.0.3",
|
||||
"instant-mocha": "^1.5.0",
|
||||
"jszip": "^3.10.1",
|
||||
"lint-staged": "^13.1.1",
|
||||
"lint-staged": "^13.1.2",
|
||||
"lit-analyzer": "^1.2.1",
|
||||
"lodash.template": "^4.5.0",
|
||||
"magic-string": "^0.25.7",
|
||||
"magic-string": "^0.30.0",
|
||||
"map-stream": "^0.0.7",
|
||||
"merge-stream": "^2.0.0",
|
||||
"mocha": "^8.4.0",
|
||||
"mocha": "^10.2.0",
|
||||
"object-hash": "^3.0.0",
|
||||
"open": "^8.4.0",
|
||||
"open": "^8.4.1",
|
||||
"pinst": "^3.0.0",
|
||||
"prettier": "^2.8.4",
|
||||
"require-dir": "^1.2.0",
|
||||
@@ -230,14 +233,14 @@
|
||||
"serve": "^11.3.2",
|
||||
"sinon": "^15.0.1",
|
||||
"source-map-url": "^0.4.1",
|
||||
"systemjs": "^6.13.0",
|
||||
"systemjs": "^6.14.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.55.1",
|
||||
"webpack": "=5.72.1",
|
||||
"webpack-cli": "^5.0.1",
|
||||
"webpack-dev-server": "^4.11.1",
|
||||
"webpack-manifest-plugin": "^5.0.0",
|
||||
|
||||
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "home-assistant-frontend"
|
||||
version = "20230202.0"
|
||||
version = "20230224.0"
|
||||
license = {text = "Apache-2.0"}
|
||||
description = "The Home Assistant frontend"
|
||||
readme = "README.md"
|
||||
|
||||
@@ -6,6 +6,9 @@ set -e
|
||||
|
||||
cd "$(dirname "$0")/.."
|
||||
|
||||
STATS=1 NODE_ENV=production ./node_modules/.bin/webpack --profile --json > compilation-stats.json
|
||||
npx webpack-bundle-analyzer compilation-stats.json hass_frontend/frontend_latest
|
||||
rm compilation-stats.json
|
||||
export STATS=1
|
||||
statsfile="compilation-stats.json"
|
||||
|
||||
./node_modules/.bin/webpack-cli --profile --node-env=production --json=$statsfile
|
||||
npx webpack-bundle-analyzer $statsfile hass_frontend/frontend_latest
|
||||
rm -f $statsfile
|
||||
|
||||
@@ -8,7 +8,7 @@ import {
|
||||
PropertyValues,
|
||||
TemplateResult,
|
||||
} from "lit";
|
||||
import { property, state } from "lit/decorators";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import "../components/ha-alert";
|
||||
import "../components/ha-checkbox";
|
||||
import { computeInitialHaFormData } from "../components/ha-form/compute-initial-ha-form-data";
|
||||
@@ -25,7 +25,8 @@ import "./ha-password-manager-polyfill";
|
||||
|
||||
type State = "loading" | "error" | "step";
|
||||
|
||||
class HaAuthFlow extends litLocalizeLiteMixin(LitElement) {
|
||||
@customElement("ha-auth-flow")
|
||||
export class HaAuthFlow extends litLocalizeLiteMixin(LitElement) {
|
||||
@property({ attribute: false }) public authProvider?: AuthProvider;
|
||||
|
||||
@property() public clientId?: string;
|
||||
@@ -407,7 +408,6 @@ class HaAuthFlow extends litLocalizeLiteMixin(LitElement) {
|
||||
`;
|
||||
}
|
||||
}
|
||||
customElements.define("ha-auth-flow", HaAuthFlow);
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { css, CSSResultGroup, html, LitElement, PropertyValues } from "lit";
|
||||
import { property, state } from "lit/decorators";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import punycode from "punycode";
|
||||
import { applyThemesOnElement } from "../common/dom/apply_themes_on_element";
|
||||
import { extractSearchParamsObject } from "../common/url/search-params";
|
||||
@@ -14,7 +14,8 @@ import "./ha-auth-flow";
|
||||
|
||||
import("./ha-pick-auth-provider");
|
||||
|
||||
class HaAuthorize extends litLocalizeLiteMixin(LitElement) {
|
||||
@customElement("ha-authorize")
|
||||
export class HaAuthorize extends litLocalizeLiteMixin(LitElement) {
|
||||
@property() public clientId?: string;
|
||||
|
||||
@property() public redirectUri?: string;
|
||||
@@ -183,4 +184,3 @@ class HaAuthorize extends litLocalizeLiteMixin(LitElement) {
|
||||
`;
|
||||
}
|
||||
}
|
||||
customElements.define("ha-authorize", HaAuthorize);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/* eslint-disable lit/prefer-static-styles */
|
||||
import { html, LitElement, TemplateResult } from "lit";
|
||||
import { css, html, LitElement, nothing } 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,35 +29,43 @@ 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;
|
||||
}
|
||||
|
||||
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 {
|
||||
protected render() {
|
||||
if (
|
||||
this.step &&
|
||||
this.step.type === "form" &&
|
||||
@@ -67,6 +75,11 @@ 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}
|
||||
>
|
||||
@@ -74,16 +87,13 @@ export class HaPasswordManagerPolyfill extends LitElement {
|
||||
this.render_input(input)
|
||||
)}
|
||||
<input type="submit" />
|
||||
<style>
|
||||
${this.styles}
|
||||
</style>
|
||||
</form>
|
||||
`;
|
||||
}
|
||||
return html``;
|
||||
return nothing;
|
||||
}
|
||||
|
||||
private render_input(schema: HaFormSchema): TemplateResult | string {
|
||||
private render_input(schema: HaFormSchema) {
|
||||
const inputType = schema.name.includes("password") ? "password" : "text";
|
||||
if (schema.type !== "string") {
|
||||
return "";
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import "@polymer/paper-item/paper-item";
|
||||
import "@polymer/paper-item/paper-item-body";
|
||||
import { css, html, LitElement } from "lit";
|
||||
import { property } from "lit/decorators";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import "../components/ha-icon-next";
|
||||
import { AuthProvider } from "../data/auth";
|
||||
@@ -13,7 +13,8 @@ declare global {
|
||||
}
|
||||
}
|
||||
|
||||
class HaPickAuthProvider extends litLocalizeLiteMixin(LitElement) {
|
||||
@customElement("ha-pick-auth-provider")
|
||||
export class HaPickAuthProvider extends litLocalizeLiteMixin(LitElement) {
|
||||
@property() public authProviders: AuthProvider[] = [];
|
||||
|
||||
protected render() {
|
||||
@@ -47,4 +48,3 @@ class HaPickAuthProvider extends litLocalizeLiteMixin(LitElement) {
|
||||
}
|
||||
`;
|
||||
}
|
||||
customElements.define("ha-pick-auth-provider", HaPickAuthProvider);
|
||||
|
||||
@@ -2,6 +2,7 @@ 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;
|
||||
|
||||
@@ -24,7 +24,6 @@ import {
|
||||
mdiDatabase,
|
||||
mdiEarHearing,
|
||||
mdiEye,
|
||||
mdiFan,
|
||||
mdiFlash,
|
||||
mdiFlower,
|
||||
mdiFormatListBulleted,
|
||||
@@ -91,7 +90,6 @@ export const FIXED_DOMAIN_ICONS = {
|
||||
conversation: mdiMicrophoneMessage,
|
||||
counter: mdiCounter,
|
||||
demo: mdiHomeAssistant,
|
||||
fan: mdiFan,
|
||||
google_assistant: mdiGoogleAssistant,
|
||||
group: mdiGoogleCirclesCommunities,
|
||||
homeassistant: mdiHomeAssistant,
|
||||
|
||||
@@ -10,11 +10,19 @@ export const createDurationData = (
|
||||
if (typeof duration !== "object") {
|
||||
if (typeof duration === "string" || isNaN(duration)) {
|
||||
const parts = duration?.toString().split(":") || [];
|
||||
if (parts.length === 1) {
|
||||
return { seconds: Number(parts[0]) };
|
||||
}
|
||||
if (parts.length > 3) {
|
||||
return undefined;
|
||||
}
|
||||
const seconds = Number(parts[2]) || 0;
|
||||
const seconds_whole = Math.floor(seconds);
|
||||
return {
|
||||
hours: Number(parts[0]) || 0,
|
||||
minutes: Number(parts[1]) || 0,
|
||||
seconds: Number(parts[2]) || 0,
|
||||
milliseconds: Number(parts[3]) || 0,
|
||||
seconds: seconds_whole,
|
||||
milliseconds: Math.floor((seconds - seconds_whole) * 1000),
|
||||
};
|
||||
}
|
||||
return { seconds: duration };
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { HassEntity } from "home-assistant-js-websocket";
|
||||
import { EntityRegistryEntry } from "../../data/entity_registry";
|
||||
import { EntityRegistryDisplayEntry } 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 EntityRegistryEntry | undefined;
|
||||
const entity = entities[entityId] as EntityRegistryDisplayEntry | 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 EntityRegistryEntry | undefined;
|
||||
const entity = entities[entityId] as EntityRegistryDisplayEntry | undefined;
|
||||
const translationKey = entity?.translation_key;
|
||||
|
||||
return (
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { HassEntity } from "home-assistant-js-websocket";
|
||||
import { UNAVAILABLE, UNKNOWN } from "../../data/entity";
|
||||
import { EntityRegistryEntry } from "../../data/entity_registry";
|
||||
import { EntityRegistryDisplayEntry } 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 EntityRegistryEntry | undefined;
|
||||
const entity = entities[entityId] as EntityRegistryDisplayEntry | undefined;
|
||||
|
||||
// Entities with a `unit_of_measurement` or `state_class` are numeric values and should use `formatNumber`
|
||||
if (isNumericFromAttributes(attributes)) {
|
||||
|
||||
@@ -15,6 +15,8 @@ import {
|
||||
mdiCheckCircleOutline,
|
||||
mdiClock,
|
||||
mdiCloseCircleOutline,
|
||||
mdiFan,
|
||||
mdiFanOff,
|
||||
mdiGestureTapButton,
|
||||
mdiLanConnect,
|
||||
mdiLanDisconnect,
|
||||
@@ -108,6 +110,9 @@ export const domainIconWithoutDefault = (
|
||||
}
|
||||
return compareState === "not_home" ? mdiAccountArrowRight : mdiAccount;
|
||||
|
||||
case "fan":
|
||||
return compareState === "off" ? mdiFanOff : mdiFan;
|
||||
|
||||
case "humidifier":
|
||||
return compareState === "off" ? mdiAirHumidifierOff : mdiAirHumidifier;
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ import {
|
||||
HassEntity,
|
||||
HassEntityAttributeBase,
|
||||
} from "home-assistant-js-websocket";
|
||||
import { EntityRegistryEntry } from "../../data/entity_registry";
|
||||
import { EntityRegistryDisplayEntry } from "../../data/entity_registry";
|
||||
import { FrontendLocaleData, NumberFormat } from "../../data/translation";
|
||||
import { round } from "./round";
|
||||
|
||||
@@ -92,11 +92,9 @@ export const formatNumber = (
|
||||
*/
|
||||
export const getNumberFormatOptions = (
|
||||
entityState: HassEntity,
|
||||
entity?: EntityRegistryEntry
|
||||
entity?: EntityRegistryDisplayEntry
|
||||
): Intl.NumberFormatOptions | undefined => {
|
||||
const precision =
|
||||
entity?.options?.sensor?.display_precision ??
|
||||
entity?.options?.sensor?.suggested_display_precision;
|
||||
const precision = entity?.display_precision;
|
||||
if (precision != null) {
|
||||
return {
|
||||
maximumFractionDigits: precision,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
const isTemplateRegex = new RegExp("{%|{{");
|
||||
const isTemplateRegex = /{%|{{/;
|
||||
|
||||
export const isTemplate = (value: string): boolean =>
|
||||
isTemplateRegex.test(value);
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { refine, string } from "superstruct";
|
||||
|
||||
export const isCustomType = (value: string) => value.startsWith("custom:");
|
||||
import { isCustomType } from "../../data/lovelace_custom_cards";
|
||||
|
||||
export const customType = () =>
|
||||
refine(string(), "custom element type", isCustomType);
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import { css, CSSResultGroup, html, LitElement } from "lit";
|
||||
import { property, query } from "lit/decorators";
|
||||
import { customElement, property, query } from "lit/decorators";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import { HomeAssistant } from "../../types";
|
||||
import "./ha-progress-button";
|
||||
|
||||
@customElement("ha-call-api-button")
|
||||
class HaCallApiButton extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@@ -69,8 +70,6 @@ class HaCallApiButton extends LitElement {
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define("ha-call-api-button", HaCallApiButton);
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-call-api-button": HaCallApiButton;
|
||||
|
||||
@@ -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,7 +99,11 @@ class StatisticsChart extends LitElement {
|
||||
if (!this.hasUpdated || changedProps.has("unit")) {
|
||||
this._createOptions();
|
||||
}
|
||||
if (changedProps.has("statisticsData") || changedProps.has("statTypes")) {
|
||||
if (
|
||||
changedProps.has("statisticsData") ||
|
||||
changedProps.has("statTypes") ||
|
||||
changedProps.has("hideLegend")
|
||||
) {
|
||||
this._generateData();
|
||||
}
|
||||
}
|
||||
@@ -328,7 +332,10 @@ class StatisticsChart extends LitElement {
|
||||
prevEndTime = end;
|
||||
};
|
||||
|
||||
const color = getGraphColorByIndex(colorIndex, this._computedStyle!);
|
||||
const color = getGraphColorByIndex(
|
||||
colorIndex,
|
||||
this._computedStyle || getComputedStyle(this)
|
||||
);
|
||||
colorIndex++;
|
||||
|
||||
const statTypes: this["statTypes"] = [];
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import "@lit-labs/virtualizer";
|
||||
import { mdiArrowDown, mdiArrowUp } from "@mdi/js";
|
||||
import deepClone from "deep-clone-simple";
|
||||
import {
|
||||
@@ -21,16 +22,15 @@ import { styleMap } from "lit/directives/style-map";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { restoreScroll } from "../../common/decorators/restore-scroll";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import "../search-input";
|
||||
import { debounce } from "../../common/util/debounce";
|
||||
import { nextRender } from "../../common/util/render-status";
|
||||
import { haStyleScrollbar } from "../../resources/styles";
|
||||
import { HomeAssistant } from "../../types";
|
||||
import "../ha-checkbox";
|
||||
import type { HaCheckbox } from "../ha-checkbox";
|
||||
import "../ha-svg-icon";
|
||||
import "../search-input";
|
||||
import { filterData, sortData } from "./sort-filter";
|
||||
import { HomeAssistant } from "../../types";
|
||||
import "@lit-labs/virtualizer";
|
||||
|
||||
declare global {
|
||||
// for fire event
|
||||
@@ -461,7 +461,9 @@ export class HaDataTable extends LitElement {
|
||||
const elapsed = curTime - startTime;
|
||||
|
||||
if (elapsed < 100) {
|
||||
await new Promise((resolve) => setTimeout(resolve, 100 - elapsed));
|
||||
await new Promise((resolve) => {
|
||||
setTimeout(resolve, 100 - elapsed);
|
||||
});
|
||||
}
|
||||
if (this.curRequest !== curRequest) {
|
||||
return;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import "@material/mwc-list/mwc-list-item";
|
||||
import { UnsubscribeFunc } from "home-assistant-js-websocket";
|
||||
import { HassEntity, UnsubscribeFunc } from "home-assistant-js-websocket";
|
||||
import { html, LitElement, PropertyValues, TemplateResult } from "lit";
|
||||
import { ComboBoxLitRenderer } from "@vaadin/combo-box/lit";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
@@ -37,6 +37,8 @@ export type HaDevicePickerDeviceFilterFunc = (
|
||||
device: DeviceRegistryEntry
|
||||
) => boolean;
|
||||
|
||||
export type HaDevicePickerEntityFilterFunc = (entity: HassEntity) => boolean;
|
||||
|
||||
const rowRenderer: ComboBoxLitRenderer<Device> = (item) => html`<mwc-list-item
|
||||
.twoline=${!!item.area}
|
||||
>
|
||||
@@ -94,6 +96,8 @@ export class HaDevicePicker extends SubscribeMixin(LitElement) {
|
||||
|
||||
@property() public deviceFilter?: HaDevicePickerDeviceFilterFunc;
|
||||
|
||||
@property() public entityFilter?: HaDevicePickerEntityFilterFunc;
|
||||
|
||||
@property({ type: Boolean }) public disabled?: boolean;
|
||||
|
||||
@property({ type: Boolean }) public required?: boolean;
|
||||
@@ -113,6 +117,7 @@ export class HaDevicePicker extends SubscribeMixin(LitElement) {
|
||||
excludeDomains: this["excludeDomains"],
|
||||
includeDeviceClasses: this["includeDeviceClasses"],
|
||||
deviceFilter: this["deviceFilter"],
|
||||
entityFilter: this["entityFilter"],
|
||||
excludeDevices: this["excludeDevices"]
|
||||
): Device[] => {
|
||||
if (!devices.length) {
|
||||
@@ -127,7 +132,12 @@ export class HaDevicePicker extends SubscribeMixin(LitElement) {
|
||||
|
||||
const deviceEntityLookup: DeviceEntityLookup = {};
|
||||
|
||||
if (includeDomains || excludeDomains || includeDeviceClasses) {
|
||||
if (
|
||||
includeDomains ||
|
||||
excludeDomains ||
|
||||
includeDeviceClasses ||
|
||||
entityFilter
|
||||
) {
|
||||
for (const entity of entities) {
|
||||
if (!entity.device_id) {
|
||||
continue;
|
||||
@@ -198,6 +208,22 @@ export class HaDevicePicker extends SubscribeMixin(LitElement) {
|
||||
});
|
||||
}
|
||||
|
||||
if (entityFilter) {
|
||||
inputDevices = inputDevices.filter((device) => {
|
||||
const devEntities = deviceEntityLookup[device.id];
|
||||
if (!devEntities || !devEntities.length) {
|
||||
return false;
|
||||
}
|
||||
return devEntities.some((entity) => {
|
||||
const stateObj = this.hass.states[entity.entity_id];
|
||||
if (!stateObj) {
|
||||
return false;
|
||||
}
|
||||
return entityFilter(stateObj);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
if (deviceFilter) {
|
||||
inputDevices = inputDevices.filter(
|
||||
(device) =>
|
||||
@@ -274,6 +300,7 @@ export class HaDevicePicker extends SubscribeMixin(LitElement) {
|
||||
this.excludeDomains,
|
||||
this.includeDeviceClasses,
|
||||
this.deviceFilter,
|
||||
this.entityFilter,
|
||||
this.excludeDevices
|
||||
);
|
||||
}
|
||||
|
||||
@@ -4,7 +4,10 @@ import { fireEvent } from "../../common/dom/fire_event";
|
||||
import { PolymerChangedEvent } from "../../polymer-types";
|
||||
import { HomeAssistant } from "../../types";
|
||||
import "./ha-device-picker";
|
||||
import type { HaDevicePickerDeviceFilterFunc } from "./ha-device-picker";
|
||||
import type {
|
||||
HaDevicePickerDeviceFilterFunc,
|
||||
HaDevicePickerEntityFilterFunc,
|
||||
} from "./ha-device-picker";
|
||||
|
||||
@customElement("ha-devices-picker")
|
||||
class HaDevicesPicker extends LitElement {
|
||||
@@ -44,6 +47,8 @@ class HaDevicesPicker extends LitElement {
|
||||
|
||||
@property() public deviceFilter?: HaDevicePickerDeviceFilterFunc;
|
||||
|
||||
@property() public entityFilter?: HaDevicePickerEntityFilterFunc;
|
||||
|
||||
protected render(): TemplateResult {
|
||||
if (!this.hass) {
|
||||
return html``;
|
||||
@@ -59,6 +64,7 @@ class HaDevicesPicker extends LitElement {
|
||||
.curValue=${entityId}
|
||||
.hass=${this.hass}
|
||||
.deviceFilter=${this.deviceFilter}
|
||||
.entityFilter=${this.entityFilter}
|
||||
.includeDomains=${this.includeDomains}
|
||||
.excludeDomains=${this.excludeDomains}
|
||||
.includeDeviceClasses=${this.includeDeviceClasses}
|
||||
@@ -76,8 +82,10 @@ class HaDevicesPicker extends LitElement {
|
||||
.hass=${this.hass}
|
||||
.helper=${this.helper}
|
||||
.deviceFilter=${this.deviceFilter}
|
||||
.entityFilter=${this.entityFilter}
|
||||
.includeDomains=${this.includeDomains}
|
||||
.excludeDomains=${this.excludeDomains}
|
||||
.excludeDevices=${currentDevices}
|
||||
.includeDeviceClasses=${this.includeDeviceClasses}
|
||||
.label=${this.pickDeviceLabel}
|
||||
.disabled=${this.disabled}
|
||||
|
||||
@@ -22,7 +22,7 @@ import {
|
||||
isNumericState,
|
||||
} from "../../common/number/format_number";
|
||||
import { isUnavailableState, UNAVAILABLE, UNKNOWN } from "../../data/entity";
|
||||
import { EntityRegistryEntry } from "../../data/entity_registry";
|
||||
import { EntityRegistryDisplayEntry } 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?: EntityRegistryEntry
|
||||
entry?: EntityRegistryDisplayEntry
|
||||
) {
|
||||
switch (domain) {
|
||||
case "alarm_control_panel":
|
||||
@@ -200,7 +200,7 @@ export class HaStateLabelBadge extends LitElement {
|
||||
private _computeShowIcon(
|
||||
domain: string,
|
||||
entityState: HassEntity,
|
||||
entry?: EntityRegistryEntry
|
||||
entry?: EntityRegistryDisplayEntry
|
||||
): boolean {
|
||||
if (entityState.state === UNAVAILABLE) {
|
||||
return false;
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
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";
|
||||
@@ -10,10 +12,11 @@ import {
|
||||
createAreaRegistryEntry,
|
||||
} from "../data/area_registry";
|
||||
import {
|
||||
DeviceEntityLookup,
|
||||
DeviceEntityDisplayLookup,
|
||||
DeviceRegistryEntry,
|
||||
getDeviceEntityDisplayLookup,
|
||||
} from "../data/device_registry";
|
||||
import { EntityRegistryEntry } from "../data/entity_registry";
|
||||
import { EntityRegistryDisplayEntry } from "../data/entity_registry";
|
||||
import {
|
||||
showAlertDialog,
|
||||
showPromptDialog,
|
||||
@@ -83,7 +86,7 @@ export class HaAreaPicker extends LitElement {
|
||||
|
||||
@property() public deviceFilter?: HaDevicePickerDeviceFilterFunc;
|
||||
|
||||
@property() public entityFilter?: (entity: EntityRegistryEntry) => boolean;
|
||||
@property() public entityFilter?: (entity: HassEntity) => boolean;
|
||||
|
||||
@property({ type: Boolean }) public disabled?: boolean;
|
||||
|
||||
@@ -111,7 +114,7 @@ export class HaAreaPicker extends LitElement {
|
||||
(
|
||||
areas: AreaRegistryEntry[],
|
||||
devices: DeviceRegistryEntry[],
|
||||
entities: EntityRegistryEntry[],
|
||||
entities: EntityRegistryDisplayEntry[],
|
||||
includeDomains: this["includeDomains"],
|
||||
excludeDomains: this["excludeDomains"],
|
||||
includeDeviceClasses: this["includeDeviceClasses"],
|
||||
@@ -131,96 +134,107 @@ export class HaAreaPicker extends LitElement {
|
||||
];
|
||||
}
|
||||
|
||||
const deviceEntityLookup: DeviceEntityLookup = {};
|
||||
let deviceEntityLookup: DeviceEntityDisplayLookup = {};
|
||||
let inputDevices: DeviceRegistryEntry[] | undefined;
|
||||
let inputEntities: EntityRegistryEntry[] | undefined;
|
||||
let inputEntities: EntityRegistryDisplayEntry[] | undefined;
|
||||
|
||||
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);
|
||||
}
|
||||
if (
|
||||
includeDomains ||
|
||||
excludeDomains ||
|
||||
includeDeviceClasses ||
|
||||
deviceFilter ||
|
||||
entityFilter
|
||||
) {
|
||||
deviceEntityLookup = getDeviceEntityDisplayLookup(entities);
|
||||
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) =>
|
||||
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))
|
||||
);
|
||||
});
|
||||
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(
|
||||
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))
|
||||
);
|
||||
});
|
||||
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) {
|
||||
return false;
|
||||
}
|
||||
return deviceEntityLookup[device.id].some((entity) => {
|
||||
const stateObj = this.hass.states[entity.entity_id];
|
||||
if (!stateObj) {
|
||||
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 (
|
||||
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)
|
||||
);
|
||||
});
|
||||
});
|
||||
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)
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (deviceFilter) {
|
||||
inputDevices = inputDevices!.filter((device) => deviceFilter!(device));
|
||||
}
|
||||
|
||||
if (entityFilter) {
|
||||
inputEntities = inputEntities!.filter((entity) =>
|
||||
entityFilter!(entity)
|
||||
);
|
||||
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);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
let outputAreas = areas;
|
||||
|
||||
@@ -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: EntityRegistryEntry) => boolean;
|
||||
@property() public entityFilter?: (entity: HassEntity) => boolean;
|
||||
|
||||
@property({ attribute: "picked-area-label" })
|
||||
public pickedAreaLabel?: string;
|
||||
|
||||
@@ -44,7 +44,6 @@ 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);
|
||||
|
||||
63
src/components/ha-control-button-group.ts
Normal file
63
src/components/ha-control-button-group.ts
Normal file
@@ -0,0 +1,63 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -9,11 +9,9 @@ import {
|
||||
state,
|
||||
} from "lit/decorators";
|
||||
import { ifDefined } from "lit/directives/if-defined";
|
||||
import "../ha-icon";
|
||||
import "../ha-svg-icon";
|
||||
|
||||
@customElement("ha-tile-button")
|
||||
export class HaTileButton extends LitElement {
|
||||
@customElement("ha-control-button")
|
||||
export class HaControlButton extends LitElement {
|
||||
@property({ type: Boolean, reflect: true }) disabled = false;
|
||||
|
||||
@property() public label?: string;
|
||||
@@ -28,7 +26,7 @@ export class HaTileButton extends LitElement {
|
||||
type="button"
|
||||
class="button"
|
||||
aria-label=${ifDefined(this.label)}
|
||||
.title=${this.label}
|
||||
title=${ifDefined(this.label)}
|
||||
.disabled=${Boolean(this.disabled)}
|
||||
@focus=${this.handleRippleFocus}
|
||||
@blur=${this.handleRippleBlur}
|
||||
@@ -81,9 +79,12 @@ export class HaTileButton extends LitElement {
|
||||
static get styles(): CSSResultGroup {
|
||||
return css`
|
||||
:host {
|
||||
--tile-button-icon-color: var(--primary-text-color);
|
||||
--tile-button-background-color: var(--disabled-color);
|
||||
--tile-button-background-opacity: 0.2;
|
||||
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;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
@@ -97,7 +98,7 @@ export class HaTileButton extends LitElement {
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border-radius: 10px;
|
||||
border-radius: var(--control-button-border-radius);
|
||||
border: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
@@ -106,7 +107,8 @@ export class HaTileButton extends LitElement {
|
||||
outline: none;
|
||||
overflow: hidden;
|
||||
background: none;
|
||||
--mdc-ripple-color: var(--tile-button-background-color);
|
||||
z-index: 1;
|
||||
--mdc-ripple-color: var(--control-button-background-color);
|
||||
}
|
||||
.button::before {
|
||||
content: "";
|
||||
@@ -115,22 +117,21 @@ export class HaTileButton extends LitElement {
|
||||
left: 0;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
background-color: var(--tile-button-background-color);
|
||||
background-color: var(--control-button-background-color);
|
||||
transition: background-color 180ms ease-in-out,
|
||||
opacity 180ms ease-in-out;
|
||||
opacity: var(--tile-button-background-opacity);
|
||||
opacity: var(--control-button-background-opacity);
|
||||
}
|
||||
.button ::slotted(*) {
|
||||
--mdc-icon-size: 20px;
|
||||
transition: color 180ms ease-in-out;
|
||||
color: var(--tile-button-icon-color);
|
||||
color: var(--control-button-icon-color);
|
||||
pointer-events: none;
|
||||
}
|
||||
.button:disabled {
|
||||
cursor: not-allowed;
|
||||
--tile-button-background-color: var(--disabled-color);
|
||||
--tile-button-icon-color: var(--disabled-text-color);
|
||||
--tile-button-background-opacity: 0.2;
|
||||
--control-button-background-color: var(--disabled-color);
|
||||
--control-button-icon-color: var(--disabled-text-color);
|
||||
--control-button-background-opacity: 0.2;
|
||||
}
|
||||
`;
|
||||
}
|
||||
@@ -138,6 +139,6 @@ export class HaTileButton extends LitElement {
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-tile-button": HaTileButton;
|
||||
"ha-control-button": HaControlButton;
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import "hammerjs";
|
||||
import { DIRECTION_ALL, Manager, Pan, Tap } from "@egjs/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-bar-slider")
|
||||
export class HaBarSlider extends LitElement {
|
||||
@property({ type: Boolean })
|
||||
@customElement("ha-control-slider")
|
||||
export class HaControlSlider extends LitElement {
|
||||
@property({ type: Boolean, reflect: true })
|
||||
public disabled = false;
|
||||
|
||||
@property()
|
||||
@@ -131,18 +131,18 @@ export class HaBarSlider extends LitElement {
|
||||
|
||||
setupListeners() {
|
||||
if (this.slider && !this._mc) {
|
||||
this._mc = new Hammer.Manager(this.slider, {
|
||||
this._mc = new Manager(this.slider, {
|
||||
touchAction: this.vertical ? "pan-x" : "pan-y",
|
||||
});
|
||||
this._mc.add(
|
||||
new Hammer.Pan({
|
||||
new Pan({
|
||||
threshold: 10,
|
||||
direction: Hammer.DIRECTION_ALL,
|
||||
direction: DIRECTION_ALL,
|
||||
enable: true,
|
||||
})
|
||||
);
|
||||
|
||||
this._mc.add(new Hammer.Tap({ event: "singletap" }));
|
||||
this._mc.add(new Tap({ event: "singletap" }));
|
||||
|
||||
let savedValue;
|
||||
this._mc.on("panstart", () => {
|
||||
@@ -245,14 +245,16 @@ export class HaBarSlider extends LitElement {
|
||||
>
|
||||
<div class="slider-track-background"></div>
|
||||
${this.mode === "cursor"
|
||||
? html`
|
||||
<div
|
||||
class=${classMap({
|
||||
"slider-track-cursor": true,
|
||||
vertical: this.vertical,
|
||||
})}
|
||||
></div>
|
||||
`
|
||||
? this.value != null
|
||||
? html`
|
||||
<div
|
||||
class=${classMap({
|
||||
"slider-track-cursor": true,
|
||||
vertical: this.vertical,
|
||||
})}
|
||||
></div>
|
||||
`
|
||||
: null
|
||||
: html`
|
||||
<div
|
||||
class=${classMap({
|
||||
@@ -271,28 +273,29 @@ export class HaBarSlider extends LitElement {
|
||||
return css`
|
||||
:host {
|
||||
display: block;
|
||||
--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);
|
||||
--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);
|
||||
width: 100%;
|
||||
border-radius: var(--slider-bar-border-radius);
|
||||
border-radius: var(--control-slider-border-radius);
|
||||
outline: none;
|
||||
transition: box-shadow 180ms ease-in-out;
|
||||
}
|
||||
:host(:focus-visible) {
|
||||
box-shadow: 0 0 0 2px var(--slider-bar-color);
|
||||
box-shadow: 0 0 0 2px var(--control-slider-color);
|
||||
}
|
||||
:host([vertical]) {
|
||||
width: var(--slider-bar-thickness);
|
||||
width: var(--control-slider-thickness);
|
||||
height: 100%;
|
||||
}
|
||||
.slider {
|
||||
position: relative;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
border-radius: var(--slider-bar-border-radius);
|
||||
border-radius: var(--control-slider-border-radius);
|
||||
transform: translateZ(0);
|
||||
overflow: hidden;
|
||||
cursor: pointer;
|
||||
@@ -306,19 +309,20 @@ export class HaBarSlider extends LitElement {
|
||||
left: 0;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
background: var(--slider-bar-background);
|
||||
opacity: var(--slider-bar-background-opacity);
|
||||
background: var(--control-slider-background);
|
||||
opacity: var(--control-slider-background-opacity);
|
||||
}
|
||||
.slider .slider-track-bar {
|
||||
--border-radius: var(--slider-bar-border-radius);
|
||||
--border-radius: var(--control-slider-border-radius);
|
||||
--handle-size: 4px;
|
||||
--handle-margin: calc(var(--slider-bar-thickness) / 8);
|
||||
--handle-margin: calc(var(--control-slider-thickness) / 8);
|
||||
--slider-size: 100%;
|
||||
position: absolute;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
background-color: var(--slider-bar-color);
|
||||
transition: transform 180ms ease-in-out;
|
||||
background-color: var(--control-slider-color);
|
||||
transition: transform 180ms ease-in-out,
|
||||
background-color 180ms ease-in-out;
|
||||
}
|
||||
.slider .slider-track-bar.show-handle {
|
||||
--slider-size: calc(
|
||||
@@ -412,7 +416,7 @@ export class HaBarSlider extends LitElement {
|
||||
}
|
||||
|
||||
.slider .slider-track-cursor {
|
||||
--cursor-size: calc(var(--slider-bar-thickness) / 4);
|
||||
--cursor-size: calc(var(--control-slider-thickness) / 4);
|
||||
--handle-size: 4px;
|
||||
position: absolute;
|
||||
background-color: white;
|
||||
@@ -445,12 +449,15 @@ export class HaBarSlider extends LitElement {
|
||||
:host([pressed]) .slider-track-cursor {
|
||||
transition: none;
|
||||
}
|
||||
:host(:disabled) .slider {
|
||||
cursor: not-allowed;
|
||||
}
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-bar-slider": HaBarSlider;
|
||||
"ha-control-slider": HaControlSlider;
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,10 @@
|
||||
import {
|
||||
DIRECTION_HORIZONTAL,
|
||||
DIRECTION_VERTICAL,
|
||||
Manager,
|
||||
Swipe,
|
||||
Tap,
|
||||
} from "@egjs/hammerjs";
|
||||
import {
|
||||
css,
|
||||
CSSResultGroup,
|
||||
@@ -6,13 +13,13 @@ import {
|
||||
PropertyValues,
|
||||
TemplateResult,
|
||||
} from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { customElement, property, query } from "lit/decorators";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import "./ha-svg-icon";
|
||||
|
||||
@customElement("ha-bar-switch")
|
||||
export class HaBarSwitch extends LitElement {
|
||||
@property({ type: Boolean, attribute: "disabled" })
|
||||
@customElement("ha-control-switch")
|
||||
export class HaControlSwitch extends LitElement {
|
||||
@property({ type: Boolean, reflect: true })
|
||||
public disabled = false;
|
||||
|
||||
@property({ type: Boolean })
|
||||
@@ -30,8 +37,11 @@ export class HaBarSwitch 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");
|
||||
@@ -40,7 +50,7 @@ export class HaBarSwitch extends LitElement {
|
||||
|
||||
protected updated(changedProps: PropertyValues) {
|
||||
super.updated(changedProps);
|
||||
if (changedProps.has("value")) {
|
||||
if (changedProps.has("checked")) {
|
||||
this.setAttribute("aria-checked", this.checked ? "true" : "false");
|
||||
}
|
||||
}
|
||||
@@ -53,14 +63,70 @@ export class HaBarSwitch extends LitElement {
|
||||
|
||||
connectedCallback(): void {
|
||||
super.connectedCallback();
|
||||
this.addEventListener("keydown", this._keydown);
|
||||
this.addEventListener("click", this._toggle);
|
||||
this.setupListeners();
|
||||
}
|
||||
|
||||
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) {
|
||||
@@ -73,7 +139,7 @@ export class HaBarSwitch extends LitElement {
|
||||
|
||||
protected render(): TemplateResult {
|
||||
return html`
|
||||
<div class="switch">
|
||||
<div id="switch" class="switch">
|
||||
<div class="background"></div>
|
||||
<div class="button" aria-hidden="true">
|
||||
${this.checked
|
||||
@@ -92,35 +158,37 @@ export class HaBarSwitch extends LitElement {
|
||||
return css`
|
||||
:host {
|
||||
display: block;
|
||||
--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;
|
||||
--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;
|
||||
--mdc-icon-size: 20px;
|
||||
height: var(--switch-bar-thickness);
|
||||
height: var(--control-switch-thickness);
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
user-select: none;
|
||||
cursor: pointer;
|
||||
border-radius: var(--switch-bar-border-radius);
|
||||
border-radius: var(--control-switch-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(--switch-bar-off-color);
|
||||
box-shadow: 0 0 0 2px var(--control-switch-off-color);
|
||||
}
|
||||
:host([checked]:focus-visible) {
|
||||
box-shadow: 0 0 0 2px var(--switch-bar-on-color);
|
||||
box-shadow: 0 0 0 2px var(--control-switch-on-color);
|
||||
}
|
||||
.switch {
|
||||
box-sizing: border-box;
|
||||
position: relative;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
border-radius: var(--switch-bar-border-radius);
|
||||
border-radius: var(--control-switch-border-radius);
|
||||
overflow: hidden;
|
||||
padding: var(--switch-bar-padding);
|
||||
padding: var(--control-switch-padding);
|
||||
display: flex;
|
||||
}
|
||||
.switch .background {
|
||||
@@ -129,31 +197,31 @@ export class HaBarSwitch extends LitElement {
|
||||
left: 0;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
background-color: var(--switch-bar-off-color);
|
||||
background-color: var(--control-switch-off-color);
|
||||
transition: background-color 180ms ease-in-out;
|
||||
opacity: var(--switch-bar-background-opacity);
|
||||
opacity: var(--control-switch-background-opacity);
|
||||
}
|
||||
.switch .button {
|
||||
width: 50%;
|
||||
height: 100%;
|
||||
background: lightgrey;
|
||||
border-radius: calc(
|
||||
var(--switch-bar-border-radius) - var(--switch-bar-padding)
|
||||
var(--control-switch-border-radius) - var(--control-switch-padding)
|
||||
);
|
||||
transition: transform 180ms ease-in-out,
|
||||
background-color 180ms ease-in-out;
|
||||
background-color: var(--switch-bar-off-color);
|
||||
background-color: var(--control-switch-off-color);
|
||||
color: white;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
:host([checked]) .switch .background {
|
||||
background-color: var(--switch-bar-on-color);
|
||||
background-color: var(--control-switch-on-color);
|
||||
}
|
||||
:host([checked]) .switch .button {
|
||||
transform: translateX(100%);
|
||||
background-color: var(--switch-bar-on-color);
|
||||
background-color: var(--control-switch-on-color);
|
||||
}
|
||||
:host([reversed]) .switch {
|
||||
flex-direction: row-reverse;
|
||||
@@ -162,7 +230,7 @@ export class HaBarSwitch extends LitElement {
|
||||
transform: translateX(-100%);
|
||||
}
|
||||
:host([vertical]) {
|
||||
width: var(--switch-bar-thickness);
|
||||
width: var(--control-switch-thickness);
|
||||
height: 100%;
|
||||
}
|
||||
:host([vertical][checked]) .switch .button {
|
||||
@@ -188,6 +256,6 @@ export class HaBarSwitch extends LitElement {
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-bar-switch": HaBarSwitch;
|
||||
"ha-control-switch": HaControlSwitch;
|
||||
}
|
||||
}
|
||||
@@ -40,6 +40,22 @@ 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 = [
|
||||
|
||||
@@ -26,6 +26,9 @@ 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;
|
||||
@@ -132,7 +135,8 @@ export class Gauge extends LitElement {
|
||||
${
|
||||
this._segment_label
|
||||
? this._segment_label
|
||||
: this.valueText || formatNumber(this.value, this.locale)
|
||||
: this.valueText ||
|
||||
formatNumber(this.value, this.locale, this.formatOptions)
|
||||
}${
|
||||
this._segment_label
|
||||
? ""
|
||||
|
||||
@@ -36,6 +36,12 @@ 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;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
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();
|
||||
@@ -20,5 +22,3 @@ declare global {
|
||||
"ha-icon-next": HaIconNext;
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define("ha-icon-next", HaIconNext);
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
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();
|
||||
@@ -20,5 +22,3 @@ declare global {
|
||||
"ha-icon-prev": HaIconPrev;
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define("ha-icon-prev", HaIconPrev);
|
||||
|
||||
@@ -6,9 +6,10 @@ import {
|
||||
PropertyValues,
|
||||
TemplateResult,
|
||||
} from "lit";
|
||||
import { property } from "lit/decorators";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { classMap } from "lit/directives/class-map";
|
||||
|
||||
@customElement("ha-label-badge")
|
||||
class HaLabelBadge extends LitElement {
|
||||
@property() public label?: string;
|
||||
|
||||
@@ -61,7 +62,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: 50%;
|
||||
border-radius: var(--ha-label-badge-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));
|
||||
|
||||
@@ -132,5 +133,3 @@ declare global {
|
||||
"ha-label-badge": HaLabelBadge;
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define("ha-label-badge", HaLabelBadge);
|
||||
|
||||
@@ -30,6 +30,30 @@ 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;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
||||
@@ -282,7 +282,9 @@ 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");
|
||||
}
|
||||
|
||||
|
||||
@@ -1,13 +1,10 @@
|
||||
import { HassEntity, UnsubscribeFunc } from "home-assistant-js-websocket";
|
||||
import { HassEntity } 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,
|
||||
@@ -17,13 +14,12 @@ 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 SubscribeMixin(LitElement) {
|
||||
export class HaAreaSelector extends LitElement {
|
||||
@property() public hass!: HomeAssistant;
|
||||
|
||||
@property() public selector!: AreaSelector;
|
||||
@@ -40,23 +36,23 @@ export class HaAreaSelector extends SubscribeMixin(LitElement) {
|
||||
|
||||
@state() private _entitySources?: EntitySources;
|
||||
|
||||
@state() private _entities?: EntityRegistryEntry[];
|
||||
|
||||
private _deviceIntegrationLookup = memoizeOne(getDeviceIntegrationLookup);
|
||||
|
||||
public hassSubscribe(): UnsubscribeFunc[] {
|
||||
return [
|
||||
subscribeEntityRegistry(this.hass.connection!, (entities) => {
|
||||
this._entities = entities.filter((entity) => entity.device_id !== null);
|
||||
}),
|
||||
];
|
||||
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))
|
||||
);
|
||||
}
|
||||
|
||||
protected updated(changedProperties: PropertyValues): void {
|
||||
if (
|
||||
changedProperties.has("selector") &&
|
||||
(this.selector.area?.device?.integration ||
|
||||
this.selector.area?.entity?.integration) &&
|
||||
this._hasIntegration(this.selector) &&
|
||||
!this._entitySources
|
||||
) {
|
||||
fetchEntitySourcesWithCache(this.hass).then((sources) => {
|
||||
@@ -66,11 +62,7 @@ export class HaAreaSelector extends SubscribeMixin(LitElement) {
|
||||
}
|
||||
|
||||
protected render(): TemplateResult {
|
||||
if (
|
||||
(this.selector.area?.device?.integration ||
|
||||
this.selector.area?.entity?.integration) &&
|
||||
!this._entitySources
|
||||
) {
|
||||
if (this._hasIntegration(this.selector) && !this._entitySources) {
|
||||
return html``;
|
||||
}
|
||||
|
||||
@@ -110,10 +102,8 @@ export class HaAreaSelector extends SubscribeMixin(LitElement) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return filterSelectorEntities(
|
||||
this.selector.area.entity,
|
||||
entity,
|
||||
this._entitySources
|
||||
return ensureArray(this.selector.area.entity).some((filter) =>
|
||||
filterSelectorEntities(filter, entity, this._entitySources)
|
||||
);
|
||||
};
|
||||
|
||||
@@ -122,15 +112,15 @@ export class HaAreaSelector extends SubscribeMixin(LitElement) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const deviceIntegrations =
|
||||
this._entitySources && this._entities
|
||||
? this._deviceIntegrationLookup(this._entitySources, this._entities)
|
||||
: undefined;
|
||||
const deviceIntegrations = this._entitySources
|
||||
? this._deviceIntegrationLookup(
|
||||
this._entitySources,
|
||||
Object.values(this.hass.entities)
|
||||
)
|
||||
: undefined;
|
||||
|
||||
return filterSelectorDevices(
|
||||
this.selector.area.device,
|
||||
device,
|
||||
deviceIntegrations
|
||||
return ensureArray(this.selector.area.device).some((filter) =>
|
||||
filterSelectorDevices(filter, device, deviceIntegrations)
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
@@ -2,12 +2,11 @@ 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 SubscribeMixin(LitElement) {
|
||||
export class HaSelectorAttribute extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public selector!: AttributeSelector;
|
||||
|
||||
@@ -1,34 +1,31 @@
|
||||
import { UnsubscribeFunc } from "home-assistant-js-websocket";
|
||||
import { HassEntity } 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 } from "../../data/selector";
|
||||
import { SubscribeMixin } from "../../mixins/subscribe-mixin";
|
||||
import {
|
||||
filterSelectorDevices,
|
||||
filterSelectorEntities,
|
||||
} from "../../data/selector";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import "../device/ha-device-picker";
|
||||
import "../device/ha-devices-picker";
|
||||
|
||||
@customElement("ha-selector-device")
|
||||
export class HaDeviceSelector extends SubscribeMixin(LitElement) {
|
||||
export class HaDeviceSelector extends 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;
|
||||
@@ -41,19 +38,24 @@ export class HaDeviceSelector extends SubscribeMixin(LitElement) {
|
||||
|
||||
private _deviceIntegrationLookup = memoizeOne(getDeviceIntegrationLookup);
|
||||
|
||||
public hassSubscribe(): UnsubscribeFunc[] {
|
||||
return [
|
||||
subscribeEntityRegistry(this.hass.connection!, (entities) => {
|
||||
this._entities = entities.filter((entity) => entity.device_id !== null);
|
||||
}),
|
||||
];
|
||||
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
|
||||
))
|
||||
);
|
||||
}
|
||||
|
||||
protected updated(changedProperties): void {
|
||||
super.updated(changedProperties);
|
||||
if (
|
||||
changedProperties.has("selector") &&
|
||||
this.selector.device?.integration &&
|
||||
this._hasIntegration(this.selector) &&
|
||||
!this._entitySources
|
||||
) {
|
||||
fetchEntitySourcesWithCache(this.hass).then((sources) => {
|
||||
@@ -63,7 +65,7 @@ export class HaDeviceSelector extends SubscribeMixin(LitElement) {
|
||||
}
|
||||
|
||||
protected render() {
|
||||
if (this.selector.device?.integration && !this._entitySources) {
|
||||
if (this._hasIntegration(this.selector) && !this._entitySources) {
|
||||
return html``;
|
||||
}
|
||||
|
||||
@@ -75,12 +77,7 @@ export class HaDeviceSelector extends SubscribeMixin(LitElement) {
|
||||
.label=${this.label}
|
||||
.helper=${this.helper}
|
||||
.deviceFilter=${this._filterDevices}
|
||||
.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}
|
||||
.entityFilter=${this._filterEntities}
|
||||
.disabled=${this.disabled}
|
||||
.required=${this.required}
|
||||
allow-custom-entity
|
||||
@@ -95,12 +92,7 @@ export class HaDeviceSelector extends SubscribeMixin(LitElement) {
|
||||
.value=${this.value}
|
||||
.helper=${this.helper}
|
||||
.deviceFilter=${this._filterDevices}
|
||||
.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}
|
||||
.entityFilter=${this._filterEntities}
|
||||
.disabled=${this.disabled}
|
||||
.required=${this.required}
|
||||
></ha-devices-picker>
|
||||
@@ -108,18 +100,27 @@ export class HaDeviceSelector extends SubscribeMixin(LitElement) {
|
||||
}
|
||||
|
||||
private _filterDevices = (device: DeviceRegistryEntry): boolean => {
|
||||
const deviceIntegrations =
|
||||
this._entitySources && this._entities
|
||||
? this._deviceIntegrationLookup(this._entitySources, this._entities)
|
||||
: undefined;
|
||||
|
||||
if (!this.selector.device) {
|
||||
if (!this.selector.device?.filter) {
|
||||
return true;
|
||||
}
|
||||
return filterSelectorDevices(
|
||||
this.selector.device,
|
||||
device,
|
||||
deviceIntegrations
|
||||
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)
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
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,
|
||||
@@ -29,7 +30,18 @@ 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}
|
||||
@@ -64,7 +76,7 @@ export class HaEntitySelector extends LitElement {
|
||||
super.updated(changedProps);
|
||||
if (
|
||||
changedProps.has("selector") &&
|
||||
this.selector.entity?.integration &&
|
||||
this._hasIntegration(this.selector) &&
|
||||
!this._entitySources
|
||||
) {
|
||||
fetchEntitySourcesWithCache(this.hass).then((sources) => {
|
||||
@@ -74,13 +86,11 @@ export class HaEntitySelector extends LitElement {
|
||||
}
|
||||
|
||||
private _filterEntities = (entity: HassEntity): boolean => {
|
||||
if (!this.selector?.entity) {
|
||||
if (!this.selector?.entity?.filter) {
|
||||
return true;
|
||||
}
|
||||
return filterSelectorEntities(
|
||||
this.selector.entity,
|
||||
entity,
|
||||
this._entitySources
|
||||
return ensureArray(this.selector.entity.filter).some((filter) =>
|
||||
filterSelectorEntities(filter, entity, this._entitySources)
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
@@ -14,7 +14,6 @@ import {
|
||||
DeviceRegistryEntry,
|
||||
getDeviceIntegrationLookup,
|
||||
} from "../../data/device_registry";
|
||||
import { EntityRegistryEntry } from "../../data/entity_registry";
|
||||
import {
|
||||
EntitySources,
|
||||
fetchEntitySourcesWithCache,
|
||||
@@ -45,12 +44,24 @@ 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.selector.target?.device?.integration ||
|
||||
this.selector.target?.entity?.integration) &&
|
||||
this._hasIntegration(this.selector) &&
|
||||
!this._entitySources
|
||||
) {
|
||||
fetchEntitySourcesWithCache(this.hass).then((sources) => {
|
||||
@@ -60,11 +71,7 @@ export class HaTargetSelector extends LitElement {
|
||||
}
|
||||
|
||||
protected render(): TemplateResult {
|
||||
if (
|
||||
(this.selector.target?.device?.integration ||
|
||||
this.selector.target?.entity?.integration) &&
|
||||
!this._entitySources
|
||||
) {
|
||||
if (this._hasIntegration(this.selector) && !this._entitySources) {
|
||||
return html``;
|
||||
}
|
||||
|
||||
@@ -73,39 +80,21 @@ export class HaTargetSelector extends LitElement {
|
||||
.value=${this.value}
|
||||
.helper=${this.helper}
|
||||
.deviceFilter=${this._filterDevices}
|
||||
.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}
|
||||
.entityFilter=${this._filterEntities}
|
||||
.disabled=${this.disabled}
|
||||
></ha-target-picker>`;
|
||||
}
|
||||
|
||||
private _filterStates = (entity: HassEntity): boolean => {
|
||||
private _filterEntities = (entity: HassEntity): boolean => {
|
||||
if (!this.selector.target?.entity) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return filterSelectorEntities(
|
||||
this.selector.target.entity,
|
||||
entity,
|
||||
this._entitySources
|
||||
return ensureArray(this.selector.target.entity).some((filter) =>
|
||||
filterSelectorEntities(filter, 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;
|
||||
@@ -118,10 +107,8 @@ export class HaTargetSelector extends LitElement {
|
||||
)
|
||||
: undefined;
|
||||
|
||||
return filterSelectorDevices(
|
||||
this.selector.target.device,
|
||||
device,
|
||||
deviceIntegrations
|
||||
return ensureArray(this.selector.target.device).some((filter) =>
|
||||
filterSelectorDevices(filter, device, deviceIntegrations)
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -1,7 +1,12 @@
|
||||
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 type { Selector } from "../../data/selector";
|
||||
import {
|
||||
Selector,
|
||||
handleLegacyEntitySelector,
|
||||
handleLegacyDeviceSelector,
|
||||
} from "../../data/selector";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
|
||||
const LOAD_ELEMENTS = {
|
||||
@@ -75,12 +80,22 @@ 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.selector,
|
||||
selector: this._handleLegacySelector(this.selector),
|
||||
value: this.value,
|
||||
label: this.label,
|
||||
placeholder: this.placeholder,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { html, LitElement } from "lit";
|
||||
import { ComboBoxLitRenderer } from "@vaadin/combo-box/lit";
|
||||
import { property, state } from "lit/decorators";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import { LocalizeFunc } from "../common/translations/localize";
|
||||
@@ -17,6 +17,7 @@ const rowRenderer: ComboBoxLitRenderer<{ service: string; name: string }> = (
|
||||
>
|
||||
</mwc-list-item>`;
|
||||
|
||||
@customElement("ha-service-picker")
|
||||
class HaServicePicker extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@@ -113,8 +114,6 @@ class HaServicePicker extends LitElement {
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define("ha-service-picker", HaServicePicker);
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-service-picker": HaServicePicker;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
// @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,
|
||||
@@ -9,32 +10,22 @@ import {
|
||||
mdiUnfoldMoreVertical,
|
||||
} from "@mdi/js";
|
||||
import "@polymer/paper-tooltip/paper-tooltip";
|
||||
import {
|
||||
HassEntity,
|
||||
HassServiceTarget,
|
||||
UnsubscribeFunc,
|
||||
} from "home-assistant-js-websocket";
|
||||
import { ComboBoxLightOpenedChangedEvent } from "@vaadin/combo-box/vaadin-combo-box-light";
|
||||
import { HassEntity, HassServiceTarget } 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 { fireEvent } from "../common/dom/fire_event";
|
||||
import { ensureArray } from "../common/array/ensure-array";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import { stopPropagation } from "../common/dom/stop_propagation";
|
||||
import { computeDomain } from "../common/entity/compute_domain";
|
||||
import { computeStateName } from "../common/entity/compute_state_name";
|
||||
import {
|
||||
AreaRegistryEntry,
|
||||
subscribeAreaRegistry,
|
||||
} from "../data/area_registry";
|
||||
import { isValidEntityId } from "../common/entity/valid_entity_id";
|
||||
import {
|
||||
computeDeviceName,
|
||||
DeviceRegistryEntry,
|
||||
subscribeDeviceRegistry,
|
||||
} from "../data/device_registry";
|
||||
import {
|
||||
EntityRegistryEntry,
|
||||
subscribeEntityRegistry,
|
||||
} from "../data/entity_registry";
|
||||
import { SubscribeMixin } from "../mixins/subscribe-mixin";
|
||||
import { EntityRegistryDisplayEntry } from "../data/entity_registry";
|
||||
import { HomeAssistant } from "../types";
|
||||
import "./device/ha-device-picker";
|
||||
import type { HaDevicePickerDeviceFilterFunc } from "./device/ha-device-picker";
|
||||
@@ -46,7 +37,7 @@ import "./ha-input-helper-text";
|
||||
import "./ha-svg-icon";
|
||||
|
||||
@customElement("ha-target-picker")
|
||||
export class HaTargetPicker extends SubscribeMixin(LitElement) {
|
||||
export class HaTargetPicker extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public value?: HassServiceTarget;
|
||||
@@ -73,67 +64,25 @@ export class HaTargetPicker extends SubscribeMixin(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 horizontal = false;
|
||||
|
||||
@state() private _areas?: { [areaId: string]: AreaRegistryEntry };
|
||||
|
||||
@state() private _devices?: {
|
||||
[deviceId: string]: DeviceRegistryEntry;
|
||||
};
|
||||
|
||||
@state() private _entities?: EntityRegistryEntry[];
|
||||
@property({ type: Boolean }) public addOnTop = false;
|
||||
|
||||
@state() private _addMode?: "area_id" | "entity_id" | "device_id";
|
||||
|
||||
@query("#input") private _inputElement?;
|
||||
|
||||
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;
|
||||
}),
|
||||
];
|
||||
}
|
||||
@query(".add-container", true) private _addContainer?: HTMLDivElement;
|
||||
|
||||
private _opened = false;
|
||||
|
||||
protected render() {
|
||||
if (!this._areas || !this._devices || !this._entities) {
|
||||
return html``;
|
||||
if (this.addOnTop) {
|
||||
return html` ${this._renderChips()} ${this._renderItems()} `;
|
||||
}
|
||||
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>
|
||||
`}
|
||||
`;
|
||||
return html` ${this._renderItems()} ${this._renderChips()} `;
|
||||
}
|
||||
|
||||
private _renderItems() {
|
||||
@@ -141,7 +90,7 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
|
||||
<div class="mdc-chip-set items">
|
||||
${this.value?.area_id
|
||||
? ensureArray(this.value.area_id).map((area_id) => {
|
||||
const area = this._areas![area_id];
|
||||
const area = this.hass.areas![area_id];
|
||||
return this._renderChip(
|
||||
"area_id",
|
||||
area_id,
|
||||
@@ -153,7 +102,7 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
|
||||
: ""}
|
||||
${this.value?.device_id
|
||||
? ensureArray(this.value.device_id).map((device_id) => {
|
||||
const device = this._devices![device_id];
|
||||
const device = this.hass.devices![device_id];
|
||||
return this._renderChip(
|
||||
"device_id",
|
||||
device_id,
|
||||
@@ -180,7 +129,7 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
|
||||
|
||||
private _renderChips() {
|
||||
return html`
|
||||
<div class="mdc-chip-set">
|
||||
<div class="mdc-chip-set add-container">
|
||||
<div
|
||||
class="mdc-chip area_id add"
|
||||
.type=${"area_id"}
|
||||
@@ -241,6 +190,7 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
${this._renderPicker()}
|
||||
</div>
|
||||
${this.helper
|
||||
? html`<ha-input-helper-text>${this.helper}</ha-input-helper-text>`
|
||||
@@ -248,11 +198,8 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
|
||||
`;
|
||||
}
|
||||
|
||||
private async _showPicker(ev) {
|
||||
private _showPicker(ev) {
|
||||
this._addMode = ev.currentTarget.type;
|
||||
await this.updateComplete;
|
||||
await this._inputElement?.focus();
|
||||
await this._inputElement?.open();
|
||||
}
|
||||
|
||||
private _renderChip(
|
||||
@@ -287,7 +234,7 @@ export class HaTargetPicker extends SubscribeMixin(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"
|
||||
@@ -330,60 +277,72 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
|
||||
}
|
||||
|
||||
private _renderPicker() {
|
||||
switch (this._addMode) {
|
||||
case "area_id":
|
||||
return html`
|
||||
<ha-area-picker
|
||||
.hass=${this.hass}
|
||||
id="input"
|
||||
.type=${"area_id"}
|
||||
.label=${this.hass.localize(
|
||||
"ui.components.target-picker.add_area_id"
|
||||
)}
|
||||
no-add
|
||||
.deviceFilter=${this.deviceFilter}
|
||||
.entityFilter=${this.entityRegFilter}
|
||||
.includeDeviceClasses=${this.includeDeviceClasses}
|
||||
.includeDomains=${this.includeDomains}
|
||||
.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>
|
||||
`;
|
||||
if (!this._addMode) {
|
||||
return html``;
|
||||
}
|
||||
return html``;
|
||||
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
|
||||
>`;
|
||||
}
|
||||
|
||||
private _targetPicked(ev) {
|
||||
@@ -393,8 +352,12 @@ export class HaTargetPicker extends SubscribeMixin(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] &&
|
||||
@@ -419,7 +382,7 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
|
||||
const newDevices: string[] = [];
|
||||
const newEntities: string[] = [];
|
||||
if (target.type === "area_id") {
|
||||
Object.values(this._devices!).forEach((device) => {
|
||||
Object.values(this.hass.devices).forEach((device) => {
|
||||
if (
|
||||
device.area_id === target.id &&
|
||||
!this.value!.device_id?.includes(device.id) &&
|
||||
@@ -428,7 +391,7 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
|
||||
newDevices.push(device.id);
|
||||
}
|
||||
});
|
||||
this._entities!.forEach((entity) => {
|
||||
Object.values(this.hass.entities).forEach((entity) => {
|
||||
if (
|
||||
entity.area_id === target.id &&
|
||||
!this.value!.entity_id?.includes(entity.entity_id) &&
|
||||
@@ -438,7 +401,7 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
|
||||
}
|
||||
});
|
||||
} else if (target.type === "device_id") {
|
||||
this._entities!.forEach((entity) => {
|
||||
Object.values(this.hass.entities).forEach((entity) => {
|
||||
if (
|
||||
entity.device_id === target.id &&
|
||||
!this.value!.entity_id?.includes(entity.entity_id) &&
|
||||
@@ -501,10 +464,36 @@ export class HaTargetPicker extends SubscribeMixin(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 = this._entities?.filter(
|
||||
const devEntities = Object.values(this.hass.entities).filter(
|
||||
(entity) => entity.device_id === device.id
|
||||
);
|
||||
|
||||
if (this.includeDomains) {
|
||||
if (!devEntities || !devEntities.length) {
|
||||
return false;
|
||||
@@ -541,15 +530,32 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
|
||||
}
|
||||
|
||||
if (this.deviceFilter) {
|
||||
return this.deviceFilter(device);
|
||||
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 true;
|
||||
}
|
||||
|
||||
private _entityRegMeetsFilter(entity: EntityRegistryEntry): boolean {
|
||||
private _entityRegMeetsFilter(entity: EntityRegistryDisplayEntry): boolean {
|
||||
if (entity.entity_category) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (
|
||||
this.includeDomains &&
|
||||
!this.includeDomains.includes(computeDomain(entity.entity_id))
|
||||
@@ -568,8 +574,15 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if (this.entityRegFilter) {
|
||||
return this.entityRegFilter(entity);
|
||||
|
||||
if (this.entityFilter) {
|
||||
const stateObj = this.hass.states[entity.entity_id];
|
||||
if (!stateObj) {
|
||||
return false;
|
||||
}
|
||||
if (!this.entityFilter!(stateObj)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
@@ -577,12 +590,6 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
|
||||
static get styles(): CSSResultGroup {
|
||||
return css`
|
||||
${unsafeCSS(chipStyles)}
|
||||
.horizontal-container {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
min-height: 56px;
|
||||
align-items: center;
|
||||
}
|
||||
.mdc-chip {
|
||||
color: var(--primary-text-color);
|
||||
}
|
||||
@@ -595,6 +602,10 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
|
||||
.mdc-chip.add {
|
||||
color: rgba(0, 0, 0, 0.87);
|
||||
}
|
||||
.add-container {
|
||||
position: relative;
|
||||
display: inline-flex;
|
||||
}
|
||||
.mdc-chip:not(.add) {
|
||||
cursor: default;
|
||||
}
|
||||
@@ -666,6 +677,15 @@ export class HaTargetPicker extends SubscribeMixin(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%;
|
||||
}
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,15 +1,24 @@
|
||||
import { mdiLightbulbOutline } from "@mdi/js";
|
||||
import { css, html, LitElement } from "lit";
|
||||
import { customElement } from "lit/decorators";
|
||||
import { css, html, LitElement, TemplateResult } from "lit";
|
||||
import { property, customElement } from "lit/decorators";
|
||||
import { HomeAssistant } from "../types";
|
||||
|
||||
import "./ha-svg-icon";
|
||||
|
||||
@customElement("ha-tip")
|
||||
class HaTip extends LitElement {
|
||||
public render() {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
public render(): TemplateResult {
|
||||
if (!this.hass) {
|
||||
return html``;
|
||||
}
|
||||
|
||||
return html`
|
||||
<ha-svg-icon .path=${mdiLightbulbOutline}></ha-svg-icon>
|
||||
<span class="prefix">Tip!</span>
|
||||
<span class="prefix"
|
||||
>${this.hass.localize("ui.panel.config.tips.tip")}</span
|
||||
>
|
||||
<span class="text"><slot></slot></span>
|
||||
`;
|
||||
}
|
||||
@@ -21,7 +30,10 @@ class HaTip extends LitElement {
|
||||
}
|
||||
|
||||
.text {
|
||||
direction: var(--direction);
|
||||
margin-left: 2px;
|
||||
margin-inline-start: 2px;
|
||||
margin-inline-end: initial;
|
||||
color: var(--secondary-text-color);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
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;
|
||||
|
||||
@@ -34,5 +36,3 @@ declare global {
|
||||
"ha-toast": HaToast;
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define("ha-toast", HaToast);
|
||||
|
||||
@@ -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-bar-slider";
|
||||
import "../ha-control-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-bar-slider
|
||||
<ha-control-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-bar-slider>
|
||||
</ha-control-slider>
|
||||
`;
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return css`
|
||||
ha-bar-slider {
|
||||
--slider-bar-color: var(--tile-slider-color, var(--primary-color));
|
||||
--slider-bar-background: var(
|
||||
ha-control-slider {
|
||||
--control-slider-color: var(--tile-slider-color, var(--primary-color));
|
||||
--control-slider-background: var(
|
||||
--tile-slider-background,
|
||||
var(--disabled-color)
|
||||
);
|
||||
--slider-bar-background-opacity: var(
|
||||
--control-slider-background-opacity: var(
|
||||
--tile-slider-background-opacity,
|
||||
0.2
|
||||
);
|
||||
--slider-bar-thickness: 40px;
|
||||
--slider-bar-border-radius: 10px;
|
||||
--control-slider-thickness: 40px;
|
||||
--control-slider-border-radius: 10px;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
mdiProgressWrench,
|
||||
mdiRecordCircleOutline,
|
||||
} from "@mdi/js";
|
||||
import { UnsubscribeFunc } from "home-assistant-js-websocket";
|
||||
import {
|
||||
css,
|
||||
CSSResultGroup,
|
||||
@@ -14,12 +15,16 @@ import {
|
||||
PropertyValues,
|
||||
TemplateResult,
|
||||
} from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { customElement, property, state } 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,
|
||||
@@ -193,6 +198,7 @@ class ActionRenderer {
|
||||
|
||||
constructor(
|
||||
private hass: HomeAssistant,
|
||||
private entityReg: EntityRegistryEntry[],
|
||||
private entries: TemplateResult[],
|
||||
private trace: AutomationTraceExtended,
|
||||
private logbookRenderer: LogbookRenderer,
|
||||
@@ -298,7 +304,7 @@ class ActionRenderer {
|
||||
|
||||
this._renderEntry(
|
||||
path,
|
||||
describeAction(this.hass, data, actionType),
|
||||
describeAction(this.hass, this.entityReg, data, actionType),
|
||||
undefined,
|
||||
data.enabled === false
|
||||
);
|
||||
@@ -441,7 +447,9 @@ class ActionRenderer {
|
||||
) as RepeatAction;
|
||||
const disabled = repeatConfig.enabled === false;
|
||||
|
||||
const name = repeatConfig.alias || describeAction(this.hass, repeatConfig);
|
||||
const name =
|
||||
repeatConfig.alias ||
|
||||
describeAction(this.hass, this.entityReg, repeatConfig);
|
||||
|
||||
this._renderEntry(repeatPath, name, undefined, disabled);
|
||||
|
||||
@@ -577,6 +585,16 @@ 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``;
|
||||
@@ -592,6 +610,7 @@ export class HaAutomationTracer extends LitElement {
|
||||
);
|
||||
const actionRenderer = new ActionRenderer(
|
||||
this.hass,
|
||||
this._entityReg,
|
||||
entries,
|
||||
this.trace,
|
||||
logbookRenderer,
|
||||
|
||||
@@ -3,6 +3,15 @@ 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,
|
||||
|
||||
@@ -146,6 +146,7 @@ export interface TimeTrigger extends BaseTrigger {
|
||||
export interface TemplateTrigger extends BaseTrigger {
|
||||
platform: "template";
|
||||
value_template: string;
|
||||
for?: string | number | ForDict;
|
||||
}
|
||||
|
||||
export interface EventTrigger extends BaseTrigger {
|
||||
|
||||
@@ -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 } from "./automation";
|
||||
import { Condition, Trigger, ForDict } from "./automation";
|
||||
import {
|
||||
DeviceCondition,
|
||||
DeviceTrigger,
|
||||
@@ -12,6 +12,18 @@ 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,
|
||||
@@ -73,14 +85,7 @@ export const describeTrigger = (
|
||||
}
|
||||
|
||||
if (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);
|
||||
}
|
||||
const duration = describeDuration(trigger.for);
|
||||
if (duration) {
|
||||
base += ` for ${duration}`;
|
||||
}
|
||||
@@ -156,15 +161,7 @@ export const describeTrigger = (
|
||||
}
|
||||
|
||||
if (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);
|
||||
}
|
||||
|
||||
const duration = describeDuration(trigger.for);
|
||||
if (duration) {
|
||||
base += ` for ${duration}`;
|
||||
}
|
||||
@@ -319,7 +316,14 @@ export const describeTrigger = (
|
||||
|
||||
// Template Trigger
|
||||
if (trigger.platform === "template") {
|
||||
return "When a template triggers";
|
||||
let base = "When a template triggers";
|
||||
if (trigger.for) {
|
||||
const duration = describeDuration(trigger.for);
|
||||
if (duration) {
|
||||
base += ` for ${duration}`;
|
||||
}
|
||||
}
|
||||
return base;
|
||||
}
|
||||
|
||||
// Webhook Trigger
|
||||
@@ -440,14 +444,7 @@ export const describeCondition = (
|
||||
base += ` ${entity} is ${states}`;
|
||||
|
||||
if (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);
|
||||
}
|
||||
const duration = describeDuration(condition.for);
|
||||
if (duration) {
|
||||
base += ` for ${duration}`;
|
||||
}
|
||||
|
||||
@@ -4,7 +4,10 @@ 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 { EntityRegistryEntry } from "./entity_registry";
|
||||
import type {
|
||||
EntityRegistryDisplayEntry,
|
||||
EntityRegistryEntry,
|
||||
} from "./entity_registry";
|
||||
import type { EntitySources } from "./entity_sources";
|
||||
|
||||
export interface DeviceRegistryEntry {
|
||||
@@ -25,6 +28,10 @@ export interface DeviceRegistryEntry {
|
||||
configuration_url: string | null;
|
||||
}
|
||||
|
||||
export interface DeviceEntityDisplayLookup {
|
||||
[deviceId: string]: EntityRegistryDisplayEntry[];
|
||||
}
|
||||
|
||||
export interface DeviceEntityLookup {
|
||||
[deviceId: string]: EntityRegistryEntry[];
|
||||
}
|
||||
@@ -147,9 +154,25 @@ 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: EntityRegistryEntry[]
|
||||
entities: EntityRegistryDisplayEntry[]
|
||||
): Record<string, string[]> => {
|
||||
const deviceIntegrations: Record<string, string[]> = {};
|
||||
|
||||
|
||||
@@ -11,10 +11,8 @@ import {
|
||||
} from "date-fns/esm";
|
||||
import { Collection, getCollection } from "home-assistant-js-websocket";
|
||||
import { groupBy } from "../common/util/group-by";
|
||||
import { subscribeOne } from "../common/util/subscribe-one";
|
||||
import { HomeAssistant } from "../types";
|
||||
import { ConfigEntry, getConfigEntries } from "./config_entries";
|
||||
import { subscribeEntityRegistry } from "./entity_registry";
|
||||
import {
|
||||
fetchStatistics,
|
||||
getStatisticMetadata,
|
||||
@@ -195,6 +193,12 @@ export interface EnergyPreferencesValidation {
|
||||
device_consumption: EnergyValidationIssue[][];
|
||||
}
|
||||
|
||||
export interface EnergyInfoAndCO2Signal {
|
||||
energyInfo: EnergyInfo;
|
||||
co2SignalEntity: string | undefined;
|
||||
co2SignalConfigEntry: ConfigEntry | undefined;
|
||||
}
|
||||
|
||||
export const getEnergyInfo = (hass: HomeAssistant) =>
|
||||
hass.callWS<EnergyInfo>({
|
||||
type: "energy/info",
|
||||
@@ -334,16 +338,11 @@ export const getReferencedStatisticIds = (
|
||||
return statIDs;
|
||||
};
|
||||
|
||||
const getEnergyData = async (
|
||||
hass: HomeAssistant,
|
||||
prefs: EnergyPreferences,
|
||||
start: Date,
|
||||
end?: Date,
|
||||
compare?: boolean
|
||||
): Promise<EnergyData> => {
|
||||
const [configEntries, entityRegistryEntries, info] = await Promise.all([
|
||||
const getEnergyInfoAndCO2Signal = async (
|
||||
hass: HomeAssistant
|
||||
): Promise<EnergyInfoAndCO2Signal> => {
|
||||
const [configEntries, info] = await Promise.all([
|
||||
getConfigEntries(hass, { domain: "co2signal" }),
|
||||
subscribeOne(hass.connection, subscribeEntityRegistry),
|
||||
getEnergyInfo(hass),
|
||||
]);
|
||||
|
||||
@@ -352,15 +351,14 @@ const getEnergyData = async (
|
||||
: undefined;
|
||||
|
||||
let co2SignalEntity: string | undefined;
|
||||
|
||||
if (co2SignalConfigEntry) {
|
||||
for (const entry of entityRegistryEntries) {
|
||||
if (entry.config_entry_id !== co2SignalConfigEntry.entry_id) {
|
||||
for (const entity of Object.values(hass.entities)) {
|
||||
if (entity.platform !== "co2signal") {
|
||||
continue;
|
||||
}
|
||||
|
||||
// The integration offers 2 entities. We want the % one.
|
||||
const co2State = hass.states[entry.entity_id];
|
||||
const co2State = hass.states[entity.entity_id];
|
||||
if (!co2State || co2State.attributes.unit_of_measurement !== "%") {
|
||||
continue;
|
||||
}
|
||||
@@ -370,6 +368,24 @@ const getEnergyData = async (
|
||||
}
|
||||
}
|
||||
|
||||
return <EnergyInfoAndCO2Signal>{
|
||||
energyInfo: info,
|
||||
co2SignalEntity: co2SignalEntity,
|
||||
co2SignalConfigEntry: co2SignalConfigEntry,
|
||||
};
|
||||
};
|
||||
|
||||
const getEnergyDataWithInfo = async (
|
||||
hass: HomeAssistant,
|
||||
energyInfoAndCO2Signal: EnergyInfoAndCO2Signal,
|
||||
prefs: EnergyPreferences,
|
||||
start: Date,
|
||||
end?: Date,
|
||||
compare?: boolean
|
||||
): Promise<EnergyData> => {
|
||||
const info = energyInfoAndCO2Signal.energyInfo;
|
||||
const co2SignalEntity = energyInfoAndCO2Signal.co2SignalEntity;
|
||||
const co2SignalConfigEntry = energyInfoAndCO2Signal.co2SignalConfigEntry;
|
||||
const consumptionStatIDs: string[] = [];
|
||||
for (const source of prefs.energy_sources) {
|
||||
// grid source
|
||||
@@ -405,34 +421,35 @@ const getEnergyData = async (
|
||||
volume: lengthUnit === "km" ? "L" : "gal",
|
||||
};
|
||||
|
||||
const stats = {
|
||||
...(energyStatIds.length
|
||||
? await fetchStatistics(
|
||||
hass!,
|
||||
startMinHour,
|
||||
end,
|
||||
energyStatIds,
|
||||
period,
|
||||
energyUnits,
|
||||
["sum"]
|
||||
)
|
||||
: {}),
|
||||
...(waterStatIds.length
|
||||
? await fetchStatistics(
|
||||
hass!,
|
||||
startMinHour,
|
||||
end,
|
||||
waterStatIds,
|
||||
period,
|
||||
waterUnits,
|
||||
["sum"]
|
||||
)
|
||||
: {}),
|
||||
};
|
||||
const _energyStats: Statistics | Promise<Statistics> = energyStatIds.length
|
||||
? fetchStatistics(
|
||||
hass!,
|
||||
startMinHour,
|
||||
end,
|
||||
energyStatIds,
|
||||
period,
|
||||
energyUnits,
|
||||
["sum"]
|
||||
)
|
||||
: {};
|
||||
const _waterStats: Statistics | Promise<Statistics> = waterStatIds.length
|
||||
? fetchStatistics(
|
||||
hass!,
|
||||
startMinHour,
|
||||
end,
|
||||
waterStatIds,
|
||||
period,
|
||||
waterUnits,
|
||||
["sum"]
|
||||
)
|
||||
: {};
|
||||
|
||||
let statsCompare;
|
||||
let startCompare;
|
||||
let endCompare;
|
||||
let _energyStatsCompare: Statistics | Promise<Statistics> = {};
|
||||
let _waterStatsCompare: Statistics | Promise<Statistics> = {};
|
||||
|
||||
if (compare) {
|
||||
if (dayDifference > 27 && dayDifference < 32) {
|
||||
// When comparing a month, we want to start at the begining of the month
|
||||
@@ -443,38 +460,36 @@ const getEnergyData = async (
|
||||
|
||||
const compareStartMinHour = addHours(startCompare, -1);
|
||||
endCompare = addMilliseconds(start, -1);
|
||||
|
||||
statsCompare = {
|
||||
...(energyStatIds.length
|
||||
? await fetchStatistics(
|
||||
hass!,
|
||||
compareStartMinHour,
|
||||
endCompare,
|
||||
energyStatIds,
|
||||
period,
|
||||
energyUnits,
|
||||
["sum"]
|
||||
)
|
||||
: {}),
|
||||
...(waterStatIds.length
|
||||
? await fetchStatistics(
|
||||
hass!,
|
||||
compareStartMinHour,
|
||||
endCompare,
|
||||
waterStatIds,
|
||||
period,
|
||||
waterUnits,
|
||||
["sum"]
|
||||
)
|
||||
: {}),
|
||||
};
|
||||
if (energyStatIds.length) {
|
||||
_energyStatsCompare = fetchStatistics(
|
||||
hass!,
|
||||
compareStartMinHour,
|
||||
endCompare,
|
||||
energyStatIds,
|
||||
period,
|
||||
energyUnits,
|
||||
["sum"]
|
||||
);
|
||||
}
|
||||
if (waterStatIds.length) {
|
||||
_waterStatsCompare = fetchStatistics(
|
||||
hass!,
|
||||
compareStartMinHour,
|
||||
endCompare,
|
||||
waterStatIds,
|
||||
period,
|
||||
waterUnits,
|
||||
["sum"]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
let fossilEnergyConsumption: FossilEnergyConsumption | undefined;
|
||||
let fossilEnergyConsumptionCompare: FossilEnergyConsumption | undefined;
|
||||
|
||||
let _fossilEnergyConsumption: undefined | Promise<FossilEnergyConsumption>;
|
||||
let _fossilEnergyConsumptionCompare:
|
||||
| undefined
|
||||
| Promise<FossilEnergyConsumption>;
|
||||
if (co2SignalEntity !== undefined) {
|
||||
fossilEnergyConsumption = await getFossilEnergyConsumption(
|
||||
_fossilEnergyConsumption = getFossilEnergyConsumption(
|
||||
hass!,
|
||||
start,
|
||||
consumptionStatIDs,
|
||||
@@ -483,7 +498,7 @@ const getEnergyData = async (
|
||||
dayDifference > 35 ? "month" : dayDifference > 2 ? "day" : "hour"
|
||||
);
|
||||
if (compare) {
|
||||
fossilEnergyConsumptionCompare = await getFossilEnergyConsumption(
|
||||
_fossilEnergyConsumptionCompare = getFossilEnergyConsumption(
|
||||
hass!,
|
||||
startCompare,
|
||||
consumptionStatIDs,
|
||||
@@ -494,6 +509,39 @@ const getEnergyData = async (
|
||||
}
|
||||
}
|
||||
|
||||
const statsMetadata: Record<string, StatisticsMetaData> = {};
|
||||
const _getStatisticMetadata:
|
||||
| Promise<StatisticsMetaData[]>
|
||||
| StatisticsMetaData[] = allStatIDs.length
|
||||
? getStatisticMetadata(hass, allStatIDs)
|
||||
: [];
|
||||
const [
|
||||
energyStats,
|
||||
waterStats,
|
||||
energyStatsCompare,
|
||||
waterStatsCompare,
|
||||
statsMetadataArray,
|
||||
fossilEnergyConsumption,
|
||||
fossilEnergyConsumptionCompare,
|
||||
] = await Promise.all([
|
||||
_energyStats,
|
||||
_waterStats,
|
||||
_energyStatsCompare,
|
||||
_waterStatsCompare,
|
||||
_getStatisticMetadata,
|
||||
_fossilEnergyConsumption,
|
||||
_fossilEnergyConsumptionCompare,
|
||||
]);
|
||||
const stats = { ...energyStats, ...waterStats };
|
||||
if (compare) {
|
||||
statsCompare = { ...energyStatsCompare, ...waterStatsCompare };
|
||||
}
|
||||
if (allStatIDs.length) {
|
||||
statsMetadataArray.forEach((x) => {
|
||||
statsMetadata[x.statistic_id] = x;
|
||||
});
|
||||
}
|
||||
|
||||
Object.values(stats).forEach((stat) => {
|
||||
// if the start of the first value is after the requested period, we have the first data point, and should add a zero point
|
||||
if (stat.length && new Date(stat[0].start) > startMinHour) {
|
||||
@@ -507,12 +555,6 @@ const getEnergyData = async (
|
||||
}
|
||||
});
|
||||
|
||||
const statsMetadataArray = await getStatisticMetadata(hass, allStatIDs);
|
||||
const statsMetadata: Record<string, StatisticsMetaData> = {};
|
||||
statsMetadataArray.forEach((x) => {
|
||||
statsMetadata[x.statistic_id] = x;
|
||||
});
|
||||
|
||||
const data: EnergyData = {
|
||||
start,
|
||||
end,
|
||||
@@ -573,6 +615,9 @@ export const getEnergyDataCollection = (
|
||||
|
||||
energyCollectionKeys.push(options.key);
|
||||
|
||||
let energyInfoAndCO2Signal: EnergyInfoAndCO2Signal | undefined;
|
||||
let forceRefreshEnergyInfo = false;
|
||||
|
||||
const collection = getCollection<EnergyData>(
|
||||
hass.connection,
|
||||
key,
|
||||
@@ -600,14 +645,20 @@ export const getEnergyDataCollection = (
|
||||
}
|
||||
nextFetch.setMinutes(20, 0, 0);
|
||||
|
||||
collection._refreshTimeout = window.setTimeout(
|
||||
() => collection.refresh(),
|
||||
nextFetch.getTime() - Date.now()
|
||||
);
|
||||
collection._refreshTimeout = window.setTimeout(() => {
|
||||
forceRefreshEnergyInfo = true;
|
||||
collection.refresh();
|
||||
}, nextFetch.getTime() - Date.now());
|
||||
}
|
||||
|
||||
return getEnergyData(
|
||||
if (!energyInfoAndCO2Signal || forceRefreshEnergyInfo) {
|
||||
energyInfoAndCO2Signal = await getEnergyInfoAndCO2Signal(hass);
|
||||
forceRefreshEnergyInfo = false;
|
||||
}
|
||||
|
||||
return getEnergyDataWithInfo(
|
||||
hass,
|
||||
energyInfoAndCO2Signal,
|
||||
collection.prefs,
|
||||
collection.start,
|
||||
collection.end,
|
||||
|
||||
@@ -6,6 +6,35 @@ 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;
|
||||
@@ -17,7 +46,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: "config" | "diagnostic" | null;
|
||||
entity_category: entityCategory | null;
|
||||
has_entity_name: boolean;
|
||||
original_name?: string;
|
||||
unique_id: string;
|
||||
@@ -154,6 +183,11 @@ 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[]>
|
||||
@@ -182,6 +216,34 @@ 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
|
||||
@@ -190,10 +252,20 @@ 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: HomeAssistant["entities"]) => {
|
||||
const entities: HomeAssistant["entities"] = {};
|
||||
for (const entity of Object.values(entries)) {
|
||||
(entries: EntityRegistryEntry[]) => {
|
||||
const entities: Record<string, EntityRegistryEntry> = {};
|
||||
for (const entity of entries) {
|
||||
entities[entity.id] = entity;
|
||||
}
|
||||
return entities;
|
||||
|
||||
@@ -2,6 +2,7 @@ import {
|
||||
HassEntityAttributeBase,
|
||||
HassEntityBase,
|
||||
} from "home-assistant-js-websocket";
|
||||
import { computeDomain } from "../common/entity/compute_domain";
|
||||
|
||||
interface GroupEntityAttributes extends HassEntityAttributeBase {
|
||||
entity_id: string[];
|
||||
@@ -13,3 +14,13 @@ 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;
|
||||
};
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import type { HassEntity } from "home-assistant-js-websocket";
|
||||
|
||||
export interface CustomCardEntry {
|
||||
type: string;
|
||||
name?: string;
|
||||
@@ -6,8 +8,16 @@ 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:";
|
||||
@@ -17,8 +27,18 @@ 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);
|
||||
|
||||
@@ -73,7 +73,9 @@ export interface MediaPlayerEntity extends HassEntityBase {
|
||||
| "off"
|
||||
| "on"
|
||||
| "unavailable"
|
||||
| "unknown";
|
||||
| "unknown"
|
||||
| "standby"
|
||||
| "buffering";
|
||||
}
|
||||
|
||||
export const enum MediaPlayerEntityFeature {
|
||||
|
||||
@@ -11,6 +11,7 @@ import { computeDeviceName } from "./device_registry";
|
||||
import {
|
||||
computeEntityRegistryName,
|
||||
entityRegistryById,
|
||||
EntityRegistryEntry,
|
||||
} from "./entity_registry";
|
||||
import { domainToName } from "./integration";
|
||||
import {
|
||||
@@ -33,6 +34,7 @@ import {
|
||||
|
||||
export const describeAction = <T extends ActionType>(
|
||||
hass: HomeAssistant,
|
||||
entityRegistry: EntityRegistryEntry[],
|
||||
action: ActionTypes[T],
|
||||
actionType?: T,
|
||||
ignoreAlias = false
|
||||
@@ -91,7 +93,7 @@ export const describeAction = <T extends ActionType>(
|
||||
targets.push(targetThing);
|
||||
}
|
||||
} else {
|
||||
const entityReg = entityRegistryById(hass.entities)[targetThing];
|
||||
const entityReg = entityRegistryById(entityRegistry)[targetThing];
|
||||
if (entityReg) {
|
||||
targets.push(
|
||||
computeEntityRegistryName(hass, entityReg) || targetThing
|
||||
|
||||
@@ -16,8 +16,10 @@ export type Selector =
|
||||
| DateSelector
|
||||
| DateTimeSelector
|
||||
| DeviceSelector
|
||||
| LegacyDeviceSelector
|
||||
| DurationSelector
|
||||
| EntitySelector
|
||||
| LegacyEntitySelector
|
||||
| FileSelector
|
||||
| IconSelector
|
||||
| LocationSelector
|
||||
@@ -48,22 +50,10 @@ 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?: SelectorEntity;
|
||||
device?: SelectorDevice;
|
||||
entity?: EntitySelectorFilter | readonly EntitySelectorFilter[];
|
||||
device?: DeviceSelectorFilter | readonly DeviceSelectorFilter[];
|
||||
multiple?: boolean;
|
||||
} | null;
|
||||
}
|
||||
@@ -108,33 +98,77 @@ export interface DateTimeSelector {
|
||||
datetime: {} | null;
|
||||
}
|
||||
|
||||
interface DeviceSelectorFilter {
|
||||
integration?: string;
|
||||
manufacturer?: string;
|
||||
model?: string;
|
||||
}
|
||||
|
||||
export interface DeviceSelector {
|
||||
device: {
|
||||
integration?: string;
|
||||
manufacturer?: string;
|
||||
model?: string;
|
||||
entity?: SelectorEntity;
|
||||
filter?: DeviceSelectorFilter | readonly DeviceSelectorFilter[];
|
||||
entity?: EntitySelectorFilter | readonly EntitySelectorFilter[];
|
||||
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;
|
||||
@@ -250,8 +284,8 @@ export interface StringSelector {
|
||||
|
||||
export interface TargetSelector {
|
||||
target: {
|
||||
entity?: SelectorEntity;
|
||||
device?: SelectorDevice;
|
||||
entity?: EntitySelectorFilter | readonly EntitySelectorFilter[];
|
||||
device?: DeviceSelectorFilter | readonly DeviceSelectorFilter[];
|
||||
} | null;
|
||||
}
|
||||
|
||||
@@ -281,7 +315,7 @@ export interface UiColorSelector {
|
||||
}
|
||||
|
||||
export const filterSelectorDevices = (
|
||||
filterDevice: SelectorDevice,
|
||||
filterDevice: DeviceSelectorFilter,
|
||||
device: DeviceRegistryEntry,
|
||||
deviceIntegrationLookup: Record<string, string[]> | undefined
|
||||
): boolean => {
|
||||
@@ -308,7 +342,7 @@ export const filterSelectorDevices = (
|
||||
};
|
||||
|
||||
export const filterSelectorEntities = (
|
||||
filterEntity: SelectorEntity,
|
||||
filterEntity: EntitySelectorFilter,
|
||||
entity: HassEntity,
|
||||
entitySources?: EntitySources
|
||||
): boolean => {
|
||||
@@ -329,11 +363,15 @@ export const filterSelectorEntities = (
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
filterDeviceClass &&
|
||||
entity.attributes.device_class !== filterDeviceClass
|
||||
) {
|
||||
return false;
|
||||
if (filterDeviceClass) {
|
||||
const entityDeviceClass = entity.attributes.device_class;
|
||||
if (
|
||||
entityDeviceClass && Array.isArray(filterDeviceClass)
|
||||
? !filterDeviceClass.includes(entityDeviceClass)
|
||||
: entityDeviceClass !== filterDeviceClass
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
@@ -345,3 +383,59 @@ 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,
|
||||
};
|
||||
};
|
||||
|
||||
89
src/data/thread.ts
Normal file
89
src/data/thread.ts
Normal file
@@ -0,0 +1,89 @@
|
||||
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,
|
||||
});
|
||||
@@ -150,7 +150,9 @@ 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();
|
||||
|
||||
|
||||
@@ -92,7 +92,23 @@ enum NodeType {
|
||||
"End Node" = 1,
|
||||
}
|
||||
|
||||
export enum FirmwareUpdateStatus {
|
||||
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 {
|
||||
Error_Timeout = -1,
|
||||
Error_Checksum = 0,
|
||||
Error_TransmissionFailed = 1,
|
||||
@@ -108,6 +124,19 @@ export enum FirmwareUpdateStatus {
|
||||
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[];
|
||||
@@ -149,6 +178,7 @@ 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;
|
||||
@@ -176,6 +206,7 @@ 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 {
|
||||
@@ -304,7 +335,7 @@ export interface ZWaveJSNodeStatusUpdatedMessage {
|
||||
status: NodeStatus;
|
||||
}
|
||||
|
||||
export interface ZWaveJSNodeFirmwareUpdateProgressMessage {
|
||||
export interface ZWaveJSFirmwareUpdateProgressMessage {
|
||||
event: "firmware update progress";
|
||||
current_file: number;
|
||||
total_files: number;
|
||||
@@ -315,12 +346,18 @@ export interface ZWaveJSNodeFirmwareUpdateProgressMessage {
|
||||
|
||||
export interface ZWaveJSNodeFirmwareUpdateFinishedMessage {
|
||||
event: "firmware update finished";
|
||||
status: FirmwareUpdateStatus;
|
||||
status: NodeFirmwareUpdateStatus;
|
||||
success: boolean;
|
||||
wait_time?: number;
|
||||
reinterview: boolean;
|
||||
}
|
||||
|
||||
export interface ZWaveJSControllerFirmwareUpdateFinishedMessage {
|
||||
event: "firmware update finished";
|
||||
status: ControllerFirmwareUpdateStatus;
|
||||
success: boolean;
|
||||
}
|
||||
|
||||
export type ZWaveJSNodeFirmwareUpdateCapabilities =
|
||||
| { firmware_upgradable: false }
|
||||
| {
|
||||
@@ -422,7 +459,8 @@ export const subscribeAddZwaveNode = (
|
||||
inclusion_strategy: InclusionStrategy = InclusionStrategy.Default,
|
||||
qr_provisioning_information?: QRProvisioningInformation,
|
||||
qr_code_string?: string,
|
||||
planned_provisioning_entry?: PlannedProvisioningEntry
|
||||
planned_provisioning_entry?: PlannedProvisioningEntry,
|
||||
dsk?: string
|
||||
): Promise<UnsubscribeFunc> =>
|
||||
hass.connection.subscribeMessage((message) => callbackFunction(message), {
|
||||
type: "zwave_js/add_node",
|
||||
@@ -431,6 +469,7 @@ export const subscribeAddZwaveNode = (
|
||||
qr_code_string,
|
||||
qr_provisioning_information,
|
||||
planned_provisioning_entry,
|
||||
dsk,
|
||||
});
|
||||
|
||||
export const stopZwaveInclusion = (hass: HomeAssistant, entry_id: string) =>
|
||||
@@ -458,6 +497,17 @@ 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,
|
||||
@@ -707,10 +757,14 @@ export const fetchZwaveNodeFirmwareUpdateCapabilities = (
|
||||
export const uploadFirmwareAndBeginUpdate = async (
|
||||
hass: HomeAssistant,
|
||||
device_id: string,
|
||||
file: File
|
||||
file: File,
|
||||
target?: number
|
||||
) => {
|
||||
const fd = new FormData();
|
||||
fd.append("file", file);
|
||||
if (target !== undefined) {
|
||||
fd.append("target", target.toString());
|
||||
}
|
||||
const resp = await hass.fetchWithAuth(
|
||||
`/api/zwave_js/firmware/upload/${device_id}`,
|
||||
{
|
||||
@@ -729,8 +783,9 @@ export const subscribeZwaveNodeFirmwareUpdate = (
|
||||
device_id: string,
|
||||
callbackFunction: (
|
||||
message:
|
||||
| ZWaveJSFirmwareUpdateProgressMessage
|
||||
| ZWaveJSControllerFirmwareUpdateFinishedMessage
|
||||
| ZWaveJSNodeFirmwareUpdateFinishedMessage
|
||||
| ZWaveJSNodeFirmwareUpdateProgressMessage
|
||||
) => void
|
||||
): Promise<UnsubscribeFunc> =>
|
||||
hass.connection.subscribeMessage(
|
||||
|
||||
@@ -101,6 +101,19 @@ 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(
|
||||
|
||||
@@ -67,6 +67,11 @@ export interface FlowConfig {
|
||||
key: string
|
||||
): string;
|
||||
|
||||
renderShowFormStepSubmitButton(
|
||||
hass: HomeAssistant,
|
||||
step: DataEntryFlowStepForm
|
||||
): string;
|
||||
|
||||
renderExternalStepHeader(
|
||||
hass: HomeAssistant,
|
||||
step: DataEntryFlowStepExternal
|
||||
|
||||
@@ -115,6 +115,19 @@ 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 "";
|
||||
},
|
||||
|
||||
@@ -70,10 +70,9 @@ class StepFlowForm extends LitElement {
|
||||
: html`
|
||||
<div>
|
||||
<mwc-button @click=${this._submitStep}>
|
||||
${this.hass.localize(
|
||||
`ui.panel.config.integrations.config_flow.${
|
||||
this.step.last_step === false ? "next" : "submit"
|
||||
}`
|
||||
${this.flowConfig.renderShowFormStepSubmitButton(
|
||||
this.hass,
|
||||
this.step
|
||||
)}
|
||||
</mwc-button>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
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%;
|
||||
}
|
||||
`;
|
||||
@@ -0,0 +1,85 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
167
src/dialogs/more-info/components/ha-more-info-toggle.ts
Normal file
167
src/dialogs/more-info/components/ha-more-info-toggle.ts
Normal file
@@ -0,0 +1,167 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,553 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
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,
|
||||
});
|
||||
};
|
||||
@@ -1,5 +1,7 @@
|
||||
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";
|
||||
|
||||
@@ -13,7 +15,8 @@ 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",
|
||||
@@ -34,7 +37,9 @@ export const DOMAINS_WITH_MORE_INFO = [
|
||||
"remote",
|
||||
"script",
|
||||
"scene",
|
||||
"siren",
|
||||
"sun",
|
||||
"switch",
|
||||
"timer",
|
||||
"update",
|
||||
"vacuum",
|
||||
@@ -88,3 +93,16 @@ 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);
|
||||
};
|
||||
|
||||
@@ -1,18 +1,19 @@
|
||||
import "@material/mwc-button";
|
||||
import type { HassEntity } from "home-assistant-js-websocket";
|
||||
import { css, html, LitElement, TemplateResult } from "lit";
|
||||
import { customElement, property, query } from "lit/decorators";
|
||||
import { css, html, LitElement, PropertyValues, TemplateResult } from "lit";
|
||||
import { customElement, property, state, 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")
|
||||
@@ -21,8 +22,51 @@ 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``;
|
||||
@@ -72,7 +116,7 @@ export class MoreInfoAlarmControlPanel extends LitElement {
|
||||
`}
|
||||
<div class="actions">
|
||||
${(this.stateObj.state === "disarmed"
|
||||
? ARM_ACTIONS
|
||||
? this._armActions
|
||||
: DISARM_ACTIONS
|
||||
).map(
|
||||
(stateAction) => html`
|
||||
|
||||
@@ -467,7 +467,9 @@ 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) {
|
||||
|
||||
@@ -9,10 +9,10 @@ import {
|
||||
} from "lit";
|
||||
import { property, state } from "lit/decorators";
|
||||
import { dynamicElement } from "../../../common/dom/dynamic-element-directive";
|
||||
import { computeStateDomain } from "../../../common/entity/compute_state_domain";
|
||||
import { GroupEntity } from "../../../data/group";
|
||||
import { computeGroupDomain, 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,20 +47,19 @@ class MoreInfoGroup extends LitElement {
|
||||
}
|
||||
|
||||
const baseStateObj = states.find((s) => s.state === "on") || states[0];
|
||||
const groupDomain = computeStateDomain(baseStateObj);
|
||||
|
||||
const groupDomain = computeGroupDomain(this.stateObj);
|
||||
|
||||
// Groups need to be filtered out or we'll show content of
|
||||
// first child above the children of the current group
|
||||
if (
|
||||
groupDomain !== "group" &&
|
||||
states.every(
|
||||
(entityState) => groupDomain === computeStateDomain(entityState)
|
||||
)
|
||||
) {
|
||||
if (groupDomain && groupDomain !== "group") {
|
||||
this._groupDomainStateObj = {
|
||||
...baseStateObj,
|
||||
entity_id: this.stateObj.entity_id,
|
||||
attributes: { ...baseStateObj.attributes },
|
||||
attributes: {
|
||||
...baseStateObj.attributes,
|
||||
friendly_name: this.stateObj.attributes.friendly_name,
|
||||
},
|
||||
};
|
||||
const type = domainMoreInfoType(groupDomain);
|
||||
importMoreInfoControl(type);
|
||||
@@ -96,12 +95,15 @@ class MoreInfoGroup extends LitElement {
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return css`
|
||||
state-card-content {
|
||||
display: block;
|
||||
margin-top: 8px;
|
||||
}
|
||||
`;
|
||||
return [
|
||||
moreInfoControlStyle,
|
||||
css`
|
||||
state-card-content {
|
||||
display: block;
|
||||
margin-top: 8px;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -147,7 +147,9 @@ 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) {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user