mirror of
https://github.com/home-assistant/frontend.git
synced 2026-08-23 15:18:07 +00:00
Compare commits
88
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5a4080f866 | ||
|
|
664ace6644 | ||
|
|
bd89553008 | ||
|
|
64993ff409 | ||
|
|
273aa3db5c | ||
|
|
75ff4a8c04 | ||
|
|
8b523a5aaa | ||
|
|
5261d7bb7b | ||
|
|
024c70c49e | ||
|
|
69e3d8e13d | ||
|
|
3ab6e389d3 | ||
|
|
9cc85bc928 | ||
|
|
4f4343d6c8 | ||
|
|
7ab27d620a | ||
|
|
fa4ee71803 | ||
|
|
82026250c5 | ||
|
|
bc7533bb42 | ||
|
|
7110c0381f | ||
|
|
87fa05accc | ||
|
|
56ee3f82fb | ||
|
|
11872b076b | ||
|
|
620db4238d | ||
|
|
ef78bec48d | ||
|
|
4dcf2287ce | ||
|
|
a9766ed66b | ||
|
|
26a83feeb0 | ||
|
|
8c5dd7cdba | ||
|
|
0d025a2355 | ||
|
|
8216778d0c | ||
|
|
fe16b689a8 | ||
|
|
2653f6c874 | ||
|
|
8093f7f4cb | ||
|
|
b26c914ff9 | ||
|
|
b2fa97b6dc | ||
|
|
8a8bba422a | ||
|
|
76ca66b1b5 | ||
|
|
280dbfc958 | ||
|
|
0b10ad3e78 | ||
|
|
04d0aa2f22 | ||
|
|
4b901101da | ||
|
|
4960284e2d | ||
|
|
11d32300e9 | ||
|
|
f131e93e63 | ||
|
|
5a540dd889 | ||
|
|
3a70310f78 | ||
|
|
feed58c33e | ||
|
|
e5585e13fe | ||
|
|
edd49e1511 | ||
|
|
057fad55e8 | ||
|
|
d1a8165de2 | ||
|
+4 |
31c555445d | ||
|
|
61bb5d33e5 | ||
|
|
20a3bab5bc | ||
|
|
e8d916acd7 | ||
|
|
8b73d664b4 | ||
|
|
10dcc08068 | ||
|
|
fc5adc3753 | ||
|
|
d1db8f456f | ||
|
|
0dd07a395a | ||
|
|
589771df5c | ||
|
|
92812048dc | ||
|
|
9fc14d6627 | ||
|
|
3da6b85593 | ||
|
|
d2a4f481be | ||
|
|
e37f67c548 | ||
|
|
e775a6770b | ||
|
|
4ba5ef6c37 | ||
|
|
d528ab06d9 | ||
|
|
03a628cfe2 | ||
|
|
973851b332 | ||
|
|
4c5795c276 | ||
|
|
41d016d96a | ||
|
|
22e647cad4 | ||
|
|
b63dd9dbbf | ||
|
|
3b3b9e269d | ||
|
|
d2c3b9ee83 | ||
|
|
5b50a8692b | ||
|
|
8a8bbee8e0 | ||
|
|
28e28d1417 | ||
|
|
ea77a0f3d6 | ||
|
|
10e09b238a | ||
|
|
f9cd2b66cb | ||
|
|
b1c0fba8cf | ||
|
|
fdae6257b3 | ||
|
|
6a48aea128 | ||
|
|
a90b173671 | ||
|
|
d9971bfaa9 | ||
|
|
369d56a809 |
@@ -21,7 +21,7 @@ jobs:
|
||||
url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }}
|
||||
steps:
|
||||
- name: Check out files from GitHub
|
||||
uses: actions/checkout@v4.2.2
|
||||
uses: actions/checkout@v5.0.0
|
||||
with:
|
||||
ref: dev
|
||||
|
||||
@@ -56,7 +56,7 @@ jobs:
|
||||
url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }}
|
||||
steps:
|
||||
- name: Check out files from GitHub
|
||||
uses: actions/checkout@v4.2.2
|
||||
uses: actions/checkout@v5.0.0
|
||||
with:
|
||||
ref: master
|
||||
|
||||
|
||||
@@ -24,7 +24,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check out files from GitHub
|
||||
uses: actions/checkout@v4.2.2
|
||||
uses: actions/checkout@v5.0.0
|
||||
- name: Setup Node
|
||||
uses: actions/[email protected]
|
||||
with:
|
||||
@@ -58,7 +58,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check out files from GitHub
|
||||
uses: actions/checkout@v4.2.2
|
||||
uses: actions/checkout@v5.0.0
|
||||
- name: Setup Node
|
||||
uses: actions/[email protected]
|
||||
with:
|
||||
@@ -76,7 +76,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check out files from GitHub
|
||||
uses: actions/checkout@v4.2.2
|
||||
uses: actions/checkout@v5.0.0
|
||||
- name: Setup Node
|
||||
uses: actions/[email protected]
|
||||
with:
|
||||
@@ -100,7 +100,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check out files from GitHub
|
||||
uses: actions/checkout@v4.2.2
|
||||
uses: actions/checkout@v5.0.0
|
||||
- name: Setup Node
|
||||
uses: actions/[email protected]
|
||||
with:
|
||||
|
||||
@@ -23,7 +23,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4.2.2
|
||||
uses: actions/checkout@v5.0.0
|
||||
with:
|
||||
# We must fetch at least the immediate parents so that if this is
|
||||
# a pull request then we can checkout the head.
|
||||
|
||||
@@ -22,7 +22,7 @@ jobs:
|
||||
url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }}
|
||||
steps:
|
||||
- name: Check out files from GitHub
|
||||
uses: actions/checkout@v4.2.2
|
||||
uses: actions/checkout@v5.0.0
|
||||
with:
|
||||
ref: dev
|
||||
|
||||
@@ -57,7 +57,7 @@ jobs:
|
||||
url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }}
|
||||
steps:
|
||||
- name: Check out files from GitHub
|
||||
uses: actions/checkout@v4.2.2
|
||||
uses: actions/checkout@v5.0.0
|
||||
with:
|
||||
ref: master
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@ jobs:
|
||||
url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }}
|
||||
steps:
|
||||
- name: Check out files from GitHub
|
||||
uses: actions/checkout@v4.2.2
|
||||
uses: actions/checkout@v5.0.0
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/[email protected]
|
||||
|
||||
@@ -21,7 +21,7 @@ jobs:
|
||||
if: github.repository == 'home-assistant/frontend' && contains(github.event.pull_request.labels.*.name, 'needs design preview')
|
||||
steps:
|
||||
- name: Check out files from GitHub
|
||||
uses: actions/checkout@v4.2.2
|
||||
uses: actions/checkout@v5.0.0
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/[email protected]
|
||||
|
||||
@@ -20,7 +20,7 @@ jobs:
|
||||
contents: write
|
||||
steps:
|
||||
- name: Checkout the repository
|
||||
uses: actions/checkout@v4.2.2
|
||||
uses: actions/checkout@v5.0.0
|
||||
|
||||
- name: Set up Python ${{ env.PYTHON_VERSION }}
|
||||
uses: actions/setup-python@v5
|
||||
|
||||
@@ -23,7 +23,7 @@ jobs:
|
||||
contents: write # Required to upload release assets
|
||||
steps:
|
||||
- name: Checkout the repository
|
||||
uses: actions/checkout@v4.2.2
|
||||
uses: actions/checkout@v5.0.0
|
||||
|
||||
- name: Set up Python ${{ env.PYTHON_VERSION }}
|
||||
uses: actions/setup-python@v5
|
||||
@@ -90,7 +90,7 @@ jobs:
|
||||
contents: write # Required to upload release assets
|
||||
steps:
|
||||
- name: Checkout the repository
|
||||
uses: actions/checkout@v4.2.2
|
||||
uses: actions/checkout@v5.0.0
|
||||
- name: Setup Node
|
||||
uses: actions/[email protected]
|
||||
with:
|
||||
@@ -119,7 +119,7 @@ jobs:
|
||||
contents: write # Required to upload release assets
|
||||
steps:
|
||||
- name: Checkout the repository
|
||||
uses: actions/checkout@v4.2.2
|
||||
uses: actions/checkout@v5.0.0
|
||||
- name: Setup Node
|
||||
uses: actions/[email protected]
|
||||
with:
|
||||
|
||||
@@ -9,7 +9,7 @@ jobs:
|
||||
check-authorization:
|
||||
runs-on: ubuntu-latest
|
||||
# Only run if this is a Task issue type (from the issue form)
|
||||
if: github.event.issue.issue_type == 'Task'
|
||||
if: github.event.issue.type.name == 'Task'
|
||||
steps:
|
||||
- name: Check if user is authorized
|
||||
uses: actions/github-script@v7
|
||||
|
||||
@@ -14,7 +14,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout the repository
|
||||
uses: actions/checkout@v4.2.2
|
||||
uses: actions/checkout@v5.0.0
|
||||
|
||||
- name: Upload Translations
|
||||
run: |
|
||||
|
||||
+354
-354
File diff suppressed because one or more lines are too long
+1
-1
@@ -6,4 +6,4 @@ enableGlobalCache: false
|
||||
|
||||
nodeLinker: node-modules
|
||||
|
||||
yarnPath: .yarn/releases/yarn-4.9.2.cjs
|
||||
yarnPath: .yarn/releases/yarn-4.9.3.cjs
|
||||
|
||||
@@ -14,5 +14,5 @@
|
||||
"name": "Home Assistant Cast",
|
||||
"short_name": "HA Cast",
|
||||
"start_url": "/?homescreen=1",
|
||||
"theme_color": "#03A9F4"
|
||||
"theme_color": "#009ac7"
|
||||
}
|
||||
|
||||
@@ -75,5 +75,5 @@
|
||||
"name": "Home Assistant Demo",
|
||||
"short_name": "HA Demo",
|
||||
"start_url": "/?homescreen=1",
|
||||
"theme_color": "#03A9F4"
|
||||
"theme_color": "#009ac7"
|
||||
}
|
||||
|
||||
@@ -101,11 +101,14 @@ const ENTITIES = [
|
||||
ClimateEntityFeature.FAN_MODE +
|
||||
ClimateEntityFeature.TARGET_TEMPERATURE_RANGE,
|
||||
}),
|
||||
getEntity("fan", "fan_direction", "on", {
|
||||
getEntity("fan", "fan_demo", "on", {
|
||||
friendly_name: "Ceiling fan",
|
||||
device_class: "fan",
|
||||
direction: "reverse",
|
||||
supported_features: [FanEntityFeature.DIRECTION],
|
||||
supported_features:
|
||||
FanEntityFeature.DIRECTION +
|
||||
FanEntityFeature.SET_SPEED +
|
||||
FanEntityFeature.OSCILLATE,
|
||||
}),
|
||||
];
|
||||
|
||||
@@ -272,11 +275,29 @@ const CONFIGS = [
|
||||
heading: "Fan direction feature",
|
||||
config: `
|
||||
- type: tile
|
||||
entity: fan.fan_direction
|
||||
entity: fan.fan_demo
|
||||
features:
|
||||
- type: fan-direction
|
||||
`,
|
||||
},
|
||||
{
|
||||
heading: "Fan speed feature",
|
||||
config: `
|
||||
- type: tile
|
||||
entity: fan.fan_demo
|
||||
features:
|
||||
- type: fan-speed
|
||||
`,
|
||||
},
|
||||
{
|
||||
heading: "Fan oscillate feature",
|
||||
config: `
|
||||
- type: tile
|
||||
entity: fan.fan_demo
|
||||
features:
|
||||
- type: fan-oscillate
|
||||
`,
|
||||
},
|
||||
];
|
||||
|
||||
@customElement("demo-lovelace-tile-card")
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
---
|
||||
title: Template
|
||||
---
|
||||
@@ -0,0 +1,65 @@
|
||||
import { css, html, LitElement } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import "../../../../src/components/ha-card";
|
||||
import "../../../../src/components/ha-template";
|
||||
import { provideHass } from "../../../../src/fake_data/provide_hass";
|
||||
import type { HomeAssistant } from "../../../../src/types";
|
||||
import { getEntity } from "../../../../src/fake_data/entity";
|
||||
|
||||
interface TemplateContent {
|
||||
content: string;
|
||||
}
|
||||
|
||||
const templates: TemplateContent[] = [
|
||||
{ content: "{{ states('sensor.temperature') }}" },
|
||||
{
|
||||
content: "{{ 'Day' if is_state('sun.sun', 'above_horizon') else 'Night' }}",
|
||||
},
|
||||
];
|
||||
|
||||
const ENTITIES = [
|
||||
getEntity("sensor", "temperature", "25", {
|
||||
friendly_name: "Temperature",
|
||||
}),
|
||||
getEntity("sun", "sun", "above_horizon", {
|
||||
friendly_name: "Controller 2",
|
||||
}),
|
||||
];
|
||||
|
||||
@customElement("demo-misc-ha-template")
|
||||
export class DemoMiscTemplate extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
protected firstUpdated() {
|
||||
const hass = provideHass(this);
|
||||
hass.addEntities(ENTITIES);
|
||||
}
|
||||
|
||||
protected render() {
|
||||
return html`
|
||||
<div class="container">
|
||||
${templates.map(
|
||||
(t) =>
|
||||
html`<ha-card>
|
||||
<pre>Template: ${t.content}</pre>
|
||||
<pre>Result: <ha-template
|
||||
.hass=${this.hass} .content=${t.content}></ha-template></pre>
|
||||
</ha-card>`
|
||||
)}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
ha-card {
|
||||
margin: 12px;
|
||||
padding: 12px;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"demo-misc-ha-template": DemoMiscTemplate;
|
||||
}
|
||||
}
|
||||
Executable
+3
@@ -0,0 +1,3 @@
|
||||
---
|
||||
title: Fan
|
||||
---
|
||||
Executable
+50
@@ -0,0 +1,50 @@
|
||||
import type { PropertyValues, TemplateResult } from "lit";
|
||||
import { html, LitElement } from "lit";
|
||||
import { customElement, property, query } from "lit/decorators";
|
||||
import "../../../../src/components/ha-card";
|
||||
import "../../../../src/dialogs/more-info/more-info-content";
|
||||
import { getEntity } from "../../../../src/fake_data/entity";
|
||||
import type { MockHomeAssistant } from "../../../../src/fake_data/provide_hass";
|
||||
import { provideHass } from "../../../../src/fake_data/provide_hass";
|
||||
import "../../components/demo-more-infos";
|
||||
import { FanEntityFeature } from "../../../../src/data/fan";
|
||||
|
||||
const ENTITIES = [
|
||||
getEntity("fan", "fan", "on", {
|
||||
friendly_name: "Fan",
|
||||
device_class: "fan",
|
||||
supported_features:
|
||||
FanEntityFeature.OSCILLATE +
|
||||
FanEntityFeature.DIRECTION +
|
||||
FanEntityFeature.SET_SPEED,
|
||||
}),
|
||||
];
|
||||
|
||||
@customElement("demo-more-info-fan")
|
||||
class DemoMoreInfoFan extends LitElement {
|
||||
@property({ attribute: false }) public hass!: MockHomeAssistant;
|
||||
|
||||
@query("demo-more-infos") private _demoRoot!: HTMLElement;
|
||||
|
||||
protected render(): TemplateResult {
|
||||
return html`
|
||||
<demo-more-infos
|
||||
.hass=${this.hass}
|
||||
.entities=${ENTITIES.map((ent) => ent.entityId)}
|
||||
></demo-more-infos>
|
||||
`;
|
||||
}
|
||||
|
||||
protected firstUpdated(changedProperties: PropertyValues) {
|
||||
super.firstUpdated(changedProperties);
|
||||
const hass = provideHass(this._demoRoot);
|
||||
hass.updateTranslations(null, "en");
|
||||
hass.addEntities(ENTITIES);
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"demo-more-info-fan": DemoMoreInfoFan;
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
export default {
|
||||
"*.?(c|m){js,ts}": [
|
||||
"eslint --flag unstable_config_lookup_from_file --cache --cache-strategy=content --cache-location=node_modules/.cache/eslint/.eslintcache --fix",
|
||||
"eslint --flag v10_config_lookup_from_file --cache --cache-strategy=content --cache-location=node_modules/.cache/eslint/.eslintcache --fix",
|
||||
"prettier --cache --write",
|
||||
"lit-analyzer --quiet",
|
||||
],
|
||||
|
||||
+20
-18
@@ -8,8 +8,8 @@
|
||||
"version": "1.0.0",
|
||||
"scripts": {
|
||||
"build": "script/build_frontend",
|
||||
"lint:eslint": "eslint --flag unstable_config_lookup_from_file \"**/src/**/*.{js,ts,html}\" --cache --cache-strategy=content --cache-location=node_modules/.cache/eslint/.eslintcache --ignore-pattern=.gitignore --max-warnings=0",
|
||||
"format:eslint": "eslint --flag unstable_config_lookup_from_file \"**/src/**/*.{js,ts,html}\" --cache --cache-strategy=content --cache-location=node_modules/.cache/eslint/.eslintcache --ignore-pattern=.gitignore --fix",
|
||||
"lint:eslint": "eslint --flag v10_config_lookup_from_file \"**/src/**/*.{js,ts,html}\" --cache --cache-strategy=content --cache-location=node_modules/.cache/eslint/.eslintcache --ignore-pattern=.gitignore --max-warnings=0",
|
||||
"format:eslint": "eslint --flag v10_config_lookup_from_file \"**/src/**/*.{js,ts,html}\" --cache --cache-strategy=content --cache-location=node_modules/.cache/eslint/.eslintcache --ignore-pattern=.gitignore --fix",
|
||||
"lint:prettier": "prettier . --cache --check",
|
||||
"format:prettier": "prettier . --cache --write",
|
||||
"lint:types": "tsc",
|
||||
@@ -27,11 +27,11 @@
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
"@awesome.me/webawesome": "3.0.0-beta.4",
|
||||
"@babel/runtime": "7.28.2",
|
||||
"@babel/runtime": "7.28.3",
|
||||
"@braintree/sanitize-url": "7.1.1",
|
||||
"@codemirror/autocomplete": "6.18.6",
|
||||
"@codemirror/commands": "6.8.1",
|
||||
"@codemirror/language": "6.11.2",
|
||||
"@codemirror/language": "6.11.3",
|
||||
"@codemirror/legacy-modes": "6.5.1",
|
||||
"@codemirror/search": "6.5.11",
|
||||
"@codemirror/state": "6.5.2",
|
||||
@@ -80,7 +80,7 @@
|
||||
"@material/mwc-top-app-bar": "0.27.0",
|
||||
"@material/mwc-top-app-bar-fixed": "0.27.0",
|
||||
"@material/top-app-bar": "=14.0.0-canary.53b3cad2f.0",
|
||||
"@material/web": "2.3.0",
|
||||
"@material/web": "2.4.0",
|
||||
"@mdi/js": "7.4.47",
|
||||
"@mdi/svg": "7.4.47",
|
||||
"@replit/codemirror-indentation-markers": "6.5.3",
|
||||
@@ -99,7 +99,7 @@
|
||||
"barcode-detector": "3.0.5",
|
||||
"color-name": "2.0.0",
|
||||
"comlink": "4.4.2",
|
||||
"core-js": "3.45.0",
|
||||
"core-js": "3.45.1",
|
||||
"cropperjs": "1.6.2",
|
||||
"culori": "4.0.2",
|
||||
"date-fns": "4.1.0",
|
||||
@@ -112,7 +112,7 @@
|
||||
"fuse.js": "7.1.0",
|
||||
"google-timezones-json": "1.2.0",
|
||||
"gulp-zopfli-green": "6.0.2",
|
||||
"hls.js": "1.6.9",
|
||||
"hls.js": "1.6.10",
|
||||
"home-assistant-js-websocket": "9.5.0",
|
||||
"idb-keyval": "6.2.2",
|
||||
"intl-messageformat": "10.7.16",
|
||||
@@ -123,9 +123,10 @@
|
||||
"lit": "3.3.1",
|
||||
"lit-html": "3.3.1",
|
||||
"luxon": "3.7.1",
|
||||
"marked": "16.1.2",
|
||||
"marked": "16.2.0",
|
||||
"memoize-one": "6.0.0",
|
||||
"node-vibrant": "4.0.3",
|
||||
"nunjucks": "3.2.4",
|
||||
"object-hash": "3.0.0",
|
||||
"punycode": "2.3.1",
|
||||
"qr-scanner": "1.4.2",
|
||||
@@ -149,18 +150,18 @@
|
||||
"xss": "1.0.15"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "7.28.0",
|
||||
"@babel/core": "7.28.3",
|
||||
"@babel/helper-define-polyfill-provider": "0.6.5",
|
||||
"@babel/plugin-transform-runtime": "7.28.0",
|
||||
"@babel/preset-env": "7.28.0",
|
||||
"@babel/plugin-transform-runtime": "7.28.3",
|
||||
"@babel/preset-env": "7.28.3",
|
||||
"@bundle-stats/plugin-webpack-filter": "4.21.2",
|
||||
"@lokalise/node-api": "15.0.0",
|
||||
"@lokalise/node-api": "15.2.1",
|
||||
"@octokit/auth-oauth-device": "8.0.1",
|
||||
"@octokit/plugin-retry": "8.0.1",
|
||||
"@octokit/rest": "22.0.0",
|
||||
"@rsdoctor/rspack-plugin": "1.2.1",
|
||||
"@rspack/cli": "1.4.11",
|
||||
"@rsdoctor/rspack-plugin": "1.2.3",
|
||||
"@rspack/core": "1.4.11",
|
||||
"@rspack/dev-server": "1.1.4",
|
||||
"@types/babel__plugin-transform-runtime": "7.9.5",
|
||||
"@types/chromecast-caf-receiver": "6.0.22",
|
||||
"@types/chromecast-caf-sender": "1.0.11",
|
||||
@@ -174,6 +175,7 @@
|
||||
"@types/lodash.merge": "4.6.9",
|
||||
"@types/luxon": "3.7.1",
|
||||
"@types/mocha": "10.0.10",
|
||||
"@types/nunjucks": "^3",
|
||||
"@types/qrcode": "1.5.5",
|
||||
"@types/sortablejs": "1.15.8",
|
||||
"@types/tar": "6.1.13",
|
||||
@@ -184,14 +186,14 @@
|
||||
"babel-plugin-template-html-minifier": "4.1.0",
|
||||
"browserslist-useragent-regexp": "4.1.3",
|
||||
"del": "8.0.0",
|
||||
"eslint": "9.33.0",
|
||||
"eslint": "9.34.0",
|
||||
"eslint-config-airbnb-base": "15.0.0",
|
||||
"eslint-config-prettier": "10.1.8",
|
||||
"eslint-import-resolver-webpack": "0.13.10",
|
||||
"eslint-plugin-import": "2.32.0",
|
||||
"eslint-plugin-lit": "2.1.1",
|
||||
"eslint-plugin-lit-a11y": "5.1.1",
|
||||
"eslint-plugin-unused-imports": "4.1.4",
|
||||
"eslint-plugin-unused-imports": "4.2.0",
|
||||
"eslint-plugin-wc": "3.0.1",
|
||||
"fancy-log": "2.0.0",
|
||||
"fs-extra": "11.3.1",
|
||||
@@ -218,7 +220,7 @@
|
||||
"terser-webpack-plugin": "5.3.14",
|
||||
"ts-lit-plugin": "2.0.2",
|
||||
"typescript": "5.9.2",
|
||||
"typescript-eslint": "8.39.0",
|
||||
"typescript-eslint": "8.40.0",
|
||||
"vite-tsconfig-paths": "5.1.4",
|
||||
"vitest": "3.2.4",
|
||||
"webpack-stats-plugin": "1.1.3",
|
||||
@@ -237,5 +239,5 @@
|
||||
"@material/mwc-list@^0.27.0": "patch:@material/mwc-list@npm%3A0.27.0#~/.yarn/patches/@material-mwc-list-npm-0.27.0-5344fc9de4.patch",
|
||||
"@vaadin/vaadin-themable-mixin": "24.8.5"
|
||||
},
|
||||
"packageManager": "[email protected].2"
|
||||
"packageManager": "[email protected].3"
|
||||
}
|
||||
|
||||
@@ -28,7 +28,10 @@ export const computeEntityEntryName = (
|
||||
hass: HomeAssistant
|
||||
): string | undefined => {
|
||||
const name =
|
||||
entry.name || ("original_name" in entry ? entry.original_name : undefined);
|
||||
entry.name ||
|
||||
("original_name" in entry && entry.original_name != null
|
||||
? String(entry.original_name)
|
||||
: undefined);
|
||||
|
||||
const device = entry.device_id ? hass.devices[entry.device_id] : undefined;
|
||||
|
||||
|
||||
@@ -0,0 +1,137 @@
|
||||
import nunjucks, { Environment, Template as NunjucksTemplate } from "nunjucks";
|
||||
import type { HomeAssistant } from "../types";
|
||||
|
||||
nunjucks.installJinjaCompat();
|
||||
|
||||
function createEnv() {
|
||||
const env = new Environment();
|
||||
// Add filters and globals that don't use hass
|
||||
env.addFilter("min", (numbers: number[]) => Math.min(...numbers));
|
||||
env.addFilter("max", (numbers: number[]) => Math.max(...numbers));
|
||||
|
||||
env.addGlobal(
|
||||
"states",
|
||||
(
|
||||
hass: HomeAssistant,
|
||||
id: string,
|
||||
round = false,
|
||||
withUnit = false
|
||||
): string => {
|
||||
if (!hass?.states[id]) {
|
||||
return "unknown";
|
||||
}
|
||||
const state = hass?.states[id]?.state;
|
||||
if (state == null) {
|
||||
return "unavailable";
|
||||
}
|
||||
if (round) {
|
||||
return String(Math.round(Number(state)));
|
||||
}
|
||||
if (withUnit) {
|
||||
return `${state} ${hass?.states[id]?.attributes.unit_of_measurement}`;
|
||||
}
|
||||
return state;
|
||||
}
|
||||
);
|
||||
env.addGlobal(
|
||||
"state_attr",
|
||||
(hass: HomeAssistant, id: string, attr: string) =>
|
||||
hass?.states[id]?.attributes[attr]
|
||||
);
|
||||
env.addGlobal(
|
||||
"is_state",
|
||||
(hass: HomeAssistant, id: string, value: string) =>
|
||||
hass?.states[id]?.state === value
|
||||
);
|
||||
env.addGlobal(
|
||||
"is_state_attr",
|
||||
(hass: HomeAssistant, id: string, attr: string, value: string) =>
|
||||
hass?.states[id]?.attributes[attr] === value
|
||||
);
|
||||
env.addGlobal(
|
||||
"has_value",
|
||||
(hass: HomeAssistant, id: string) => hass?.states[id]?.state != null
|
||||
);
|
||||
env.addGlobal("state_translated", (hass: HomeAssistant, id: string) => {
|
||||
try {
|
||||
return hass?.formatEntityState(hass?.states[id], hass?.states[id]?.state);
|
||||
} catch {
|
||||
return hass?.states[id]?.state ?? undefined;
|
||||
}
|
||||
});
|
||||
return env;
|
||||
}
|
||||
|
||||
export class HaTemplate {
|
||||
private _njTemplate?: NunjucksTemplate;
|
||||
|
||||
private _hass?: HomeAssistant;
|
||||
|
||||
private _content?: string;
|
||||
|
||||
private _context?: Record<string, any>;
|
||||
|
||||
private _value = "";
|
||||
|
||||
public entityIds = new Set<string>();
|
||||
|
||||
public shouldUpdate = false;
|
||||
|
||||
private _env = createEnv();
|
||||
|
||||
constructor() {
|
||||
// functions that access the hass state have to be dynamic
|
||||
// in order to track which entities are used in the template
|
||||
[
|
||||
"states",
|
||||
"state_attr",
|
||||
"is_state",
|
||||
"is_state_attr",
|
||||
"has_value",
|
||||
"state_translated",
|
||||
].forEach((func) => {
|
||||
const original = this._env.getGlobal(func);
|
||||
this._env.addGlobal(func, (id: string, ...args: any[]): string => {
|
||||
this.entityIds.add(id);
|
||||
return original(this._hass, id, ...args);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
public render(): string {
|
||||
if (this.shouldUpdate) {
|
||||
this.shouldUpdate = false;
|
||||
this.entityIds.clear();
|
||||
this._value = this._njTemplate!.render(this._context);
|
||||
}
|
||||
return this._value;
|
||||
}
|
||||
|
||||
public set content(content: string) {
|
||||
if (this._content !== content) {
|
||||
this._content = content;
|
||||
this._njTemplate = new NunjucksTemplate(content, this._env);
|
||||
this.shouldUpdate = true;
|
||||
}
|
||||
}
|
||||
|
||||
public set context(context: Record<string, any>) {
|
||||
if (this._context !== context) {
|
||||
this._context = context;
|
||||
this.shouldUpdate = true;
|
||||
}
|
||||
}
|
||||
|
||||
public set hass(hass: HomeAssistant) {
|
||||
if (this._hass !== hass) {
|
||||
if (!this.shouldUpdate) {
|
||||
this.shouldUpdate =
|
||||
!this._hass !== !hass ||
|
||||
Array.from(this.entityIds).some(
|
||||
(id) => this._hass?.states[id]?.state !== hass.states[id]?.state
|
||||
);
|
||||
}
|
||||
this._hass = hass;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -14,6 +14,7 @@ export type LocalizeKeys =
|
||||
| `ui.card.weather.attributes.${string}`
|
||||
| `ui.card.weather.cardinal_direction.${string}`
|
||||
| `ui.card.lawn_mower.actions.${string}`
|
||||
| `ui.common.${string}`
|
||||
| `ui.components.calendar.event.rrule.${string}`
|
||||
| `ui.components.selectors.file.${string}`
|
||||
| `ui.components.logbook.messages.detected_device_classes.${string}`
|
||||
@@ -26,6 +27,7 @@ export type LocalizeKeys =
|
||||
| `ui.dialogs.unsupported.reasons.${string}`
|
||||
| `ui.panel.config.${string}.${"caption" | "description"}`
|
||||
| `ui.panel.config.dashboard.${string}`
|
||||
| `ui.panel.config.storage.segments.${string}`
|
||||
| `ui.panel.config.zha.${string}`
|
||||
| `ui.panel.config.zwave_js.${string}`
|
||||
| `ui.panel.lovelace.card.${string}`
|
||||
|
||||
@@ -39,6 +39,7 @@ export type CustomLegendOption = ECOption["legend"] & {
|
||||
type: "custom";
|
||||
data?: {
|
||||
id?: string;
|
||||
secondaryIds?: string[]; // Other dataset IDs that should be controlled by this legend item.
|
||||
name: string;
|
||||
itemStyle?: Record<string, any>;
|
||||
}[];
|
||||
@@ -181,6 +182,10 @@ export class HaChartBase extends LitElement {
|
||||
return;
|
||||
}
|
||||
let chartOptions: ECOption = {};
|
||||
if (changedProps.has("options")) {
|
||||
// Separate 'if' from below since this must updated before _getSeries()
|
||||
this._updateHiddenStatsFromOptions(this.options);
|
||||
}
|
||||
if (changedProps.has("data") || changedProps.has("_hiddenDatasets")) {
|
||||
chartOptions.series = this._getSeries();
|
||||
}
|
||||
@@ -451,14 +456,7 @@ export class HaChartBase extends LitElement {
|
||||
});
|
||||
}
|
||||
|
||||
const legend = ensureArray(this.options?.legend || [])[0] as
|
||||
| LegendComponentOption
|
||||
| undefined;
|
||||
Object.entries(legend?.selected || {}).forEach(([stat, selected]) => {
|
||||
if (selected === false) {
|
||||
this._hiddenDatasets.add(stat);
|
||||
}
|
||||
});
|
||||
this._updateHiddenStatsFromOptions(this.options);
|
||||
|
||||
this.chart.setOption({
|
||||
...this._createOptions(),
|
||||
@@ -469,6 +467,42 @@ export class HaChartBase extends LitElement {
|
||||
}
|
||||
}
|
||||
|
||||
// Return an array of all IDs associated with the legend item of the primaryId
|
||||
private _getAllIdsFromLegend(
|
||||
options: ECOption | undefined,
|
||||
primaryId: string
|
||||
): string[] {
|
||||
if (!options) return [primaryId];
|
||||
const legend = ensureArray(this.options?.legend || [])[0] as
|
||||
| LegendComponentOption
|
||||
| undefined;
|
||||
|
||||
let customLegendItem;
|
||||
if (legend?.type === "custom") {
|
||||
customLegendItem = (legend as CustomLegendOption).data?.find(
|
||||
(li) => typeof li === "object" && li.id === primaryId
|
||||
);
|
||||
}
|
||||
|
||||
return [primaryId, ...(customLegendItem?.secondaryIds || [])];
|
||||
}
|
||||
|
||||
// Parses the options structure and adds all ids of unselected legend items to hiddenDatasets.
|
||||
// No known need to remove items at this time.
|
||||
private _updateHiddenStatsFromOptions(options: ECOption | undefined) {
|
||||
if (!options) return;
|
||||
const legend = ensureArray(this.options?.legend || [])[0] as
|
||||
| LegendComponentOption
|
||||
| undefined;
|
||||
Object.entries(legend?.selected || {}).forEach(([stat, selected]) => {
|
||||
if (selected === false) {
|
||||
this._getAllIdsFromLegend(options, stat).forEach((id) =>
|
||||
this._hiddenDatasets.add(id)
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private _getDataZoomConfig(): DataZoomComponentOption | undefined {
|
||||
const xAxis = (this.options?.xAxis?.[0] ?? this.options?.xAxis) as
|
||||
| XAXisOption
|
||||
@@ -844,10 +878,14 @@ export class HaChartBase extends LitElement {
|
||||
}
|
||||
const id = ev.currentTarget?.id;
|
||||
if (this._hiddenDatasets.has(id)) {
|
||||
this._hiddenDatasets.delete(id);
|
||||
this._getAllIdsFromLegend(this.options, id).forEach((i) =>
|
||||
this._hiddenDatasets.delete(i)
|
||||
);
|
||||
fireEvent(this, "dataset-unhidden", { id });
|
||||
} else {
|
||||
this._hiddenDatasets.add(id);
|
||||
this._getAllIdsFromLegend(this.options, id).forEach((i) =>
|
||||
this._hiddenDatasets.add(i)
|
||||
);
|
||||
fireEvent(this, "dataset-hidden", { id });
|
||||
}
|
||||
this.requestUpdate("_hiddenDatasets");
|
||||
|
||||
@@ -18,7 +18,6 @@ export interface Node {
|
||||
value: number;
|
||||
index: number; // like z-index but for x/y
|
||||
label?: string;
|
||||
tooltip?: string;
|
||||
color?: string;
|
||||
passThrough?: boolean;
|
||||
}
|
||||
|
||||
@@ -101,7 +101,7 @@ export class StateHistoryChartTimeline extends LitElement {
|
||||
fill: api.value(4) as string,
|
||||
},
|
||||
};
|
||||
const text = api.value(3) as string;
|
||||
const text = (api.value(3) as string).replaceAll("\n", " ");
|
||||
const textWidth = measureTextWidth(text, 12);
|
||||
const LABEL_PADDING = 4;
|
||||
if (textWidth < rectShape.width - LABEL_PADDING * 2) {
|
||||
|
||||
@@ -97,9 +97,6 @@ class HaAlert extends LitElement {
|
||||
content: "";
|
||||
border-radius: 4px;
|
||||
}
|
||||
.icon {
|
||||
z-index: 1;
|
||||
}
|
||||
.icon.no-title {
|
||||
align-self: center;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,268 @@
|
||||
import { css, html, LitElement } from "lit";
|
||||
import { customElement, query, state } from "lit/decorators";
|
||||
import { styleMap } from "lit/directives/style-map";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
|
||||
const ANIMATION_DURATION_MS = 300;
|
||||
|
||||
/**
|
||||
* A bottom sheet component that slides up from the bottom of the screen.
|
||||
*
|
||||
* The bottom sheet provides a draggable interface that allows users to resize
|
||||
* the sheet by dragging the handle at the top. It supports both mouse and touch
|
||||
* interactions and automatically closes when dragged below a 20% of screen height.
|
||||
*
|
||||
* @fires bottom-sheet-closed - Fired when the bottom sheet is closed
|
||||
*
|
||||
* @cssprop --ha-bottom-sheet-border-width - Border width for the sheet
|
||||
* @cssprop --ha-bottom-sheet-border-style - Border style for the sheet
|
||||
* @cssprop --ha-bottom-sheet-border-color - Border color for the sheet
|
||||
*/
|
||||
@customElement("ha-bottom-sheet")
|
||||
export class HaBottomSheet extends LitElement {
|
||||
@query("dialog") private _dialog!: HTMLDialogElement;
|
||||
|
||||
private _dragging = false;
|
||||
|
||||
private _dragStartY = 0;
|
||||
|
||||
private _initialSize = 0;
|
||||
|
||||
@state() private _dialogMaxViewpointHeight = 70;
|
||||
|
||||
@state() private _dialogViewportHeight?: number;
|
||||
|
||||
render() {
|
||||
return html`<dialog
|
||||
open
|
||||
@transitionend=${this._handleTransitionEnd}
|
||||
style=${styleMap({
|
||||
height: this._dialogViewportHeight
|
||||
? `${this._dialogViewportHeight}vh`
|
||||
: "auto",
|
||||
maxHeight: `${this._dialogMaxViewpointHeight}vh`,
|
||||
})}
|
||||
>
|
||||
<div class="handle-wrapper">
|
||||
<div
|
||||
@mousedown=${this._handleMouseDown}
|
||||
@touchstart=${this._handleTouchStart}
|
||||
class="handle"
|
||||
></div>
|
||||
</div>
|
||||
<slot></slot>
|
||||
</dialog>`;
|
||||
}
|
||||
|
||||
protected firstUpdated(changedProperties) {
|
||||
super.firstUpdated(changedProperties);
|
||||
this._openSheet();
|
||||
}
|
||||
|
||||
private _openSheet() {
|
||||
requestAnimationFrame(() => {
|
||||
// trigger opening animation
|
||||
this._dialog.classList.add("show");
|
||||
});
|
||||
}
|
||||
|
||||
public closeSheet() {
|
||||
requestAnimationFrame(() => {
|
||||
this._dialog.classList.remove("show");
|
||||
});
|
||||
}
|
||||
|
||||
private _handleTransitionEnd() {
|
||||
if (this._dialog.classList.contains("show")) {
|
||||
// after show animation is done
|
||||
// - set the height to the natural height, to prevent content shift when switch content
|
||||
// - set max height to 90vh, so it opens at max 70vh but can be resized to 90vh
|
||||
this._dialogViewportHeight =
|
||||
(this._dialog.offsetHeight / window.innerHeight) * 100;
|
||||
this._dialogMaxViewpointHeight = 90;
|
||||
} else {
|
||||
// after close animation is done close dialog element and fire closed event
|
||||
this._dialog.close();
|
||||
fireEvent(this, "bottom-sheet-closed");
|
||||
}
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
super.connectedCallback();
|
||||
|
||||
// register event listeners for drag handling
|
||||
document.addEventListener("mousemove", this._handleMouseMove);
|
||||
document.addEventListener("mouseup", this._handleMouseUp);
|
||||
document.addEventListener("touchmove", this._handleTouchMove, {
|
||||
passive: false,
|
||||
});
|
||||
document.addEventListener("touchend", this._handleTouchEnd);
|
||||
document.addEventListener("touchcancel", this._handleTouchEnd);
|
||||
}
|
||||
|
||||
disconnectedCallback() {
|
||||
super.disconnectedCallback();
|
||||
|
||||
// unregister event listeners for drag handling
|
||||
document.removeEventListener("mousemove", this._handleMouseMove);
|
||||
document.removeEventListener("mouseup", this._handleMouseUp);
|
||||
document.removeEventListener("touchmove", this._handleTouchMove);
|
||||
document.removeEventListener("touchend", this._handleTouchEnd);
|
||||
document.removeEventListener("touchcancel", this._handleTouchEnd);
|
||||
}
|
||||
|
||||
private _handleMouseDown = (ev: MouseEvent) => {
|
||||
this._startDrag(ev.clientY);
|
||||
};
|
||||
|
||||
private _handleTouchStart = (ev: TouchEvent) => {
|
||||
// Prevent the browser from interpreting this as a scroll/PTR gesture.
|
||||
ev.preventDefault();
|
||||
this._startDrag(ev.touches[0].clientY);
|
||||
};
|
||||
|
||||
private _startDrag(clientY: number) {
|
||||
this._dragging = true;
|
||||
this._dragStartY = clientY;
|
||||
this._initialSize = (this._dialog.offsetHeight / window.innerHeight) * 100;
|
||||
document.body.style.setProperty("cursor", "grabbing");
|
||||
}
|
||||
|
||||
private _handleMouseMove = (ev: MouseEvent) => {
|
||||
if (!this._dragging) {
|
||||
return;
|
||||
}
|
||||
this._updateSize(ev.clientY);
|
||||
};
|
||||
|
||||
private _handleTouchMove = (ev: TouchEvent) => {
|
||||
if (!this._dragging) {
|
||||
return;
|
||||
}
|
||||
ev.preventDefault(); // Prevent scrolling
|
||||
this._updateSize(ev.touches[0].clientY);
|
||||
};
|
||||
|
||||
private _updateSize(clientY: number) {
|
||||
const deltaY = this._dragStartY - clientY;
|
||||
const viewportHeight = window.innerHeight;
|
||||
const deltaVh = (deltaY / viewportHeight) * 100;
|
||||
|
||||
// Calculate new size and clamp between 10vh and 90vh
|
||||
let newSize = this._initialSize + deltaVh;
|
||||
newSize = Math.max(10, Math.min(90, newSize));
|
||||
|
||||
// on drag down and below 20vh
|
||||
if (newSize < 20 && deltaY < 0) {
|
||||
this._endDrag();
|
||||
this.closeSheet();
|
||||
return;
|
||||
}
|
||||
|
||||
this._dialogViewportHeight = newSize;
|
||||
}
|
||||
|
||||
private _handleMouseUp = () => {
|
||||
this._endDrag();
|
||||
};
|
||||
|
||||
private _handleTouchEnd = () => {
|
||||
this._endDrag();
|
||||
};
|
||||
|
||||
private _endDrag() {
|
||||
if (!this._dragging) {
|
||||
return;
|
||||
}
|
||||
this._dragging = false;
|
||||
document.body.style.removeProperty("cursor");
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
.handle-wrapper {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
padding-bottom: 2px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
cursor: grab;
|
||||
touch-action: none;
|
||||
}
|
||||
.handle-wrapper .handle {
|
||||
height: 20px;
|
||||
width: 200px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
z-index: 7;
|
||||
}
|
||||
.handle-wrapper .handle::after {
|
||||
content: "";
|
||||
border-radius: 8px;
|
||||
height: 4px;
|
||||
background: var(--divider-color, #e0e0e0);
|
||||
width: 80px;
|
||||
}
|
||||
.handle-wrapper .handle:active::after {
|
||||
cursor: grabbing;
|
||||
}
|
||||
dialog {
|
||||
height: auto;
|
||||
max-height: 70vh;
|
||||
min-height: 30vh;
|
||||
background-color: var(
|
||||
--ha-dialog-surface-background,
|
||||
var(--mdc-theme-surface, #fff)
|
||||
);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
top: 0;
|
||||
inset-inline-start: 0;
|
||||
position: fixed;
|
||||
width: calc(100% - 4px);
|
||||
max-width: 100%;
|
||||
border: none;
|
||||
box-shadow: var(--wa-shadow-l);
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
|
||||
top: auto;
|
||||
inset-inline-end: auto;
|
||||
bottom: 0;
|
||||
inset-inline-start: 0;
|
||||
box-shadow: 0px -8px 16px rgba(0, 0, 0, 0.2);
|
||||
border-top-left-radius: var(
|
||||
--ha-dialog-border-radius,
|
||||
var(--ha-border-radius-2xl)
|
||||
);
|
||||
border-top-right-radius: var(
|
||||
--ha-dialog-border-radius,
|
||||
var(--ha-border-radius-2xl)
|
||||
);
|
||||
transform: translateY(100%);
|
||||
transition: transform ${ANIMATION_DURATION_MS}ms ease;
|
||||
border-top-width: var(--ha-bottom-sheet-border-width);
|
||||
border-right-width: var(--ha-bottom-sheet-border-width);
|
||||
border-left-width: var(--ha-bottom-sheet-border-width);
|
||||
border-bottom-width: 0;
|
||||
border-style: var(--ha-bottom-sheet-border-style);
|
||||
border-color: var(--ha-bottom-sheet-border-color);
|
||||
}
|
||||
|
||||
dialog.show {
|
||||
transform: translateY(0);
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-bottom-sheet": HaBottomSheet;
|
||||
}
|
||||
|
||||
interface HASSDomEvents {
|
||||
"bottom-sheet-closed": undefined;
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,8 @@
|
||||
import SlAnimation from "@shoelace-style/shoelace/dist/components/animation/animation.component";
|
||||
import WaAnimation from "@awesome.me/webawesome/dist/components/animation/animation";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
|
||||
@customElement("ha-fade-in")
|
||||
export class HaFadeIn extends SlAnimation {
|
||||
export class HaFadeIn extends WaAnimation {
|
||||
@property() public name = "fadeIn";
|
||||
|
||||
@property() public fill: FillMode = "both";
|
||||
|
||||
@@ -329,9 +329,8 @@ export class HaFilterFloorAreas extends LitElement {
|
||||
}
|
||||
.subdir {
|
||||
margin-inline-end: 8px;
|
||||
opacity: .6;
|
||||
opacity: 0.6;
|
||||
}
|
||||
.
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
||||
@@ -1,12 +1,21 @@
|
||||
import ProgressRing from "@shoelace-style/shoelace/dist/components/progress-ring/progress-ring.component";
|
||||
import progressRingStyles from "@shoelace-style/shoelace/dist/components/progress-ring/progress-ring.styles";
|
||||
import ProgressRing from "@awesome.me/webawesome/dist/components/progress-ring/progress-ring";
|
||||
import { css } from "lit";
|
||||
import type { CSSResultGroup } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { StateSet } from "../resources/polyfills/stateset";
|
||||
|
||||
@customElement("ha-progress-ring")
|
||||
export class HaProgressRing extends ProgressRing {
|
||||
@property() public size?: "tiny" | "small" | "medium" | "large";
|
||||
|
||||
attachInternals() {
|
||||
const internals = super.attachInternals();
|
||||
Object.defineProperty(internals, "states", {
|
||||
value: new StateSet(this, internals.states),
|
||||
});
|
||||
return internals;
|
||||
}
|
||||
|
||||
public updated(changedProps) {
|
||||
super.updated(changedProps);
|
||||
|
||||
@@ -31,24 +40,26 @@ export class HaProgressRing extends ProgressRing {
|
||||
}
|
||||
}
|
||||
|
||||
static override styles = [
|
||||
progressRingStyles,
|
||||
css`
|
||||
:host {
|
||||
--indicator-color: var(
|
||||
--ha-progress-ring-indicator-color,
|
||||
var(--primary-color)
|
||||
);
|
||||
--track-color: var(
|
||||
--ha-progress-ring-divider-color,
|
||||
var(--divider-color)
|
||||
);
|
||||
--track-width: 4px;
|
||||
--speed: 3.5s;
|
||||
--size: var(--ha-progress-ring-size, 48px);
|
||||
}
|
||||
`,
|
||||
];
|
||||
static get styles(): CSSResultGroup {
|
||||
return [
|
||||
ProgressRing.styles,
|
||||
css`
|
||||
:host {
|
||||
--indicator-color: var(
|
||||
--ha-progress-ring-indicator-color,
|
||||
var(--primary-color)
|
||||
);
|
||||
--track-color: var(
|
||||
--ha-progress-ring-divider-color,
|
||||
var(--divider-color)
|
||||
);
|
||||
--track-width: 4px;
|
||||
--speed: 3.5s;
|
||||
--size: var(--ha-progress-ring-size, 48px);
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
|
||||
@@ -0,0 +1,123 @@
|
||||
import type { TemplateResult } from "lit";
|
||||
import { css, html, LitElement } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { styleMap } from "lit/directives/style-map";
|
||||
import "./ha-tooltip";
|
||||
|
||||
export interface Segment {
|
||||
value: number;
|
||||
color: string;
|
||||
label: TemplateResult | string;
|
||||
}
|
||||
|
||||
@customElement("ha-segmented-bar")
|
||||
class HaSegmentedBar extends LitElement {
|
||||
@property({ attribute: false }) public segments!: Segment[];
|
||||
|
||||
@property({ type: String }) public heading!: string;
|
||||
|
||||
@property({ type: String }) public description?: string;
|
||||
|
||||
protected render(): TemplateResult {
|
||||
const totalValue = this.segments.reduce(
|
||||
(acc, segment) => acc + segment.value,
|
||||
0
|
||||
);
|
||||
return html`
|
||||
<div class="container">
|
||||
<div class="heading">
|
||||
<span>${this.heading}</span>
|
||||
<span>${this.description}</span>
|
||||
</div>
|
||||
<div class="bar">
|
||||
${this.segments.map(
|
||||
(segment) => html`
|
||||
<ha-tooltip>
|
||||
<span slot="content">${segment.label}</span>
|
||||
<div
|
||||
style=${styleMap({
|
||||
width: `${(segment.value / totalValue) * 100}%`,
|
||||
backgroundColor: segment.color,
|
||||
})}
|
||||
></div>
|
||||
</ha-tooltip>
|
||||
`
|
||||
)}
|
||||
</div>
|
||||
<ul class="legend">
|
||||
${this.segments.map(
|
||||
(segment) => html`
|
||||
<li>
|
||||
<div
|
||||
class="bullet"
|
||||
style=${styleMap({
|
||||
backgroundColor: segment.color,
|
||||
})}
|
||||
></div>
|
||||
<span class="label">${segment.label}</span>
|
||||
</li>
|
||||
`
|
||||
)}
|
||||
</ul>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
.container {
|
||||
width: 100%;
|
||||
}
|
||||
.heading span {
|
||||
color: var(--secondary-text-color);
|
||||
line-height: var(--ha-line-height-expanded);
|
||||
margin-right: 8px;
|
||||
}
|
||||
.heading span:first-child {
|
||||
color: var(--primary-text-color);
|
||||
}
|
||||
.bar {
|
||||
display: flex;
|
||||
overflow: hidden;
|
||||
border-radius: var(--ha-bar-border-radius, 4px);
|
||||
width: 100%;
|
||||
height: 12px;
|
||||
margin: 2px 0;
|
||||
background-color: var(
|
||||
--ha-bar-background-color,
|
||||
var(--secondary-background-color)
|
||||
);
|
||||
}
|
||||
.bar div {
|
||||
height: 100%;
|
||||
}
|
||||
.bar div:hover {
|
||||
opacity: 0.8;
|
||||
}
|
||||
.legend {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
gap: 12px;
|
||||
margin: 12px 0;
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
}
|
||||
.legend li {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
font-size: var(--ha-font-size-s);
|
||||
}
|
||||
.legend li .bullet {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
border-radius: 50%;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-segmented-bar": HaSegmentedBar;
|
||||
}
|
||||
}
|
||||
@@ -8,7 +8,6 @@ import type {
|
||||
LocalizeKeys,
|
||||
} from "../../common/translations/localize";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import "../ha-alert";
|
||||
import "../ha-form/ha-form";
|
||||
|
||||
const SELECTOR_DEFAULTS = {
|
||||
@@ -156,6 +155,8 @@ export class HaSelectorSelector extends LitElement {
|
||||
|
||||
@property({ type: Boolean, reflect: true }) public disabled = false;
|
||||
|
||||
@property({ type: Boolean }) public narrow = false;
|
||||
|
||||
@property({ type: Boolean, reflect: true }) public required = true;
|
||||
|
||||
private _yamlMode = false;
|
||||
@@ -172,10 +173,10 @@ export class HaSelectorSelector extends LitElement {
|
||||
[
|
||||
{
|
||||
name: "type",
|
||||
required: true,
|
||||
selector: {
|
||||
select: {
|
||||
mode: "dropdown",
|
||||
required: true,
|
||||
options: Object.keys(SELECTOR_SCHEMAS)
|
||||
.concat("manual")
|
||||
.map((key) => ({
|
||||
@@ -228,17 +229,17 @@ export class HaSelectorSelector extends LitElement {
|
||||
|
||||
const schema = this._schema(type, this.hass.localize);
|
||||
|
||||
return html`<ha-card>
|
||||
<div class="card-content">
|
||||
<p>${this.label ? this.label : ""}</p>
|
||||
<ha-form
|
||||
.hass=${this.hass}
|
||||
.data=${data}
|
||||
.schema=${schema}
|
||||
.computeLabel=${this._computeLabelCallback}
|
||||
@value-changed=${this._valueChanged}
|
||||
></ha-form></div
|
||||
></ha-card>`;
|
||||
return html`<div>
|
||||
<p>${this.label ? this.label : ""}</p>
|
||||
<ha-form
|
||||
.hass=${this.hass}
|
||||
.data=${data}
|
||||
.schema=${schema}
|
||||
.computeLabel=${this._computeLabelCallback}
|
||||
@value-changed=${this._valueChanged}
|
||||
.narrow=${this.narrow}
|
||||
></ha-form>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
private _valueChanged(ev: CustomEvent) {
|
||||
@@ -285,23 +286,6 @@ export class HaSelectorSelector extends LitElement {
|
||||
) || schema.name;
|
||||
|
||||
static styles = css`
|
||||
:host {
|
||||
--expansion-panel-summary-padding: 0 16px;
|
||||
}
|
||||
ha-alert {
|
||||
display: block;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
ha-card {
|
||||
margin: 0 0 16px 0;
|
||||
}
|
||||
ha-card.disabled {
|
||||
pointer-events: none;
|
||||
color: var(--disabled-text-color);
|
||||
}
|
||||
.card-content {
|
||||
padding: 0px 16px 16px 16px;
|
||||
}
|
||||
.title {
|
||||
font-size: var(--ha-font-size-l);
|
||||
padding-top: 16px;
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
import { LitElement } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import type { HomeAssistant } from "../types";
|
||||
import { HaTemplate } from "../common/template";
|
||||
|
||||
@customElement("ha-template")
|
||||
export class HaTemplateElement extends LitElement {
|
||||
@property({ attribute: false }) public hass?: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public content!: string;
|
||||
|
||||
@property({ attribute: false }) public context: Record<string, any> = {};
|
||||
|
||||
private _template = new HaTemplate();
|
||||
|
||||
protected shouldUpdate(): boolean {
|
||||
if (this.hass) {
|
||||
this._template.hass = this.hass;
|
||||
this._template.content = this.content;
|
||||
this._template.context = this.context;
|
||||
}
|
||||
return this._template.shouldUpdate;
|
||||
}
|
||||
|
||||
public render() {
|
||||
try {
|
||||
return this._template.render();
|
||||
} catch (error) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.debug(`Error rendering template: ${error}`);
|
||||
return this.content;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-template": HaTemplateElement;
|
||||
}
|
||||
}
|
||||
@@ -38,7 +38,7 @@ class MediaManageButton extends LitElement {
|
||||
return nothing;
|
||||
}
|
||||
return html`
|
||||
<ha-button appearance="plain" size="small" @click=${this._manage}>
|
||||
<ha-button appearance="filled" size="small" @click=${this._manage}>
|
||||
<ha-svg-icon .path=${mdiFolderEdit} slot="start"></ha-svg-icon>
|
||||
${this.hass.localize(
|
||||
"ui.components.media-browser.file_management.manage"
|
||||
|
||||
+75
-3
@@ -7,10 +7,10 @@ import { navigate } from "../common/navigate";
|
||||
import { createSearchParam } from "../common/url/search-params";
|
||||
import type { Context, HomeAssistant } from "../types";
|
||||
import type { BlueprintInput } from "./blueprint";
|
||||
import type { DeviceCondition, DeviceTrigger } from "./device_automation";
|
||||
import type { Action, MODES } from "./script";
|
||||
import { migrateAutomationAction } from "./script";
|
||||
import { CONDITION_BUILDING_BLOCKS } from "./condition";
|
||||
import type { DeviceCondition, DeviceTrigger } from "./device_automation";
|
||||
import type { Action, Field, MODES } from "./script";
|
||||
import { migrateAutomationAction } from "./script";
|
||||
|
||||
export const AUTOMATION_DEFAULT_MODE: (typeof MODES)[number] = "single";
|
||||
export const AUTOMATION_DEFAULT_MAX = 10;
|
||||
@@ -513,6 +513,14 @@ export const isCondition = (config: unknown): boolean => {
|
||||
return "condition" in condition && typeof condition.condition === "string";
|
||||
};
|
||||
|
||||
export const isScriptField = (config: unknown): boolean => {
|
||||
if (!config || typeof config !== "object") {
|
||||
return false;
|
||||
}
|
||||
const field = config as Record<string, unknown>;
|
||||
return "field" in field && typeof field.field === "object";
|
||||
};
|
||||
|
||||
export const subscribeTrigger = (
|
||||
hass: HomeAssistant,
|
||||
onChange: (result: {
|
||||
@@ -546,3 +554,67 @@ export interface AutomationClipboard {
|
||||
condition?: Condition;
|
||||
action?: Action;
|
||||
}
|
||||
|
||||
export interface BaseSidebarConfig {
|
||||
toggleYamlMode: () => boolean;
|
||||
delete: () => void;
|
||||
}
|
||||
|
||||
export interface TriggerSidebarConfig extends BaseSidebarConfig {
|
||||
save: (value: Trigger) => void;
|
||||
close: () => void;
|
||||
rename: () => void;
|
||||
disable: () => void;
|
||||
config: Trigger;
|
||||
yamlMode: boolean;
|
||||
uiSupported: boolean;
|
||||
}
|
||||
|
||||
export interface ConditionSidebarConfig extends BaseSidebarConfig {
|
||||
save: (value: Condition) => void;
|
||||
close: () => void;
|
||||
rename: () => void;
|
||||
disable: () => void;
|
||||
config: Condition;
|
||||
yamlMode: boolean;
|
||||
uiSupported: boolean;
|
||||
}
|
||||
|
||||
export interface ActionSidebarConfig extends BaseSidebarConfig {
|
||||
save: (value: Action) => void;
|
||||
close: () => void;
|
||||
rename: () => void;
|
||||
disable: () => void;
|
||||
config: Action;
|
||||
yamlMode: boolean;
|
||||
uiSupported: boolean;
|
||||
}
|
||||
|
||||
export interface OptionSidebarConfig extends BaseSidebarConfig {
|
||||
close: () => void;
|
||||
rename: () => void;
|
||||
}
|
||||
|
||||
export interface ScriptFieldSidebarConfig extends BaseSidebarConfig {
|
||||
save: (value: Field) => void;
|
||||
close: () => void;
|
||||
config: {
|
||||
field: Field;
|
||||
selector: boolean;
|
||||
key: string;
|
||||
excludeKeys: string[];
|
||||
};
|
||||
yamlMode: boolean;
|
||||
}
|
||||
|
||||
export type SidebarConfig =
|
||||
| TriggerSidebarConfig
|
||||
| ConditionSidebarConfig
|
||||
| ActionSidebarConfig
|
||||
| OptionSidebarConfig
|
||||
| ScriptFieldSidebarConfig;
|
||||
|
||||
export interface ShowAutomationEditorParams {
|
||||
data?: Partial<AutomationConfig>;
|
||||
expanded?: boolean;
|
||||
}
|
||||
|
||||
@@ -44,6 +44,14 @@ export interface DatadiskList {
|
||||
disks: Datadisk[];
|
||||
}
|
||||
|
||||
export interface HostDisksUsage {
|
||||
total_bytes?: number;
|
||||
used_bytes: number;
|
||||
id: string;
|
||||
label: string;
|
||||
children?: HostDisksUsage[];
|
||||
}
|
||||
|
||||
export const fetchHassioHostInfo = async (
|
||||
hass: HomeAssistant
|
||||
): Promise<HassioHostInfo> => {
|
||||
@@ -180,3 +188,20 @@ export const listDatadisks = async (
|
||||
await hass.callApi<HassioResponse<DatadiskList>>("GET", "/os/datadisk/list")
|
||||
);
|
||||
};
|
||||
|
||||
export const fetchHostDisksUsage = async (hass: HomeAssistant) => {
|
||||
if (atLeastVersion(hass.config.version, 2021, 2, 4)) {
|
||||
return hass.callWS<HostDisksUsage>({
|
||||
type: "supervisor/api",
|
||||
endpoint: "/host/disks/default/usage",
|
||||
method: "get",
|
||||
});
|
||||
}
|
||||
|
||||
return hassioApiResultExtractor(
|
||||
await hass.callApi<HassioResponse<HostDisksUsage>>(
|
||||
"GET",
|
||||
"hassio/host/disks/default/usage"
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
@@ -23,7 +23,6 @@ export interface LovelaceViewElement extends HTMLElement {
|
||||
badges?: HuiBadge[];
|
||||
sections?: HuiSection[];
|
||||
isStrategy: boolean;
|
||||
allowEdit: boolean;
|
||||
setConfig(config: LovelaceViewConfig): void;
|
||||
}
|
||||
|
||||
@@ -36,7 +35,6 @@ export interface LovelaceSectionElement extends HTMLElement {
|
||||
cards?: HuiCard[];
|
||||
isStrategy: boolean;
|
||||
importOnly?: boolean;
|
||||
allowEdit: boolean;
|
||||
setConfig(config: LovelaceSectionConfig): void;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import type { HASSDomEvent } from "../../common/dom/fire_event";
|
||||
|
||||
export interface ActionHandlerOptions {
|
||||
hasTap?: boolean;
|
||||
hasHold?: boolean;
|
||||
hasDoubleClick?: boolean;
|
||||
disabled?: boolean;
|
||||
|
||||
@@ -2,10 +2,16 @@ import type { Condition } from "../../../panels/lovelace/common/validate-conditi
|
||||
import type { LovelaceCardConfig } from "./card";
|
||||
import type { LovelaceStrategyConfig } from "./strategy";
|
||||
|
||||
export interface LovelaceSectionStyleConfig {
|
||||
background_color?: string;
|
||||
background_opacity?: number;
|
||||
}
|
||||
|
||||
export interface LovelaceBaseSectionConfig {
|
||||
visibility?: Condition[];
|
||||
column_span?: number;
|
||||
row_span?: number;
|
||||
style?: LovelaceSectionStyleConfig;
|
||||
/**
|
||||
* @deprecated Use heading card instead.
|
||||
*/
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
export const SELECTOR_SELECTOR_BUILDING_BLOCKS = ["condition", "action"];
|
||||
@@ -12,6 +12,7 @@ export interface Zone {
|
||||
}
|
||||
|
||||
export interface HomeZoneMutableParams {
|
||||
name?: string;
|
||||
latitude: number;
|
||||
longitude: number;
|
||||
radius: number;
|
||||
|
||||
@@ -840,7 +840,9 @@ export class QuickBar extends LitElement {
|
||||
const additionalItems = [
|
||||
{
|
||||
path: "",
|
||||
primaryText: this.hass.localize("ui.panel.config.info.shortcuts"),
|
||||
primaryText: this.hass.localize(
|
||||
"ui.dialogs.quick-bar.commands.navigation.shortcuts"
|
||||
),
|
||||
action: () => showShortcutsDialog(this),
|
||||
iconPath: mdiKeyboard,
|
||||
},
|
||||
@@ -856,7 +858,9 @@ export class QuickBar extends LitElement {
|
||||
|
||||
private _generateNavigationPanelCommands(): BaseNavigationCommand[] {
|
||||
return Object.keys(this.hass.panels)
|
||||
.filter((panelKey) => panelKey !== "_my_redirect")
|
||||
.filter(
|
||||
(panelKey) => panelKey !== "_my_redirect" && panelKey !== "hassio"
|
||||
)
|
||||
.map((panelKey) => {
|
||||
const panel = this.hass.panels[panelKey];
|
||||
const translationKey = getPanelNameTranslationKey(panel);
|
||||
|
||||
@@ -112,7 +112,9 @@ export class HaVoiceAssistantSetupStepSuccess extends LitElement {
|
||||
this.assistConfiguration.available_wake_words.length > 1
|
||||
? html`<div class="row">
|
||||
<ha-select
|
||||
.label=${"Wake word"}
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.voice_assistants.assistants.pipeline.detail.form.wake_word_id"
|
||||
)}
|
||||
@closed=${stopPropagation}
|
||||
fixedMenuPosition
|
||||
naturalMenuWidth
|
||||
@@ -144,7 +146,9 @@ export class HaVoiceAssistantSetupStepSuccess extends LitElement {
|
||||
${pipelineEntity
|
||||
? html`<div class="row">
|
||||
<ha-select
|
||||
.label=${"Assistant"}
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.voice_assistants.assistants.pipeline.devices.pipeline"
|
||||
)}
|
||||
@closed=${stopPropagation}
|
||||
.value=${pipelineEntity?.state}
|
||||
fixedMenuPosition
|
||||
|
||||
@@ -441,6 +441,35 @@ class WaterHeaterEntity extends Entity {
|
||||
}
|
||||
}
|
||||
|
||||
class FanEntity extends Entity {
|
||||
static CAPABILITY_ATTRIBUTES = new Set([
|
||||
...CAPABILITY_ATTRIBUTES,
|
||||
"direction",
|
||||
"oscillating",
|
||||
"percentage",
|
||||
]);
|
||||
|
||||
public async handleService(domain, service, data) {
|
||||
if (domain !== this.domain) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (["turn_on", "turn_off"].includes(service)) {
|
||||
this.update(service === "turn_on" ? "on" : "off");
|
||||
} else if (
|
||||
["set_direction", "oscillate", "set_percentage"].includes(service)
|
||||
) {
|
||||
const { entity_id, ...toSet } = data;
|
||||
this.update(this.state, {
|
||||
...this.attributes,
|
||||
...toSet,
|
||||
});
|
||||
} else {
|
||||
super.handleService(domain, service, data);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class GroupEntity extends Entity {
|
||||
public async handleService(domain, service, data) {
|
||||
if (!["homeassistant", this.domain].includes(domain)) {
|
||||
@@ -463,6 +492,7 @@ const TYPES = {
|
||||
alarm_control_panel: AlarmControlPanelEntity,
|
||||
climate: ClimateEntity,
|
||||
cover: CoverEntity,
|
||||
fan: FanEntity,
|
||||
group: GroupEntity,
|
||||
input_boolean: ToggleEntity,
|
||||
input_number: InputNumberEntity,
|
||||
|
||||
@@ -41,9 +41,11 @@ import {
|
||||
YAML_ONLY_ACTION_TYPES,
|
||||
} from "../../../../data/action";
|
||||
import type {
|
||||
ActionSidebarConfig,
|
||||
AutomationClipboard,
|
||||
Condition,
|
||||
} from "../../../../data/automation";
|
||||
import { CONDITION_BUILDING_BLOCKS } from "../../../../data/condition";
|
||||
import { validateConfig } from "../../../../data/config";
|
||||
import {
|
||||
floorsContext,
|
||||
@@ -87,7 +89,6 @@ import "./types/ha-automation-action-set_conversation_response";
|
||||
import "./types/ha-automation-action-stop";
|
||||
import "./types/ha-automation-action-wait_for_trigger";
|
||||
import "./types/ha-automation-action-wait_template";
|
||||
import { CONDITION_BUILDING_BLOCKS } from "../../../../data/condition";
|
||||
|
||||
export const getAutomationActionType = memoizeOne(
|
||||
(action: Action | undefined) => {
|
||||
@@ -441,9 +442,9 @@ export default class HaAutomationActionRow extends LitElement {
|
||||
(blockType === "condition" &&
|
||||
CONDITION_BUILDING_BLOCKS.includes(
|
||||
(this.action as Condition).condition
|
||||
))) &&
|
||||
!this._collapsed
|
||||
)))
|
||||
? html`<ha-automation-action-editor
|
||||
class=${this._collapsed ? "hidden" : ""}
|
||||
.hass=${this.hass}
|
||||
.action=${this.action}
|
||||
.narrow=${this.narrow}
|
||||
@@ -641,9 +642,6 @@ export default class HaAutomationActionRow extends LitElement {
|
||||
}
|
||||
|
||||
public openSidebar(action?: Action): void {
|
||||
if (this.narrow) {
|
||||
this.scrollIntoView();
|
||||
}
|
||||
const sidebarAction = action ?? this.action;
|
||||
const actionType = getAutomationActionType(sidebarAction);
|
||||
|
||||
@@ -665,11 +663,17 @@ export default class HaAutomationActionRow extends LitElement {
|
||||
disable: this._onDisable,
|
||||
delete: this._onDelete,
|
||||
config: sidebarAction,
|
||||
type: "action",
|
||||
uiSupported: actionType ? this._uiSupported(actionType) : false,
|
||||
yamlMode: this._yamlMode,
|
||||
});
|
||||
} satisfies ActionSidebarConfig);
|
||||
this._selected = true;
|
||||
|
||||
if (this.narrow) {
|
||||
this.scrollIntoView({
|
||||
block: "start",
|
||||
behavior: "smooth",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public expand() {
|
||||
|
||||
@@ -164,10 +164,15 @@ export default class HaAutomationAction extends LitElement {
|
||||
!ACTION_BUILDING_BLOCKS.includes(type)
|
||||
) {
|
||||
row.openSidebar();
|
||||
if (this.narrow) {
|
||||
row.scrollIntoView({
|
||||
block: "start",
|
||||
behavior: "smooth",
|
||||
});
|
||||
}
|
||||
} else if (!this.optionsInSidebar) {
|
||||
row.expand();
|
||||
}
|
||||
row.scrollIntoView();
|
||||
row.focus();
|
||||
});
|
||||
}
|
||||
@@ -183,6 +188,10 @@ export default class HaAutomationAction extends LitElement {
|
||||
}
|
||||
|
||||
private _addActionDialog() {
|
||||
if (this.narrow) {
|
||||
fireEvent(this, "close-sidebar");
|
||||
}
|
||||
|
||||
showAddAutomationElementDialog(this, {
|
||||
type: "action",
|
||||
add: this._addAction,
|
||||
|
||||
@@ -1,24 +1,31 @@
|
||||
import { mdiClose, mdiPlus } from "@mdi/js";
|
||||
import { dump } from "js-yaml";
|
||||
import type { CSSResultGroup } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { mdiClose, mdiPlus } from "@mdi/js";
|
||||
import { dump } from "js-yaml";
|
||||
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||
import "../../../../components/chips/ha-assist-chip";
|
||||
import "../../../../components/chips/ha-chip-set";
|
||||
import "../../../../components/ha-alert";
|
||||
import "../../../../components/ha-area-picker";
|
||||
import "../../../../components/ha-domain-icon";
|
||||
import "../../../../components/ha-expansion-panel";
|
||||
import "../../../../components/ha-icon-picker";
|
||||
import "../../../../components/ha-svg-icon";
|
||||
import "../../../../components/ha-textarea";
|
||||
import "../../../../components/ha-textfield";
|
||||
import "../../../../components/ha-labels-picker";
|
||||
import "../../../../components/ha-suggest-with-ai-button";
|
||||
import type { SuggestWithAIGenerateTask } from "../../../../components/ha-suggest-with-ai-button";
|
||||
import "../../../../components/ha-svg-icon";
|
||||
import "../../../../components/ha-textarea";
|
||||
import "../../../../components/ha-textfield";
|
||||
import "../../category/ha-category-picker";
|
||||
import "../../../../components/ha-expansion-panel";
|
||||
import "../../../../components/chips/ha-chip-set";
|
||||
import "../../../../components/chips/ha-assist-chip";
|
||||
import "../../../../components/ha-area-picker";
|
||||
|
||||
import { computeStateDomain } from "../../../../common/entity/compute_state_domain";
|
||||
import { supportsMarkdownHelper } from "../../../../common/translations/markdown_support";
|
||||
import { subscribeOne } from "../../../../common/util/subscribe-one";
|
||||
import type { GenDataTaskResult } from "../../../../data/ai_task";
|
||||
import { fetchCategoryRegistry } from "../../../../data/category_registry";
|
||||
import { subscribeEntityRegistry } from "../../../../data/entity_registry";
|
||||
import { subscribeLabelRegistry } from "../../../../data/label_registry";
|
||||
import type { HassDialog } from "../../../../dialogs/make-dialog-manager";
|
||||
import { haStyle, haStyleDialog } from "../../../../resources/styles";
|
||||
import type { HomeAssistant } from "../../../../types";
|
||||
@@ -26,13 +33,6 @@ import type {
|
||||
EntityRegistryUpdate,
|
||||
SaveDialogParams,
|
||||
} from "./show-dialog-automation-save";
|
||||
import { supportsMarkdownHelper } from "../../../../common/translations/markdown_support";
|
||||
import type { GenDataTaskResult } from "../../../../data/ai_task";
|
||||
import { computeStateDomain } from "../../../../common/entity/compute_state_domain";
|
||||
import { subscribeOne } from "../../../../common/util/subscribe-one";
|
||||
import { subscribeLabelRegistry } from "../../../../data/label_registry";
|
||||
import { subscribeEntityRegistry } from "../../../../data/entity_registry";
|
||||
import { fetchCategoryRegistry } from "../../../../data/category_registry";
|
||||
|
||||
@customElement("ha-dialog-automation-save")
|
||||
class DialogAutomationSave extends LitElement implements HassDialog {
|
||||
@@ -242,7 +242,7 @@ class DialogAutomationSave extends LitElement implements HassDialog {
|
||||
const title = this.hass.localize(
|
||||
this._params.config.alias
|
||||
? "ui.panel.config.automation.editor.rename"
|
||||
: "ui.panel.config.automation.editor.save"
|
||||
: "ui.common.save"
|
||||
);
|
||||
|
||||
return html`
|
||||
@@ -289,7 +289,7 @@ class DialogAutomationSave extends LitElement implements HassDialog {
|
||||
${this.hass.localize(
|
||||
this._params.config.alias && !this._params.onDiscard
|
||||
? "ui.panel.config.automation.editor.rename"
|
||||
: "ui.panel.config.automation.editor.save"
|
||||
: "ui.common.save"
|
||||
)}
|
||||
</ha-button>
|
||||
</div>
|
||||
|
||||
@@ -6,6 +6,7 @@ import { fireEvent } from "../../../common/dom/fire_event";
|
||||
import "../../../components/ha-alert";
|
||||
import "../../../components/ha-button";
|
||||
import "../../../components/ha-markdown";
|
||||
import "../../../components/ha-fab";
|
||||
import type { BlueprintAutomationConfig } from "../../../data/automation";
|
||||
import { fetchBlueprints } from "../../../data/blueprint";
|
||||
import { HaBlueprintGenericEditor } from "../blueprint/blueprint-generic-editor";
|
||||
@@ -58,7 +59,7 @@ export class HaBlueprintAutomationEditor extends HaBlueprintGenericEditor {
|
||||
<ha-fab
|
||||
slot="fab"
|
||||
class=${this.dirty ? "dirty" : ""}
|
||||
.label=${this.hass.localize("ui.panel.config.automation.editor.save")}
|
||||
.label=${this.hass.localize("ui.common.save")}
|
||||
.disabled=${this.saving}
|
||||
extended
|
||||
@click=${this._saveAutomation}
|
||||
|
||||
@@ -35,6 +35,7 @@ import "../../../../components/ha-md-menu-item";
|
||||
import type {
|
||||
AutomationClipboard,
|
||||
Condition,
|
||||
ConditionSidebarConfig,
|
||||
} from "../../../../data/automation";
|
||||
import { testCondition } from "../../../../data/automation";
|
||||
import { describeCondition } from "../../../../data/automation_i18n";
|
||||
@@ -370,9 +371,9 @@ export default class HaAutomationConditionRow extends LitElement {
|
||||
</ha-card>
|
||||
|
||||
${this.optionsInSidebar &&
|
||||
CONDITION_BUILDING_BLOCKS.includes(this.condition.condition) &&
|
||||
!this._collapsed
|
||||
CONDITION_BUILDING_BLOCKS.includes(this.condition.condition)
|
||||
? html`<ha-automation-condition-editor
|
||||
class=${this._collapsed ? "hidden" : ""}
|
||||
.hass=${this.hass}
|
||||
.condition=${this.condition}
|
||||
.disabled=${this.disabled}
|
||||
@@ -598,10 +599,6 @@ export default class HaAutomationConditionRow extends LitElement {
|
||||
}
|
||||
|
||||
public openSidebar(condition?: Condition): void {
|
||||
if (this.narrow) {
|
||||
this.scrollIntoView();
|
||||
}
|
||||
|
||||
const sidebarCondition = condition || this.condition;
|
||||
fireEvent(this, "open-sidebar", {
|
||||
save: (value) => {
|
||||
@@ -621,11 +618,17 @@ export default class HaAutomationConditionRow extends LitElement {
|
||||
disable: this._onDisable,
|
||||
delete: this._onDelete,
|
||||
config: sidebarCondition,
|
||||
type: "condition",
|
||||
uiSupported: this._uiSupported(sidebarCondition.condition),
|
||||
yamlMode: this._yamlMode,
|
||||
});
|
||||
} satisfies ConditionSidebarConfig);
|
||||
this._selected = true;
|
||||
|
||||
if (this.narrow) {
|
||||
this.scrollIntoView({
|
||||
block: "start",
|
||||
behavior: "smooth",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private _uiSupported = memoizeOne(
|
||||
@@ -655,8 +658,13 @@ export default class HaAutomationConditionRow extends LitElement {
|
||||
overflow: hidden;
|
||||
transition: max-height 0.3s;
|
||||
text-align: center;
|
||||
border-top-right-radius: var(--ha-card-border-radius, var(--ha-border-radius-lg));
|
||||
border-top-left-radius: var(--ha-card-border-radius, var(--ha-border-radius-lg));
|
||||
border-top-right-radius: var(
|
||||
--ha-card-border-radius,
|
||||
var(--ha-border-radius-lg)
|
||||
);
|
||||
border-top-left-radius: var(
|
||||
--ha-card-border-radius,
|
||||
var(--ha-border-radius-lg)
|
||||
);
|
||||
}
|
||||
.testing.active {
|
||||
|
||||
@@ -16,6 +16,7 @@ import type {
|
||||
AutomationClipboard,
|
||||
Condition,
|
||||
} from "../../../../data/automation";
|
||||
import { CONDITION_BUILDING_BLOCKS } from "../../../../data/condition";
|
||||
import type { HomeAssistant } from "../../../../types";
|
||||
import {
|
||||
PASTE_VALUE,
|
||||
@@ -23,7 +24,6 @@ import {
|
||||
} from "../show-add-automation-element-dialog";
|
||||
import "./ha-automation-condition-row";
|
||||
import type HaAutomationConditionRow from "./ha-automation-condition-row";
|
||||
import { CONDITION_BUILDING_BLOCKS } from "../../../../data/condition";
|
||||
|
||||
@customElement("ha-automation-condition")
|
||||
export default class HaAutomationCondition extends LitElement {
|
||||
@@ -108,10 +108,15 @@ export default class HaAutomationCondition extends LitElement {
|
||||
!CONDITION_BUILDING_BLOCKS.includes(row.condition.condition)
|
||||
) {
|
||||
row.openSidebar();
|
||||
if (this.narrow) {
|
||||
row.scrollIntoView({
|
||||
block: "start",
|
||||
behavior: "smooth",
|
||||
});
|
||||
}
|
||||
} else if (!this.optionsInSidebar) {
|
||||
row.expand();
|
||||
}
|
||||
row.scrollIntoView();
|
||||
row.focus();
|
||||
});
|
||||
}
|
||||
@@ -203,6 +208,9 @@ export default class HaAutomationCondition extends LitElement {
|
||||
}
|
||||
|
||||
private _addConditionDialog() {
|
||||
if (this.narrow) {
|
||||
fireEvent(this, "close-sidebar");
|
||||
}
|
||||
showAddAutomationElementDialog(this, {
|
||||
type: "condition",
|
||||
add: this._addCondition,
|
||||
|
||||
@@ -535,12 +535,10 @@ export class HaAutomationEditor extends PreventUnsavedMixin(
|
||||
<ha-fab
|
||||
slot="fab"
|
||||
class=${this._dirty ? "dirty" : ""}
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.automation.editor.save"
|
||||
)}
|
||||
.label=${this.hass.localize("ui.common.save")}
|
||||
.disabled=${this._saving}
|
||||
extended
|
||||
@click=${this._saveAutomation}
|
||||
@click=${this._handleSaveAutomation}
|
||||
>
|
||||
<ha-svg-icon
|
||||
slot="icon"
|
||||
|
||||
@@ -1,84 +1,137 @@
|
||||
import {
|
||||
mdiClose,
|
||||
mdiDelete,
|
||||
mdiDotsVertical,
|
||||
mdiIdentifier,
|
||||
mdiPlayCircleOutline,
|
||||
mdiPlaylistEdit,
|
||||
mdiRenameBox,
|
||||
mdiStopCircleOutline,
|
||||
} from "@mdi/js";
|
||||
import { css, html, LitElement, nothing } 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 { stopPropagation } from "../../../common/dom/stop_propagation";
|
||||
import { handleStructError } from "../../../common/structs/handle-errors";
|
||||
import type { LocalizeKeys } from "../../../common/translations/localize";
|
||||
import "../../../components/ha-card";
|
||||
import "../../../components/ha-dialog-header";
|
||||
import "../../../components/ha-icon-button";
|
||||
import "../../../components/ha-md-button-menu";
|
||||
import "../../../components/ha-md-divider";
|
||||
import "../../../components/ha-md-menu-item";
|
||||
import type { Condition, Trigger } from "../../../data/automation";
|
||||
import type { Action, RepeatAction } from "../../../data/script";
|
||||
import "../../../components/ha-bottom-sheet";
|
||||
import type { HaBottomSheet } from "../../../components/ha-bottom-sheet";
|
||||
import {
|
||||
isCondition,
|
||||
isScriptField,
|
||||
isTrigger,
|
||||
type ActionSidebarConfig,
|
||||
type ConditionSidebarConfig,
|
||||
type ScriptFieldSidebarConfig,
|
||||
type SidebarConfig,
|
||||
type TriggerSidebarConfig,
|
||||
} from "../../../data/automation";
|
||||
import { isTriggerList } from "../../../data/trigger";
|
||||
import type { HomeAssistant } from "../../../types";
|
||||
import "./action/ha-automation-action-editor";
|
||||
import { getAutomationActionType } from "./action/ha-automation-action-row";
|
||||
import { getRepeatType } from "./action/types/ha-automation-action-repeat";
|
||||
import "./condition/ha-automation-condition-editor";
|
||||
import type HaAutomationConditionEditor from "./condition/ha-automation-condition-editor";
|
||||
import "./ha-automation-editor-warning";
|
||||
import "./trigger/ha-automation-trigger-editor";
|
||||
import type HaAutomationTriggerEditor from "./trigger/ha-automation-trigger-editor";
|
||||
import { ACTION_BUILDING_BLOCKS } from "../../../data/action";
|
||||
import { CONDITION_BUILDING_BLOCKS } from "../../../data/condition";
|
||||
|
||||
export interface OpenSidebarConfig {
|
||||
save: (config: Trigger | Condition | Action) => void;
|
||||
close: () => void;
|
||||
rename: () => void;
|
||||
toggleYamlMode: () => boolean;
|
||||
disable: () => void;
|
||||
delete: () => void;
|
||||
config: Trigger | Condition | Action;
|
||||
type: "trigger" | "condition" | "action" | "option";
|
||||
uiSupported: boolean;
|
||||
yamlMode: boolean;
|
||||
}
|
||||
import "./sidebar/ha-automation-sidebar-action";
|
||||
import "./sidebar/ha-automation-sidebar-condition";
|
||||
import "./sidebar/ha-automation-sidebar-option";
|
||||
import "./sidebar/ha-automation-sidebar-script-field";
|
||||
import "./sidebar/ha-automation-sidebar-script-field-selector";
|
||||
import "./sidebar/ha-automation-sidebar-trigger";
|
||||
|
||||
@customElement("ha-automation-sidebar")
|
||||
export default class HaAutomationSidebar extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public config?: OpenSidebarConfig;
|
||||
@property({ attribute: false }) public config?: SidebarConfig;
|
||||
|
||||
@property({ type: Boolean, attribute: "wide" }) public isWide = false;
|
||||
|
||||
@property({ type: Boolean }) public disabled = false;
|
||||
|
||||
@property({ type: Boolean }) public narrow = false;
|
||||
|
||||
@state() private _yamlMode = false;
|
||||
|
||||
@state() private _requestShowId = false;
|
||||
@query("ha-bottom-sheet") private _bottomSheetElement?: HaBottomSheet;
|
||||
|
||||
@state() private _warnings?: string[];
|
||||
private _renderContent() {
|
||||
// get config type
|
||||
const type = this._getType();
|
||||
|
||||
@query(".sidebar-editor")
|
||||
public editor?: HaAutomationTriggerEditor | HaAutomationConditionEditor;
|
||||
|
||||
protected willUpdate(changedProperties) {
|
||||
if (changedProperties.has("config")) {
|
||||
this._requestShowId = false;
|
||||
this._warnings = undefined;
|
||||
if (this.config) {
|
||||
this._yamlMode = this.config.yamlMode;
|
||||
if (this._yamlMode) {
|
||||
this.editor?.yamlEditor?.setValue(this.config.config);
|
||||
}
|
||||
}
|
||||
if (type === "trigger") {
|
||||
return html`
|
||||
<ha-automation-sidebar-trigger
|
||||
class="sidebar-content"
|
||||
.hass=${this.hass}
|
||||
.config=${this.config}
|
||||
.isWide=${this.isWide}
|
||||
.narrow=${this.narrow}
|
||||
.disabled=${this.disabled}
|
||||
.yamlMode=${this._yamlMode}
|
||||
@toggle-yaml-mode=${this._toggleYamlMode}
|
||||
@close-sidebar=${this._handleCloseSidebar}
|
||||
></ha-automation-sidebar-trigger>
|
||||
`;
|
||||
}
|
||||
if (type === "condition") {
|
||||
return html`
|
||||
<ha-automation-sidebar-condition
|
||||
class="sidebar-content"
|
||||
.hass=${this.hass}
|
||||
.config=${this.config}
|
||||
.isWide=${this.isWide}
|
||||
.narrow=${this.narrow}
|
||||
.disabled=${this.disabled}
|
||||
.yamlMode=${this._yamlMode}
|
||||
@toggle-yaml-mode=${this._toggleYamlMode}
|
||||
@close-sidebar=${this._handleCloseSidebar}
|
||||
></ha-automation-sidebar-condition>
|
||||
`;
|
||||
}
|
||||
if (type === "action") {
|
||||
return html`
|
||||
<ha-automation-sidebar-action
|
||||
class="sidebar-content"
|
||||
.hass=${this.hass}
|
||||
.config=${this.config}
|
||||
.isWide=${this.isWide}
|
||||
.narrow=${this.narrow}
|
||||
.disabled=${this.disabled}
|
||||
.yamlMode=${this._yamlMode}
|
||||
@toggle-yaml-mode=${this._toggleYamlMode}
|
||||
@close-sidebar=${this._handleCloseSidebar}
|
||||
></ha-automation-sidebar-action>
|
||||
`;
|
||||
}
|
||||
if (type === "option") {
|
||||
return html`
|
||||
<ha-automation-sidebar-option
|
||||
class="sidebar-content"
|
||||
.hass=${this.hass}
|
||||
.config=${this.config}
|
||||
.isWide=${this.isWide}
|
||||
.narrow=${this.narrow}
|
||||
.disabled=${this.disabled}
|
||||
@close-sidebar=${this._handleCloseSidebar}
|
||||
></ha-automation-sidebar-option>
|
||||
`;
|
||||
}
|
||||
if (type === "script-field-selector") {
|
||||
return html`
|
||||
<ha-automation-sidebar-script-field-selector
|
||||
class="sidebar-content"
|
||||
.hass=${this.hass}
|
||||
.config=${this.config}
|
||||
.isWide=${this.isWide}
|
||||
.narrow=${this.narrow}
|
||||
.disabled=${this.disabled}
|
||||
.yamlMode=${this._yamlMode}
|
||||
@toggle-yaml-mode=${this._toggleYamlMode}
|
||||
@close-sidebar=${this._handleCloseSidebar}
|
||||
></ha-automation-sidebar-script-field-selector>
|
||||
`;
|
||||
}
|
||||
if (type === "script-field") {
|
||||
return html`
|
||||
<ha-automation-sidebar-script-field
|
||||
class="sidebar-content"
|
||||
.hass=${this.hass}
|
||||
.config=${this.config}
|
||||
.isWide=${this.isWide}
|
||||
.narrow=${this.narrow}
|
||||
.disabled=${this.disabled}
|
||||
.yamlMode=${this._yamlMode}
|
||||
@toggle-yaml-mode=${this._toggleYamlMode}
|
||||
@close-sidebar=${this._handleCloseSidebar}
|
||||
></ha-automation-sidebar-script-field>
|
||||
`;
|
||||
}
|
||||
|
||||
return nothing;
|
||||
}
|
||||
|
||||
protected render() {
|
||||
@@ -86,245 +139,63 @@ export default class HaAutomationSidebar extends LitElement {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
const disabled =
|
||||
this.disabled ||
|
||||
("enabled" in this.config.config && this.config.config.enabled === false);
|
||||
let type = isTriggerList(this.config.config as Trigger)
|
||||
? "list"
|
||||
: this.config.type === "action"
|
||||
? getAutomationActionType(this.config.config as Action)
|
||||
: this.config.config[this.config.type];
|
||||
|
||||
if (this.config.type === "action" && type === "repeat") {
|
||||
type = `repeat_${getRepeatType((this.config.config as RepeatAction).repeat)}`;
|
||||
if (this.narrow) {
|
||||
return html`
|
||||
<ha-bottom-sheet @bottom-sheet-closed=${this._closeSidebar}>
|
||||
${this._renderContent()}
|
||||
</ha-bottom-sheet>
|
||||
`;
|
||||
}
|
||||
|
||||
const isBuildingBlock = [
|
||||
...CONDITION_BUILDING_BLOCKS,
|
||||
...ACTION_BUILDING_BLOCKS,
|
||||
].includes(type);
|
||||
|
||||
const subtitle = this.hass.localize(
|
||||
(this.config.type === "option"
|
||||
? "ui.panel.config.automation.editor.actions.type.choose.label"
|
||||
: `ui.panel.config.automation.editor.${this.config.type}s.${this.config.type}`) as LocalizeKeys
|
||||
);
|
||||
const title =
|
||||
this.hass.localize(
|
||||
(this.config.type === "option"
|
||||
? "ui.panel.config.automation.editor.actions.type.choose.option_label"
|
||||
: `ui.panel.config.automation.editor.${this.config.type}s.type.${type}.label`) as LocalizeKeys
|
||||
) || type;
|
||||
|
||||
const description =
|
||||
isBuildingBlock || this.config.type === "option"
|
||||
? this.hass.localize(
|
||||
(this.config.type === "option"
|
||||
? "ui.panel.config.automation.editor.actions.type.choose.option_description"
|
||||
: `ui.panel.config.automation.editor.${this.config.type}s.type.${type}.description.picker`) as LocalizeKeys
|
||||
)
|
||||
: "";
|
||||
|
||||
return html`
|
||||
<ha-card
|
||||
outlined
|
||||
class=${classMap({
|
||||
mobile: !this.isWide,
|
||||
yaml: this._yamlMode,
|
||||
})}
|
||||
>
|
||||
<ha-dialog-header>
|
||||
<ha-icon-button
|
||||
slot="navigationIcon"
|
||||
.label=${this.hass.localize("ui.common.close")}
|
||||
.path=${mdiClose}
|
||||
@click=${this._closeSidebar}
|
||||
></ha-icon-button>
|
||||
<span slot="title">${title}</span>
|
||||
<span slot="subtitle">${subtitle}</span>
|
||||
<ha-md-button-menu
|
||||
slot="actionItems"
|
||||
@click=${this._openOverflowMenu}
|
||||
@keydown=${stopPropagation}
|
||||
@closed=${stopPropagation}
|
||||
positioning="fixed"
|
||||
>
|
||||
<ha-icon-button
|
||||
slot="trigger"
|
||||
.label=${this.hass.localize("ui.common.menu")}
|
||||
.path=${mdiDotsVertical}
|
||||
></ha-icon-button>
|
||||
<ha-md-menu-item
|
||||
.clickAction=${this.config.rename}
|
||||
.disabled=${disabled || type === "list"}
|
||||
>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.automation.editor.triggers.rename"
|
||||
)}
|
||||
<ha-svg-icon slot="start" .path=${mdiRenameBox}></ha-svg-icon>
|
||||
</ha-md-menu-item>
|
||||
|
||||
${this.config.type === "trigger" &&
|
||||
!this._yamlMode &&
|
||||
!("id" in this.config.config) &&
|
||||
!this._requestShowId
|
||||
? html`<ha-md-menu-item
|
||||
.clickAction=${this._showTriggerId}
|
||||
.disabled=${disabled || type === "list"}
|
||||
>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.automation.editor.triggers.edit_id"
|
||||
)}
|
||||
<ha-svg-icon
|
||||
slot="start"
|
||||
.path=${mdiIdentifier}
|
||||
></ha-svg-icon>
|
||||
</ha-md-menu-item>`
|
||||
: nothing}
|
||||
${this.config.type !== "option"
|
||||
? html`
|
||||
<ha-md-menu-item
|
||||
.clickAction=${this._toggleYamlMode}
|
||||
.disabled=${!this.config.uiSupported || !!this._warnings}
|
||||
>
|
||||
${this.hass.localize(
|
||||
`ui.panel.config.automation.editor.edit_${!this._yamlMode ? "yaml" : "ui"}`
|
||||
)}
|
||||
<ha-svg-icon
|
||||
slot="start"
|
||||
.path=${mdiPlaylistEdit}
|
||||
></ha-svg-icon>
|
||||
</ha-md-menu-item>
|
||||
`
|
||||
: nothing}
|
||||
|
||||
<ha-md-divider role="separator" tabindex="-1"></ha-md-divider>
|
||||
|
||||
${this.config.type !== "option"
|
||||
? html`
|
||||
<ha-md-menu-item
|
||||
.clickAction=${this.config.disable}
|
||||
.disabled=${this.disabled || type === "list"}
|
||||
>
|
||||
${disabled
|
||||
? this.hass.localize(
|
||||
"ui.panel.config.automation.editor.actions.enable"
|
||||
)
|
||||
: this.hass.localize(
|
||||
"ui.panel.config.automation.editor.actions.disable"
|
||||
)}
|
||||
<ha-svg-icon
|
||||
slot="start"
|
||||
.path=${disabled
|
||||
? mdiPlayCircleOutline
|
||||
: mdiStopCircleOutline}
|
||||
></ha-svg-icon>
|
||||
</ha-md-menu-item>
|
||||
`
|
||||
: nothing}
|
||||
<ha-md-menu-item
|
||||
.clickAction=${this.config.delete}
|
||||
class="warning"
|
||||
.disabled=${this.disabled}
|
||||
>
|
||||
${this.hass.localize(
|
||||
`ui.panel.config.automation.editor.actions.${this.config.type !== "option" ? "delete" : "type.choose.remove_option"}`
|
||||
)}
|
||||
<ha-svg-icon
|
||||
class="warning"
|
||||
slot="start"
|
||||
.path=${mdiDelete}
|
||||
></ha-svg-icon>
|
||||
</ha-md-menu-item>
|
||||
</ha-md-button-menu>
|
||||
</ha-dialog-header>
|
||||
${this._warnings
|
||||
? html`<ha-automation-editor-warning
|
||||
.localize=${this.hass.localize}
|
||||
.warnings=${this._warnings}
|
||||
>
|
||||
</ha-automation-editor-warning>`
|
||||
: nothing}
|
||||
<div class="card-content">
|
||||
${this.config.type === "trigger"
|
||||
? html`<ha-automation-trigger-editor
|
||||
class="sidebar-editor"
|
||||
.hass=${this.hass}
|
||||
.trigger=${this.config.config as Trigger}
|
||||
@value-changed=${this._valueChangedSidebar}
|
||||
.uiSupported=${this.config.uiSupported}
|
||||
.showId=${this._requestShowId}
|
||||
.yamlMode=${this._yamlMode}
|
||||
.disabled=${this.disabled}
|
||||
@ui-mode-not-available=${this._handleUiModeNotAvailable}
|
||||
></ha-automation-trigger-editor>`
|
||||
: this.config.type === "condition" &&
|
||||
(this._yamlMode || !CONDITION_BUILDING_BLOCKS.includes(type))
|
||||
? html`
|
||||
<ha-automation-condition-editor
|
||||
class="sidebar-editor"
|
||||
.hass=${this.hass}
|
||||
.condition=${this.config.config as Condition}
|
||||
.yamlMode=${this._yamlMode}
|
||||
.uiSupported=${this.config.uiSupported}
|
||||
@value-changed=${this._valueChangedSidebar}
|
||||
.disabled=${this.disabled}
|
||||
@ui-mode-not-available=${this._handleUiModeNotAvailable}
|
||||
></ha-automation-condition-editor>
|
||||
`
|
||||
: this.config.type === "action" &&
|
||||
(this._yamlMode || !ACTION_BUILDING_BLOCKS.includes(type))
|
||||
? html`
|
||||
<ha-automation-action-editor
|
||||
class="sidebar-editor"
|
||||
.hass=${this.hass}
|
||||
.action=${this.config.config as Action}
|
||||
.yamlMode=${this._yamlMode}
|
||||
.uiSupported=${this.config.uiSupported}
|
||||
@value-changed=${this._valueChangedSidebar}
|
||||
sidebar
|
||||
narrow
|
||||
.disabled=${this.disabled}
|
||||
@ui-mode-not-available=${this._handleUiModeNotAvailable}
|
||||
></ha-automation-action-editor>
|
||||
`
|
||||
: description || nothing}
|
||||
</div>
|
||||
</ha-card>
|
||||
`;
|
||||
return this._renderContent();
|
||||
}
|
||||
|
||||
private _handleUiModeNotAvailable(ev: CustomEvent) {
|
||||
this._warnings = handleStructError(this.hass, ev.detail).warnings;
|
||||
if (!this._yamlMode) {
|
||||
this._yamlMode = true;
|
||||
private _getType() {
|
||||
if (
|
||||
(this.config as TriggerSidebarConfig)?.config &&
|
||||
(isTrigger((this.config as TriggerSidebarConfig)?.config) ||
|
||||
isTriggerList((this.config as TriggerSidebarConfig)?.config))
|
||||
) {
|
||||
return "trigger";
|
||||
}
|
||||
if (isCondition((this.config as ConditionSidebarConfig)?.config)) {
|
||||
return "condition";
|
||||
}
|
||||
if (
|
||||
(this.config as ScriptFieldSidebarConfig)?.config &&
|
||||
isScriptField((this.config as ScriptFieldSidebarConfig)?.config)
|
||||
) {
|
||||
return (this.config as ScriptFieldSidebarConfig)?.config.selector
|
||||
? "script-field-selector"
|
||||
: "script-field";
|
||||
}
|
||||
|
||||
// option is always a building block and doesn't have a config
|
||||
if (this.config && !(this.config as any)?.config) {
|
||||
return "option";
|
||||
}
|
||||
|
||||
if ((this.config as ActionSidebarConfig)?.config) {
|
||||
return "action";
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
private _valueChangedSidebar(ev: CustomEvent) {
|
||||
private _handleCloseSidebar(ev: CustomEvent) {
|
||||
ev.stopPropagation();
|
||||
|
||||
this.config?.save(ev.detail.value);
|
||||
|
||||
if (this.config) {
|
||||
fireEvent(this, "value-changed", {
|
||||
value: {
|
||||
...this.config,
|
||||
config: ev.detail.value,
|
||||
},
|
||||
});
|
||||
if (this.narrow) {
|
||||
this._bottomSheetElement?.closeSheet();
|
||||
return;
|
||||
}
|
||||
|
||||
this._closeSidebar();
|
||||
}
|
||||
|
||||
private _closeSidebar() {
|
||||
this.config?.close();
|
||||
}
|
||||
|
||||
private _openOverflowMenu(ev: MouseEvent) {
|
||||
ev.stopPropagation();
|
||||
ev.preventDefault();
|
||||
}
|
||||
|
||||
private _toggleYamlMode = () => {
|
||||
this._yamlMode = this.config!.toggleYamlMode();
|
||||
fireEvent(this, "value-changed", {
|
||||
@@ -335,10 +206,6 @@ export default class HaAutomationSidebar extends LitElement {
|
||||
});
|
||||
};
|
||||
|
||||
private _showTriggerId = () => {
|
||||
this._requestShowId = true;
|
||||
};
|
||||
|
||||
static styles = css`
|
||||
:host {
|
||||
height: 100%;
|
||||
@@ -347,59 +214,14 @@ export default class HaAutomationSidebar extends LitElement {
|
||||
var(--ha-border-radius-2xl)
|
||||
);
|
||||
border-radius: var(--ha-card-border-radius);
|
||||
}
|
||||
|
||||
ha-card {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
border-color: var(--primary-color);
|
||||
border-width: 2px;
|
||||
display: block;
|
||||
}
|
||||
ha-card.mobile {
|
||||
border-bottom-right-radius: var(--ha-border-radius-square);
|
||||
border-bottom-left-radius: var(--ha-border-radius-square);
|
||||
--ha-bottom-sheet-border-width: 2px;
|
||||
--ha-bottom-sheet-border-style: solid;
|
||||
--ha-bottom-sheet-border-color: var(--primary-color);
|
||||
}
|
||||
|
||||
@media all and (max-width: 870px) {
|
||||
ha-card.mobile {
|
||||
max-height: 70vh;
|
||||
max-height: 70dvh;
|
||||
border-width: 2px 2px 0;
|
||||
}
|
||||
ha-card.mobile.yaml {
|
||||
height: 70vh;
|
||||
height: 70dvh;
|
||||
}
|
||||
}
|
||||
|
||||
ha-dialog-header {
|
||||
border-radius: var(--ha-card-border-radius);
|
||||
}
|
||||
.sidebar-editor {
|
||||
padding-top: 64px;
|
||||
}
|
||||
|
||||
.card-content {
|
||||
max-height: calc(100% - 80px);
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
@media (min-width: 450px) and (min-height: 500px) {
|
||||
.card-content {
|
||||
max-height: calc(100% - 104px);
|
||||
overflow: auto;
|
||||
}
|
||||
}
|
||||
|
||||
@media all and (max-width: 870px) {
|
||||
ha-card.mobile .card-content {
|
||||
max-height: calc(
|
||||
70vh - 88px - max(var(--safe-area-inset-bottom), 16px)
|
||||
);
|
||||
max-height: calc(
|
||||
70dvh - 88px - max(var(--safe-area-inset-bottom), 16px)
|
||||
);
|
||||
.sidebar-content {
|
||||
max-height: 100%;
|
||||
}
|
||||
}
|
||||
`;
|
||||
@@ -409,4 +231,8 @@ declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-automation-sidebar": HaAutomationSidebar;
|
||||
}
|
||||
|
||||
interface HASSDomEvents {
|
||||
"toggle-yaml-mode": undefined;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { mdiContentSave, mdiHelpCircle } from "@mdi/js";
|
||||
import type { HassEntity } from "home-assistant-js-websocket";
|
||||
import { load } from "js-yaml";
|
||||
import type { CSSResultGroup, PropertyValues } from "lit";
|
||||
import type { CSSResultGroup } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import { classMap } from "lit/directives/class-map";
|
||||
import {
|
||||
any,
|
||||
@@ -18,11 +18,6 @@ import {
|
||||
import { ensureArray } from "../../../common/array/ensure-array";
|
||||
import { canOverrideAlphanumericInput } from "../../../common/dom/can-override-input";
|
||||
import { fireEvent } from "../../../common/dom/fire_event";
|
||||
import { constructUrlCurrentPath } from "../../../common/url/construct-url";
|
||||
import {
|
||||
extractSearchParam,
|
||||
removeSearchParam,
|
||||
} from "../../../common/url/search-params";
|
||||
import "../../../components/ha-button";
|
||||
import "../../../components/ha-fab";
|
||||
import "../../../components/ha-icon-button";
|
||||
@@ -31,6 +26,7 @@ import type {
|
||||
AutomationConfig,
|
||||
Condition,
|
||||
ManualAutomationConfig,
|
||||
SidebarConfig,
|
||||
Trigger,
|
||||
} from "../../../data/automation";
|
||||
import {
|
||||
@@ -43,15 +39,11 @@ import type { HomeAssistant } from "../../../types";
|
||||
import { documentationUrl } from "../../../util/documentation-url";
|
||||
import { showToast } from "../../../util/toast";
|
||||
import "./action/ha-automation-action";
|
||||
import type HaAutomationAction from "./action/ha-automation-action";
|
||||
import "./condition/ha-automation-condition";
|
||||
import type HaAutomationCondition from "./condition/ha-automation-condition";
|
||||
import "./ha-automation-sidebar";
|
||||
import type { OpenSidebarConfig } from "./ha-automation-sidebar";
|
||||
import { showPasteReplaceDialog } from "./paste-replace-dialog/show-dialog-paste-replace";
|
||||
import { saveFabStyles } from "./styles";
|
||||
import "./trigger/ha-automation-trigger";
|
||||
import type HaAutomationTrigger from "./trigger/ha-automation-trigger";
|
||||
|
||||
const baseConfigStruct = object({
|
||||
alias: optional(string()),
|
||||
@@ -90,7 +82,10 @@ export class HaManualAutomationEditor extends LitElement {
|
||||
|
||||
@state() private _pastedConfig?: ManualAutomationConfig;
|
||||
|
||||
@state() private _sidebarConfig?: OpenSidebarConfig;
|
||||
@state() private _sidebarConfig?: SidebarConfig;
|
||||
|
||||
@query(".content")
|
||||
private _contentElement?: HTMLDivElement;
|
||||
|
||||
private _previousConfig?: ManualAutomationConfig;
|
||||
|
||||
@@ -104,31 +99,6 @@ export class HaManualAutomationEditor extends LitElement {
|
||||
super.disconnectedCallback();
|
||||
}
|
||||
|
||||
protected firstUpdated(changedProps: PropertyValues): void {
|
||||
super.firstUpdated(changedProps);
|
||||
const expanded = extractSearchParam("expanded");
|
||||
if (expanded === "1") {
|
||||
this._clearParam("expanded");
|
||||
const items = this.shadowRoot!.querySelectorAll<
|
||||
HaAutomationTrigger | HaAutomationCondition | HaAutomationAction
|
||||
>("ha-automation-trigger, ha-automation-condition, ha-automation-action");
|
||||
|
||||
items.forEach((el) => {
|
||||
el.updateComplete.then(() => {
|
||||
el.expandAll();
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private _clearParam(param: string) {
|
||||
window.history.replaceState(
|
||||
null,
|
||||
"",
|
||||
constructUrlCurrentPath(removeSearchParam(param))
|
||||
);
|
||||
}
|
||||
|
||||
private _renderContent() {
|
||||
return html`
|
||||
${this.stateObj?.state === "off"
|
||||
@@ -294,9 +264,7 @@ export class HaManualAutomationEditor extends LitElement {
|
||||
<ha-fab
|
||||
slot="fab"
|
||||
class=${this.dirty ? "dirty" : ""}
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.automation.editor.save"
|
||||
)}
|
||||
.label=${this.hass.localize("ui.common.save")}
|
||||
.disabled=${this.saving}
|
||||
extended
|
||||
@click=${this._saveAutomation}
|
||||
@@ -308,10 +276,11 @@ export class HaManualAutomationEditor extends LitElement {
|
||||
class=${classMap({
|
||||
sidebar: true,
|
||||
hidden: !this._sidebarConfig,
|
||||
overlay: !this.isWide,
|
||||
overlay: !this.isWide && !this.narrow,
|
||||
})}
|
||||
.isWide=${this.isWide}
|
||||
.hass=${this.hass}
|
||||
.narrow=${this.narrow}
|
||||
.config=${this._sidebarConfig}
|
||||
@value-changed=${this._sidebarConfigChanged}
|
||||
.disabled=${this.disabled}
|
||||
@@ -320,13 +289,13 @@ export class HaManualAutomationEditor extends LitElement {
|
||||
`;
|
||||
}
|
||||
|
||||
private _openSidebar(ev: CustomEvent<OpenSidebarConfig>) {
|
||||
private _openSidebar(ev: CustomEvent<SidebarConfig>) {
|
||||
// deselect previous selected row
|
||||
this._sidebarConfig?.close?.();
|
||||
this._sidebarConfig = ev.detail;
|
||||
}
|
||||
|
||||
private _sidebarConfigChanged(ev: CustomEvent<{ value: OpenSidebarConfig }>) {
|
||||
private _sidebarConfigChanged(ev: CustomEvent<{ value: SidebarConfig }>) {
|
||||
ev.stopPropagation();
|
||||
if (!this._sidebarConfig) {
|
||||
return;
|
||||
@@ -348,6 +317,8 @@ export class HaManualAutomationEditor extends LitElement {
|
||||
|
||||
private _handleCloseSidebar() {
|
||||
this._sidebarConfig = undefined;
|
||||
// fix content shift when bottom rows are scrolled into view
|
||||
this._contentElement?.scrollIntoView();
|
||||
}
|
||||
|
||||
private _triggerChanged(ev: CustomEvent): void {
|
||||
@@ -647,26 +618,23 @@ export class HaManualAutomationEditor extends LitElement {
|
||||
|
||||
.sidebar.overlay {
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
height: calc(100% - 64px);
|
||||
bottom: 8px;
|
||||
right: 8px;
|
||||
height: calc(100% - 70px);
|
||||
padding: 0;
|
||||
z-index: 5;
|
||||
box-shadow: -8px 0 16px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
@media all and (max-width: 870px) {
|
||||
.sidebar.overlay {
|
||||
max-height: 70vh;
|
||||
max-height: 70dvh;
|
||||
height: auto;
|
||||
width: 100%;
|
||||
box-shadow: 0px -8px 16px rgba(0, 0, 0, 0.2);
|
||||
.split-view {
|
||||
gap: 0;
|
||||
margin-right: -8px;
|
||||
}
|
||||
}
|
||||
|
||||
@media all and (max-width: 870px) {
|
||||
.sidebar.overlay.hidden {
|
||||
.sidebar {
|
||||
height: 0;
|
||||
width: 0;
|
||||
flex: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -718,7 +686,7 @@ declare global {
|
||||
}
|
||||
|
||||
interface HASSDomEvents {
|
||||
"open-sidebar": OpenSidebarConfig;
|
||||
"open-sidebar": SidebarConfig;
|
||||
"close-sidebar": undefined;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,7 +23,10 @@ import "../../../../components/ha-icon-button";
|
||||
import "../../../../components/ha-md-button-menu";
|
||||
import "../../../../components/ha-md-menu-item";
|
||||
import "../../../../components/ha-svg-icon";
|
||||
import type { Condition } from "../../../../data/automation";
|
||||
import type {
|
||||
Condition,
|
||||
OptionSidebarConfig,
|
||||
} from "../../../../data/automation";
|
||||
import { describeCondition } from "../../../../data/automation_i18n";
|
||||
import { fullEntitiesContext } from "../../../../data/context";
|
||||
import type { EntityRegistryEntry } from "../../../../data/entity_registry";
|
||||
@@ -189,6 +192,7 @@ export default class HaAutomationOptionRow extends LitElement {
|
||||
"card-content": true,
|
||||
indent: this.optionsInSidebar,
|
||||
selected: this._selected,
|
||||
hidden: this._collapsed,
|
||||
})}
|
||||
>
|
||||
<h4>
|
||||
@@ -245,9 +249,7 @@ export default class HaAutomationOptionRow extends LitElement {
|
||||
`}
|
||||
</ha-card>
|
||||
|
||||
${this.optionsInSidebar && !this._collapsed
|
||||
? this._renderContent()
|
||||
: nothing}
|
||||
${this.optionsInSidebar ? this._renderContent() : nothing}
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -341,14 +343,7 @@ export default class HaAutomationOptionRow extends LitElement {
|
||||
}
|
||||
|
||||
public openSidebar(): void {
|
||||
if (this.narrow) {
|
||||
this.scrollIntoView();
|
||||
}
|
||||
|
||||
fireEvent(this, "open-sidebar", {
|
||||
save: () => {
|
||||
// nothing to save for an option in the sidebar
|
||||
},
|
||||
close: () => {
|
||||
this._selected = false;
|
||||
fireEvent(this, "close-sidebar");
|
||||
@@ -357,16 +352,16 @@ export default class HaAutomationOptionRow extends LitElement {
|
||||
this._renameOption();
|
||||
},
|
||||
toggleYamlMode: () => false, // no yaml mode for options
|
||||
disable: () => {
|
||||
// option cannot be disabled
|
||||
},
|
||||
delete: this._removeOption,
|
||||
config: {},
|
||||
type: "option",
|
||||
uiSupported: true,
|
||||
yamlMode: false,
|
||||
});
|
||||
} satisfies OptionSidebarConfig);
|
||||
this._selected = true;
|
||||
|
||||
if (this.narrow) {
|
||||
this.scrollIntoView({
|
||||
block: "start",
|
||||
behavior: "smooth",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public expand() {
|
||||
|
||||
@@ -132,8 +132,12 @@ export default class HaAutomationOption extends LitElement {
|
||||
row.updateComplete.then(() => {
|
||||
if (!this.optionsInSidebar) {
|
||||
row.expand();
|
||||
} else if (this.narrow) {
|
||||
row.scrollIntoView({
|
||||
block: "start",
|
||||
behavior: "smooth",
|
||||
});
|
||||
}
|
||||
row.scrollIntoView();
|
||||
row.focus();
|
||||
});
|
||||
}
|
||||
|
||||
@@ -0,0 +1,196 @@
|
||||
import {
|
||||
mdiDelete,
|
||||
mdiPlayCircleOutline,
|
||||
mdiPlaylistEdit,
|
||||
mdiRenameBox,
|
||||
mdiStopCircleOutline,
|
||||
} from "@mdi/js";
|
||||
import { css, html, LitElement } from "lit";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||
import { handleStructError } from "../../../../common/structs/handle-errors";
|
||||
import type { LocalizeKeys } from "../../../../common/translations/localize";
|
||||
import "../../../../components/ha-md-divider";
|
||||
import "../../../../components/ha-md-menu-item";
|
||||
import { ACTION_BUILDING_BLOCKS } from "../../../../data/action";
|
||||
import type { ActionSidebarConfig } from "../../../../data/automation";
|
||||
import type { RepeatAction } from "../../../../data/script";
|
||||
import type { HomeAssistant } from "../../../../types";
|
||||
import type HaAutomationConditionEditor from "../action/ha-automation-action-editor";
|
||||
import { getAutomationActionType } from "../action/ha-automation-action-row";
|
||||
import { getRepeatType } from "../action/types/ha-automation-action-repeat";
|
||||
import "../trigger/ha-automation-trigger-editor";
|
||||
import "./ha-automation-sidebar-card";
|
||||
|
||||
@customElement("ha-automation-sidebar-action")
|
||||
export default class HaAutomationSidebarAction extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public config!: ActionSidebarConfig;
|
||||
|
||||
@property({ type: Boolean, attribute: "wide" }) public isWide = false;
|
||||
|
||||
@property({ type: Boolean }) public disabled = false;
|
||||
|
||||
@property({ type: Boolean, attribute: "yaml-mode" }) public yamlMode = false;
|
||||
|
||||
@property({ type: Boolean }) public narrow = false;
|
||||
|
||||
@state() private _warnings?: string[];
|
||||
|
||||
@query(".sidebar-editor")
|
||||
public editor?: HaAutomationConditionEditor;
|
||||
|
||||
protected willUpdate(changedProperties) {
|
||||
if (changedProperties.has("config")) {
|
||||
this._warnings = undefined;
|
||||
if (this.config) {
|
||||
this.yamlMode = this.config.yamlMode;
|
||||
if (this.yamlMode) {
|
||||
this.editor?.yamlEditor?.setValue(this.config.config);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected render() {
|
||||
const disabled =
|
||||
this.disabled ||
|
||||
("enabled" in this.config.config && this.config.config.enabled === false);
|
||||
|
||||
const actionType = getAutomationActionType(this.config.config);
|
||||
|
||||
const type =
|
||||
actionType !== "repeat"
|
||||
? actionType
|
||||
: `repeat_${getRepeatType((this.config.config as RepeatAction).repeat)}`;
|
||||
|
||||
const isBuildingBlock = ACTION_BUILDING_BLOCKS.includes(type || "");
|
||||
|
||||
const subtitle = this.hass.localize(
|
||||
"ui.panel.config.automation.editor.actions.action"
|
||||
);
|
||||
|
||||
const title =
|
||||
this.hass.localize(
|
||||
`ui.panel.config.automation.editor.actions.type.${type}.label` as LocalizeKeys
|
||||
) || type;
|
||||
|
||||
const description = isBuildingBlock
|
||||
? this.hass.localize(
|
||||
`ui.panel.config.automation.editor.actions.type.${type}.description.picker` as LocalizeKeys
|
||||
)
|
||||
: "";
|
||||
|
||||
return html`<ha-automation-sidebar-card
|
||||
.hass=${this.hass}
|
||||
.isWide=${this.isWide}
|
||||
.yamlMode=${this.yamlMode}
|
||||
.warnings=${this._warnings}
|
||||
.narrow=${this.narrow}
|
||||
>
|
||||
<span slot="title">${title}</span>
|
||||
<span slot="subtitle">${subtitle}</span>
|
||||
<ha-md-menu-item
|
||||
slot="menu-items"
|
||||
.clickAction=${this.config.rename}
|
||||
.disabled=${!!disabled}
|
||||
>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.automation.editor.triggers.rename"
|
||||
)}
|
||||
<ha-svg-icon slot="start" .path=${mdiRenameBox}></ha-svg-icon>
|
||||
</ha-md-menu-item>
|
||||
<ha-md-menu-item
|
||||
slot="menu-items"
|
||||
.clickAction=${this._toggleYamlMode}
|
||||
.disabled=${!this.config.uiSupported || !!this._warnings}
|
||||
>
|
||||
${this.hass.localize(
|
||||
`ui.panel.config.automation.editor.edit_${!this.yamlMode ? "yaml" : "ui"}`
|
||||
)}
|
||||
<ha-svg-icon slot="start" .path=${mdiPlaylistEdit}></ha-svg-icon>
|
||||
</ha-md-menu-item>
|
||||
<ha-md-divider
|
||||
slot="menu-items"
|
||||
role="separator"
|
||||
tabindex="-1"
|
||||
></ha-md-divider>
|
||||
<ha-md-menu-item slot="menu-items" .clickAction=${this.config.disable}>
|
||||
${this.hass.localize(
|
||||
`ui.panel.config.automation.editor.actions.${disabled ? "enable" : "disable"}`
|
||||
)}
|
||||
<ha-svg-icon
|
||||
slot="start"
|
||||
.path=${this.disabled ? mdiPlayCircleOutline : mdiStopCircleOutline}
|
||||
></ha-svg-icon>
|
||||
</ha-md-menu-item>
|
||||
<ha-md-menu-item
|
||||
slot="menu-items"
|
||||
.clickAction=${this.config.delete}
|
||||
.disabled=${this.disabled}
|
||||
class="warning"
|
||||
>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.automation.editor.actions.delete"
|
||||
)}
|
||||
<ha-svg-icon slot="start" .path=${mdiDelete}></ha-svg-icon>
|
||||
</ha-md-menu-item>
|
||||
${description && !this.yamlMode
|
||||
? html`<div class="description">${description}</div>`
|
||||
: html`<ha-automation-action-editor
|
||||
class="sidebar-editor"
|
||||
.hass=${this.hass}
|
||||
.action=${this.config.config}
|
||||
.yamlMode=${this.yamlMode}
|
||||
.uiSupported=${this.config.uiSupported}
|
||||
@value-changed=${this._valueChangedSidebar}
|
||||
sidebar
|
||||
narrow
|
||||
.disabled=${this.disabled}
|
||||
@ui-mode-not-available=${this._handleUiModeNotAvailable}
|
||||
></ha-automation-action-editor>`}
|
||||
</ha-automation-sidebar-card>`;
|
||||
}
|
||||
|
||||
private _handleUiModeNotAvailable(ev: CustomEvent) {
|
||||
this._warnings = handleStructError(this.hass, ev.detail).warnings;
|
||||
if (!this.yamlMode) {
|
||||
this.yamlMode = true;
|
||||
}
|
||||
}
|
||||
|
||||
private _valueChangedSidebar(ev: CustomEvent) {
|
||||
ev.stopPropagation();
|
||||
|
||||
this.config?.save?.(ev.detail.value);
|
||||
|
||||
if (this.config) {
|
||||
fireEvent(this, "value-changed", {
|
||||
value: {
|
||||
...this.config,
|
||||
config: ev.detail.value,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private _toggleYamlMode = () => {
|
||||
fireEvent(this, "toggle-yaml-mode");
|
||||
};
|
||||
|
||||
static styles = css`
|
||||
.sidebar-editor {
|
||||
padding-top: 64px;
|
||||
}
|
||||
.description {
|
||||
padding-top: 16px;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-automation-sidebar-action": HaAutomationSidebarAction;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,168 @@
|
||||
import { mdiClose, mdiDotsVertical } from "@mdi/js";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import {
|
||||
customElement,
|
||||
eventOptions,
|
||||
property,
|
||||
query,
|
||||
state,
|
||||
} from "lit/decorators";
|
||||
import { classMap } from "lit/directives/class-map";
|
||||
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||
import { stopPropagation } from "../../../../common/dom/stop_propagation";
|
||||
import "../../../../components/ha-card";
|
||||
import "../../../../components/ha-dialog-header";
|
||||
import "../../../../components/ha-icon-button";
|
||||
import "../../../../components/ha-md-button-menu";
|
||||
import "../../../../components/ha-md-divider";
|
||||
import "../../../../components/ha-md-menu-item";
|
||||
import type { HomeAssistant } from "../../../../types";
|
||||
import "../ha-automation-editor-warning";
|
||||
|
||||
export interface SidebarOverflowMenuEntry {
|
||||
clickAction: () => void;
|
||||
disabled?: boolean;
|
||||
label: string;
|
||||
icon?: string;
|
||||
danger?: boolean;
|
||||
}
|
||||
|
||||
export type SidebarOverflowMenu = (SidebarOverflowMenuEntry | "separator")[];
|
||||
|
||||
@customElement("ha-automation-sidebar-card")
|
||||
export default class HaAutomationSidebarCard extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ type: Boolean, attribute: "wide" }) public isWide = false;
|
||||
|
||||
@property({ type: Boolean, attribute: "yaml-mode" }) public yamlMode = false;
|
||||
|
||||
@property({ attribute: false }) public warnings?: string[];
|
||||
|
||||
@property({ type: Boolean }) public narrow = false;
|
||||
|
||||
@state() private _contentScrolled = false;
|
||||
|
||||
@query(".card-content") private _contentElement?: HTMLDivElement;
|
||||
|
||||
protected render() {
|
||||
return html`
|
||||
<ha-card
|
||||
outlined
|
||||
class=${classMap({
|
||||
mobile: !this.isWide,
|
||||
yaml: this.yamlMode,
|
||||
})}
|
||||
>
|
||||
<ha-dialog-header
|
||||
class=${classMap({ scrolled: this._contentScrolled })}
|
||||
>
|
||||
<ha-icon-button
|
||||
slot="navigationIcon"
|
||||
.label=${this.hass.localize("ui.common.close")}
|
||||
.path=${mdiClose}
|
||||
@click=${this._closeSidebar}
|
||||
></ha-icon-button>
|
||||
<slot slot="title" name="title"></slot>
|
||||
<slot slot="subtitle" name="subtitle"></slot>
|
||||
<slot name="overflow-menu" slot="actionItems">
|
||||
<ha-md-button-menu
|
||||
@click=${this._openOverflowMenu}
|
||||
@keydown=${stopPropagation}
|
||||
@closed=${stopPropagation}
|
||||
.positioning=${this.narrow ? "absolute" : "fixed"}
|
||||
>
|
||||
<ha-icon-button
|
||||
slot="trigger"
|
||||
.label=${this.hass.localize("ui.common.menu")}
|
||||
.path=${mdiDotsVertical}
|
||||
></ha-icon-button>
|
||||
<slot name="menu-items"></slot>
|
||||
</ha-md-button-menu>
|
||||
</slot>
|
||||
</ha-dialog-header>
|
||||
${this.warnings
|
||||
? html`<ha-automation-editor-warning
|
||||
.localize=${this.hass.localize}
|
||||
.warnings=${this.warnings}
|
||||
>
|
||||
</ha-automation-editor-warning>`
|
||||
: nothing}
|
||||
<div class="card-content" @scroll=${this._onScroll}>
|
||||
<slot></slot>
|
||||
</div>
|
||||
</ha-card>
|
||||
`;
|
||||
}
|
||||
|
||||
@eventOptions({ passive: true })
|
||||
private _onScroll() {
|
||||
const top = this._contentElement?.scrollTop ?? 0;
|
||||
this._contentScrolled = top > 0;
|
||||
}
|
||||
|
||||
private _closeSidebar() {
|
||||
fireEvent(this, "close-sidebar");
|
||||
}
|
||||
|
||||
private _openOverflowMenu(ev: MouseEvent) {
|
||||
ev.stopPropagation();
|
||||
ev.preventDefault();
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
ha-card {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
border-color: var(--primary-color);
|
||||
border-width: 2px;
|
||||
display: block;
|
||||
}
|
||||
|
||||
@media all and (max-width: 870px) {
|
||||
ha-card.mobile {
|
||||
border: none;
|
||||
}
|
||||
ha-card.mobile {
|
||||
border-bottom-right-radius: var(--ha-border-radius-square);
|
||||
border-bottom-left-radius: var(--ha-border-radius-square);
|
||||
}
|
||||
}
|
||||
|
||||
ha-dialog-header {
|
||||
border-radius: var(--ha-card-border-radius);
|
||||
box-shadow: none;
|
||||
transition: box-shadow 180ms ease-in-out;
|
||||
border-bottom-left-radius: 0;
|
||||
border-bottom-right-radius: 0;
|
||||
z-index: 6;
|
||||
position: relative;
|
||||
background-color: var(
|
||||
--ha-dialog-surface-background,
|
||||
var(--mdc-theme-surface, #fff)
|
||||
);
|
||||
}
|
||||
|
||||
ha-dialog-header.scrolled {
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.12);
|
||||
}
|
||||
|
||||
.card-content {
|
||||
max-height: calc(100% - 80px);
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
@media (min-width: 450px) and (min-height: 500px) {
|
||||
.card-content {
|
||||
max-height: calc(100% - 104px);
|
||||
overflow: auto;
|
||||
}
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-automation-sidebar-card": HaAutomationSidebarCard;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,183 @@
|
||||
import {
|
||||
mdiDelete,
|
||||
mdiPlayCircleOutline,
|
||||
mdiPlaylistEdit,
|
||||
mdiRenameBox,
|
||||
mdiStopCircleOutline,
|
||||
} from "@mdi/js";
|
||||
import { css, html, LitElement } from "lit";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||
import { handleStructError } from "../../../../common/structs/handle-errors";
|
||||
import type { ConditionSidebarConfig } from "../../../../data/automation";
|
||||
import { CONDITION_BUILDING_BLOCKS } from "../../../../data/condition";
|
||||
import type { HomeAssistant } from "../../../../types";
|
||||
import "../condition/ha-automation-condition-editor";
|
||||
import type HaAutomationConditionEditor from "../condition/ha-automation-condition-editor";
|
||||
import "./ha-automation-sidebar-card";
|
||||
|
||||
@customElement("ha-automation-sidebar-condition")
|
||||
export default class HaAutomationSidebarCondition extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public config!: ConditionSidebarConfig;
|
||||
|
||||
@property({ type: Boolean, attribute: "wide" }) public isWide = false;
|
||||
|
||||
@property({ type: Boolean }) public disabled = false;
|
||||
|
||||
@property({ type: Boolean, attribute: "yaml-mode" }) public yamlMode = false;
|
||||
|
||||
@property({ type: Boolean }) public narrow = false;
|
||||
|
||||
@state() private _warnings?: string[];
|
||||
|
||||
@query(".sidebar-editor")
|
||||
public editor?: HaAutomationConditionEditor;
|
||||
|
||||
protected willUpdate(changedProperties) {
|
||||
if (changedProperties.has("config")) {
|
||||
this._warnings = undefined;
|
||||
if (this.config) {
|
||||
this.yamlMode = this.config.yamlMode;
|
||||
if (this.yamlMode) {
|
||||
this.editor?.yamlEditor?.setValue(this.config.config);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected render() {
|
||||
const disabled =
|
||||
this.disabled ||
|
||||
("enabled" in this.config.config && this.config.config.enabled === false);
|
||||
|
||||
const type = this.config.config.condition;
|
||||
|
||||
const isBuildingBlock = CONDITION_BUILDING_BLOCKS.includes(type);
|
||||
|
||||
const subtitle = this.hass.localize(
|
||||
"ui.panel.config.automation.editor.conditions.condition"
|
||||
);
|
||||
|
||||
const title =
|
||||
this.hass.localize(
|
||||
`ui.panel.config.automation.editor.conditions.type.${type}.label`
|
||||
) || type;
|
||||
|
||||
const description = isBuildingBlock
|
||||
? this.hass.localize(
|
||||
`ui.panel.config.automation.editor.conditions.type.${type}.description.picker`
|
||||
)
|
||||
: "";
|
||||
|
||||
return html`<ha-automation-sidebar-card
|
||||
.hass=${this.hass}
|
||||
.isWide=${this.isWide}
|
||||
.yamlMode=${this.yamlMode}
|
||||
.warnings=${this._warnings}
|
||||
.narrow=${this.narrow}
|
||||
>
|
||||
<span slot="title">${title}</span>
|
||||
<span slot="subtitle">${subtitle}</span>
|
||||
<ha-md-menu-item
|
||||
slot="menu-items"
|
||||
.clickAction=${this.config.rename}
|
||||
.disabled=${!!disabled}
|
||||
>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.automation.editor.triggers.rename"
|
||||
)}
|
||||
<ha-svg-icon slot="start" .path=${mdiRenameBox}></ha-svg-icon>
|
||||
</ha-md-menu-item>
|
||||
<ha-md-menu-item
|
||||
slot="menu-items"
|
||||
.clickAction=${this._toggleYamlMode}
|
||||
.disabled=${!this.config.uiSupported || !!this._warnings}
|
||||
>
|
||||
${this.hass.localize(
|
||||
`ui.panel.config.automation.editor.edit_${!this.yamlMode ? "yaml" : "ui"}`
|
||||
)}
|
||||
<ha-svg-icon slot="start" .path=${mdiPlaylistEdit}></ha-svg-icon>
|
||||
</ha-md-menu-item>
|
||||
<ha-md-divider
|
||||
slot="menu-items"
|
||||
role="separator"
|
||||
tabindex="-1"
|
||||
></ha-md-divider>
|
||||
<ha-md-menu-item slot="menu-items" .clickAction=${this.config.disable}>
|
||||
${this.hass.localize(
|
||||
`ui.panel.config.automation.editor.actions.${disabled ? "enable" : "disable"}`
|
||||
)}
|
||||
<ha-svg-icon
|
||||
slot="start"
|
||||
.path=${this.disabled ? mdiPlayCircleOutline : mdiStopCircleOutline}
|
||||
></ha-svg-icon>
|
||||
</ha-md-menu-item>
|
||||
<ha-md-menu-item
|
||||
slot="menu-items"
|
||||
.clickAction=${this.config.delete}
|
||||
.disabled=${this.disabled}
|
||||
class="warning"
|
||||
>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.automation.editor.actions.delete"
|
||||
)}
|
||||
<ha-svg-icon slot="start" .path=${mdiDelete}></ha-svg-icon>
|
||||
</ha-md-menu-item>
|
||||
${description && !this.yamlMode
|
||||
? html`<div class="description">${description}</div>`
|
||||
: html`<ha-automation-condition-editor
|
||||
class="sidebar-editor"
|
||||
.hass=${this.hass}
|
||||
.condition=${this.config.config}
|
||||
.yamlMode=${this.yamlMode}
|
||||
.uiSupported=${this.config.uiSupported}
|
||||
@value-changed=${this._valueChangedSidebar}
|
||||
.disabled=${this.disabled}
|
||||
@ui-mode-not-available=${this._handleUiModeNotAvailable}
|
||||
></ha-automation-condition-editor> `}
|
||||
</ha-automation-sidebar-card>`;
|
||||
}
|
||||
|
||||
private _handleUiModeNotAvailable(ev: CustomEvent) {
|
||||
this._warnings = handleStructError(this.hass, ev.detail).warnings;
|
||||
if (!this.yamlMode) {
|
||||
this.yamlMode = true;
|
||||
}
|
||||
}
|
||||
|
||||
private _valueChangedSidebar(ev: CustomEvent) {
|
||||
ev.stopPropagation();
|
||||
|
||||
this.config?.save?.(ev.detail.value);
|
||||
|
||||
if (this.config) {
|
||||
fireEvent(this, "value-changed", {
|
||||
value: {
|
||||
...this.config,
|
||||
config: ev.detail.value,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private _toggleYamlMode = () => {
|
||||
fireEvent(this, "toggle-yaml-mode");
|
||||
};
|
||||
|
||||
static styles = css`
|
||||
.sidebar-editor {
|
||||
padding-top: 64px;
|
||||
}
|
||||
.description {
|
||||
padding-top: 16px;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-automation-sidebar-condition": HaAutomationSidebarCondition;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
import { mdiDelete, mdiRenameBox } from "@mdi/js";
|
||||
import { css, html, LitElement } from "lit";
|
||||
import { customElement, property, query } from "lit/decorators";
|
||||
import type { OptionSidebarConfig } from "../../../../data/automation";
|
||||
import type { HomeAssistant } from "../../../../types";
|
||||
import type HaAutomationConditionEditor from "../action/ha-automation-action-editor";
|
||||
import "./ha-automation-sidebar-card";
|
||||
|
||||
@customElement("ha-automation-sidebar-option")
|
||||
export default class HaAutomationSidebarOption extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public config!: OptionSidebarConfig;
|
||||
|
||||
@property({ type: Boolean, attribute: "wide" }) public isWide = false;
|
||||
|
||||
@property({ type: Boolean }) public disabled = false;
|
||||
|
||||
@property({ type: Boolean }) public narrow = false;
|
||||
|
||||
@query(".sidebar-editor")
|
||||
public editor?: HaAutomationConditionEditor;
|
||||
|
||||
protected render() {
|
||||
const disabled = this.disabled;
|
||||
|
||||
const subtitle = this.hass.localize(
|
||||
"ui.panel.config.automation.editor.actions.type.choose.label"
|
||||
);
|
||||
|
||||
const title = this.hass.localize(
|
||||
"ui.panel.config.automation.editor.actions.type.choose.option_label"
|
||||
);
|
||||
|
||||
const description = this.hass.localize(
|
||||
"ui.panel.config.automation.editor.actions.type.choose.option_description"
|
||||
);
|
||||
|
||||
return html`<ha-automation-sidebar-card
|
||||
.hass=${this.hass}
|
||||
.isWide=${this.isWide}
|
||||
.narrow=${this.narrow}
|
||||
>
|
||||
<span slot="title">${title}</span>
|
||||
<span slot="subtitle">${subtitle}</span>
|
||||
<ha-md-menu-item
|
||||
slot="menu-items"
|
||||
.clickAction=${this.config.rename}
|
||||
.disabled=${!!disabled}
|
||||
>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.automation.editor.triggers.rename"
|
||||
)}
|
||||
<ha-svg-icon slot="start" .path=${mdiRenameBox}></ha-svg-icon>
|
||||
</ha-md-menu-item>
|
||||
<ha-md-divider
|
||||
slot="menu-items"
|
||||
role="separator"
|
||||
tabindex="-1"
|
||||
></ha-md-divider>
|
||||
<ha-md-menu-item
|
||||
slot="menu-items"
|
||||
.clickAction=${this.config.delete}
|
||||
.disabled=${this.disabled}
|
||||
class="warning"
|
||||
>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.automation.editor.actions.type.choose.remove_option"
|
||||
)}
|
||||
<ha-svg-icon slot="start" .path=${mdiDelete}></ha-svg-icon>
|
||||
</ha-md-menu-item>
|
||||
<div class="description">${description}</div>
|
||||
</ha-automation-sidebar-card>`;
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
.sidebar-editor {
|
||||
padding-top: 64px;
|
||||
}
|
||||
.description {
|
||||
padding-top: 16px;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-automation-sidebar-option": HaAutomationSidebarOption;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,133 @@
|
||||
import { mdiDelete, mdiPlaylistEdit } from "@mdi/js";
|
||||
import { css, html, LitElement } from "lit";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||
import type { LocalizeKeys } from "../../../../common/translations/localize";
|
||||
import type { ScriptFieldSidebarConfig } from "../../../../data/automation";
|
||||
import type { HomeAssistant } from "../../../../types";
|
||||
import "../../script/ha-script-field-selector-editor";
|
||||
import type HaAutomationConditionEditor from "../action/ha-automation-action-editor";
|
||||
import "./ha-automation-sidebar-card";
|
||||
|
||||
@customElement("ha-automation-sidebar-script-field-selector")
|
||||
export default class HaAutomationSidebarScriptFieldSelector extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public config!: ScriptFieldSidebarConfig;
|
||||
|
||||
@property({ type: Boolean, attribute: "wide" }) public isWide = false;
|
||||
|
||||
@property({ type: Boolean }) public disabled = false;
|
||||
|
||||
@property({ type: Boolean, attribute: "yaml-mode" }) public yamlMode = false;
|
||||
|
||||
@property({ type: Boolean }) public narrow = false;
|
||||
|
||||
@state() private _warnings?: string[];
|
||||
|
||||
@query(".sidebar-editor")
|
||||
public editor?: HaAutomationConditionEditor;
|
||||
|
||||
protected willUpdate(changedProperties) {
|
||||
if (changedProperties.has("config")) {
|
||||
this._warnings = undefined;
|
||||
if (this.config) {
|
||||
this.yamlMode = this.config.yamlMode;
|
||||
if (this.yamlMode) {
|
||||
this.editor?.yamlEditor?.setValue(this.config.config);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected render() {
|
||||
const subtitle = this.hass.localize(
|
||||
"ui.panel.config.script.editor.field.field_selector"
|
||||
);
|
||||
|
||||
const title =
|
||||
this.hass.localize(
|
||||
`ui.components.selectors.selector.types.${Object.keys(this.config.config.field.selector)[0]}` as LocalizeKeys
|
||||
) || Object.keys(this.config.config.field.selector)[0];
|
||||
|
||||
return html`<ha-automation-sidebar-card
|
||||
.hass=${this.hass}
|
||||
.isWide=${this.isWide}
|
||||
.yamlMode=${this.yamlMode}
|
||||
.warnings=${this._warnings}
|
||||
.narrow=${this.narrow}
|
||||
>
|
||||
<span slot="title">${title}</span>
|
||||
<span slot="subtitle">${subtitle}</span>
|
||||
<ha-md-menu-item
|
||||
slot="menu-items"
|
||||
.clickAction=${this._toggleYamlMode}
|
||||
.disabled=${!!this._warnings}
|
||||
>
|
||||
${this.hass.localize(
|
||||
`ui.panel.config.automation.editor.edit_${!this.yamlMode ? "yaml" : "ui"}`
|
||||
)}
|
||||
<ha-svg-icon slot="start" .path=${mdiPlaylistEdit}></ha-svg-icon>
|
||||
</ha-md-menu-item>
|
||||
<ha-md-menu-item
|
||||
slot="menu-items"
|
||||
.clickAction=${this.config.delete}
|
||||
.disabled=${this.disabled}
|
||||
class="warning"
|
||||
>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.automation.editor.actions.delete"
|
||||
)}
|
||||
<ha-svg-icon slot="start" .path=${mdiDelete}></ha-svg-icon>
|
||||
</ha-md-menu-item>
|
||||
<ha-script-field-selector-editor
|
||||
class="sidebar-editor"
|
||||
.hass=${this.hass}
|
||||
.field=${this.config.config.field}
|
||||
.disabled=${this.disabled}
|
||||
@value-changed=${this._valueChangedSidebar}
|
||||
.yamlMode=${this.yamlMode}
|
||||
></ha-script-field-selector-editor>
|
||||
</ha-automation-sidebar-card>`;
|
||||
}
|
||||
|
||||
private _valueChangedSidebar(ev: CustomEvent) {
|
||||
ev.stopPropagation();
|
||||
|
||||
this.config?.save?.({
|
||||
...this.config.config.field,
|
||||
key: this.config.config.key,
|
||||
...ev.detail.value,
|
||||
});
|
||||
|
||||
if (this.config) {
|
||||
fireEvent(this, "value-changed", {
|
||||
value: {
|
||||
...this.config,
|
||||
config: {
|
||||
field: ev.detail.value,
|
||||
key: this.config.config.key,
|
||||
excludeKeys: this.config.config.excludeKeys,
|
||||
selector: true,
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private _toggleYamlMode = () => {
|
||||
fireEvent(this, "toggle-yaml-mode");
|
||||
};
|
||||
|
||||
static styles = css`
|
||||
.sidebar-editor {
|
||||
padding-top: 64px;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-automation-sidebar-script-field-selector": HaAutomationSidebarScriptFieldSelector;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,127 @@
|
||||
import { mdiDelete, mdiPlaylistEdit } from "@mdi/js";
|
||||
import { css, html, LitElement } from "lit";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||
import type { ScriptFieldSidebarConfig } from "../../../../data/automation";
|
||||
import type { HomeAssistant } from "../../../../types";
|
||||
import "../../script/ha-script-field-editor";
|
||||
import type HaAutomationConditionEditor from "../action/ha-automation-action-editor";
|
||||
import "./ha-automation-sidebar-card";
|
||||
|
||||
@customElement("ha-automation-sidebar-script-field")
|
||||
export default class HaAutomationSidebarScriptField extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public config!: ScriptFieldSidebarConfig;
|
||||
|
||||
@property({ type: Boolean, attribute: "wide" }) public isWide = false;
|
||||
|
||||
@property({ type: Boolean }) public disabled = false;
|
||||
|
||||
@property({ type: Boolean, attribute: "yaml-mode" }) public yamlMode = false;
|
||||
|
||||
@property({ type: Boolean }) public narrow = false;
|
||||
|
||||
@state() private _warnings?: string[];
|
||||
|
||||
@query(".sidebar-editor")
|
||||
public editor?: HaAutomationConditionEditor;
|
||||
|
||||
protected willUpdate(changedProperties) {
|
||||
if (changedProperties.has("config")) {
|
||||
this._warnings = undefined;
|
||||
if (this.config) {
|
||||
this.yamlMode = this.config.yamlMode;
|
||||
if (this.yamlMode) {
|
||||
this.editor?.yamlEditor?.setValue(this.config.config);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected render() {
|
||||
const title = this.hass.localize(
|
||||
"ui.panel.config.script.editor.field.label"
|
||||
);
|
||||
|
||||
return html`<ha-automation-sidebar-card
|
||||
.hass=${this.hass}
|
||||
.isWide=${this.isWide}
|
||||
.yamlMode=${this.yamlMode}
|
||||
.warnings=${this._warnings}
|
||||
.narrow=${this.narrow}
|
||||
>
|
||||
<span slot="title">${title}</span>
|
||||
<ha-md-menu-item
|
||||
slot="menu-items"
|
||||
.clickAction=${this._toggleYamlMode}
|
||||
.disabled=${!!this._warnings}
|
||||
>
|
||||
${this.hass.localize(
|
||||
`ui.panel.config.automation.editor.edit_${!this.yamlMode ? "yaml" : "ui"}`
|
||||
)}
|
||||
<ha-svg-icon slot="start" .path=${mdiPlaylistEdit}></ha-svg-icon>
|
||||
</ha-md-menu-item>
|
||||
<ha-md-menu-item
|
||||
slot="menu-items"
|
||||
.clickAction=${this.config.delete}
|
||||
.disabled=${this.disabled}
|
||||
class="warning"
|
||||
>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.automation.editor.actions.delete"
|
||||
)}
|
||||
<ha-svg-icon slot="start" .path=${mdiDelete}></ha-svg-icon>
|
||||
</ha-md-menu-item>
|
||||
<ha-script-field-editor
|
||||
class="sidebar-editor"
|
||||
.hass=${this.hass}
|
||||
.field=${this.config.config.field}
|
||||
.key=${this.config.config.key}
|
||||
.excludeKeys=${this.config.config.excludeKeys}
|
||||
.disabled=${this.disabled}
|
||||
.yamlMode=${this.yamlMode}
|
||||
@value-changed=${this._valueChangedSidebar}
|
||||
></ha-script-field-editor>
|
||||
</ha-automation-sidebar-card>`;
|
||||
}
|
||||
|
||||
private _valueChangedSidebar(ev: CustomEvent) {
|
||||
ev.stopPropagation();
|
||||
|
||||
this.config?.save?.({
|
||||
...this.config.config.field,
|
||||
key: ev.detail.value.key ?? this.config.config.key,
|
||||
...ev.detail.value,
|
||||
});
|
||||
|
||||
if (this.config) {
|
||||
fireEvent(this, "value-changed", {
|
||||
value: {
|
||||
...this.config,
|
||||
config: {
|
||||
field: ev.detail.value,
|
||||
key: ev.detail.value.key ?? this.config.config.key,
|
||||
excludeKeys: this.config.config.excludeKeys,
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private _toggleYamlMode = () => {
|
||||
fireEvent(this, "toggle-yaml-mode");
|
||||
};
|
||||
|
||||
static styles = css`
|
||||
.sidebar-editor {
|
||||
padding-top: 64px;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-automation-sidebar-script-field": HaAutomationSidebarScriptField;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,199 @@
|
||||
import {
|
||||
mdiDelete,
|
||||
mdiIdentifier,
|
||||
mdiPlayCircleOutline,
|
||||
mdiPlaylistEdit,
|
||||
mdiRenameBox,
|
||||
mdiStopCircleOutline,
|
||||
} from "@mdi/js";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||
import { handleStructError } from "../../../../common/structs/handle-errors";
|
||||
import type { TriggerSidebarConfig } from "../../../../data/automation";
|
||||
import { isTriggerList } from "../../../../data/trigger";
|
||||
import type { HomeAssistant } from "../../../../types";
|
||||
import "../trigger/ha-automation-trigger-editor";
|
||||
import type HaAutomationTriggerEditor from "../trigger/ha-automation-trigger-editor";
|
||||
import "./ha-automation-sidebar-card";
|
||||
|
||||
@customElement("ha-automation-sidebar-trigger")
|
||||
export default class HaAutomationSidebarTrigger extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public config!: TriggerSidebarConfig;
|
||||
|
||||
@property({ type: Boolean, attribute: "wide" }) public isWide = false;
|
||||
|
||||
@property({ type: Boolean }) public disabled = false;
|
||||
|
||||
@property({ type: Boolean, attribute: "yaml-mode" }) public yamlMode = false;
|
||||
|
||||
@property({ type: Boolean }) public narrow = false;
|
||||
|
||||
@state() private _requestShowId = false;
|
||||
|
||||
@state() private _warnings?: string[];
|
||||
|
||||
@query(".sidebar-editor")
|
||||
public editor?: HaAutomationTriggerEditor;
|
||||
|
||||
protected willUpdate(changedProperties) {
|
||||
if (changedProperties.has("config")) {
|
||||
this._requestShowId = false;
|
||||
this._warnings = undefined;
|
||||
if (this.config) {
|
||||
this.yamlMode = this.config.yamlMode;
|
||||
if (this.yamlMode) {
|
||||
this.editor?.yamlEditor?.setValue(this.config.config);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected render() {
|
||||
const disabled =
|
||||
this.disabled ||
|
||||
("enabled" in this.config.config && this.config.config.enabled === false);
|
||||
const type = isTriggerList(this.config.config)
|
||||
? "list"
|
||||
: this.config.config.trigger;
|
||||
|
||||
const subtitle = this.hass.localize(
|
||||
"ui.panel.config.automation.editor.triggers.trigger"
|
||||
);
|
||||
|
||||
const title = this.hass.localize(
|
||||
`ui.panel.config.automation.editor.triggers.type.${type}.label`
|
||||
);
|
||||
|
||||
return html`
|
||||
<ha-automation-sidebar-card
|
||||
.hass=${this.hass}
|
||||
.isWide=${this.isWide}
|
||||
.yamlMode=${this.yamlMode}
|
||||
.warnings=${this._warnings}
|
||||
.narrow=${this.narrow}
|
||||
>
|
||||
<span slot="title">${title}</span>
|
||||
<span slot="subtitle">${subtitle}</span>
|
||||
<ha-md-menu-item
|
||||
slot="menu-items"
|
||||
.clickAction=${this.config.rename}
|
||||
.disabled=${disabled || type === "list"}
|
||||
>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.automation.editor.triggers.rename"
|
||||
)}
|
||||
<ha-svg-icon slot="start" .path=${mdiRenameBox}></ha-svg-icon>
|
||||
</ha-md-menu-item>
|
||||
${!this.yamlMode &&
|
||||
!("id" in this.config.config) &&
|
||||
!this._requestShowId
|
||||
? html`<ha-md-menu-item
|
||||
slot="menu-items"
|
||||
.clickAction=${this._showTriggerId}
|
||||
.disabled=${disabled || type === "list"}
|
||||
>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.automation.editor.triggers.edit_id"
|
||||
)}
|
||||
<ha-svg-icon slot="start" .path=${mdiIdentifier}></ha-svg-icon>
|
||||
</ha-md-menu-item>`
|
||||
: nothing}
|
||||
<ha-md-menu-item
|
||||
slot="menu-items"
|
||||
.clickAction=${this._toggleYamlMode}
|
||||
.disabled=${!this.config.uiSupported || !!this._warnings}
|
||||
>
|
||||
${this.hass.localize(
|
||||
`ui.panel.config.automation.editor.edit_${!this.yamlMode ? "yaml" : "ui"}`
|
||||
)}
|
||||
<ha-svg-icon slot="start" .path=${mdiPlaylistEdit}></ha-svg-icon>
|
||||
</ha-md-menu-item>
|
||||
<ha-md-divider
|
||||
slot="menu-items"
|
||||
role="separator"
|
||||
tabindex="-1"
|
||||
></ha-md-divider>
|
||||
<ha-md-menu-item
|
||||
slot="menu-items"
|
||||
.clickAction=${this.config.disable}
|
||||
.disabled=${type === "list"}
|
||||
>
|
||||
${this.hass.localize(
|
||||
`ui.panel.config.automation.editor.actions.${disabled ? "enable" : "disable"}`
|
||||
)}
|
||||
<ha-svg-icon
|
||||
slot="start"
|
||||
.path=${this.disabled ? mdiPlayCircleOutline : mdiStopCircleOutline}
|
||||
></ha-svg-icon>
|
||||
</ha-md-menu-item>
|
||||
<ha-md-menu-item
|
||||
slot="menu-items"
|
||||
.clickAction=${this.config.delete}
|
||||
.disabled=${this.disabled}
|
||||
class="warning"
|
||||
>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.automation.editor.actions.delete"
|
||||
)}
|
||||
<ha-svg-icon slot="start" .path=${mdiDelete}></ha-svg-icon>
|
||||
</ha-md-menu-item>
|
||||
<ha-automation-trigger-editor
|
||||
class="sidebar-editor"
|
||||
.hass=${this.hass}
|
||||
.trigger=${this.config.config}
|
||||
@value-changed=${this._valueChangedSidebar}
|
||||
.uiSupported=${this.config.uiSupported}
|
||||
.showId=${this._requestShowId}
|
||||
.yamlMode=${this.yamlMode}
|
||||
.disabled=${this.disabled}
|
||||
@ui-mode-not-available=${this._handleUiModeNotAvailable}
|
||||
></ha-automation-trigger-editor>
|
||||
</ha-automation-sidebar-card>
|
||||
`;
|
||||
}
|
||||
|
||||
private _handleUiModeNotAvailable(ev: CustomEvent) {
|
||||
this._warnings = handleStructError(this.hass, ev.detail).warnings;
|
||||
if (!this.yamlMode) {
|
||||
this.yamlMode = true;
|
||||
}
|
||||
}
|
||||
|
||||
private _valueChangedSidebar(ev: CustomEvent) {
|
||||
ev.stopPropagation();
|
||||
|
||||
this.config?.save?.(ev.detail.value);
|
||||
|
||||
if (this.config) {
|
||||
fireEvent(this, "value-changed", {
|
||||
value: {
|
||||
...this.config,
|
||||
config: ev.detail.value,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private _toggleYamlMode = () => {
|
||||
fireEvent(this, "toggle-yaml-mode");
|
||||
};
|
||||
|
||||
private _showTriggerId = () => {
|
||||
this._requestShowId = true;
|
||||
};
|
||||
|
||||
static styles = css`
|
||||
.sidebar-editor {
|
||||
padding-top: 64px;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-automation-sidebar-trigger": HaAutomationSidebarTrigger;
|
||||
}
|
||||
}
|
||||
@@ -43,6 +43,9 @@ export const rowStyles = css`
|
||||
border-color: var(--state-inactive-color);
|
||||
box-shadow: var(--shadow-default), var(--shadow-focus);
|
||||
}
|
||||
.hidden {
|
||||
display: none;
|
||||
}
|
||||
`;
|
||||
|
||||
export const editorStyles = css`
|
||||
|
||||
@@ -34,7 +34,11 @@ import "../../../../components/ha-icon-button";
|
||||
import "../../../../components/ha-md-button-menu";
|
||||
import "../../../../components/ha-md-divider";
|
||||
import "../../../../components/ha-md-menu-item";
|
||||
import type { AutomationClipboard, Trigger } from "../../../../data/automation";
|
||||
import type {
|
||||
AutomationClipboard,
|
||||
Trigger,
|
||||
TriggerSidebarConfig,
|
||||
} from "../../../../data/automation";
|
||||
import { subscribeTrigger } from "../../../../data/automation";
|
||||
import { describeTrigger } from "../../../../data/automation_i18n";
|
||||
import { validateConfig } from "../../../../data/config";
|
||||
@@ -465,9 +469,6 @@ export default class HaAutomationTriggerRow extends LitElement {
|
||||
}
|
||||
|
||||
public openSidebar(trigger?: Trigger): void {
|
||||
if (this.narrow) {
|
||||
this.scrollIntoView();
|
||||
}
|
||||
fireEvent(this, "open-sidebar", {
|
||||
save: (value) => {
|
||||
fireEvent(this, "value-changed", { value });
|
||||
@@ -486,11 +487,17 @@ export default class HaAutomationTriggerRow extends LitElement {
|
||||
disable: this._onDisable,
|
||||
delete: this._onDelete,
|
||||
config: trigger || this.trigger,
|
||||
type: "trigger",
|
||||
uiSupported: this._uiSupported(this._getType(trigger || this.trigger)),
|
||||
yamlMode: this._yamlMode,
|
||||
});
|
||||
} satisfies TriggerSidebarConfig);
|
||||
this._selected = true;
|
||||
|
||||
if (this.narrow) {
|
||||
this.scrollIntoView({
|
||||
block: "start",
|
||||
behavior: "smooth",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private _setClipboard() {
|
||||
|
||||
@@ -135,6 +135,9 @@ export default class HaAutomationTrigger extends LitElement {
|
||||
}
|
||||
|
||||
private _addTriggerDialog() {
|
||||
if (this.narrow) {
|
||||
fireEvent(this, "close-sidebar");
|
||||
}
|
||||
showAddAutomationElementDialog(this, {
|
||||
type: "trigger",
|
||||
add: this._addTrigger,
|
||||
@@ -177,10 +180,15 @@ export default class HaAutomationTrigger extends LitElement {
|
||||
row.updateComplete.then(() => {
|
||||
if (this.optionsInSidebar) {
|
||||
row.openSidebar();
|
||||
if (this.narrow) {
|
||||
row.scrollIntoView({
|
||||
block: "start",
|
||||
behavior: "smooth",
|
||||
});
|
||||
}
|
||||
} else {
|
||||
row.expand();
|
||||
}
|
||||
row.scrollIntoView();
|
||||
row.focus();
|
||||
});
|
||||
}
|
||||
|
||||
@@ -15,8 +15,6 @@ import type { CloudStatus } from "../../../data/cloud";
|
||||
import { fetchCloudStatus } from "../../../data/cloud";
|
||||
import type { HardwareInfo } from "../../../data/hardware";
|
||||
import { BOARD_NAMES } from "../../../data/hardware";
|
||||
import type { HassioBackup } from "../../../data/hassio/backup";
|
||||
import { fetchHassioBackups } from "../../../data/hassio/backup";
|
||||
import type {
|
||||
HassioHassOSInfo,
|
||||
HassioHostInfo,
|
||||
@@ -44,7 +42,7 @@ class HaConfigSystemNavigation extends LitElement {
|
||||
|
||||
@property({ attribute: false }) public showAdvanced = false;
|
||||
|
||||
@state() private _latestBackupDate?: string;
|
||||
@state() private _latestBackupDate?: Date;
|
||||
|
||||
@state() private _boardName?: string;
|
||||
|
||||
@@ -63,7 +61,7 @@ class HaConfigSystemNavigation extends LitElement {
|
||||
description = this._latestBackupDate
|
||||
? this.hass.localize("ui.panel.config.backup.description", {
|
||||
relative_time: relativeTime(
|
||||
new Date(this._latestBackupDate),
|
||||
this._latestBackupDate,
|
||||
this.hass.locale
|
||||
),
|
||||
})
|
||||
@@ -155,26 +153,24 @@ class HaConfigSystemNavigation extends LitElement {
|
||||
|
||||
this._fetchNetworkStatus();
|
||||
const isHassioLoaded = isComponentLoaded(this.hass, "hassio");
|
||||
this._fetchBackupInfo(isHassioLoaded);
|
||||
this._fetchBackupInfo();
|
||||
this._fetchHardwareInfo(isHassioLoaded);
|
||||
if (isHassioLoaded) {
|
||||
this._fetchStorageInfo();
|
||||
}
|
||||
}
|
||||
|
||||
private async _fetchBackupInfo(isHassioLoaded: boolean) {
|
||||
const backups: BackupContent[] | HassioBackup[] = isHassioLoaded
|
||||
? await fetchHassioBackups(this.hass)
|
||||
: isComponentLoaded(this.hass, "backup")
|
||||
? await fetchBackupInfo(this.hass).then(
|
||||
(backupData) => backupData.backups
|
||||
)
|
||||
: [];
|
||||
private async _fetchBackupInfo() {
|
||||
const backups: BackupContent[] = isComponentLoaded(this.hass, "backup")
|
||||
? await fetchBackupInfo(this.hass).then(
|
||||
(backupData) => backupData.backups
|
||||
)
|
||||
: [];
|
||||
|
||||
if (backups.length > 0) {
|
||||
this._latestBackupDate = (backups as any[]).reduce((a, b) =>
|
||||
a.date > b.date ? a : b
|
||||
).date;
|
||||
this._latestBackupDate = backups
|
||||
.map((backup) => new Date(backup.date))
|
||||
.reduce((a, b) => (a > b ? a : b));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -331,6 +331,9 @@ export class EntityRegistrySettingsEditor extends LitElement {
|
||||
}
|
||||
if (changedProps.has("_entityId")) {
|
||||
const domain = computeDomain(this.entry.entity_id);
|
||||
if (domain === "switch") {
|
||||
this.hass.loadBackendTranslation("title", SWITCH_AS_DOMAINS, false);
|
||||
}
|
||||
|
||||
if (domain === "weather") {
|
||||
const { units } = await getWeatherConvertibleUnits(this.hass);
|
||||
@@ -342,6 +345,7 @@ export class EntityRegistrySettingsEditor extends LitElement {
|
||||
if (changedProps.has("helperConfigEntry")) {
|
||||
if (this.helperConfigEntry?.domain === "switch_as_x") {
|
||||
this._switchAsDomain = computeDomain(this.entry.entity_id);
|
||||
this.hass.loadBackendTranslation("title", SWITCH_AS_DOMAINS, false);
|
||||
} else {
|
||||
this._switchAsDomain = "switch";
|
||||
this._switchAsInvert = false;
|
||||
|
||||
-1
@@ -1,5 +1,4 @@
|
||||
import { mdiChevronLeft, mdiClose } from "@mdi/js";
|
||||
import "@shoelace-style/shoelace/dist/components/animation/animation";
|
||||
import type { UnsubscribeFunc } from "home-assistant-js-websocket";
|
||||
import type { CSSResultGroup, TemplateResult } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
|
||||
+3
-3
@@ -1,6 +1,6 @@
|
||||
import { mdiCheckCircleOutline } from "@mdi/js";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import "@shoelace-style/shoelace/dist/components/animation/animation";
|
||||
import "@awesome.me/webawesome/dist/components/animation/animation";
|
||||
import { css, html, LitElement } from "lit";
|
||||
import type { HomeAssistant } from "../../../../../../types";
|
||||
|
||||
@@ -17,9 +17,9 @@ export class ZWaveJsAddNodeFinished extends LitElement {
|
||||
|
||||
render() {
|
||||
return html`
|
||||
<sl-animation name="zoomIn" .iterations=${1} play>
|
||||
<wa-animation name="zoomIn" .iterations=${1} play>
|
||||
<ha-svg-icon .path=${mdiCheckCircleOutline}></ha-svg-icon>
|
||||
</sl-animation>
|
||||
</wa-animation>
|
||||
<ha-alert alert-type="warning">
|
||||
${this.reason
|
||||
? this.hass.localize(
|
||||
|
||||
-1
@@ -1,5 +1,4 @@
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import "@shoelace-style/shoelace/dist/components/animation/animation";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import type { HomeAssistant } from "../../../../../../types";
|
||||
import { SecurityClass } from "../../../../../../data/zwave_js";
|
||||
|
||||
+3
-3
@@ -1,4 +1,4 @@
|
||||
import "@shoelace-style/shoelace/dist/components/animation/animation";
|
||||
import "@awesome.me/webawesome/dist/components/animation/animation";
|
||||
import { mdiRestart } from "@mdi/js";
|
||||
|
||||
import { customElement, property } from "lit/decorators";
|
||||
@@ -48,9 +48,9 @@ export class ZWaveJsAddNodeSearchingDevices extends WakeLockMixin(LitElement) {
|
||||
<div class="spinner">
|
||||
<ha-spinner></ha-spinner>
|
||||
</div>
|
||||
<sl-animation name="pulse" easing="linear" .duration=${2000} play>
|
||||
<wa-animation name="pulse" easing="linear" .duration=${2000} play>
|
||||
<div class="circle"></div>
|
||||
</sl-animation>
|
||||
</wa-animation>
|
||||
</div>
|
||||
${this.smartStart
|
||||
? html`<ha-alert
|
||||
|
||||
@@ -85,7 +85,7 @@ class DialogPersonDetail extends LitElement implements HassDialog {
|
||||
this._deviceTrackers = this._params.entry.device_trackers || [];
|
||||
this._picture = this._params.entry.picture || null;
|
||||
this._user = this._userId
|
||||
? this._params.users.find((user) => user.id === this._userId)
|
||||
? this._params.users?.find((user) => user.id === this._userId)
|
||||
: undefined;
|
||||
this._isAdmin = this._user?.group_ids.includes(SYSTEM_GROUP_ID_ADMIN);
|
||||
this._localOnly = this._user?.local_only;
|
||||
@@ -372,10 +372,10 @@ class DialogPersonDetail extends LitElement implements HassDialog {
|
||||
userAddedCallback: async (user?: User) => {
|
||||
if (user) {
|
||||
target.checked = true;
|
||||
if (this._params!.entry) {
|
||||
if (this._params!.entry && this._params!.updateEntry) {
|
||||
await this._params!.updateEntry({ user_id: user.id });
|
||||
}
|
||||
this._params?.refreshUsers();
|
||||
this._params?.refreshUsers?.();
|
||||
this._user = user;
|
||||
this._userId = user.id;
|
||||
this._isAdmin = user.group_ids.includes(SYSTEM_GROUP_ID_ADMIN);
|
||||
@@ -403,7 +403,7 @@ class DialogPersonDetail extends LitElement implements HassDialog {
|
||||
return;
|
||||
}
|
||||
await deleteUser(this.hass, this._userId);
|
||||
this._params?.refreshUsers();
|
||||
this._params?.refreshUsers?.();
|
||||
this._userId = undefined;
|
||||
this._user = undefined;
|
||||
this._isAdmin = undefined;
|
||||
@@ -466,7 +466,7 @@ class DialogPersonDetail extends LitElement implements HassDialog {
|
||||
if (newUsername) {
|
||||
try {
|
||||
await adminChangeUsername(this.hass, this._user.id, newUsername);
|
||||
this._params?.refreshUsers();
|
||||
this._params?.refreshUsers?.();
|
||||
this._user = { ...this._user, username: newUsername };
|
||||
showAlertDialog(this, {
|
||||
text: this.hass.localize(
|
||||
@@ -500,7 +500,7 @@ class DialogPersonDetail extends LitElement implements HassDialog {
|
||||
],
|
||||
local_only: this._localOnly,
|
||||
});
|
||||
this._params?.refreshUsers();
|
||||
this._params?.refreshUsers?.();
|
||||
}
|
||||
const values: PersonMutableParams = {
|
||||
name: this._name.trim(),
|
||||
@@ -509,9 +509,9 @@ class DialogPersonDetail extends LitElement implements HassDialog {
|
||||
picture: this._picture,
|
||||
};
|
||||
if (this._params!.entry) {
|
||||
await this._params!.updateEntry(values);
|
||||
await this._params!.updateEntry?.(values);
|
||||
} else {
|
||||
await this._params!.createEntry(values);
|
||||
await this._params!.createEntry?.(values);
|
||||
this._personExists = true;
|
||||
}
|
||||
this.closeDialog();
|
||||
@@ -525,7 +525,7 @@ class DialogPersonDetail extends LitElement implements HassDialog {
|
||||
private async _deleteEntry() {
|
||||
this._submitting = true;
|
||||
try {
|
||||
if (await this._params!.removeEntry()) {
|
||||
if (await this._params!.removeEntry?.()) {
|
||||
if (this._params!.entry!.user_id) {
|
||||
deleteUser(this.hass, this._params!.entry!.user_id);
|
||||
}
|
||||
|
||||
@@ -4,22 +4,22 @@ import type { User } from "../../../data/user";
|
||||
|
||||
export interface PersonDetailDialogParams {
|
||||
entry?: Person;
|
||||
users: User[];
|
||||
refreshUsers: () => void;
|
||||
createEntry: (values: PersonMutableParams) => Promise<unknown>;
|
||||
updateEntry: (updates: Partial<PersonMutableParams>) => Promise<unknown>;
|
||||
removeEntry: () => Promise<boolean>;
|
||||
users?: User[];
|
||||
refreshUsers?: () => void;
|
||||
createEntry?: (values: PersonMutableParams) => Promise<unknown>;
|
||||
updateEntry?: (updates: Partial<PersonMutableParams>) => Promise<unknown>;
|
||||
removeEntry?: () => Promise<boolean>;
|
||||
}
|
||||
|
||||
export const loadPersonDetailDialog = () => import("./dialog-person-detail");
|
||||
|
||||
export const showPersonDetailDialog = (
|
||||
element: HTMLElement,
|
||||
systemLogDetailParams: PersonDetailDialogParams
|
||||
params: PersonDetailDialogParams
|
||||
): void => {
|
||||
fireEvent(element, "show-dialog", {
|
||||
dialogTag: "dialog-person-detail",
|
||||
dialogImport: loadPersonDetailDialog,
|
||||
dialogParams: systemLogDetailParams,
|
||||
dialogParams: params,
|
||||
});
|
||||
};
|
||||
|
||||
@@ -1,14 +1,22 @@
|
||||
import { html, nothing } from "lit";
|
||||
import { mdiContentSave } from "@mdi/js";
|
||||
import { css, html, nothing, type CSSResultGroup } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { fireEvent } from "../../../common/dom/fire_event";
|
||||
import "../../../components/ha-fab";
|
||||
import "../../../components/ha-markdown";
|
||||
import { fetchBlueprints } from "../../../data/blueprint";
|
||||
import type { BlueprintScriptConfig } from "../../../data/script";
|
||||
import { saveFabStyles } from "../automation/styles";
|
||||
import { HaBlueprintGenericEditor } from "../blueprint/blueprint-generic-editor";
|
||||
|
||||
@customElement("blueprint-script-editor")
|
||||
export class HaBlueprintScriptEditor extends HaBlueprintGenericEditor {
|
||||
@property({ attribute: false }) public config!: BlueprintScriptConfig;
|
||||
|
||||
@property({ type: Boolean }) public saving = false;
|
||||
|
||||
@property({ type: Boolean }) public dirty = false;
|
||||
|
||||
protected get _config(): BlueprintScriptConfig {
|
||||
return this.config;
|
||||
}
|
||||
@@ -23,12 +31,45 @@ export class HaBlueprintScriptEditor extends HaBlueprintGenericEditor {
|
||||
></ha-markdown>`
|
||||
: nothing}
|
||||
${this.renderCard()}
|
||||
|
||||
<ha-fab
|
||||
slot="fab"
|
||||
class=${this.dirty ? "dirty" : ""}
|
||||
.label=${this.hass.localize("ui.common.save")}
|
||||
.disabled=${this.saving}
|
||||
extended
|
||||
@click=${this._saveScript}
|
||||
>
|
||||
<ha-svg-icon slot="icon" .path=${mdiContentSave}></ha-svg-icon>
|
||||
</ha-fab>
|
||||
`;
|
||||
}
|
||||
|
||||
private _saveScript() {
|
||||
fireEvent(this, "save-script");
|
||||
}
|
||||
|
||||
protected async _getBlueprints() {
|
||||
this._blueprints = await fetchBlueprints(this.hass, "script");
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return [
|
||||
HaBlueprintGenericEditor.styles,
|
||||
saveFabStyles,
|
||||
css`
|
||||
:host {
|
||||
position: relative;
|
||||
height: 100%;
|
||||
min-height: calc(100vh - 85px);
|
||||
min-height: calc(100dvh - 85px);
|
||||
}
|
||||
ha-fab {
|
||||
position: fixed;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
}
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
|
||||
@@ -358,59 +358,61 @@ export class HaScriptEditor extends SubscribeMixin(
|
||||
</ha-svg-icon>
|
||||
</ha-list-item>
|
||||
</ha-button-menu>
|
||||
<div
|
||||
class="content ${classMap({
|
||||
"yaml-mode": this._mode === "yaml",
|
||||
})}"
|
||||
>
|
||||
${this._errors || stateObj?.state === UNAVAILABLE
|
||||
? html`<ha-alert
|
||||
alert-type="error"
|
||||
.title=${stateObj?.state === UNAVAILABLE
|
||||
? this.hass.localize(
|
||||
"ui.panel.config.script.editor.unavailable"
|
||||
)
|
||||
: undefined}
|
||||
>
|
||||
${this._errors || this._validationErrors}
|
||||
${stateObj?.state === UNAVAILABLE
|
||||
? html`<ha-svg-icon
|
||||
slot="icon"
|
||||
.path=${mdiRobotConfused}
|
||||
></ha-svg-icon>`
|
||||
: nothing}
|
||||
</ha-alert>`
|
||||
: ""}
|
||||
${this._blueprintConfig
|
||||
? html`<ha-alert alert-type="info">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.script.editor.confirm_take_control"
|
||||
)}
|
||||
<div slot="action" style="display: flex;">
|
||||
<ha-button appearance="plain" @click=${this._takeControlSave}
|
||||
>${this.hass.localize("ui.common.yes")}</ha-button
|
||||
>
|
||||
<ha-button appearance="plain" @click=${this._revertBlueprint}
|
||||
>${this.hass.localize("ui.common.no")}</ha-button
|
||||
>
|
||||
</div>
|
||||
</ha-alert>`
|
||||
: this._readOnly
|
||||
? html`<ha-alert alert-type="warning" dismissable
|
||||
>${this.hass.localize(
|
||||
"ui.panel.config.script.editor.read_only"
|
||||
)}
|
||||
<ha-button
|
||||
appearance="plain"
|
||||
slot="action"
|
||||
@click=${this._duplicate}
|
||||
>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.script.editor.migrate"
|
||||
)}
|
||||
</ha-button>
|
||||
<div class=${this._mode === "yaml" ? "yaml-mode" : ""}>
|
||||
<div class="error-wrapper">
|
||||
${this._errors || stateObj?.state === UNAVAILABLE
|
||||
? html`<ha-alert
|
||||
alert-type="error"
|
||||
.title=${stateObj?.state === UNAVAILABLE
|
||||
? this.hass.localize(
|
||||
"ui.panel.config.script.editor.unavailable"
|
||||
)
|
||||
: undefined}
|
||||
>
|
||||
${this._errors || this._validationErrors}
|
||||
${stateObj?.state === UNAVAILABLE
|
||||
? html`<ha-svg-icon
|
||||
slot="icon"
|
||||
.path=${mdiRobotConfused}
|
||||
></ha-svg-icon>`
|
||||
: nothing}
|
||||
</ha-alert>`
|
||||
: nothing}
|
||||
${this._blueprintConfig
|
||||
? html`<ha-alert alert-type="info">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.script.editor.confirm_take_control"
|
||||
)}
|
||||
<div slot="action" style="display: flex;">
|
||||
<ha-button
|
||||
appearance="plain"
|
||||
@click=${this._takeControlSave}
|
||||
>${this.hass.localize("ui.common.yes")}</ha-button
|
||||
>
|
||||
<ha-button
|
||||
appearance="plain"
|
||||
@click=${this._revertBlueprint}
|
||||
>${this.hass.localize("ui.common.no")}</ha-button
|
||||
>
|
||||
</div>
|
||||
</ha-alert>`
|
||||
: this._readOnly
|
||||
? html`<ha-alert alert-type="warning" dismissable
|
||||
>${this.hass.localize(
|
||||
"ui.panel.config.script.editor.read_only"
|
||||
)}
|
||||
<ha-button
|
||||
appearance="plain"
|
||||
slot="action"
|
||||
@click=${this._duplicate}
|
||||
>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.script.editor.migrate"
|
||||
)}
|
||||
</ha-button>
|
||||
</ha-alert>`
|
||||
: nothing}
|
||||
</div>
|
||||
${this._mode === "gui"
|
||||
? html`
|
||||
<div
|
||||
@@ -426,7 +428,10 @@ export class HaScriptEditor extends SubscribeMixin(
|
||||
.isWide=${this.isWide}
|
||||
.config=${this._config}
|
||||
.disabled=${this._readOnly}
|
||||
.saving=${this._saving}
|
||||
.dirty=${this._dirty}
|
||||
@value-changed=${this._valueChanged}
|
||||
@save-script=${this._handleSaveScript}
|
||||
></blueprint-script-editor>
|
||||
`
|
||||
: html`
|
||||
@@ -437,39 +442,40 @@ export class HaScriptEditor extends SubscribeMixin(
|
||||
.config=${this._config}
|
||||
.disabled=${this._readOnly}
|
||||
.dirty=${this._dirty}
|
||||
.saving=${this._saving}
|
||||
@value-changed=${this._valueChanged}
|
||||
@editor-save=${this._handleSave}
|
||||
@editor-save=${this._handleSaveScript}
|
||||
@save-script=${this._handleSaveScript}
|
||||
></manual-script-editor>
|
||||
`}
|
||||
</div>
|
||||
`
|
||||
: this._mode === "yaml"
|
||||
? html`<ha-yaml-editor
|
||||
copy-clipboard
|
||||
.hass=${this.hass}
|
||||
.defaultValue=${this._preprocessYaml()}
|
||||
.readOnly=${this._readOnly}
|
||||
disable-fullscreen
|
||||
@value-changed=${this._yamlChanged}
|
||||
@editor-save=${this._handleSave}
|
||||
.showErrors=${false}
|
||||
></ha-yaml-editor>`
|
||||
copy-clipboard
|
||||
.hass=${this.hass}
|
||||
.defaultValue=${this._preprocessYaml()}
|
||||
.readOnly=${this._readOnly}
|
||||
disable-fullscreen
|
||||
@value-changed=${this._yamlChanged}
|
||||
@editor-save=${this._handleSaveScript}
|
||||
.showErrors=${false}
|
||||
></ha-yaml-editor>
|
||||
<ha-fab
|
||||
slot="fab"
|
||||
class=${!this._readOnly && this._dirty ? "dirty" : ""}
|
||||
.label=${this.hass.localize("ui.common.save")}
|
||||
.disabled=${this._saving}
|
||||
extended
|
||||
@click=${this._handleSaveScript}
|
||||
>
|
||||
<ha-svg-icon
|
||||
slot="icon"
|
||||
.path=${mdiContentSave}
|
||||
></ha-svg-icon>
|
||||
</ha-fab>`
|
||||
: nothing}
|
||||
</div>
|
||||
<ha-fab
|
||||
slot="fab"
|
||||
class=${classMap({
|
||||
dirty: !this._readOnly && this._dirty,
|
||||
})}
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.script.editor.save_script"
|
||||
)}
|
||||
.disabled=${this._saving}
|
||||
extended
|
||||
@click=${this._handleSave}
|
||||
>
|
||||
<ha-svg-icon slot="icon" .path=${mdiContentSave}></ha-svg-icon>
|
||||
</ha-fab>
|
||||
</hass-subpage>
|
||||
`;
|
||||
}
|
||||
@@ -905,7 +911,7 @@ export class HaScriptEditor extends SubscribeMixin(
|
||||
});
|
||||
}
|
||||
|
||||
private async _handleSave() {
|
||||
private async _handleSaveScript() {
|
||||
if (this._yamlErrors) {
|
||||
showToast(this, {
|
||||
message: this._yamlErrors,
|
||||
@@ -1012,7 +1018,7 @@ export class HaScriptEditor extends SubscribeMixin(
|
||||
|
||||
protected supportedShortcuts(): SupportedShortcuts {
|
||||
return {
|
||||
s: () => this._handleSave(),
|
||||
s: () => this._handleSaveScript(),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1028,33 +1034,40 @@ export class HaScriptEditor extends SubscribeMixin(
|
||||
return [
|
||||
haStyle,
|
||||
css`
|
||||
p {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
.errors {
|
||||
padding: 20px;
|
||||
font-weight: var(--ha-font-weight-bold);
|
||||
color: var(--error-color);
|
||||
}
|
||||
.yaml-mode {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding-bottom: 0;
|
||||
}
|
||||
.config-container,
|
||||
manual-script-editor,
|
||||
blueprint-script-editor,
|
||||
:not(.yaml-mode) > ha-alert {
|
||||
blueprint-script-editor {
|
||||
margin: 0 auto;
|
||||
max-width: 1040px;
|
||||
padding: 28px 20px 0;
|
||||
display: block;
|
||||
}
|
||||
.config-container ha-alert {
|
||||
margin-bottom: 16px;
|
||||
display: block;
|
||||
|
||||
:not(.yaml-mode) > .error-wrapper {
|
||||
position: absolute;
|
||||
top: 4px;
|
||||
z-index: 3;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
:not(.yaml-mode) > .error-wrapper ha-alert {
|
||||
background-color: var(--card-background-color);
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
|
||||
border-radius: var(--ha-border-radius-sm);
|
||||
}
|
||||
|
||||
manual-script-editor {
|
||||
max-width: 1540px;
|
||||
padding: 0 12px;
|
||||
}
|
||||
|
||||
ha-yaml-editor {
|
||||
flex-grow: 1;
|
||||
--actions-border-radius: 0;
|
||||
@@ -1063,16 +1076,20 @@ export class HaScriptEditor extends SubscribeMixin(
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
p {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
span[slot="introduction"] a {
|
||||
color: var(--primary-color);
|
||||
}
|
||||
ha-fab {
|
||||
position: relative;
|
||||
position: fixed;
|
||||
right: 16px;
|
||||
bottom: calc(-80px - var(--safe-area-inset-bottom));
|
||||
transition: bottom 0.3s;
|
||||
}
|
||||
ha-fab.dirty {
|
||||
bottom: 0;
|
||||
bottom: 16px;
|
||||
}
|
||||
li[role="separator"] {
|
||||
border-bottom-color: var(--divider-color);
|
||||
@@ -1105,4 +1122,8 @@ declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-script-editor": HaScriptEditor;
|
||||
}
|
||||
|
||||
interface HASSDomEvents {
|
||||
"save-script": undefined;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,187 @@
|
||||
import { LitElement, html, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { fireEvent } from "../../../common/dom/fire_event";
|
||||
import { slugify } from "../../../common/string/slugify";
|
||||
import "../../../components/ha-alert";
|
||||
import "../../../components/ha-form/ha-form";
|
||||
import type { SchemaUnion } from "../../../components/ha-form/types";
|
||||
import "../../../components/ha-yaml-editor";
|
||||
import type { Field } from "../../../data/script";
|
||||
import { haStyle } from "../../../resources/styles";
|
||||
import type { HomeAssistant } from "../../../types";
|
||||
|
||||
@customElement("ha-script-field-editor")
|
||||
export default class HaScriptFieldEditor extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property() public key!: string;
|
||||
|
||||
@property({ attribute: false, type: Array }) public excludeKeys: string[] =
|
||||
[];
|
||||
|
||||
@property({ attribute: false }) public field!: Field;
|
||||
|
||||
@property({ type: Boolean }) public disabled = false;
|
||||
|
||||
@property({ type: Boolean, attribute: "yaml-mode" }) public yamlMode = false;
|
||||
|
||||
@state() private _uiError?: Record<string, string>;
|
||||
|
||||
@state() private _yamlError?: undefined | "yaml_error" | "key_not_unique";
|
||||
|
||||
private _errorKey?: string;
|
||||
|
||||
private _schema = memoizeOne(
|
||||
() =>
|
||||
[
|
||||
{
|
||||
name: "name",
|
||||
selector: { text: {} },
|
||||
},
|
||||
{
|
||||
name: "key",
|
||||
selector: { text: {} },
|
||||
},
|
||||
{
|
||||
name: "description",
|
||||
selector: { text: {} },
|
||||
},
|
||||
{
|
||||
name: "required",
|
||||
selector: { boolean: {} },
|
||||
},
|
||||
] as const
|
||||
);
|
||||
|
||||
protected render() {
|
||||
const schema = this._schema();
|
||||
const data = { ...this.field, key: this._errorKey ?? this.key };
|
||||
|
||||
const yamlValue = { [this.key]: this.field };
|
||||
|
||||
return html`
|
||||
${this.yamlMode
|
||||
? html`${this._yamlError
|
||||
? html`<ha-alert alert-type="error">
|
||||
${this.hass.localize(
|
||||
`ui.panel.config.script.editor.field.${this._yamlError}`
|
||||
)}
|
||||
</ha-alert>`
|
||||
: nothing}
|
||||
<ha-yaml-editor
|
||||
.hass=${this.hass}
|
||||
.defaultValue=${yamlValue}
|
||||
@value-changed=${this._onYamlChange}
|
||||
></ha-yaml-editor>`
|
||||
: html`<ha-form
|
||||
.schema=${schema}
|
||||
.data=${data}
|
||||
.error=${this._uiError}
|
||||
.hass=${this.hass}
|
||||
.disabled=${this.disabled}
|
||||
.computeLabel=${this._computeLabelCallback}
|
||||
.computeError=${this._computeError}
|
||||
@value-changed=${this._valueChanged}
|
||||
></ha-form>`}
|
||||
`;
|
||||
}
|
||||
|
||||
private _maybeSetKey(value): void {
|
||||
const nameChanged = value.name !== this.field.name;
|
||||
const keyChanged = value.key !== this.key;
|
||||
if (!nameChanged || keyChanged) {
|
||||
return;
|
||||
}
|
||||
const slugifyName = this.field.name
|
||||
? slugify(this.field.name)
|
||||
: this.hass.localize("ui.panel.config.script.editor.field.field") ||
|
||||
"field";
|
||||
const regex = new RegExp(`^${slugifyName}(_\\d)?$`);
|
||||
if (regex.test(this.key)) {
|
||||
let key = !value.name
|
||||
? this.hass.localize("ui.panel.config.script.editor.field.field") ||
|
||||
"field"
|
||||
: slugify(value.name);
|
||||
if (this.excludeKeys.includes(key)) {
|
||||
let uniqueKey = key;
|
||||
let i = 2;
|
||||
do {
|
||||
uniqueKey = `${key}_${i}`;
|
||||
i++;
|
||||
} while (this.excludeKeys.includes(uniqueKey));
|
||||
key = uniqueKey;
|
||||
}
|
||||
value.key = key;
|
||||
}
|
||||
}
|
||||
|
||||
private _valueChanged(ev: CustomEvent) {
|
||||
ev.stopPropagation();
|
||||
const value = { ...ev.detail.value };
|
||||
|
||||
this._maybeSetKey(value);
|
||||
|
||||
// Don't allow to set an empty key, or duplicate an existing key.
|
||||
if (!value.key || this.excludeKeys.includes(value.key)) {
|
||||
this._uiError = value.key
|
||||
? {
|
||||
key: "key_not_unique",
|
||||
}
|
||||
: {
|
||||
key: "key_not_null",
|
||||
};
|
||||
this._errorKey = value.key ?? "";
|
||||
return;
|
||||
}
|
||||
this._errorKey = undefined;
|
||||
this._uiError = undefined;
|
||||
|
||||
// If we render the default with an incompatible selector, it risks throwing an exception and not rendering.
|
||||
// Clear the default when changing the selector type.
|
||||
if (
|
||||
Object.keys(this.field.selector)[0] !== Object.keys(value.selector)[0]
|
||||
) {
|
||||
delete value.default;
|
||||
}
|
||||
|
||||
fireEvent(this, "value-changed", { value });
|
||||
}
|
||||
|
||||
private _onYamlChange(ev: CustomEvent) {
|
||||
ev.stopPropagation();
|
||||
const value = { ...ev.detail.value };
|
||||
|
||||
if (typeof value !== "object" || Object.keys(value).length !== 1) {
|
||||
this._yamlError = "yaml_error";
|
||||
return;
|
||||
}
|
||||
const key = Object.keys(value)[0];
|
||||
if (this.excludeKeys.includes(key)) {
|
||||
this._yamlError = "key_not_unique";
|
||||
return;
|
||||
}
|
||||
this._yamlError = undefined;
|
||||
|
||||
const newValue = { ...value[key], key };
|
||||
|
||||
fireEvent(this, "value-changed", { value: newValue });
|
||||
}
|
||||
|
||||
private _computeLabelCallback = (
|
||||
schema: SchemaUnion<ReturnType<typeof this._schema>>
|
||||
): string =>
|
||||
this.hass.localize(`ui.panel.config.script.editor.field.${schema.name}`);
|
||||
|
||||
private _computeError = (error: string) =>
|
||||
this.hass.localize(`ui.panel.config.script.editor.field.${error}` as any) ||
|
||||
error;
|
||||
|
||||
static styles = haStyle;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-script-field-editor": HaScriptFieldEditor;
|
||||
}
|
||||
}
|
||||
@@ -1,27 +1,24 @@
|
||||
import type { ActionDetail } from "@material/mwc-list/mwc-list-foundation";
|
||||
import { mdiDelete, mdiDotsVertical, mdiPlaylistEdit } from "@mdi/js";
|
||||
import { mdiDelete, mdiDotsVertical } from "@mdi/js";
|
||||
import type { CSSResultGroup } from "lit";
|
||||
import { LitElement, css, html, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { classMap } from "lit/directives/class-map";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { fireEvent } from "../../../common/dom/fire_event";
|
||||
import { slugify } from "../../../common/string/slugify";
|
||||
import "../../../components/ha-alert";
|
||||
import "../../../components/ha-button-menu";
|
||||
import { preventDefaultStopPropagation } from "../../../common/dom/prevent_default_stop_propagation";
|
||||
import { stopPropagation } from "../../../common/dom/stop_propagation";
|
||||
import type { LocalizeKeys } from "../../../common/translations/localize";
|
||||
import "../../../components/ha-automation-row";
|
||||
import "../../../components/ha-card";
|
||||
import "../../../components/ha-form/ha-form";
|
||||
import "../../../components/ha-expansion-panel";
|
||||
import "../../../components/ha-list-item";
|
||||
import type { SchemaUnion } from "../../../components/ha-form/types";
|
||||
import "../../../components/ha-icon-button";
|
||||
import "../../../components/ha-yaml-editor";
|
||||
import "../../../components/ha-md-button-menu";
|
||||
import "../../../components/ha-md-menu-item";
|
||||
import type { ScriptFieldSidebarConfig } from "../../../data/automation";
|
||||
import type { Field } from "../../../data/script";
|
||||
import { SELECTOR_SELECTOR_BUILDING_BLOCKS } from "../../../data/selector/selector_selector";
|
||||
import { showConfirmationDialog } from "../../../dialogs/generic/show-dialog-box";
|
||||
import { haStyle } from "../../../resources/styles";
|
||||
import type { HomeAssistant } from "../../../types";
|
||||
|
||||
const preventDefault = (ev) => ev.preventDefault();
|
||||
import "./ha-script-field-selector-editor";
|
||||
|
||||
@customElement("ha-script-field-row")
|
||||
export default class HaScriptFieldRow extends LitElement {
|
||||
@@ -36,61 +33,38 @@ export default class HaScriptFieldRow extends LitElement {
|
||||
|
||||
@property({ type: Boolean }) public disabled = false;
|
||||
|
||||
@state() private _uiError?: Record<string, string>;
|
||||
|
||||
@state() private _yamlError?: undefined | "yaml_error" | "key_not_unique";
|
||||
@property({ type: Boolean }) public narrow = false;
|
||||
|
||||
@state() private _yamlMode = false;
|
||||
|
||||
private _errorKey?: string;
|
||||
@state() private _selected = false;
|
||||
|
||||
private _schema = memoizeOne(
|
||||
(selector: any) =>
|
||||
[
|
||||
{
|
||||
name: "name",
|
||||
selector: { text: {} },
|
||||
},
|
||||
{
|
||||
name: "key",
|
||||
selector: { text: {} },
|
||||
},
|
||||
{
|
||||
name: "description",
|
||||
selector: { text: {} },
|
||||
},
|
||||
{
|
||||
name: "selector",
|
||||
selector: { selector: {} },
|
||||
},
|
||||
{
|
||||
name: "default",
|
||||
selector: selector && typeof selector === "object" ? selector : {},
|
||||
},
|
||||
{
|
||||
name: "required",
|
||||
selector: { boolean: {} },
|
||||
},
|
||||
] as const
|
||||
);
|
||||
@state() private _collapsed = false;
|
||||
|
||||
@state() private _selectorRowSelected = false;
|
||||
|
||||
@state() private _selectorRowCollapsed = false;
|
||||
|
||||
protected render() {
|
||||
const schema = this._schema(this.field.selector);
|
||||
const data = { ...this.field, key: this._errorKey ?? this.key };
|
||||
|
||||
const yamlValue = { [this.key]: this.field };
|
||||
|
||||
return html`
|
||||
<ha-card outlined>
|
||||
<ha-expansion-panel left-chevron>
|
||||
<ha-automation-row
|
||||
.disabled=${this.disabled}
|
||||
@click=${this._toggleSidebar}
|
||||
.selected=${this._selected}
|
||||
left-chevron
|
||||
@toggle-collapsed=${this._toggleCollapse}
|
||||
.collapsed=${this._collapsed}
|
||||
>
|
||||
<h3 slot="header">${this.key}</h3>
|
||||
|
||||
<slot name="icons" slot="icons"></slot>
|
||||
<ha-button-menu
|
||||
<ha-md-button-menu
|
||||
slot="icons"
|
||||
@action=${this._handleAction}
|
||||
@click=${preventDefault}
|
||||
fixed
|
||||
@click=${preventDefaultStopPropagation}
|
||||
@keydown=${stopPropagation}
|
||||
@closed=${stopPropagation}
|
||||
positioning="fixed"
|
||||
>
|
||||
<ha-icon-button
|
||||
slot="trigger"
|
||||
@@ -98,19 +72,9 @@ export default class HaScriptFieldRow extends LitElement {
|
||||
.path=${mdiDotsVertical}
|
||||
></ha-icon-button>
|
||||
|
||||
<ha-list-item graphic="icon">
|
||||
${this.hass.localize(
|
||||
`ui.panel.config.automation.editor.edit_${!this._yamlMode ? "yaml" : "ui"}`
|
||||
)}
|
||||
<ha-svg-icon
|
||||
slot="graphic"
|
||||
.path=${mdiPlaylistEdit}
|
||||
></ha-svg-icon>
|
||||
</ha-list-item>
|
||||
|
||||
<ha-list-item
|
||||
<ha-md-menu-item
|
||||
class="warning"
|
||||
graphic="icon"
|
||||
.clickAction=${this._onDelete}
|
||||
.disabled=${this.disabled}
|
||||
>
|
||||
${this.hass.localize(
|
||||
@@ -121,54 +85,148 @@ export default class HaScriptFieldRow extends LitElement {
|
||||
slot="graphic"
|
||||
.path=${mdiDelete}
|
||||
></ha-svg-icon>
|
||||
</ha-list-item>
|
||||
</ha-button-menu>
|
||||
<div
|
||||
class=${classMap({
|
||||
"card-content": true,
|
||||
})}
|
||||
>
|
||||
${this._yamlMode
|
||||
? html` ${this._yamlError
|
||||
? html`<ha-alert alert-type="error">
|
||||
${this.hass.localize(
|
||||
`ui.panel.config.script.editor.field.${this._yamlError}`
|
||||
)}
|
||||
</ha-alert>`
|
||||
: nothing}
|
||||
<ha-yaml-editor
|
||||
.hass=${this.hass}
|
||||
.defaultValue=${yamlValue}
|
||||
@value-changed=${this._onYamlChange}
|
||||
></ha-yaml-editor>`
|
||||
: html`<ha-form
|
||||
.schema=${schema}
|
||||
.data=${data}
|
||||
.error=${this._uiError}
|
||||
.hass=${this.hass}
|
||||
.disabled=${this.disabled}
|
||||
.computeLabel=${this._computeLabelCallback}
|
||||
.computeError=${this._computeError}
|
||||
@value-changed=${this._valueChanged}
|
||||
></ha-form>`}
|
||||
</div>
|
||||
</ha-expansion-panel>
|
||||
</ha-md-menu-item>
|
||||
</ha-md-button-menu>
|
||||
</ha-automation-row>
|
||||
</ha-card>
|
||||
<div
|
||||
class=${classMap({
|
||||
"selector-row": true,
|
||||
"parent-selected": this._selected,
|
||||
hidden: this._collapsed,
|
||||
})}
|
||||
>
|
||||
<ha-card>
|
||||
<ha-automation-row
|
||||
.selected=${this._selectorRowSelected}
|
||||
@click=${this._toggleSelectorSidebar}
|
||||
.collapsed=${this._selectorRowCollapsed}
|
||||
@toggle-collapsed=${this._toggleSelectorRowCollapse}
|
||||
.leftChevron=${SELECTOR_SELECTOR_BUILDING_BLOCKS.includes(
|
||||
Object.keys(this.field.selector)[0]
|
||||
)}
|
||||
>
|
||||
<h3 slot="header">
|
||||
${this.hass.localize(
|
||||
`ui.components.selectors.selector.types.${Object.keys(this.field.selector)[0]}` as LocalizeKeys
|
||||
)}
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.script.editor.field.selector"
|
||||
)}
|
||||
</h3>
|
||||
</ha-automation-row>
|
||||
</ha-card>
|
||||
${typeof this.field.selector === "object" &&
|
||||
SELECTOR_SELECTOR_BUILDING_BLOCKS.includes(
|
||||
Object.keys(this.field.selector)[0]
|
||||
)
|
||||
? html`
|
||||
<ha-script-field-selector-editor
|
||||
class=${this._selectorRowCollapsed ? "hidden" : ""}
|
||||
.selected=${this._selectorRowSelected}
|
||||
.hass=${this.hass}
|
||||
.field=${this.field}
|
||||
.disabled=${this.disabled}
|
||||
indent
|
||||
@value-changed=${this._selectorValueChanged}
|
||||
.narrow=${this.narrow}
|
||||
></ha-script-field-selector-editor>
|
||||
`
|
||||
: nothing}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
private async _handleAction(ev: CustomEvent<ActionDetail>) {
|
||||
switch (ev.detail.index) {
|
||||
case 0:
|
||||
this._yamlMode = !this._yamlMode;
|
||||
break;
|
||||
case 1:
|
||||
this._onDelete();
|
||||
break;
|
||||
private _toggleCollapse() {
|
||||
this._collapsed = !this._collapsed;
|
||||
}
|
||||
|
||||
private _toggleSelectorRowCollapse() {
|
||||
this._selectorRowCollapsed = !this._selectorRowCollapsed;
|
||||
}
|
||||
|
||||
private _toggleSidebar(ev: Event) {
|
||||
ev?.stopPropagation();
|
||||
|
||||
if (this._selected) {
|
||||
this._selected = false;
|
||||
fireEvent(this, "close-sidebar");
|
||||
return;
|
||||
}
|
||||
|
||||
this._selected = true;
|
||||
this.openSidebar();
|
||||
}
|
||||
|
||||
private _toggleSelectorSidebar(ev: Event) {
|
||||
ev?.stopPropagation();
|
||||
|
||||
if (this._selectorRowSelected) {
|
||||
this._selectorRowSelected = false;
|
||||
fireEvent(this, "close-sidebar");
|
||||
return;
|
||||
}
|
||||
|
||||
this._selectorRowSelected = true;
|
||||
this.openSidebar(true);
|
||||
}
|
||||
|
||||
private _selectorValueChanged(ev: CustomEvent) {
|
||||
ev.stopPropagation();
|
||||
|
||||
fireEvent(this, "value-changed", {
|
||||
value: {
|
||||
...this.field,
|
||||
key: this.key,
|
||||
...ev.detail.value,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
public openSidebar(selectorEditor = false): void {
|
||||
if (!selectorEditor) {
|
||||
this._selected = true;
|
||||
}
|
||||
|
||||
fireEvent(this, "open-sidebar", {
|
||||
save: (value) => {
|
||||
fireEvent(this, "value-changed", { value });
|
||||
},
|
||||
close: () => {
|
||||
if (selectorEditor) {
|
||||
this._selectorRowSelected = false;
|
||||
} else {
|
||||
this._selected = false;
|
||||
}
|
||||
fireEvent(this, "close-sidebar");
|
||||
},
|
||||
toggleYamlMode: () => {
|
||||
this._toggleYamlMode();
|
||||
return this._yamlMode;
|
||||
},
|
||||
delete: this._onDelete,
|
||||
config: {
|
||||
field: this.field,
|
||||
selector: selectorEditor,
|
||||
key: this.key,
|
||||
excludeKeys: this.excludeKeys,
|
||||
},
|
||||
yamlMode: this._yamlMode,
|
||||
} satisfies ScriptFieldSidebarConfig);
|
||||
|
||||
if (this.narrow) {
|
||||
this.scrollIntoView({
|
||||
block: "start",
|
||||
behavior: "smooth",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private _onDelete() {
|
||||
private _toggleYamlMode = () => {
|
||||
this._yamlMode = !this._yamlMode;
|
||||
};
|
||||
|
||||
private _onDelete = () => {
|
||||
showConfirmationDialog(this, {
|
||||
title: this.hass.localize(
|
||||
"ui.panel.config.script.editor.field_delete_confirm_title"
|
||||
@@ -181,112 +239,13 @@ export default class HaScriptFieldRow extends LitElement {
|
||||
destructive: true,
|
||||
confirm: () => {
|
||||
fireEvent(this, "value-changed", { value: null });
|
||||
if (this._selected || this._selectorRowSelected) {
|
||||
fireEvent(this, "close-sidebar");
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
private _onYamlChange(ev: CustomEvent) {
|
||||
ev.stopPropagation();
|
||||
const value = { ...ev.detail.value };
|
||||
|
||||
if (typeof value !== "object" || Object.keys(value).length !== 1) {
|
||||
this._yamlError = "yaml_error";
|
||||
return;
|
||||
}
|
||||
const key = Object.keys(value)[0];
|
||||
if (this.excludeKeys.includes(key)) {
|
||||
this._yamlError = "key_not_unique";
|
||||
return;
|
||||
}
|
||||
this._yamlError = undefined;
|
||||
|
||||
const newValue = { ...value[key], key };
|
||||
|
||||
fireEvent(this, "value-changed", { value: newValue });
|
||||
}
|
||||
|
||||
private _maybeSetKey(value): void {
|
||||
const nameChanged = value.name !== this.field.name;
|
||||
const keyChanged = value.key !== this.key;
|
||||
if (!nameChanged || keyChanged) {
|
||||
return;
|
||||
}
|
||||
const slugifyName = this.field.name
|
||||
? slugify(this.field.name)
|
||||
: this.hass.localize("ui.panel.config.script.editor.field.field") ||
|
||||
"field";
|
||||
const regex = new RegExp(`^${slugifyName}(_\\d)?$`);
|
||||
if (regex.test(this.key)) {
|
||||
let key = !value.name
|
||||
? this.hass.localize("ui.panel.config.script.editor.field.field") ||
|
||||
"field"
|
||||
: slugify(value.name);
|
||||
if (this.excludeKeys.includes(key)) {
|
||||
let uniqueKey = key;
|
||||
let i = 2;
|
||||
do {
|
||||
uniqueKey = `${key}_${i}`;
|
||||
i++;
|
||||
} while (this.excludeKeys.includes(uniqueKey));
|
||||
key = uniqueKey;
|
||||
}
|
||||
value.key = key;
|
||||
}
|
||||
}
|
||||
|
||||
private _valueChanged(ev: CustomEvent) {
|
||||
ev.stopPropagation();
|
||||
const value = { ...ev.detail.value };
|
||||
|
||||
this._maybeSetKey(value);
|
||||
|
||||
// Don't allow to set an empty key, or duplicate an existing key.
|
||||
if (!value.key || this.excludeKeys.includes(value.key)) {
|
||||
this._uiError = value.key
|
||||
? {
|
||||
key: "key_not_unique",
|
||||
}
|
||||
: {
|
||||
key: "key_not_null",
|
||||
};
|
||||
this._errorKey = value.key ?? "";
|
||||
return;
|
||||
}
|
||||
this._errorKey = undefined;
|
||||
this._uiError = undefined;
|
||||
|
||||
// If we render the default with an incompatible selector, it risks throwing an exception and not rendering.
|
||||
// Clear the default when changing the selector type.
|
||||
if (
|
||||
Object.keys(this.field.selector)[0] !== Object.keys(value.selector)[0]
|
||||
) {
|
||||
delete value.default;
|
||||
}
|
||||
|
||||
fireEvent(this, "value-changed", { value });
|
||||
}
|
||||
|
||||
public expand() {
|
||||
this.updateComplete.then(() => {
|
||||
this.shadowRoot!.querySelector("ha-expansion-panel")!.expanded = true;
|
||||
});
|
||||
}
|
||||
|
||||
private _computeLabelCallback = (
|
||||
schema: SchemaUnion<ReturnType<typeof this._schema>>
|
||||
): string => {
|
||||
switch (schema.name) {
|
||||
default:
|
||||
return this.hass.localize(
|
||||
`ui.panel.config.script.editor.field.${schema.name}`
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
private _computeError = (error: string) =>
|
||||
this.hass.localize(`ui.panel.config.script.editor.field.${error}` as any) ||
|
||||
error;
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return [
|
||||
haStyle,
|
||||
@@ -299,9 +258,8 @@ export default class HaScriptFieldRow extends LitElement {
|
||||
opacity: 0.5;
|
||||
pointer-events: none;
|
||||
}
|
||||
ha-expansion-panel {
|
||||
--expansion-panel-summary-padding: 0 0 0 8px;
|
||||
--expansion-panel-content-padding: 0;
|
||||
.hidden {
|
||||
display: none;
|
||||
}
|
||||
h3 {
|
||||
margin: 0;
|
||||
@@ -341,7 +299,7 @@ export default class HaScriptFieldRow extends LitElement {
|
||||
);
|
||||
}
|
||||
|
||||
ha-list-item[disabled] {
|
||||
ha-md-menu-item[disabled] {
|
||||
--mdc-theme-text-primary-on-background: var(--disabled-text-color);
|
||||
}
|
||||
.warning ul {
|
||||
@@ -359,6 +317,18 @@ export default class HaScriptFieldRow extends LitElement {
|
||||
border-color: var(--state-inactive-color);
|
||||
box-shadow: var(--shadow-default), var(--shadow-focus);
|
||||
}
|
||||
.selector-row {
|
||||
margin-left: 12px;
|
||||
padding: 12px 4px 16px 16px;
|
||||
margin-right: -4px;
|
||||
border-left: 2px solid var(--ha-color-border-neutral-quiet);
|
||||
}
|
||||
.selector-row.parent-selected {
|
||||
border-color: var(--primary-color);
|
||||
background-color: var(--ha-color-fill-primary-quiet-resting);
|
||||
border-top-right-radius: var(--ha-border-radius-xl);
|
||||
border-bottom-right-radius: var(--ha-border-radius-xl);
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
||||
@@ -0,0 +1,167 @@
|
||||
import type { CSSResultGroup } from "lit";
|
||||
import { LitElement, css, html, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { fireEvent } from "../../../common/dom/fire_event";
|
||||
import type { LocalizeKeys } from "../../../common/translations/localize";
|
||||
import "../../../components/ha-alert";
|
||||
import "../../../components/ha-form/ha-form";
|
||||
import type { SchemaUnion } from "../../../components/ha-form/types";
|
||||
import "../../../components/ha-yaml-editor";
|
||||
import type { Field } from "../../../data/script";
|
||||
import { SELECTOR_SELECTOR_BUILDING_BLOCKS } from "../../../data/selector/selector_selector";
|
||||
import { haStyle } from "../../../resources/styles";
|
||||
import type { HomeAssistant } from "../../../types";
|
||||
|
||||
@customElement("ha-script-field-selector-editor")
|
||||
export default class HaScriptFieldSelectorEditor extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public field!: Field;
|
||||
|
||||
@property({ type: Boolean }) public narrow = false;
|
||||
|
||||
@property({ type: Boolean, reflect: true }) public selected = false;
|
||||
|
||||
@property({ type: Boolean }) public disabled = false;
|
||||
|
||||
@property({ type: Boolean }) public indent = false;
|
||||
|
||||
@property({ type: Boolean, attribute: "yaml-mode" }) public yamlMode = false;
|
||||
|
||||
@state() private _uiError?: Record<string, string>;
|
||||
|
||||
@state() private _yamlError?: undefined | "yaml_error" | "key_not_unique";
|
||||
|
||||
private _schema = memoizeOne(
|
||||
(selector: any) =>
|
||||
[
|
||||
...(!this.indent
|
||||
? [
|
||||
{
|
||||
name: "selector",
|
||||
selector: { selector: {} },
|
||||
},
|
||||
]
|
||||
: []),
|
||||
...(selector &&
|
||||
typeof selector === "object" &&
|
||||
(this.indent ||
|
||||
!SELECTOR_SELECTOR_BUILDING_BLOCKS.includes(Object.keys(selector)[0]))
|
||||
? [
|
||||
{
|
||||
name: "default",
|
||||
selector: !this.indent
|
||||
? selector
|
||||
: {
|
||||
[Object.keys(selector)[0]]: {
|
||||
...selector[Object.keys(selector)[0]],
|
||||
optionsInSidebar: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
]
|
||||
: []),
|
||||
] as const
|
||||
);
|
||||
|
||||
protected render() {
|
||||
const schema = this._schema(this.field.selector);
|
||||
const data = { selector: this.field.selector, default: this.field.default };
|
||||
|
||||
return html`
|
||||
${this.yamlMode
|
||||
? html`${this._yamlError
|
||||
? html`<ha-alert alert-type="error">
|
||||
${this.hass.localize(
|
||||
`ui.panel.config.script.editor.field.${this._yamlError}`
|
||||
)}
|
||||
</ha-alert>`
|
||||
: nothing}
|
||||
<ha-yaml-editor
|
||||
.hass=${this.hass}
|
||||
.defaultValue=${data}
|
||||
@value-changed=${this._onYamlChange}
|
||||
></ha-yaml-editor>`
|
||||
: html`<ha-form
|
||||
.schema=${schema}
|
||||
.data=${data}
|
||||
.error=${this._uiError}
|
||||
.hass=${this.hass}
|
||||
.disabled=${this.disabled}
|
||||
.computeLabel=${this._computeLabelCallback}
|
||||
.computeError=${this._computeError}
|
||||
@value-changed=${this._valueChanged}
|
||||
.narrow=${this.narrow}
|
||||
></ha-form>`}
|
||||
`;
|
||||
}
|
||||
|
||||
private _valueChanged(ev: CustomEvent) {
|
||||
ev.stopPropagation();
|
||||
const value = { ...ev.detail.value };
|
||||
|
||||
this._uiError = undefined;
|
||||
|
||||
// If we render the default with an incompatible selector, it risks throwing an exception and not rendering.
|
||||
// Clear the default when changing the selector type.
|
||||
if (
|
||||
!this.indent &&
|
||||
Object.keys(this.field.selector)[0] !== Object.keys(value.selector)[0]
|
||||
) {
|
||||
value.default = undefined;
|
||||
}
|
||||
|
||||
fireEvent(this, "value-changed", { value });
|
||||
}
|
||||
|
||||
private _onYamlChange(ev: CustomEvent) {
|
||||
ev.stopPropagation();
|
||||
const value = { ...ev.detail.value };
|
||||
|
||||
if (typeof value !== "object" || Object.keys(value).length !== 2) {
|
||||
this._yamlError = "yaml_error";
|
||||
return;
|
||||
}
|
||||
|
||||
fireEvent(this, "value-changed", { value });
|
||||
}
|
||||
|
||||
private _computeLabelCallback = (
|
||||
schema: SchemaUnion<ReturnType<typeof this._schema>>
|
||||
): string =>
|
||||
this.hass.localize(
|
||||
`ui.panel.config.script.editor.field.${schema.name}` as LocalizeKeys
|
||||
) ?? schema.name;
|
||||
|
||||
private _computeError = (error: string) =>
|
||||
this.hass.localize(`ui.panel.config.script.editor.field.${error}` as any) ||
|
||||
error;
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return [
|
||||
haStyle,
|
||||
css`
|
||||
:host([indent]) ha-form {
|
||||
display: block;
|
||||
margin-left: 12px;
|
||||
padding: 12px 20px 16px 16px;
|
||||
margin-right: -4px;
|
||||
border-left: 2px solid var(--ha-color-border-neutral-quiet);
|
||||
}
|
||||
:host([selected]) ha-form {
|
||||
border-color: var(--primary-color);
|
||||
background-color: var(--ha-color-fill-primary-quiet-resting);
|
||||
border-top-right-radius: var(--ha-border-radius-xl);
|
||||
border-bottom-right-radius: var(--ha-border-radius-xl);
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-script-field-selector-editor": HaScriptFieldSelectorEditor;
|
||||
}
|
||||
}
|
||||
@@ -21,6 +21,8 @@ export default class HaScriptFields extends LitElement {
|
||||
|
||||
@property({ attribute: false }) public highlightedFields?: Fields;
|
||||
|
||||
@property({ type: Boolean }) public narrow = false;
|
||||
|
||||
private _focusLastActionOnChange = false;
|
||||
|
||||
protected render() {
|
||||
@@ -39,6 +41,7 @@ export default class HaScriptFields extends LitElement {
|
||||
@value-changed=${this._fieldChanged}
|
||||
.hass=${this.hass}
|
||||
?highlight=${this.highlightedFields?.[key] !== undefined}
|
||||
.narrow=${this.narrow}
|
||||
>
|
||||
</ha-script-field-row>
|
||||
`
|
||||
@@ -71,9 +74,15 @@ export default class HaScriptFields extends LitElement {
|
||||
"ha-script-field-row:last-of-type"
|
||||
)!;
|
||||
row.updateComplete.then(() => {
|
||||
row.expand();
|
||||
row.scrollIntoView();
|
||||
row.openSidebar();
|
||||
row.focus();
|
||||
|
||||
if (this.narrow) {
|
||||
row.scrollIntoView({
|
||||
block: "start",
|
||||
behavior: "smooth",
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { mdiHelpCircle } from "@mdi/js";
|
||||
import type { CSSResultGroup, PropertyValues } from "lit";
|
||||
import { LitElement, css, html, nothing } from "lit";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import { mdiContentSave, mdiHelpCircle } from "@mdi/js";
|
||||
import { load } from "js-yaml";
|
||||
import type { CSSResultGroup } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import { classMap } from "lit/directives/class-map";
|
||||
import {
|
||||
any,
|
||||
array,
|
||||
@@ -13,32 +14,27 @@ import {
|
||||
optional,
|
||||
string,
|
||||
} from "superstruct";
|
||||
import { ensureArray } from "../../../common/array/ensure-array";
|
||||
import { canOverrideAlphanumericInput } from "../../../common/dom/can-override-input";
|
||||
import { fireEvent } from "../../../common/dom/fire_event";
|
||||
import { constructUrlCurrentPath } from "../../../common/url/construct-url";
|
||||
import {
|
||||
extractSearchParam,
|
||||
removeSearchParam,
|
||||
} from "../../../common/url/search-params";
|
||||
import "../../../components/ha-card";
|
||||
import "../../../components/ha-icon-button";
|
||||
import "../../../components/ha-markdown";
|
||||
import type { SidebarConfig } from "../../../data/automation";
|
||||
import type { Action, Fields, ScriptConfig } from "../../../data/script";
|
||||
import {
|
||||
getActionType,
|
||||
MODES,
|
||||
normalizeScriptConfig,
|
||||
} from "../../../data/script";
|
||||
import { haStyle } from "../../../resources/styles";
|
||||
import type { HomeAssistant } from "../../../types";
|
||||
import { documentationUrl } from "../../../util/documentation-url";
|
||||
import { showToast } from "../../../util/toast";
|
||||
import "../automation/action/ha-automation-action";
|
||||
import type HaAutomationAction from "../automation/action/ha-automation-action";
|
||||
import "../automation/ha-automation-sidebar";
|
||||
import { showPasteReplaceDialog } from "../automation/paste-replace-dialog/show-dialog-paste-replace";
|
||||
import { saveFabStyles } from "../automation/styles";
|
||||
import "./ha-script-fields";
|
||||
import type HaScriptFields from "./ha-script-fields";
|
||||
import { canOverrideAlphanumericInput } from "../../../common/dom/can-override-input";
|
||||
import { showToast } from "../../../util/toast";
|
||||
import { showPasteReplaceDialog } from "../automation/paste-replace-dialog/show-dialog-paste-replace";
|
||||
import { ensureArray } from "../../../common/array/ensure-array";
|
||||
|
||||
const scriptConfigStruct = object({
|
||||
alias: optional(string()),
|
||||
@@ -60,6 +56,8 @@ export class HaManualScriptEditor extends LitElement {
|
||||
|
||||
@property({ type: Boolean }) public disabled = false;
|
||||
|
||||
@property({ type: Boolean }) public saving = false;
|
||||
|
||||
@property({ attribute: false }) public config!: ScriptConfig;
|
||||
|
||||
@property({ attribute: false }) public dirty = false;
|
||||
@@ -71,6 +69,8 @@ export class HaManualScriptEditor extends LitElement {
|
||||
|
||||
@state() private _pastedConfig?: ScriptConfig;
|
||||
|
||||
@state() private _sidebarConfig?: SidebarConfig;
|
||||
|
||||
private _previousConfig?: ScriptConfig;
|
||||
|
||||
public addFields() {
|
||||
@@ -99,41 +99,19 @@ export class HaManualScriptEditor extends LitElement {
|
||||
}
|
||||
}
|
||||
|
||||
protected firstUpdated(changedProps: PropertyValues): void {
|
||||
super.firstUpdated(changedProps);
|
||||
const expanded = extractSearchParam("expanded");
|
||||
if (expanded === "1") {
|
||||
this._clearParam("expanded");
|
||||
const items = this.shadowRoot!.querySelectorAll<HaAutomationAction>(
|
||||
"ha-automation-action"
|
||||
);
|
||||
|
||||
items.forEach((el) => {
|
||||
el.updateComplete.then(() => {
|
||||
el.expandAll();
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private _clearParam(param: string) {
|
||||
window.history.replaceState(
|
||||
null,
|
||||
"",
|
||||
constructUrlCurrentPath(removeSearchParam(param))
|
||||
);
|
||||
}
|
||||
|
||||
protected render() {
|
||||
private _renderContent() {
|
||||
return html`
|
||||
${this.config.description
|
||||
? html`<ha-markdown
|
||||
class="description"
|
||||
breaks
|
||||
.content=${this.config.description}
|
||||
></ha-markdown>`
|
||||
: nothing}
|
||||
${this.config.fields
|
||||
${
|
||||
this.config.description
|
||||
? html`<ha-markdown
|
||||
class="description"
|
||||
breaks
|
||||
.content=${this.config.description}
|
||||
></ha-markdown>`
|
||||
: nothing
|
||||
}
|
||||
${
|
||||
this.config.fields
|
||||
? html`<div class="header">
|
||||
<h2 id="fields-heading" class="name">
|
||||
${this.hass.localize(
|
||||
@@ -165,39 +143,78 @@ export class HaManualScriptEditor extends LitElement {
|
||||
@value-changed=${this._fieldsChanged}
|
||||
.hass=${this.hass}
|
||||
.disabled=${this.disabled}
|
||||
.narrow=${this.narrow}
|
||||
@open-sidebar=${this._openSidebar}
|
||||
@close-sidebar=${this._handleCloseSidebar}
|
||||
></ha-script-fields>`
|
||||
: nothing}
|
||||
: nothing
|
||||
}
|
||||
|
||||
<div class="header">
|
||||
<h2 id="sequence-heading" class="name">
|
||||
${this.hass.localize("ui.panel.config.script.editor.sequence")}
|
||||
</h2>
|
||||
<a
|
||||
href=${documentationUrl(this.hass, "/docs/scripts/")}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
<ha-icon-button
|
||||
.path=${mdiHelpCircle}
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.script.editor.link_available_actions"
|
||||
)}
|
||||
></ha-icon-button>
|
||||
</a>
|
||||
<div class="header">
|
||||
<h2 id="sequence-heading" class="name">
|
||||
${this.hass.localize("ui.panel.config.script.editor.sequence")}
|
||||
</h2>
|
||||
<a
|
||||
href=${documentationUrl(this.hass, "/docs/scripts/")}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
<ha-icon-button
|
||||
.path=${mdiHelpCircle}
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.script.editor.link_available_actions"
|
||||
)}
|
||||
></ha-icon-button>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<ha-automation-action
|
||||
role="region"
|
||||
aria-labelledby="sequence-heading"
|
||||
.actions=${this.config.sequence || []}
|
||||
.highlightedActions=${this._pastedConfig?.sequence || []}
|
||||
@value-changed=${this._sequenceChanged}
|
||||
@open-sidebar=${this._openSidebar}
|
||||
@close-sidebar=${this._handleCloseSidebar}
|
||||
.hass=${this.hass}
|
||||
.narrow=${this.narrow}
|
||||
.disabled=${this.disabled || this.saving}
|
||||
root
|
||||
sidebar
|
||||
></ha-automation-action>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
protected render() {
|
||||
return html`
|
||||
<div class="split-view">
|
||||
<div class="content-wrapper">
|
||||
<div class="content">${this._renderContent()}</div>
|
||||
<ha-fab
|
||||
slot="fab"
|
||||
class=${this.dirty ? "dirty" : ""}
|
||||
.label=${this.hass.localize("ui.common.save")}
|
||||
.disabled=${this.saving}
|
||||
extended
|
||||
@click=${this._saveScript}
|
||||
>
|
||||
<ha-svg-icon slot="icon" .path=${mdiContentSave}></ha-svg-icon>
|
||||
</ha-fab>
|
||||
</div>
|
||||
<ha-automation-sidebar
|
||||
class=${classMap({
|
||||
sidebar: true,
|
||||
hidden: !this._sidebarConfig,
|
||||
overlay: !this.isWide,
|
||||
})}
|
||||
.narrow=${this.narrow}
|
||||
.isWide=${this.isWide}
|
||||
.hass=${this.hass}
|
||||
.config=${this._sidebarConfig}
|
||||
@value-changed=${this._sidebarConfigChanged}
|
||||
.disabled=${this.disabled}
|
||||
></ha-automation-sidebar>
|
||||
</div>
|
||||
|
||||
<ha-automation-action
|
||||
role="region"
|
||||
aria-labelledby="sequence-heading"
|
||||
.actions=${this.config.sequence || []}
|
||||
.highlightedActions=${this._pastedConfig?.sequence || []}
|
||||
.path=${["sequence"]}
|
||||
@value-changed=${this._sequenceChanged}
|
||||
.hass=${this.hass}
|
||||
.narrow=${this.narrow}
|
||||
.disabled=${this.disabled}
|
||||
root
|
||||
></ha-automation-action>
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -406,22 +423,112 @@ export class HaManualScriptEditor extends LitElement {
|
||||
});
|
||||
}
|
||||
|
||||
private _openSidebar(ev: CustomEvent<SidebarConfig>) {
|
||||
// deselect previous selected row
|
||||
this._sidebarConfig?.close?.();
|
||||
this._sidebarConfig = ev.detail;
|
||||
}
|
||||
|
||||
private _sidebarConfigChanged(ev: CustomEvent<{ value: SidebarConfig }>) {
|
||||
ev.stopPropagation();
|
||||
if (!this._sidebarConfig) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._sidebarConfig = {
|
||||
...this._sidebarConfig,
|
||||
...ev.detail.value,
|
||||
};
|
||||
}
|
||||
|
||||
private _closeSidebar() {
|
||||
if (this._sidebarConfig) {
|
||||
const closeRow = this._sidebarConfig?.close;
|
||||
this._sidebarConfig = undefined;
|
||||
closeRow?.();
|
||||
}
|
||||
}
|
||||
|
||||
private _handleCloseSidebar() {
|
||||
this._sidebarConfig = undefined;
|
||||
}
|
||||
|
||||
private _saveScript() {
|
||||
this._closeSidebar();
|
||||
fireEvent(this, "save-script");
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return [
|
||||
haStyle,
|
||||
saveFabStyles,
|
||||
css`
|
||||
:host {
|
||||
display: block;
|
||||
}
|
||||
ha-card {
|
||||
|
||||
.split-view {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
height: 100%;
|
||||
position: relative;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.content-wrapper {
|
||||
position: relative;
|
||||
flex: 6;
|
||||
}
|
||||
|
||||
.content {
|
||||
padding: 32px 16px 64px 0;
|
||||
height: calc(100vh - 153px);
|
||||
height: calc(100dvh - 153px);
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
.sidebar {
|
||||
padding: 12px 0;
|
||||
flex: 4;
|
||||
height: calc(100vh - 81px);
|
||||
height: calc(100dvh - 81px);
|
||||
width: 40%;
|
||||
}
|
||||
.sidebar.hidden {
|
||||
border-color: transparent;
|
||||
border-width: 0;
|
||||
overflow: hidden;
|
||||
flex: 0;
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
.sidebar.overlay {
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
height: calc(100% - 64px);
|
||||
padding: 0;
|
||||
z-index: 5;
|
||||
}
|
||||
|
||||
@media all and (max-width: 870px) {
|
||||
.split-view {
|
||||
gap: 0;
|
||||
margin-right: -8px;
|
||||
}
|
||||
.sidebar {
|
||||
height: 0;
|
||||
width: 0;
|
||||
flex: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.sidebar.overlay.hidden {
|
||||
width: 0;
|
||||
}
|
||||
.description {
|
||||
margin: 0;
|
||||
}
|
||||
p {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
.header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
import type { PropertyValues, TemplateResult } from "lit";
|
||||
import { LitElement, css, html, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { isComponentLoaded } from "../../../common/config/is_component_loaded";
|
||||
import { navigate } from "../../../common/navigate";
|
||||
import "../../../components/ha-alert";
|
||||
@@ -18,10 +19,14 @@ import "../../../components/ha-icon-next";
|
||||
import "../../../components/ha-list";
|
||||
import "../../../components/ha-list-item";
|
||||
import "../../../components/ha-metric";
|
||||
import "../../../components/ha-segmented-bar";
|
||||
import "../../../components/ha-svg-icon";
|
||||
import { extractApiErrorMessage } from "../../../data/hassio/common";
|
||||
import type { HassioHostInfo } from "../../../data/hassio/host";
|
||||
import { fetchHassioHostInfo } from "../../../data/hassio/host";
|
||||
import type { HassioHostInfo, HostDisksUsage } from "../../../data/hassio/host";
|
||||
import {
|
||||
fetchHassioHostInfo,
|
||||
fetchHostDisksUsage,
|
||||
} from "../../../data/hassio/host";
|
||||
import type {
|
||||
SupervisorMount,
|
||||
SupervisorMounts,
|
||||
@@ -36,13 +41,12 @@ import {
|
||||
import { showAlertDialog } from "../../../dialogs/generic/show-dialog-box";
|
||||
import "../../../layouts/hass-subpage";
|
||||
import type { HomeAssistant, Route } from "../../../types";
|
||||
import {
|
||||
getValueInPercentage,
|
||||
roundWithOneDecimal,
|
||||
} from "../../../util/calculate";
|
||||
import { roundWithOneDecimal } from "../../../util/calculate";
|
||||
import "../core/ha-config-analytics";
|
||||
import { showMoveDatadiskDialog } from "./show-dialog-move-datadisk";
|
||||
import { showMountViewDialog } from "./show-dialog-view-mount";
|
||||
import type { Segment } from "../../../components/ha-segmented-bar";
|
||||
import { getGraphColorByIndex } from "../../../common/color/colors";
|
||||
|
||||
@customElement("ha-config-section-storage")
|
||||
class HaConfigSectionStorage extends LitElement {
|
||||
@@ -56,6 +60,8 @@ class HaConfigSectionStorage extends LitElement {
|
||||
|
||||
@state() private _hostInfo?: HassioHostInfo;
|
||||
|
||||
@state() private _storageInfo?: HostDisksUsage | null;
|
||||
|
||||
@state() private _mountsInfo?: SupervisorMounts | null;
|
||||
|
||||
protected firstUpdated(changedProps: PropertyValues) {
|
||||
@@ -97,26 +103,10 @@ class HaConfigSectionStorage extends LitElement {
|
||||
)}
|
||||
>
|
||||
<div class="card-content">
|
||||
<ha-metric
|
||||
.heading=${this.hass.localize(
|
||||
"ui.panel.config.storage.used_space"
|
||||
)}
|
||||
.value=${this._getUsedSpace(
|
||||
this._hostInfo?.disk_used,
|
||||
this._hostInfo?.disk_total
|
||||
)}
|
||||
.tooltip=${`${this._hostInfo.disk_used} GB/${this._hostInfo.disk_total} GB`}
|
||||
></ha-metric>
|
||||
<div class="detailed-storage-info">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.storage.detailed_description",
|
||||
{
|
||||
used: `${this._hostInfo?.disk_used} GB`,
|
||||
total: `${this._hostInfo?.disk_total} GB`,
|
||||
free_space: `${this._hostInfo.disk_free} GB`,
|
||||
}
|
||||
)}
|
||||
</div>
|
||||
${this._renderStorageMetrics(
|
||||
this._hostInfo,
|
||||
this._storageInfo
|
||||
)}
|
||||
${this._hostInfo.disk_life_time !== null
|
||||
? // prettier-ignore
|
||||
html`
|
||||
@@ -247,7 +237,100 @@ class HaConfigSectionStorage extends LitElement {
|
||||
`;
|
||||
}
|
||||
|
||||
private _renderStorageMetrics = memoizeOne(
|
||||
(hostInfo?: HassioHostInfo, storageInfo?: HostDisksUsage | null) => {
|
||||
if (!hostInfo) {
|
||||
return nothing;
|
||||
}
|
||||
const computedStyles = getComputedStyle(this);
|
||||
let totalSpaceGB = hostInfo.disk_total;
|
||||
let usedSpaceGB = hostInfo.disk_used;
|
||||
// hostInfo.disk_free is sometimes 0, so we may need to calculate it
|
||||
let freeSpaceGB =
|
||||
hostInfo.disk_free || hostInfo.disk_total - hostInfo.disk_used;
|
||||
const segments: Segment[] = [];
|
||||
if (storageInfo) {
|
||||
const totalSpace =
|
||||
storageInfo.total_bytes ?? this._gbToBytes(hostInfo.disk_total);
|
||||
totalSpaceGB = this._bytesToGB(totalSpace);
|
||||
usedSpaceGB = this._bytesToGB(storageInfo.used_bytes);
|
||||
freeSpaceGB = this._bytesToGB(totalSpace - storageInfo.used_bytes);
|
||||
storageInfo.children?.forEach((child, index) => {
|
||||
if (child.used_bytes > 0) {
|
||||
const space = this._bytesToGB(child.used_bytes);
|
||||
segments.push({
|
||||
value: space,
|
||||
color: getGraphColorByIndex(index, computedStyles),
|
||||
label: html`${this.hass.localize(
|
||||
`ui.panel.config.storage.segments.${child.id}`
|
||||
) ||
|
||||
child.label ||
|
||||
child.id}
|
||||
<span style="color: var(--secondary-text-color)"
|
||||
>${roundWithOneDecimal(space)} GB</span
|
||||
>`,
|
||||
});
|
||||
}
|
||||
});
|
||||
} else {
|
||||
segments.push({
|
||||
value: usedSpaceGB,
|
||||
color: "var(--primary-color)",
|
||||
label: html`${this.hass.localize(
|
||||
"ui.panel.config.storage.segments.used"
|
||||
)}
|
||||
<span style="color: var(--secondary-text-color)"
|
||||
>${roundWithOneDecimal(usedSpaceGB)} GB</span
|
||||
>`,
|
||||
});
|
||||
}
|
||||
segments.push({
|
||||
value: freeSpaceGB,
|
||||
color:
|
||||
"var(--ha-bar-background-color, var(--secondary-background-color))",
|
||||
label: html`${this.hass.localize(
|
||||
"ui.panel.config.storage.segments.free"
|
||||
)}
|
||||
<span style="color: var(--secondary-text-color)"
|
||||
>${roundWithOneDecimal(freeSpaceGB)} GB</span
|
||||
>`,
|
||||
});
|
||||
const chart = html`
|
||||
<ha-segmented-bar
|
||||
.heading=${this.hass.localize("ui.panel.config.storage.used_space")}
|
||||
.description=${this.hass.localize(
|
||||
"ui.panel.config.storage.detailed_description",
|
||||
{
|
||||
used: `${roundWithOneDecimal(usedSpaceGB)} GB`,
|
||||
total: `${roundWithOneDecimal(totalSpaceGB)} GB`,
|
||||
}
|
||||
)}
|
||||
.segments=${segments}
|
||||
></ha-segmented-bar>
|
||||
`;
|
||||
return storageInfo || storageInfo === null
|
||||
? chart
|
||||
: html`
|
||||
<div class="loading-container">
|
||||
${chart}
|
||||
<div class="loading-overlay">
|
||||
<ha-spinner></ha-spinner>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
);
|
||||
|
||||
private _bytesToGB(bytes: number) {
|
||||
return bytes / 1024 / 1024 / 1024;
|
||||
}
|
||||
|
||||
private _gbToBytes(GB: number) {
|
||||
return GB * 1024 * 1024 * 1024;
|
||||
}
|
||||
|
||||
private async _load() {
|
||||
this._loadStorageInfo();
|
||||
try {
|
||||
this._hostInfo = await fetchHassioHostInfo(this.hass);
|
||||
} catch (err: any) {
|
||||
@@ -260,6 +343,15 @@ class HaConfigSectionStorage extends LitElement {
|
||||
}
|
||||
}
|
||||
|
||||
private async _loadStorageInfo() {
|
||||
try {
|
||||
this._storageInfo = await fetchHostDisksUsage(this.hass);
|
||||
} catch (err: any) {
|
||||
this._error = err.message || err;
|
||||
this._storageInfo = null;
|
||||
}
|
||||
}
|
||||
|
||||
private _moveDatadisk(): void {
|
||||
showMoveDatadiskDialog(this, {
|
||||
hostInfo: this._hostInfo!,
|
||||
@@ -311,9 +403,6 @@ class HaConfigSectionStorage extends LitElement {
|
||||
}
|
||||
}
|
||||
|
||||
private _getUsedSpace = (used: number, total: number) =>
|
||||
roundWithOneDecimal(getValueInPercentage(used, 0, total));
|
||||
|
||||
static styles = css`
|
||||
.content {
|
||||
padding: 28px 20px 0;
|
||||
@@ -337,10 +426,22 @@ class HaConfigSectionStorage extends LitElement {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.detailed-storage-info {
|
||||
font-size: var(--ha-font-size-s);
|
||||
color: var(--secondary-text-color);
|
||||
.loading-container {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.loading-overlay {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: rgba(var(--rgb-card-background-color), 0.75);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.mount-state-failed {
|
||||
color: var(--error-color);
|
||||
}
|
||||
|
||||
@@ -34,6 +34,7 @@ class DialogHomeZoneDetail extends LitElement {
|
||||
this._params = params;
|
||||
this._error = undefined;
|
||||
this._data = {
|
||||
name: this.hass.config.location_name,
|
||||
latitude: this.hass.config.latitude,
|
||||
longitude: this.hass.config.longitude,
|
||||
radius: this.hass.config.radius,
|
||||
@@ -63,7 +64,7 @@ class DialogHomeZoneDetail extends LitElement {
|
||||
escapeKeyAction
|
||||
.heading=${createCloseHeading(
|
||||
this.hass,
|
||||
this.hass!.localize("ui.panel.config.zone.edit_home")
|
||||
this.hass!.localize("ui.common.edit_item", { name: this._data.name })
|
||||
)}
|
||||
>
|
||||
<div>
|
||||
|
||||
@@ -85,7 +85,9 @@ class DialogZoneDetail extends LitElement {
|
||||
.heading=${createCloseHeading(
|
||||
this.hass,
|
||||
this._params.entry
|
||||
? this._params.entry.name
|
||||
? this.hass!.localize("ui.common.edit_item", {
|
||||
name: this._params.entry.name,
|
||||
})
|
||||
: this.hass!.localize("ui.panel.config.zone.detail.new_zone")
|
||||
)}
|
||||
>
|
||||
|
||||
@@ -165,9 +165,9 @@ export class HaConfigZone extends SubscribeMixin(LitElement) {
|
||||
.entry=${entry}
|
||||
@click=${this._openEditEntry}
|
||||
.path=${mdiPencil}
|
||||
.label=${hass.localize(
|
||||
"ui.panel.config.zone.edit_zone"
|
||||
)}
|
||||
.label=${hass.localize("ui.common.edit_item", {
|
||||
name: entry.name,
|
||||
})}
|
||||
></ha-icon-button>
|
||||
</div>
|
||||
`
|
||||
@@ -218,9 +218,9 @@ export class HaConfigZone extends SubscribeMixin(LitElement) {
|
||||
this._canEditCore
|
||||
? mdiPencil
|
||||
: mdiPencilOff}
|
||||
.label=${stateObject.entity_id === "zone.home"
|
||||
? hass.localize("ui.panel.config.zone.edit_home")
|
||||
: hass.localize("ui.panel.config.zone.edit_zone")}
|
||||
.label=${hass.localize("ui.common.edit_item", {
|
||||
name: hass.config.location_name,
|
||||
})}
|
||||
@click=${this._editHomeZone}
|
||||
></ha-icon-button>
|
||||
</ha-tooltip>`}
|
||||
|
||||
@@ -0,0 +1,366 @@
|
||||
import {
|
||||
mdiClipboardTextMultipleOutline,
|
||||
mdiInformationOutline,
|
||||
} from "@mdi/js";
|
||||
import type { HassEntity } from "home-assistant-js-websocket";
|
||||
import { dump } from "js-yaml";
|
||||
import type { CSSResultGroup, PropertyValues, TemplateResult } from "lit";
|
||||
import type { RenderItemFunction } from "@lit-labs/virtualizer/virtualize";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { classMap } from "lit/directives/class-map";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { fireEvent } from "../../../common/dom/fire_event";
|
||||
import { loadVirtualizer } from "../../../resources/virtualizer";
|
||||
import { copyToClipboard } from "../../../common/util/copy-clipboard";
|
||||
import "../../../components/ha-checkbox";
|
||||
import "../../../components/ha-svg-icon";
|
||||
import type { HomeAssistant } from "../../../types";
|
||||
import { showToast } from "../../../util/toast";
|
||||
import { haStyle } from "../../../resources/styles";
|
||||
|
||||
@customElement("developer-tools-state-renderer")
|
||||
class HaPanelDevStateRenderer extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public entities: HassEntity[] = [];
|
||||
|
||||
@property({ type: Boolean, attribute: "narrow" })
|
||||
public narrow = false;
|
||||
|
||||
@property({ type: Boolean, attribute: "virtualize", reflect: true })
|
||||
public virtualize = true;
|
||||
|
||||
@property({ type: Boolean, attribute: false })
|
||||
public showAttributes = true;
|
||||
|
||||
protected willUpdate(changedProps: PropertyValues<this>) {
|
||||
super.willUpdate(changedProps);
|
||||
if (
|
||||
(!this.hasUpdated && this.virtualize) ||
|
||||
(changedProps.has("virtualize") && this.virtualize)
|
||||
) {
|
||||
loadVirtualizer();
|
||||
}
|
||||
}
|
||||
|
||||
protected shouldUpdate(changedProps: PropertyValues<this>) {
|
||||
super.shouldUpdate(changedProps);
|
||||
const oldHass = changedProps.get("hass") as HomeAssistant | undefined;
|
||||
const languageChanged =
|
||||
oldHass === undefined || oldHass.locale !== this.hass.locale;
|
||||
|
||||
return (
|
||||
changedProps.has("entities") ||
|
||||
changedProps.has("narrow") ||
|
||||
changedProps.has("virtualize") ||
|
||||
changedProps.has("showAttributes") ||
|
||||
languageChanged
|
||||
);
|
||||
}
|
||||
|
||||
protected render() {
|
||||
const showAttributes = !this.narrow && this.showAttributes;
|
||||
return html`
|
||||
<div
|
||||
class=${classMap({ entities: true, "hide-attributes": !showAttributes })}
|
||||
role="table"
|
||||
>
|
||||
<div class="row" role="row" aria-rowindex="1">
|
||||
<div class="header" role="columnheader">
|
||||
<span class="padded">
|
||||
${this.hass.localize(
|
||||
"ui.panel.developer-tools.tabs.states.entity"
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
<div class="header" role="columnheader">
|
||||
<span class="padded">
|
||||
${this.hass.localize(
|
||||
"ui.panel.developer-tools.tabs.states.state"
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
<div class="header" role="columnheader">
|
||||
<span class="padded">
|
||||
${this.hass.localize(
|
||||
"ui.panel.developer-tools.tabs.states.attributes"
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row filters" role="row" aria-rowindex="2">
|
||||
<div class="header filter-entities" role="columnheader">
|
||||
<slot name="filter-entities"></slot>
|
||||
</div></span>
|
||||
<div class="header filter-states" role="columnheader">
|
||||
<slot name="filter-states"></slot>
|
||||
</div>
|
||||
<div class="header filter-attributes" role="columnheader">
|
||||
<slot name="filter-attributes"></slot>
|
||||
</div>
|
||||
</div>
|
||||
${
|
||||
this.entities.length === 0
|
||||
? html` <div class="row" role="row" aria-rowindex="3">
|
||||
<div class="cell" role="cell" aria-colspan="3">
|
||||
<span class="padded">
|
||||
${this.hass.localize(
|
||||
"ui.panel.developer-tools.tabs.states.no_entities"
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
</div>`
|
||||
: nothing
|
||||
}
|
||||
${
|
||||
this.virtualize
|
||||
? html`<lit-virtualizer
|
||||
.items=${this.entities}
|
||||
.renderItem=${this._renderStateItem}
|
||||
>
|
||||
</lit-virtualizer>`
|
||||
: this.entities.map((item, index) =>
|
||||
this._renderStateItem(item, index)
|
||||
)
|
||||
}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
private _renderStateItem: RenderItemFunction<HassEntity> = (
|
||||
item: HassEntity,
|
||||
index: number
|
||||
): TemplateResult | any => {
|
||||
if (!item || index === undefined) {
|
||||
return nothing;
|
||||
}
|
||||
return html`
|
||||
<div
|
||||
class=${classMap({
|
||||
row: true,
|
||||
odd: index % 2 === 0,
|
||||
even: index % 2 !== 0,
|
||||
})}
|
||||
role="row"
|
||||
aria-rowindex=${index + 3}
|
||||
>
|
||||
<div class="cell" role="cell">
|
||||
<span class="padded">
|
||||
<div class="id-name-container">
|
||||
<div class="id-name-row">
|
||||
<ha-svg-icon
|
||||
@click=${this._copyEntity}
|
||||
.entity=${item}
|
||||
alt=${this.hass.localize(
|
||||
"ui.panel.developer-tools.tabs.states.copy_id"
|
||||
)}
|
||||
title=${this.hass.localize(
|
||||
"ui.panel.developer-tools.tabs.states.copy_id"
|
||||
)}
|
||||
.path=${mdiClipboardTextMultipleOutline}
|
||||
></ha-svg-icon>
|
||||
<a href="#" .entity=${item} @click=${this._entitySelected}
|
||||
>${item.entity_id}</a
|
||||
>
|
||||
</div>
|
||||
<div class="id-name-row">
|
||||
<ha-svg-icon
|
||||
@click=${this._entityMoreInfo}
|
||||
.entity=${item}
|
||||
alt=${this.hass.localize(
|
||||
"ui.panel.developer-tools.tabs.states.more_info"
|
||||
)}
|
||||
title=${this.hass.localize(
|
||||
"ui.panel.developer-tools.tabs.states.more_info"
|
||||
)}
|
||||
.path=${mdiInformationOutline}
|
||||
></ha-svg-icon>
|
||||
<span class="secondary">
|
||||
${item.attributes.friendly_name}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</span>
|
||||
</div>
|
||||
<div class="cell" role="cell">
|
||||
<span class="padded">${item.state}</span>
|
||||
</div>
|
||||
<div class="cell" role="cell">
|
||||
<span class="padded">${this._attributeString(item)}</span>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
};
|
||||
|
||||
private _formatAttributeValue(value) {
|
||||
if (
|
||||
(Array.isArray(value) && value.some((val) => val instanceof Object)) ||
|
||||
(!Array.isArray(value) && value instanceof Object)
|
||||
) {
|
||||
return `\n${dump(value)}`;
|
||||
}
|
||||
return Array.isArray(value) ? value.join(", ") : value;
|
||||
}
|
||||
|
||||
private _attributeString(entity) {
|
||||
const output = "";
|
||||
|
||||
if (entity && entity.attributes) {
|
||||
return Object.keys(entity.attributes).map(
|
||||
(key) =>
|
||||
`${key}: ${this._formatAttributeValue(entity.attributes[key])}\n`
|
||||
);
|
||||
}
|
||||
|
||||
return output;
|
||||
}
|
||||
|
||||
private _copyEntity = async (ev) => {
|
||||
ev.preventDefault();
|
||||
const entity = (ev.currentTarget! as any).entity;
|
||||
await copyToClipboard(entity.entity_id);
|
||||
showToast(this, {
|
||||
message: this.hass.localize("ui.common.copied_clipboard"),
|
||||
});
|
||||
};
|
||||
|
||||
private _entityMoreInfo(ev) {
|
||||
ev.preventDefault();
|
||||
const entity = (ev.currentTarget! as any).entity;
|
||||
fireEvent(this, "hass-more-info", { entityId: entity.entity_id });
|
||||
}
|
||||
|
||||
private _entitySelected(ev) {
|
||||
ev.preventDefault();
|
||||
const entity = (ev.currentTarget! as any).entity;
|
||||
fireEvent(this, "states-tool-entity-selected", {
|
||||
entity: entity,
|
||||
});
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return [
|
||||
haStyle,
|
||||
css`
|
||||
:host([virtualize]) {
|
||||
display: block;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.entities {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.entities .row {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.entities .odd .cell {
|
||||
background-color: var(--table-row-background-color, #fff);
|
||||
}
|
||||
|
||||
.entities .even .cell {
|
||||
background-color: var(--table-row-alternative-background-color, #eee);
|
||||
}
|
||||
|
||||
.header,
|
||||
.cell {
|
||||
min-width: 200px;
|
||||
flex: 1;
|
||||
display: flex;
|
||||
margin: 0 1px 0 1px;
|
||||
}
|
||||
|
||||
.header {
|
||||
font-weight: bold;
|
||||
text-align: var(--float-start);
|
||||
direction: var(--direction);
|
||||
}
|
||||
|
||||
.header .padded {
|
||||
padding: 0 8px;
|
||||
}
|
||||
|
||||
.filters .header {
|
||||
background-color: var(--table-row-alternative-background-color, #eee);
|
||||
--mdc-text-field-fill-color: var(
|
||||
--table-row-alternative-background-color,
|
||||
#eee
|
||||
);
|
||||
}
|
||||
|
||||
.cell {
|
||||
word-break: break-word;
|
||||
vertical-align: top;
|
||||
direction: ltr;
|
||||
}
|
||||
|
||||
.cell .padded {
|
||||
padding: 4px;
|
||||
}
|
||||
|
||||
.entities .row .header:nth-child(1),
|
||||
.entities .row .cell:nth-child(1) {
|
||||
min-width: 300px;
|
||||
width: 30%;
|
||||
flex: 2;
|
||||
}
|
||||
|
||||
.entities .row .header:nth-child(3),
|
||||
.entities .row .cell:nth-child(3) {
|
||||
flex: 2;
|
||||
}
|
||||
|
||||
.entities .row .cell:nth-child(3) {
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
.hide-attributes .filter-attributes {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.hide-attributes .row .header:nth-child(3),
|
||||
.hide-attributes .row .cell:nth-child(3) {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.entities ha-svg-icon {
|
||||
--mdc-icon-size: 20px;
|
||||
padding: 4px;
|
||||
cursor: pointer;
|
||||
flex-shrink: 0;
|
||||
margin-right: 8px;
|
||||
margin-inline-end: 8px;
|
||||
margin-inline-start: initial;
|
||||
}
|
||||
|
||||
.entities a {
|
||||
color: var(--primary-color);
|
||||
}
|
||||
|
||||
.entities .id-name-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.entities .id-name-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"developer-tools-state-renderer": HaPanelDevStateRenderer;
|
||||
}
|
||||
|
||||
interface HASSDomEvents {
|
||||
"states-tool-entity-selected": {
|
||||
entity: Partial<HassEntity>;
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,23 +1,16 @@
|
||||
import {
|
||||
mdiClipboardTextMultipleOutline,
|
||||
mdiContentCopy,
|
||||
mdiInformationOutline,
|
||||
mdiRefresh,
|
||||
} from "@mdi/js";
|
||||
import { mdiContentCopy, mdiRefresh } from "@mdi/js";
|
||||
import { addHours } from "date-fns";
|
||||
import type {
|
||||
HassEntities,
|
||||
HassEntity,
|
||||
HassEntityAttributeBase,
|
||||
} from "home-assistant-js-websocket";
|
||||
import { dump } from "js-yaml";
|
||||
import type { CSSResultGroup } from "lit";
|
||||
import { LitElement, css, html, nothing } from "lit";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { formatDateTimeWithSeconds } from "../../../common/datetime/format_date_time";
|
||||
import { storage } from "../../../common/decorators/storage";
|
||||
import { fireEvent } from "../../../common/dom/fire_event";
|
||||
import { escapeRegExp } from "../../../common/string/escape_regexp";
|
||||
import { copyToClipboard } from "../../../common/util/copy-clipboard";
|
||||
import "../../../components/entity/ha-entity-picker";
|
||||
@@ -36,6 +29,14 @@ import { showAlertDialog } from "../../../dialogs/generic/show-dialog-box";
|
||||
import { haStyle } from "../../../resources/styles";
|
||||
import type { HomeAssistant } from "../../../types";
|
||||
import { showToast } from "../../../util/toast";
|
||||
import "./developer-tools-state-renderer";
|
||||
|
||||
// Use virtualizer after threshold to avoid performance issues
|
||||
// NOTE: If virtualizer is used when filtered entiity state
|
||||
// array size is 1, the virtualizer will scroll up the page on
|
||||
// render updates until the view matches to near the top of the
|
||||
// virtualized list, an undesirable effect.
|
||||
const VIRTUALIZE_THRESHOLD = 100;
|
||||
|
||||
@customElement("developer-tools-state")
|
||||
class HaPanelDevState extends LitElement {
|
||||
@@ -95,14 +96,28 @@ class HaPanelDevState extends LitElement {
|
||||
this._attributeFilter,
|
||||
this.hass.states
|
||||
);
|
||||
const showAttributes = !this.narrow && this._showAttributes;
|
||||
|
||||
return html`
|
||||
<h1>
|
||||
${this.hass.localize(
|
||||
"ui.panel.developer-tools.tabs.states.current_entities"
|
||||
)}
|
||||
</h1>
|
||||
<div class="heading">
|
||||
<h1>
|
||||
${this.hass.localize(
|
||||
"ui.panel.developer-tools.tabs.states.current_entities"
|
||||
)}
|
||||
</h1>
|
||||
${!this.narrow
|
||||
? html` <ha-formfield
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.developer-tools.tabs.states.attributes"
|
||||
)}
|
||||
>
|
||||
<ha-checkbox
|
||||
.checked=${this._showAttributes}
|
||||
@change=${this._saveAttributeCheckboxState}
|
||||
reducedTouchTarget
|
||||
></ha-checkbox>
|
||||
</ha-formfield>`
|
||||
: nothing}
|
||||
</div>
|
||||
<ha-expansion-panel
|
||||
.header=${this.hass.localize(
|
||||
"ui.panel.developer-tools.tabs.states.set_state"
|
||||
@@ -120,7 +135,7 @@ class HaPanelDevState extends LitElement {
|
||||
)}
|
||||
</p>
|
||||
${this._error
|
||||
? html`<ha-alert alert-type="error">${this._error}}</ha-alert>`
|
||||
? html`<ha-alert alert-type="error">${this._error}</ha-alert>`
|
||||
: nothing}
|
||||
<div class="state-wrapper flex-horizontal">
|
||||
<div class="inputs">
|
||||
@@ -212,136 +227,47 @@ class HaPanelDevState extends LitElement {
|
||||
</div>
|
||||
</div>
|
||||
</ha-expansion-panel>
|
||||
<div class="table-wrapper">
|
||||
<table class="entities">
|
||||
<tr>
|
||||
<th>
|
||||
${this.hass.localize(
|
||||
"ui.panel.developer-tools.tabs.states.entity"
|
||||
)}
|
||||
</th>
|
||||
<th>
|
||||
${this.hass.localize(
|
||||
"ui.panel.developer-tools.tabs.states.state"
|
||||
)}
|
||||
</th>
|
||||
${!this.narrow
|
||||
? html`<th class="attributes">
|
||||
${this.hass.localize(
|
||||
"ui.panel.developer-tools.tabs.states.attributes"
|
||||
)}
|
||||
<ha-checkbox
|
||||
.checked=${this._showAttributes}
|
||||
@change=${this._saveAttributeCheckboxState}
|
||||
reducedTouchTarget
|
||||
></ha-checkbox>
|
||||
</th>`
|
||||
: nothing}
|
||||
</tr>
|
||||
<tr class="filters">
|
||||
<th>
|
||||
<search-input
|
||||
.hass=${this.hass}
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.developer-tools.tabs.states.filter_entities"
|
||||
)}
|
||||
.value=${this._entityFilter}
|
||||
@value-changed=${this._entityFilterChanged}
|
||||
></search-input>
|
||||
</th>
|
||||
<th>
|
||||
<search-input
|
||||
.hass=${this.hass}
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.developer-tools.tabs.states.filter_states"
|
||||
)}
|
||||
type="search"
|
||||
.value=${this._stateFilter}
|
||||
@value-changed=${this._stateFilterChanged}
|
||||
></search-input>
|
||||
</th>
|
||||
${showAttributes
|
||||
? html`<th>
|
||||
<search-input
|
||||
.hass=${this.hass}
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.developer-tools.tabs.states.filter_attributes"
|
||||
)}
|
||||
type="search"
|
||||
.value=${this._attributeFilter}
|
||||
@value-changed=${this._attributeFilterChanged}
|
||||
></search-input>
|
||||
</th>`
|
||||
: nothing}
|
||||
</tr>
|
||||
${entities.length === 0
|
||||
? html`<tr>
|
||||
<td colspan="3">
|
||||
${this.hass.localize(
|
||||
"ui.panel.developer-tools.tabs.states.no_entities"
|
||||
)}
|
||||
</td>
|
||||
</tr>`
|
||||
: nothing}
|
||||
${entities.map(
|
||||
(entity) =>
|
||||
html`<tr>
|
||||
<td>
|
||||
<div class="id-name-container">
|
||||
<div class="id-name-row">
|
||||
<ha-svg-icon
|
||||
@click=${this._copyEntity}
|
||||
.entity=${entity}
|
||||
alt=${this.hass.localize(
|
||||
"ui.panel.developer-tools.tabs.states.copy_id"
|
||||
)}
|
||||
title=${this.hass.localize(
|
||||
"ui.panel.developer-tools.tabs.states.copy_id"
|
||||
)}
|
||||
.path=${mdiClipboardTextMultipleOutline}
|
||||
></ha-svg-icon>
|
||||
<a
|
||||
href="#"
|
||||
.entity=${entity}
|
||||
@click=${this._entitySelected}
|
||||
>${entity.entity_id}</a
|
||||
>
|
||||
</div>
|
||||
<div class="id-name-row">
|
||||
<ha-svg-icon
|
||||
@click=${this._entityMoreInfo}
|
||||
.entity=${entity}
|
||||
alt=${this.hass.localize(
|
||||
"ui.panel.developer-tools.tabs.states.more_info"
|
||||
)}
|
||||
title=${this.hass.localize(
|
||||
"ui.panel.developer-tools.tabs.states.more_info"
|
||||
)}
|
||||
.path=${mdiInformationOutline}
|
||||
></ha-svg-icon>
|
||||
<span class="secondary">
|
||||
${entity.attributes.friendly_name}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td>${entity.state}</td>
|
||||
${showAttributes
|
||||
? html`<td>${this._attributeString(entity)}</td>`
|
||||
: nothing}
|
||||
</tr>`
|
||||
<developer-tools-state-renderer
|
||||
.hass=${this.hass}
|
||||
.narrow=${this.narrow}
|
||||
.entities=${entities}
|
||||
.virtualize=${entities.length > VIRTUALIZE_THRESHOLD}
|
||||
.showAttributes=${this._showAttributes}
|
||||
@states-tool-entity-selected=${this._entitySelected}
|
||||
>
|
||||
<search-input
|
||||
slot="filter-entities"
|
||||
.hass=${this.hass}
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.developer-tools.tabs.states.filter_entities"
|
||||
)}
|
||||
</table>
|
||||
</div>
|
||||
.value=${this._entityFilter}
|
||||
@value-changed=${this._entityFilterChanged}
|
||||
></search-input>
|
||||
<search-input
|
||||
slot="filter-states"
|
||||
.hass=${this.hass}
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.developer-tools.tabs.states.filter_states"
|
||||
)}
|
||||
type="search"
|
||||
.value=${this._stateFilter}
|
||||
@value-changed=${this._stateFilterChanged}
|
||||
></search-input>
|
||||
<search-input
|
||||
slot="filter-attributes"
|
||||
.hass=${this.hass}
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.developer-tools.tabs.states.filter_attributes"
|
||||
)}
|
||||
type="search"
|
||||
.value=${this._attributeFilter}
|
||||
@value-changed=${this._attributeFilterChanged}
|
||||
></search-input>
|
||||
</developer-tools-state-renderer>
|
||||
`;
|
||||
}
|
||||
|
||||
private async _copyEntity(ev) {
|
||||
ev.preventDefault();
|
||||
const entity = (ev.currentTarget! as any).entity;
|
||||
await copyToClipboard(entity.entity_id);
|
||||
}
|
||||
|
||||
private async _copyStateEntity(ev) {
|
||||
ev.preventDefault();
|
||||
await copyToClipboard(this._entityId);
|
||||
@@ -351,7 +277,7 @@ class HaPanelDevState extends LitElement {
|
||||
}
|
||||
|
||||
private _entitySelected(ev) {
|
||||
const entityState: HassEntity = (ev.currentTarget! as any).entity;
|
||||
const entityState: HassEntity = ev.detail.entity;
|
||||
this._entityId = entityState.entity_id;
|
||||
this._entity = entityState;
|
||||
this._state = entityState.state;
|
||||
@@ -359,6 +285,7 @@ class HaPanelDevState extends LitElement {
|
||||
this._updateEditor();
|
||||
this._expanded = true;
|
||||
ev.preventDefault();
|
||||
window.scrollTo({ top: 0 });
|
||||
}
|
||||
|
||||
private _updateEditor() {
|
||||
@@ -420,12 +347,14 @@ class HaPanelDevState extends LitElement {
|
||||
|
||||
private _expandedChanged(ev) {
|
||||
this._expanded = ev.detail.expanded;
|
||||
}
|
||||
|
||||
private _entityMoreInfo(ev) {
|
||||
ev.preventDefault();
|
||||
const entity = (ev.currentTarget! as any).entity;
|
||||
fireEvent(this, "hass-more-info", { entityId: entity.entity_id });
|
||||
if (!ev.detail.expanded) {
|
||||
// lit-virtulizer in state renderer will not show items
|
||||
// if none were in view when panel was expanded
|
||||
// so we fire scroll event to trigger a re-render
|
||||
setTimeout(() => {
|
||||
window.dispatchEvent(new Event("scroll"));
|
||||
}, 100);
|
||||
}
|
||||
}
|
||||
|
||||
private async _handleSetState() {
|
||||
@@ -539,29 +468,6 @@ class HaPanelDevState extends LitElement {
|
||||
});
|
||||
}
|
||||
|
||||
private _formatAttributeValue(value) {
|
||||
if (
|
||||
(Array.isArray(value) && value.some((val) => val instanceof Object)) ||
|
||||
(!Array.isArray(value) && value instanceof Object)
|
||||
) {
|
||||
return `\n${dump(value)}`;
|
||||
}
|
||||
return Array.isArray(value) ? value.join(", ") : value;
|
||||
}
|
||||
|
||||
private _attributeString(entity) {
|
||||
const output = "";
|
||||
|
||||
if (entity && entity.attributes) {
|
||||
return Object.keys(entity.attributes).map(
|
||||
(key) =>
|
||||
`${key}: ${this._formatAttributeValue(entity.attributes[key])}\n`
|
||||
);
|
||||
}
|
||||
|
||||
return output;
|
||||
}
|
||||
|
||||
private _lastChangedString(entity) {
|
||||
return formatDateTimeWithSeconds(
|
||||
new Date(entity.last_changed),
|
||||
@@ -591,6 +497,11 @@ class HaPanelDevState extends LitElement {
|
||||
return [
|
||||
haStyle,
|
||||
css`
|
||||
:host {
|
||||
display: block;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
:host {
|
||||
-ms-user-select: initial;
|
||||
-webkit-user-select: initial;
|
||||
@@ -603,10 +514,26 @@ class HaPanelDevState extends LitElement {
|
||||
max(16px, var(--safe-area-inset-left));
|
||||
}
|
||||
|
||||
:host search-input {
|
||||
display: block;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
ha-textfield {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.heading {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.heading ha-formfield {
|
||||
margin-right: 8px;
|
||||
--mdc-typography-body2-font-size: var(--ha-font-size-m);
|
||||
--mdc-typography-body2-font-weight: var(--ha-font-weight-medium);
|
||||
}
|
||||
|
||||
.entity-id {
|
||||
display: block;
|
||||
font-family: var(--ha-font-family-code);
|
||||
@@ -656,83 +583,6 @@ class HaPanelDevState extends LitElement {
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.table-wrapper {
|
||||
width: 100%;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.entities th {
|
||||
padding: 0 8px;
|
||||
text-align: var(--float-start);
|
||||
direction: var(--direction);
|
||||
}
|
||||
|
||||
.filters th {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.filters search-input {
|
||||
display: block;
|
||||
--mdc-text-field-fill-color: transparent;
|
||||
}
|
||||
|
||||
th.attributes {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
th.attributes ha-checkbox {
|
||||
position: absolute;
|
||||
bottom: -8px;
|
||||
}
|
||||
|
||||
.entities tr {
|
||||
vertical-align: top;
|
||||
direction: ltr;
|
||||
}
|
||||
|
||||
.entities tr:nth-child(odd) {
|
||||
background-color: var(--table-row-background-color, #fff);
|
||||
}
|
||||
|
||||
.entities tr:nth-child(even) {
|
||||
background-color: var(--table-row-alternative-background-color, #eee);
|
||||
}
|
||||
.entities td {
|
||||
padding: 4px;
|
||||
min-width: 200px;
|
||||
word-break: break-word;
|
||||
}
|
||||
.entities ha-svg-icon {
|
||||
--mdc-icon-size: 20px;
|
||||
padding: 4px;
|
||||
cursor: pointer;
|
||||
flex-shrink: 0;
|
||||
margin-right: 8px;
|
||||
margin-inline-end: 8px;
|
||||
margin-inline-start: initial;
|
||||
}
|
||||
.entities td:nth-child(1) {
|
||||
min-width: 300px;
|
||||
width: 30%;
|
||||
}
|
||||
.entities td:nth-child(3) {
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.entities a {
|
||||
color: var(--primary-color);
|
||||
}
|
||||
|
||||
.entities .id-name-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.entities .id-name-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
:host([narrow]) .state-wrapper {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
import { css, LitElement, nothing, html } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { computeDomain } from "../../../common/entity/compute_domain";
|
||||
import type { HomeAssistant } from "../../../types";
|
||||
import type { LovelaceCardFeature } from "../types";
|
||||
import type {
|
||||
LovelaceCardFeatureContext,
|
||||
BarGaugeCardFeatureConfig,
|
||||
} from "./types";
|
||||
|
||||
export const supportsBarGaugeCardFeature = (
|
||||
hass: HomeAssistant,
|
||||
context: LovelaceCardFeatureContext
|
||||
) => {
|
||||
const stateObj = context.entity_id
|
||||
? hass.states[context.entity_id]
|
||||
: undefined;
|
||||
if (!stateObj) return false;
|
||||
const domain = computeDomain(stateObj.entity_id);
|
||||
return domain === "sensor" && stateObj.attributes.unit_of_measurement === "%";
|
||||
};
|
||||
|
||||
@customElement("hui-bar-gauge-card-feature")
|
||||
class HuiBarGaugeCardFeature extends LitElement implements LovelaceCardFeature {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public context!: LovelaceCardFeatureContext;
|
||||
|
||||
@state() private _config?: BarGaugeCardFeatureConfig;
|
||||
|
||||
static getStubConfig(): BarGaugeCardFeatureConfig {
|
||||
return {
|
||||
type: "bar-gauge",
|
||||
};
|
||||
}
|
||||
|
||||
public setConfig(config: BarGaugeCardFeatureConfig): void {
|
||||
if (!config) {
|
||||
throw new Error("Invalid configuration");
|
||||
}
|
||||
this._config = config;
|
||||
}
|
||||
|
||||
render() {
|
||||
if (
|
||||
!this._config ||
|
||||
!this.hass ||
|
||||
!this.context ||
|
||||
!this.context.entity_id ||
|
||||
!this.hass.states[this.context.entity_id] ||
|
||||
!supportsBarGaugeCardFeature(this.hass, this.context)
|
||||
) {
|
||||
return nothing;
|
||||
}
|
||||
const stateObj = this.hass.states[this.context.entity_id];
|
||||
const value = stateObj.state;
|
||||
return html`<div style="width: ${value}%"></div>
|
||||
<div class="bar-gauge-background"></div>`;
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
:host {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
height: var(--feature-height);
|
||||
border-radius: var(--feature-border-radius);
|
||||
overflow: hidden;
|
||||
}
|
||||
:host > div {
|
||||
height: 100%;
|
||||
background-color: var(--feature-color);
|
||||
transition: width 180ms ease-in-out;
|
||||
}
|
||||
.bar-gauge-background {
|
||||
flex: 1;
|
||||
opacity: 0.2;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"hui-bar-gauge-card-feature": HuiBarGaugeCardFeature;
|
||||
}
|
||||
}
|
||||
@@ -21,7 +21,7 @@ export const supportsButtonCardFeature = (
|
||||
: undefined;
|
||||
if (!stateObj) return false;
|
||||
const domain = computeDomain(stateObj.entity_id);
|
||||
return ["button", "input_button", "script"].includes(domain);
|
||||
return ["button", "input_button", "scene", "script"].includes(domain);
|
||||
};
|
||||
|
||||
@customElement("hui-button-card-feature")
|
||||
@@ -78,7 +78,7 @@ class HuiButtonCardFeature extends LitElement implements LovelaceCardFeature {
|
||||
return html`
|
||||
<ha-control-button-group>
|
||||
<ha-control-button
|
||||
.disabled=${["unavailable", "unknown"].includes(this._stateObj.state)}
|
||||
.disabled=${this._stateObj.state === "unavailable"}
|
||||
class="press-button"
|
||||
@click=${this._pressButton}
|
||||
>
|
||||
|
||||
@@ -0,0 +1,143 @@
|
||||
import { html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { computeDomain } from "../../../common/entity/compute_domain";
|
||||
import "../../../components/ha-control-slider";
|
||||
import type { HomeAssistant } from "../../../types";
|
||||
import type { LovelaceCardFeature } from "../types";
|
||||
import { cardFeatureStyles } from "./common/card-feature-styles";
|
||||
import type {
|
||||
LovelaceCardFeatureContext,
|
||||
DateSetCardFeatureConfig,
|
||||
} from "./types";
|
||||
import { fireEvent } from "../../../common/dom/fire_event";
|
||||
import type { DatePickerDialogParams } from "../../../components/ha-date-input";
|
||||
import { firstWeekdayIndex } from "../../../common/datetime/first_weekday";
|
||||
import "../../../components/ha-control-button";
|
||||
import "../../../components/ha-control-button-group";
|
||||
|
||||
const loadDatePickerDialog = () =>
|
||||
import("../../../components/ha-dialog-date-picker");
|
||||
|
||||
export const supportsDateSetCardFeature = (
|
||||
hass: HomeAssistant,
|
||||
context: LovelaceCardFeatureContext
|
||||
) => {
|
||||
const stateObj = context.entity_id
|
||||
? hass.states[context.entity_id]
|
||||
: undefined;
|
||||
if (!stateObj) return false;
|
||||
const domain = computeDomain(stateObj.entity_id);
|
||||
return (
|
||||
(domain === "input_datetime" && stateObj.attributes.has_date) ||
|
||||
["datetime", "date"].includes(domain)
|
||||
);
|
||||
};
|
||||
|
||||
@customElement("hui-date-set-card-feature")
|
||||
class HuiDateSetCardFeature extends LitElement implements LovelaceCardFeature {
|
||||
@property({ attribute: false }) public hass?: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public context?: LovelaceCardFeatureContext;
|
||||
|
||||
@property({ attribute: false }) public color?: string;
|
||||
|
||||
@state() private _config?: DateSetCardFeatureConfig;
|
||||
|
||||
private get _stateObj() {
|
||||
if (!this.hass || !this.context || !this.context.entity_id) {
|
||||
return undefined;
|
||||
}
|
||||
return this.hass.states[this.context.entity_id!] ?? undefined;
|
||||
}
|
||||
|
||||
private _pressButton() {
|
||||
if (!this.hass || !this._stateObj) return;
|
||||
|
||||
const dialogParams: DatePickerDialogParams = {
|
||||
min: "1970-01-01",
|
||||
value: this._stateObj.state,
|
||||
onChange: (value) => this._dateChanged(value),
|
||||
locale: this.hass.locale.language,
|
||||
firstWeekday: firstWeekdayIndex(this.hass.locale),
|
||||
};
|
||||
|
||||
fireEvent(this, "show-dialog", {
|
||||
dialogTag: "ha-dialog-date-picker",
|
||||
dialogImport: loadDatePickerDialog,
|
||||
dialogParams,
|
||||
});
|
||||
}
|
||||
|
||||
private _dateChanged(value: string | undefined) {
|
||||
if (!this.hass || !this._stateObj || !value) return;
|
||||
|
||||
const domain = computeDomain(this._stateObj.entity_id);
|
||||
const service = domain === "input_datetime" ? "set_datetime" : "set_value";
|
||||
|
||||
// datetime requires a full datetime string
|
||||
if (domain === "datetime") {
|
||||
const dateObj = new Date(this._stateObj.state);
|
||||
const selectedDate = new Date(`${value}T00:00:00`);
|
||||
dateObj.setFullYear(
|
||||
selectedDate.getFullYear(),
|
||||
selectedDate.getMonth(),
|
||||
selectedDate.getDate()
|
||||
);
|
||||
|
||||
this.hass.callService(domain, service, {
|
||||
entity_id: this._stateObj.entity_id,
|
||||
datetime: dateObj.toISOString(),
|
||||
});
|
||||
} else {
|
||||
this.hass.callService(domain, service, {
|
||||
entity_id: this._stateObj.entity_id,
|
||||
date: value,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
static getStubConfig(): DateSetCardFeatureConfig {
|
||||
return {
|
||||
type: "date-set",
|
||||
};
|
||||
}
|
||||
|
||||
public setConfig(config: DateSetCardFeatureConfig): void {
|
||||
if (!config) {
|
||||
throw new Error("Invalid configuration");
|
||||
}
|
||||
this._config = config;
|
||||
}
|
||||
|
||||
protected render() {
|
||||
if (
|
||||
!this._config ||
|
||||
!this.hass ||
|
||||
!this.context ||
|
||||
!this._stateObj ||
|
||||
!supportsDateSetCardFeature(this.hass, this.context)
|
||||
) {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
return html`
|
||||
<ha-control-button-group>
|
||||
<ha-control-button
|
||||
.disabled=${["unavailable", "unknown"].includes(this._stateObj.state)}
|
||||
class="press-button"
|
||||
@click=${this._pressButton}
|
||||
>
|
||||
${this.hass.localize("ui.card.date.set_date")}
|
||||
</ha-control-button>
|
||||
</ha-control-button-group>
|
||||
`;
|
||||
}
|
||||
|
||||
static styles = cardFeatureStyles;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"hui-date-set-card-feature": HuiDateSetCardFeature;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,150 @@
|
||||
import { mdiArrowOscillating, mdiArrowOscillatingOff } from "@mdi/js";
|
||||
import type { PropertyValues, TemplateResult } from "lit";
|
||||
import { LitElement, html } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { styleMap } from "lit/directives/style-map";
|
||||
import { computeDomain } from "../../../common/entity/compute_domain";
|
||||
import { stateColorCss } from "../../../common/entity/state_color";
|
||||
import "../../../components/ha-control-select";
|
||||
import type { ControlSelectOption } from "../../../components/ha-control-select";
|
||||
import { UNAVAILABLE } from "../../../data/entity";
|
||||
import type { FanEntity } from "../../../data/fan";
|
||||
import { FanEntityFeature } from "../../../data/fan";
|
||||
import type { HomeAssistant } from "../../../types";
|
||||
import type { LovelaceCardFeature } from "../types";
|
||||
import { cardFeatureStyles } from "./common/card-feature-styles";
|
||||
import type {
|
||||
FanOscillateCardFeatureConfig,
|
||||
LovelaceCardFeatureContext,
|
||||
} from "./types";
|
||||
import { supportsFeature } from "../../../common/entity/supports-feature";
|
||||
|
||||
export const supportsFanOscilatteCardFeature = (
|
||||
hass: HomeAssistant,
|
||||
context: LovelaceCardFeatureContext
|
||||
) => {
|
||||
const stateObj = context.entity_id
|
||||
? hass.states[context.entity_id]
|
||||
: undefined;
|
||||
if (!stateObj) return false;
|
||||
const domain = computeDomain(stateObj.entity_id);
|
||||
return (
|
||||
domain === "fan" && supportsFeature(stateObj, FanEntityFeature.OSCILLATE)
|
||||
);
|
||||
};
|
||||
|
||||
@customElement("hui-fan-oscillate-card-feature")
|
||||
class HuiFanOscillateCardFeature
|
||||
extends LitElement
|
||||
implements LovelaceCardFeature
|
||||
{
|
||||
@property({ attribute: false }) public hass?: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public context?: LovelaceCardFeatureContext;
|
||||
|
||||
@state() private _config?: FanOscillateCardFeatureConfig;
|
||||
|
||||
@state() _oscillate?: boolean;
|
||||
|
||||
private get _stateObj() {
|
||||
if (!this.hass || !this.context || !this.context.entity_id) {
|
||||
return undefined;
|
||||
}
|
||||
return this.hass.states[this.context.entity_id!] as FanEntity | undefined;
|
||||
}
|
||||
|
||||
static getStubConfig(): FanOscillateCardFeatureConfig {
|
||||
return {
|
||||
type: "fan-oscillate",
|
||||
};
|
||||
}
|
||||
|
||||
public setConfig(config: FanOscillateCardFeatureConfig): void {
|
||||
if (!config) {
|
||||
throw new Error("Invalid configuration");
|
||||
}
|
||||
this._config = config;
|
||||
}
|
||||
|
||||
protected willUpdate(changedProp: PropertyValues): void {
|
||||
if (
|
||||
(changedProp.has("hass") || changedProp.has("context")) &&
|
||||
this._stateObj
|
||||
) {
|
||||
const oldHass = changedProp.get("hass") as HomeAssistant | undefined;
|
||||
const oldStateObj = oldHass?.states[this.context!.entity_id!];
|
||||
if (oldStateObj !== this._stateObj) {
|
||||
this._oscillate = this._stateObj.attributes.oscillating;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async _valueChanged(ev: CustomEvent) {
|
||||
const shouldOscillate = (ev.detail as any).value === "yes";
|
||||
|
||||
if (shouldOscillate === this._stateObj!.attributes.oscillating) return;
|
||||
|
||||
const wasOscillating = this._stateObj!.attributes.oscillating;
|
||||
this._oscillate = shouldOscillate;
|
||||
|
||||
try {
|
||||
await this._updateOscillate(shouldOscillate);
|
||||
} catch (_err) {
|
||||
this._oscillate = wasOscillating;
|
||||
}
|
||||
}
|
||||
|
||||
private async _updateOscillate(oscillate: boolean) {
|
||||
await this.hass!.callService("fan", "oscillate", {
|
||||
entity_id: this._stateObj!.entity_id,
|
||||
oscillating: oscillate,
|
||||
});
|
||||
}
|
||||
|
||||
protected render(): TemplateResult | null {
|
||||
if (
|
||||
!this._config ||
|
||||
!this.hass ||
|
||||
!this.context ||
|
||||
!this._stateObj ||
|
||||
!supportsFanOscilatteCardFeature(this.hass, this.context)
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const color = stateColorCss(this._stateObj);
|
||||
|
||||
const yesNo = ["no", "yes"] as const;
|
||||
const options = yesNo.map<ControlSelectOption>((oscillating) => ({
|
||||
value: oscillating,
|
||||
label: this.hass!.localize(`ui.common.${oscillating}`),
|
||||
path:
|
||||
oscillating === "yes" ? mdiArrowOscillating : mdiArrowOscillatingOff,
|
||||
}));
|
||||
|
||||
return html`
|
||||
<ha-control-select
|
||||
.options=${options}
|
||||
.value=${this._oscillate ? "yes" : "no"}
|
||||
@value-changed=${this._valueChanged}
|
||||
hide-option-label
|
||||
.label=${this.hass.localize("ui.card.fan.oscillate")}
|
||||
style=${styleMap({
|
||||
"--control-select-color": color,
|
||||
})}
|
||||
.disabled=${this._stateObj!.state === UNAVAILABLE}
|
||||
>
|
||||
</ha-control-select>
|
||||
`;
|
||||
}
|
||||
|
||||
static get styles() {
|
||||
return cardFeatureStyles;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"hui-fan-oscillate-card-feature": HuiFanOscillateCardFeature;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,301 @@
|
||||
import { css, html, LitElement, nothing, svg } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { computeDomain } from "../../../common/entity/compute_domain";
|
||||
import {
|
||||
computeHistory,
|
||||
subscribeHistoryStatesTimeWindow,
|
||||
} from "../../../data/history";
|
||||
import type {
|
||||
HistoryResult,
|
||||
LineChartUnit,
|
||||
TimelineEntity,
|
||||
} from "../../../data/history";
|
||||
import type { HomeAssistant } from "../../../types";
|
||||
import type { LovelaceCardFeature } from "../types";
|
||||
import type {
|
||||
LovelaceCardFeatureContext,
|
||||
HistoryChartCardFeatureConfig,
|
||||
} from "./types";
|
||||
import { getSensorNumericDeviceClasses } from "../../../data/sensor";
|
||||
import { isComponentLoaded } from "../../../common/config/is_component_loaded";
|
||||
import { SubscribeMixin } from "../../../mixins/subscribe-mixin";
|
||||
import { getGraphColorByIndex } from "../../../common/color/colors";
|
||||
import { computeTimelineColor } from "../../../components/chart/timeline-color";
|
||||
import { downSampleLineData } from "../../../components/chart/down-sample";
|
||||
import { fireEvent } from "../../../common/dom/fire_event";
|
||||
|
||||
export const supportsHistoryChartCardFeature = (
|
||||
_hass: HomeAssistant,
|
||||
context: LovelaceCardFeatureContext
|
||||
) =>
|
||||
!!context.entity_id &&
|
||||
["sensor", "binary_sensor"].includes(computeDomain(context.entity_id));
|
||||
|
||||
@customElement("hui-history-chart-card-feature")
|
||||
class HuiHistoryChartCardFeature
|
||||
extends SubscribeMixin(LitElement)
|
||||
implements LovelaceCardFeature
|
||||
{
|
||||
@property({ attribute: false, hasChanged: () => false })
|
||||
public hass?: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public context?: LovelaceCardFeatureContext;
|
||||
|
||||
@state() private _config?: HistoryChartCardFeatureConfig;
|
||||
|
||||
@state() private _stateHistory?: HistoryResult;
|
||||
|
||||
private _interval?: number;
|
||||
|
||||
static getStubConfig(): HistoryChartCardFeatureConfig {
|
||||
return {
|
||||
type: "history-chart",
|
||||
hours_to_show: 24,
|
||||
};
|
||||
}
|
||||
|
||||
public setConfig(config: HistoryChartCardFeatureConfig): void {
|
||||
if (!config) {
|
||||
throw new Error("Invalid configuration");
|
||||
}
|
||||
this._config = config;
|
||||
}
|
||||
|
||||
public connectedCallback() {
|
||||
super.connectedCallback();
|
||||
// redraw the graph every minute to update the time axis
|
||||
clearInterval(this._interval);
|
||||
this._interval = window.setInterval(() => this.requestUpdate(), 1000 * 60);
|
||||
}
|
||||
|
||||
public disconnectedCallback() {
|
||||
super.disconnectedCallback();
|
||||
clearInterval(this._interval);
|
||||
}
|
||||
|
||||
protected hassSubscribe() {
|
||||
return [this._subscribeHistory()];
|
||||
}
|
||||
|
||||
protected render() {
|
||||
if (
|
||||
!this._config ||
|
||||
!this.hass ||
|
||||
!this.context ||
|
||||
!this._stateHistory ||
|
||||
!supportsHistoryChartCardFeature(this.hass, this.context)
|
||||
) {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
const line = this._stateHistory.line[0];
|
||||
const timeline = this._stateHistory.timeline[0];
|
||||
const width = this.clientWidth;
|
||||
const height = this.clientHeight;
|
||||
if (line) {
|
||||
const points = this._generateLinePoints(line);
|
||||
const { paths, filledPaths } = this._getLinePaths(points);
|
||||
const color = getGraphColorByIndex(0, this.style);
|
||||
|
||||
return html`
|
||||
<div class="line" @click=${this._handleClick}>
|
||||
${svg`<svg width="${width}" height="${height}" viewBox="0 0 ${width} ${height}">
|
||||
${paths.map(
|
||||
(path) =>
|
||||
svg`<path d="${path}" stroke="${color}" stroke-width="1" stroke-linecap="round" fill="none" />`
|
||||
)}
|
||||
${filledPaths.map(
|
||||
(path) =>
|
||||
svg`<path d="${path}" stroke="none" stroke-linecap="round" fill="${color}" fill-opacity="0.2" />`
|
||||
)}
|
||||
</svg>`}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
if (timeline) {
|
||||
const ranges = this._generateTimelineRanges(timeline);
|
||||
return html`
|
||||
<div class="timeline" @click=${this._handleClick}>
|
||||
${svg`<svg width="${width}" height="${height}" viewBox="0 0 ${width} ${height}">
|
||||
<g>
|
||||
${ranges.map((r) => svg`<rect x="${r.startX}" y="0" width="${r.endX - r.startX}" height="${height}" fill="${r.color}" />`)}
|
||||
</g>
|
||||
</svg>`}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
return nothing;
|
||||
}
|
||||
|
||||
private _handleClick() {
|
||||
// open more info dialog to show more detailed history
|
||||
fireEvent(this, "hass-more-info", { entityId: this.context!.entity_id! });
|
||||
}
|
||||
|
||||
private async _subscribeHistory(): Promise<() => Promise<void>> {
|
||||
if (
|
||||
!isComponentLoaded(this.hass!, "history") ||
|
||||
!this.context?.entity_id ||
|
||||
!this._config
|
||||
) {
|
||||
return () => Promise.resolve();
|
||||
}
|
||||
|
||||
const { numeric_device_classes: sensorNumericDeviceClasses } =
|
||||
await getSensorNumericDeviceClasses(this.hass!);
|
||||
|
||||
return subscribeHistoryStatesTimeWindow(
|
||||
this.hass!,
|
||||
(historyStates) => {
|
||||
this._stateHistory = computeHistory(
|
||||
this.hass!,
|
||||
historyStates,
|
||||
[this.context!.entity_id!],
|
||||
this.hass!.localize,
|
||||
sensorNumericDeviceClasses,
|
||||
false
|
||||
);
|
||||
},
|
||||
this._config!.hours_to_show ?? 24,
|
||||
[this.context!.entity_id!]
|
||||
);
|
||||
}
|
||||
|
||||
private _generateLinePoints(line: LineChartUnit): { x: number; y: number }[] {
|
||||
const width = this.clientWidth;
|
||||
const height = this.clientHeight;
|
||||
let minY = Number(line.data[0].states[0].state);
|
||||
let maxY = Number(line.data[0].states[0].state);
|
||||
const minX = line.data[0].states[0].last_changed;
|
||||
const maxX = Date.now();
|
||||
line.data[0].states.forEach((stateData) => {
|
||||
const stateValue = Number(stateData.state);
|
||||
if (stateValue < minY) {
|
||||
minY = stateValue;
|
||||
}
|
||||
if (stateValue > maxY) {
|
||||
maxY = stateValue;
|
||||
}
|
||||
});
|
||||
const rangeY = maxY - minY || minY * 0.1;
|
||||
const sampledData = downSampleLineData(
|
||||
line.data[0].states.map((stateData) => [
|
||||
stateData.last_changed,
|
||||
Number(stateData.state),
|
||||
]),
|
||||
width,
|
||||
minX,
|
||||
maxX
|
||||
);
|
||||
// add margin to the min and max
|
||||
minY -= rangeY * 0.1;
|
||||
maxY += rangeY * 0.1;
|
||||
const yDenom = maxY - minY || 1;
|
||||
const xDenom = maxX - minX || 1;
|
||||
const points = sampledData!.map((point) => {
|
||||
const x = ((point![0] - minX) / xDenom) * width;
|
||||
const y = height - ((Number(point![1]) - minY) / yDenom) * height;
|
||||
return { x, y };
|
||||
});
|
||||
points.push({ x: width, y: points[points.length - 1].y });
|
||||
return points;
|
||||
}
|
||||
|
||||
private _generateTimelineRanges(timeline: TimelineEntity) {
|
||||
if (timeline.data.length === 0) {
|
||||
return [];
|
||||
}
|
||||
const computedStyles = getComputedStyle(this);
|
||||
const width = this.clientWidth;
|
||||
const minX = timeline.data[0].last_changed;
|
||||
const maxX = Date.now();
|
||||
let prevEndX = 0;
|
||||
let prevStateColor = "";
|
||||
const ranges = timeline.data.map((t) => {
|
||||
const x = ((t.last_changed - minX) / (maxX - minX)) * width;
|
||||
const range = {
|
||||
startX: prevEndX,
|
||||
endX: x,
|
||||
color: prevStateColor,
|
||||
};
|
||||
prevStateColor = computeTimelineColor(
|
||||
t.state,
|
||||
computedStyles,
|
||||
this.hass!.states[timeline.entity_id]
|
||||
);
|
||||
prevEndX = x;
|
||||
return range;
|
||||
});
|
||||
ranges.push({
|
||||
startX: prevEndX,
|
||||
endX: width,
|
||||
color: prevStateColor,
|
||||
});
|
||||
return ranges;
|
||||
}
|
||||
|
||||
private _getLinePaths(points: { x: number; y: number }[]) {
|
||||
const paths: string[] = [];
|
||||
const filledPaths: string[] = [];
|
||||
if (!points.length) {
|
||||
return { paths, filledPaths };
|
||||
}
|
||||
// path can interupted by missing data, so we need to split the path into segments
|
||||
const pathSegments: { x: number; y: number }[][] = [[]];
|
||||
points.forEach((point) => {
|
||||
if (!isNaN(point.y)) {
|
||||
pathSegments[pathSegments.length - 1].push(point);
|
||||
} else if (pathSegments[pathSegments.length - 1].length > 0) {
|
||||
pathSegments.push([]);
|
||||
}
|
||||
});
|
||||
|
||||
pathSegments.forEach((pathPoints) => {
|
||||
// create a smoothed path
|
||||
let next: { x: number; y: number };
|
||||
let path = "";
|
||||
let last = pathPoints[0];
|
||||
|
||||
path += `M ${last.x},${last.y}`;
|
||||
|
||||
pathPoints.forEach((coord) => {
|
||||
next = coord;
|
||||
path += ` ${(next.x + last.x) / 2},${(next.y + last.y) / 2}`;
|
||||
path += ` Q${next.x},${next.y}`;
|
||||
last = next;
|
||||
});
|
||||
|
||||
path += ` ${next!.x},${next!.y}`;
|
||||
paths.push(path);
|
||||
filledPaths.push(
|
||||
path +
|
||||
` L ${next!.x},${this.clientHeight} L ${pathPoints[0].x},${this.clientHeight} Z`
|
||||
);
|
||||
});
|
||||
|
||||
return { paths, filledPaths };
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
:host {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: var(--feature-height);
|
||||
}
|
||||
:host > div {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
cursor: pointer;
|
||||
}
|
||||
.timeline {
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"hui-history-chart-card-feature": HuiHistoryChartCardFeature;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,323 @@
|
||||
import type { PropertyValues } from "lit";
|
||||
import { html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import {
|
||||
mdiPause,
|
||||
mdiPlay,
|
||||
mdiPlayPause,
|
||||
mdiPower,
|
||||
mdiSkipNext,
|
||||
mdiSkipPrevious,
|
||||
mdiStop,
|
||||
} from "@mdi/js";
|
||||
import { computeDomain } from "../../../common/entity/compute_domain";
|
||||
import type { HomeAssistant } from "../../../types";
|
||||
import type { LovelaceCardFeature } from "../types";
|
||||
import { cardFeatureStyles } from "./common/card-feature-styles";
|
||||
import type {
|
||||
LovelaceCardFeatureContext,
|
||||
MediaPlayerPlaybackCardFeatureConfig,
|
||||
} from "./types";
|
||||
import type {
|
||||
ControlButton,
|
||||
MediaPlayerEntity,
|
||||
} from "../../../data/media-player";
|
||||
import { MediaPlayerEntityFeature } from "../../../data/media-player";
|
||||
import { supportsFeature } from "../../../common/entity/supports-feature";
|
||||
import { stateActive } from "../../../common/entity/state_active";
|
||||
import { isUnavailableState } from "../../../data/entity";
|
||||
import { hasConfigChanged } from "../common/has-changed";
|
||||
import "../../../components/ha-control-button-group";
|
||||
import "../../../components/ha-control-button";
|
||||
import "../../../components/ha-icon-button";
|
||||
import "../../../components/ha-icon";
|
||||
|
||||
export const supportsMediaPlayerPlaybackCardFeature = (
|
||||
hass: HomeAssistant,
|
||||
context: LovelaceCardFeatureContext
|
||||
) => {
|
||||
const stateObj = context.entity_id
|
||||
? hass.states[context.entity_id]
|
||||
: undefined;
|
||||
if (!stateObj) return false;
|
||||
const domain = computeDomain(stateObj.entity_id);
|
||||
return domain === "media_player";
|
||||
};
|
||||
|
||||
@customElement("hui-media-player-playback-card-feature")
|
||||
class HuiMediaPlayerPlaybackCardFeature
|
||||
extends LitElement
|
||||
implements LovelaceCardFeature
|
||||
{
|
||||
@property({ attribute: false }) public hass?: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public context?: LovelaceCardFeatureContext;
|
||||
|
||||
@property({ attribute: false }) public color?: string;
|
||||
|
||||
@state() private _config?: MediaPlayerPlaybackCardFeatureConfig;
|
||||
|
||||
@state() private _narrow?: boolean = false;
|
||||
|
||||
private get _stateObj() {
|
||||
if (!this.hass || !this.context || !this.context.entity_id) {
|
||||
return undefined;
|
||||
}
|
||||
return this.hass.states[this.context.entity_id] as
|
||||
| MediaPlayerEntity
|
||||
| undefined;
|
||||
}
|
||||
|
||||
static getStubConfig(): MediaPlayerPlaybackCardFeatureConfig {
|
||||
return {
|
||||
type: "media-player-playback",
|
||||
};
|
||||
}
|
||||
|
||||
public setConfig(config: MediaPlayerPlaybackCardFeatureConfig): void {
|
||||
if (!config) {
|
||||
throw new Error("Invalid configuration");
|
||||
}
|
||||
this._config = config;
|
||||
}
|
||||
|
||||
public willUpdate(): void {
|
||||
if (!this.hasUpdated) {
|
||||
this._measureCard();
|
||||
}
|
||||
}
|
||||
|
||||
protected shouldUpdate(changedProps: PropertyValues): boolean {
|
||||
const oldHass = changedProps.get("hass") as HomeAssistant | undefined;
|
||||
const entityId = this.context?.entity_id;
|
||||
return (
|
||||
hasConfigChanged(this, changedProps) ||
|
||||
(changedProps.has("hass") &&
|
||||
(!oldHass ||
|
||||
!entityId ||
|
||||
oldHass.states[entityId] !== this.hass!.states[entityId]))
|
||||
);
|
||||
}
|
||||
|
||||
protected render() {
|
||||
if (
|
||||
!this._config ||
|
||||
!this.hass ||
|
||||
!this.context ||
|
||||
!supportsMediaPlayerPlaybackCardFeature(this.hass, this.context) ||
|
||||
!this._stateObj
|
||||
) {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
const buttons = this._computeButtons(this._stateObj);
|
||||
|
||||
return html`
|
||||
<ha-control-button-group>
|
||||
${supportsFeature(this._stateObj, MediaPlayerEntityFeature.TURN_OFF) &&
|
||||
stateActive(this._stateObj)
|
||||
? html`
|
||||
<ha-control-button
|
||||
.label=${this.hass.localize("ui.card.media_player.turn_off")}
|
||||
@click=${this._togglePower}
|
||||
>
|
||||
<ha-svg-icon .path=${mdiPower}></ha-svg-icon>
|
||||
</ha-control-button>
|
||||
`
|
||||
: ""}
|
||||
${supportsFeature(this._stateObj, MediaPlayerEntityFeature.TURN_ON) &&
|
||||
!stateActive(this._stateObj) &&
|
||||
!isUnavailableState(this._stateObj.state)
|
||||
? html`
|
||||
<ha-control-button
|
||||
.label=${this.hass.localize("ui.card.media_player.turn_on")}
|
||||
@click=${this._togglePower}
|
||||
>
|
||||
<ha-svg-icon .path=${mdiPower}></ha-svg-icon>
|
||||
</ha-control-button>
|
||||
`
|
||||
: buttons.map(
|
||||
(button) => html`
|
||||
<ha-control-button
|
||||
key=${button.action}
|
||||
.label=${this.hass?.localize(
|
||||
`ui.card.media_player.${button.action}`
|
||||
)}
|
||||
@click=${this._action}
|
||||
>
|
||||
<ha-svg-icon .path=${button.icon}></ha-svg-icon>
|
||||
</ha-control-button>
|
||||
`
|
||||
)}
|
||||
</ha-control-button-group>
|
||||
`;
|
||||
}
|
||||
|
||||
private _measureCard() {
|
||||
if (!this.isConnected) {
|
||||
return;
|
||||
}
|
||||
const host = (this.getRootNode() as ShadowRoot).host as
|
||||
| HTMLElement
|
||||
| undefined;
|
||||
const width = host?.clientWidth ?? this.clientWidth ?? 0;
|
||||
this._narrow = width < 300;
|
||||
}
|
||||
|
||||
private _computeControlButton(stateObj: MediaPlayerEntity): ControlButton {
|
||||
return stateObj.state === "on"
|
||||
? { icon: mdiPlayPause, action: "media_play_pause" }
|
||||
: stateObj.state !== "playing"
|
||||
? { icon: mdiPlay, action: "media_play" }
|
||||
: supportsFeature(stateObj, MediaPlayerEntityFeature.PAUSE)
|
||||
? { icon: mdiPause, action: "media_pause" }
|
||||
: { icon: mdiStop, action: "media_stop" };
|
||||
}
|
||||
|
||||
private _computeButtons(stateObj: MediaPlayerEntity): ControlButton[] {
|
||||
const controlButton = this._computeControlButton(stateObj);
|
||||
const assumedState = stateObj.attributes.assumed_state === true;
|
||||
|
||||
const controls: ControlButton[] = [];
|
||||
|
||||
if (
|
||||
!this._narrow &&
|
||||
(stateObj.state === "playing" || assumedState) &&
|
||||
supportsFeature(stateObj, MediaPlayerEntityFeature.PREVIOUS_TRACK)
|
||||
) {
|
||||
controls.push({ icon: mdiSkipPrevious, action: "media_previous_track" });
|
||||
}
|
||||
|
||||
if (
|
||||
!assumedState &&
|
||||
((stateObj.state === "playing" &&
|
||||
(supportsFeature(stateObj, MediaPlayerEntityFeature.PAUSE) ||
|
||||
supportsFeature(stateObj, MediaPlayerEntityFeature.STOP))) ||
|
||||
((stateObj.state === "paused" || stateObj.state === "idle") &&
|
||||
supportsFeature(stateObj, MediaPlayerEntityFeature.PLAY)) ||
|
||||
(stateObj.state === "on" &&
|
||||
(supportsFeature(stateObj, MediaPlayerEntityFeature.PLAY) ||
|
||||
supportsFeature(stateObj, MediaPlayerEntityFeature.PAUSE))))
|
||||
) {
|
||||
controls.push({ icon: controlButton.icon, action: controlButton.action });
|
||||
}
|
||||
|
||||
if (assumedState) {
|
||||
if (supportsFeature(stateObj, MediaPlayerEntityFeature.PLAY)) {
|
||||
controls.push({ icon: mdiPlay, action: "media_play" });
|
||||
}
|
||||
|
||||
if (supportsFeature(stateObj, MediaPlayerEntityFeature.PAUSE)) {
|
||||
controls.push({ icon: mdiPause, action: "media_pause" });
|
||||
}
|
||||
|
||||
if (supportsFeature(stateObj, MediaPlayerEntityFeature.STOP)) {
|
||||
controls.push({ icon: mdiStop, action: "media_stop" });
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
(stateObj.state === "playing" || assumedState) &&
|
||||
supportsFeature(stateObj, MediaPlayerEntityFeature.NEXT_TRACK)
|
||||
) {
|
||||
controls.push({ icon: mdiSkipNext, action: "media_next_track" });
|
||||
}
|
||||
|
||||
return controls;
|
||||
}
|
||||
|
||||
private _togglePower(): void {
|
||||
if (!this._stateObj) return;
|
||||
this.hass!.callService(
|
||||
"media_player",
|
||||
stateActive(this._stateObj) ? "turn_off" : "turn_on",
|
||||
{
|
||||
entity_id: this._stateObj.entity_id,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
private _action(e: Event): void {
|
||||
const action = (e.currentTarget as HTMLElement).getAttribute("key");
|
||||
if (!action) return;
|
||||
|
||||
switch (action) {
|
||||
case "media_play_pause":
|
||||
this._playPauseStop();
|
||||
break;
|
||||
case "media_play":
|
||||
this._play();
|
||||
break;
|
||||
case "media_pause":
|
||||
this._pause();
|
||||
break;
|
||||
case "media_stop":
|
||||
this._stop();
|
||||
break;
|
||||
case "media_previous_track":
|
||||
this._previousTrack();
|
||||
break;
|
||||
case "media_next_track":
|
||||
this._nextTrack();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private _playPauseStop(): void {
|
||||
if (!this._stateObj) return;
|
||||
|
||||
const service =
|
||||
this._stateObj.state !== "playing"
|
||||
? "media_play"
|
||||
: supportsFeature(this._stateObj, MediaPlayerEntityFeature.PAUSE)
|
||||
? "media_pause"
|
||||
: "media_stop";
|
||||
|
||||
this.hass!.callService("media_player", service, {
|
||||
entity_id: this._stateObj.entity_id,
|
||||
});
|
||||
}
|
||||
|
||||
private _play(): void {
|
||||
if (!this._stateObj) return;
|
||||
this.hass!.callService("media_player", "media_play", {
|
||||
entity_id: this._stateObj.entity_id,
|
||||
});
|
||||
}
|
||||
|
||||
private _pause(): void {
|
||||
if (!this._stateObj) return;
|
||||
this.hass!.callService("media_player", "media_pause", {
|
||||
entity_id: this._stateObj.entity_id,
|
||||
});
|
||||
}
|
||||
|
||||
private _stop(): void {
|
||||
if (!this._stateObj) return;
|
||||
this.hass!.callService("media_player", "media_stop", {
|
||||
entity_id: this._stateObj.entity_id,
|
||||
});
|
||||
}
|
||||
|
||||
private _previousTrack(): void {
|
||||
if (!this._stateObj) return;
|
||||
this.hass!.callService("media_player", "media_previous_track", {
|
||||
entity_id: this._stateObj.entity_id,
|
||||
});
|
||||
}
|
||||
|
||||
private _nextTrack(): void {
|
||||
if (!this._stateObj) return;
|
||||
this.hass!.callService("media_player", "media_next_track", {
|
||||
entity_id: this._stateObj.entity_id,
|
||||
});
|
||||
}
|
||||
|
||||
static styles = cardFeatureStyles;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"hui-media-player-playback-card-feature": HuiMediaPlayerPlaybackCardFeature;
|
||||
}
|
||||
}
|
||||
@@ -39,6 +39,10 @@ export interface LockOpenDoorCardFeatureConfig {
|
||||
type: "lock-open-door";
|
||||
}
|
||||
|
||||
export interface MediaPlayerPlaybackCardFeatureConfig {
|
||||
type: "media-player-playback";
|
||||
}
|
||||
|
||||
export interface MediaPlayerVolumeSliderCardFeatureConfig {
|
||||
type: "media-player-volume-slider";
|
||||
}
|
||||
@@ -47,6 +51,10 @@ export interface FanDirectionCardFeatureConfig {
|
||||
type: "fan-direction";
|
||||
}
|
||||
|
||||
export interface FanOscillateCardFeatureConfig {
|
||||
type: "fan-oscillate";
|
||||
}
|
||||
|
||||
export interface FanPresetModesCardFeatureConfig {
|
||||
type: "fan-preset-modes";
|
||||
style?: "dropdown" | "icons";
|
||||
@@ -101,6 +109,10 @@ export interface CounterActionsCardFeatureConfig {
|
||||
actions?: CounterActions[];
|
||||
}
|
||||
|
||||
export interface DateSetCardFeatureConfig {
|
||||
type: "date-set";
|
||||
}
|
||||
|
||||
export interface SelectOptionsCardFeatureConfig {
|
||||
type: "select-options";
|
||||
options?: string[];
|
||||
@@ -175,6 +187,11 @@ export interface UpdateActionsCardFeatureConfig {
|
||||
backup?: "yes" | "no" | "ask";
|
||||
}
|
||||
|
||||
export interface HistoryChartCardFeatureConfig {
|
||||
type: "history-chart";
|
||||
hours_to_show: number;
|
||||
}
|
||||
|
||||
export const AREA_CONTROLS = [
|
||||
"light",
|
||||
"fan",
|
||||
@@ -198,6 +215,10 @@ export interface AreaControlsCardFeatureConfig {
|
||||
controls?: AreaControl[];
|
||||
}
|
||||
|
||||
export interface BarGaugeCardFeatureConfig {
|
||||
type: "bar-gauge";
|
||||
}
|
||||
|
||||
export type LovelaceCardFeaturePosition = "bottom" | "inline";
|
||||
|
||||
export type LovelaceCardFeatureConfig =
|
||||
@@ -213,7 +234,9 @@ export type LovelaceCardFeatureConfig =
|
||||
| CoverPositionCardFeatureConfig
|
||||
| CoverTiltPositionCardFeatureConfig
|
||||
| CoverTiltCardFeatureConfig
|
||||
| DateSetCardFeatureConfig
|
||||
| FanDirectionCardFeatureConfig
|
||||
| FanOscillateCardFeatureConfig
|
||||
| FanPresetModesCardFeatureConfig
|
||||
| FanSpeedCardFeatureConfig
|
||||
| HumidifierToggleCardFeatureConfig
|
||||
@@ -223,9 +246,11 @@ export type LovelaceCardFeatureConfig =
|
||||
| LightColorTempCardFeatureConfig
|
||||
| LockCommandsCardFeatureConfig
|
||||
| LockOpenDoorCardFeatureConfig
|
||||
| MediaPlayerPlaybackCardFeatureConfig
|
||||
| MediaPlayerVolumeSliderCardFeatureConfig
|
||||
| NumericInputCardFeatureConfig
|
||||
| SelectOptionsCardFeatureConfig
|
||||
| HistoryChartCardFeatureConfig
|
||||
| TargetHumidityCardFeatureConfig
|
||||
| TargetTemperatureCardFeatureConfig
|
||||
| ToggleCardFeatureConfig
|
||||
@@ -234,7 +259,8 @@ export type LovelaceCardFeatureConfig =
|
||||
| ValveOpenCloseCardFeatureConfig
|
||||
| ValvePositionCardFeatureConfig
|
||||
| WaterHeaterOperationModesCardFeatureConfig
|
||||
| AreaControlsCardFeatureConfig;
|
||||
| AreaControlsCardFeatureConfig
|
||||
| BarGaugeCardFeatureConfig;
|
||||
|
||||
export interface LovelaceCardFeatureContext {
|
||||
entity_id?: string;
|
||||
|
||||
@@ -174,7 +174,11 @@ function formatTooltip(
|
||||
const values = params
|
||||
.map((param) => {
|
||||
const y = param.value?.[1] as number;
|
||||
const value = formatNumber(y, locale);
|
||||
const value = formatNumber(
|
||||
y,
|
||||
locale,
|
||||
y < 0.1 ? { maximumFractionDigits: 3 } : undefined
|
||||
);
|
||||
if (value === "0") {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -148,13 +148,18 @@ export class HuiEnergyDevicesDetailGraphCard
|
||||
{ num: formatNumber(total, this.hass.locale), unit: UNIT }
|
||||
);
|
||||
|
||||
// ha-chart-base will track hidden per ID (so it will have two entries for ID and compare-ID)
|
||||
// But it will only fire the event for the primary ID, and we will convert and store a list of statistic ids only
|
||||
private _datasetHidden(ev) {
|
||||
this._hiddenStats = [...this._hiddenStats, ev.detail.id];
|
||||
this._hiddenStats = [
|
||||
...this._hiddenStats,
|
||||
this._getStatIdFromId(ev.detail.id),
|
||||
];
|
||||
}
|
||||
|
||||
private _datasetUnhidden(ev) {
|
||||
this._hiddenStats = this._hiddenStats.filter(
|
||||
(stat) => stat !== ev.detail.id
|
||||
(stat) => stat !== this._getStatIdFromId(ev.detail.id)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -179,16 +184,25 @@ export class HuiEnergyDevicesDetailGraphCard
|
||||
this._formatTotal
|
||||
);
|
||||
|
||||
const selected = this._legendData
|
||||
? this._legendData
|
||||
.filter(
|
||||
(d) =>
|
||||
d.id && this._hiddenStats.includes(this._getStatIdFromId(d.id))
|
||||
)
|
||||
.reduce((acc, d) => {
|
||||
acc[d.id!] = false;
|
||||
return acc;
|
||||
}, {})
|
||||
: {};
|
||||
|
||||
return {
|
||||
...commonOptions,
|
||||
legend: {
|
||||
show: true,
|
||||
type: "custom",
|
||||
data: this._legendData,
|
||||
selected: this._hiddenStats.reduce((acc, stat) => {
|
||||
acc[stat] = false;
|
||||
return acc;
|
||||
}, {}),
|
||||
selected,
|
||||
},
|
||||
grid: {
|
||||
top: 15,
|
||||
@@ -315,6 +329,7 @@ export class HuiEnergyDevicesDetailGraphCard
|
||||
datasets.push(...processedData);
|
||||
this._legendData = processedData.map((d) => ({
|
||||
id: d.id as string,
|
||||
secondaryIds: [`compare-${d.id}`],
|
||||
name: d.name as string,
|
||||
itemStyle: {
|
||||
color: d.color as string,
|
||||
@@ -332,6 +347,7 @@ export class HuiEnergyDevicesDetailGraphCard
|
||||
datasets.push(untrackedData);
|
||||
this._legendData.push({
|
||||
id: untrackedData.id as string,
|
||||
secondaryIds: [`compare-${untrackedData.id}`],
|
||||
name: untrackedData.name as string,
|
||||
itemStyle: {
|
||||
color: untrackedData.color as string,
|
||||
|
||||
@@ -7,10 +7,7 @@ import memoizeOne from "memoize-one";
|
||||
import type { BarSeriesOption } from "echarts/charts";
|
||||
import type { ECElementEvent } from "echarts/types/dist/shared";
|
||||
import { getGraphColorByIndex } from "../../../../common/color/colors";
|
||||
import {
|
||||
formatNumber,
|
||||
getNumberFormatOptions,
|
||||
} from "../../../../common/number/format_number";
|
||||
import { formatNumber } from "../../../../common/number/format_number";
|
||||
import "../../../../components/chart/ha-chart-base";
|
||||
import type { EnergyData } from "../../../../data/energy";
|
||||
import { getEnergyDataCollection } from "../../../../data/energy";
|
||||
@@ -105,7 +102,7 @@ export class HuiEnergyDevicesGraphCard
|
||||
const value = `${formatNumber(
|
||||
params.value[0] as number,
|
||||
this.hass.locale,
|
||||
getNumberFormatOptions(undefined, this.hass.entities[params.value[1]])
|
||||
params.value[0] < 0.1 ? { maximumFractionDigits: 3 } : undefined
|
||||
)} kWh`;
|
||||
return `${title}${params.marker} ${params.seriesName}: ${value}`;
|
||||
}
|
||||
|
||||
@@ -125,7 +125,6 @@ class HuiEnergySankeyCard
|
||||
"ui.panel.lovelace.cards.energy.energy_distribution.battery"
|
||||
),
|
||||
value: totalBatteryOut,
|
||||
tooltip: `${formatNumber(totalBatteryOut, this.hass.locale)} kWh`,
|
||||
color: computedStyle.getPropertyValue("--energy-battery-out-color"),
|
||||
index: 0,
|
||||
});
|
||||
@@ -142,7 +141,6 @@ class HuiEnergySankeyCard
|
||||
"ui.panel.lovelace.cards.energy.energy_distribution.battery"
|
||||
),
|
||||
value: totalBatteryIn,
|
||||
tooltip: `${formatNumber(totalBatteryIn, this.hass.locale)} kWh`,
|
||||
color: computedStyle.getPropertyValue("--energy-battery-in-color"),
|
||||
index: 1,
|
||||
});
|
||||
@@ -171,7 +169,6 @@ class HuiEnergySankeyCard
|
||||
"ui.panel.lovelace.cards.energy.energy_distribution.grid"
|
||||
),
|
||||
value: totalFromGrid,
|
||||
tooltip: `${formatNumber(totalFromGrid, this.hass.locale)} kWh`,
|
||||
color: computedStyle.getPropertyValue(
|
||||
"--energy-grid-consumption-color"
|
||||
),
|
||||
@@ -195,7 +192,6 @@ class HuiEnergySankeyCard
|
||||
"ui.panel.lovelace.cards.energy.energy_distribution.solar"
|
||||
),
|
||||
value: totalSolarProduction,
|
||||
tooltip: `${formatNumber(totalSolarProduction, this.hass.locale)} kWh`,
|
||||
color: computedStyle.getPropertyValue("--energy-solar-color"),
|
||||
index: 0,
|
||||
});
|
||||
@@ -217,7 +213,6 @@ class HuiEnergySankeyCard
|
||||
"ui.panel.lovelace.cards.energy.energy_distribution.grid"
|
||||
),
|
||||
value: totalToGrid,
|
||||
tooltip: `${formatNumber(totalToGrid, this.hass.locale)} kWh`,
|
||||
color: computedStyle.getPropertyValue("--energy-grid-return-color"),
|
||||
index: 1,
|
||||
});
|
||||
@@ -260,7 +255,6 @@ class HuiEnergySankeyCard
|
||||
this._data!.statsMetadata[device.stat_consumption]
|
||||
),
|
||||
value,
|
||||
tooltip: `${formatNumber(value, this.hass.locale)} kWh`,
|
||||
color: getGraphColorByIndex(idx, computedStyle),
|
||||
index: 4,
|
||||
parent: device.included_in_stat,
|
||||
@@ -300,7 +294,6 @@ class HuiEnergySankeyCard
|
||||
id: floorNodeId,
|
||||
label: this.hass.floors[floorId].name,
|
||||
value: floors[floorId].value,
|
||||
tooltip: `${formatNumber(floors[floorId].value, this.hass.locale)} kWh`,
|
||||
index: 2,
|
||||
color: computedStyle.getPropertyValue("--primary-color"),
|
||||
});
|
||||
@@ -322,7 +315,6 @@ class HuiEnergySankeyCard
|
||||
id: areaNodeId,
|
||||
label: this.hass.areas[areaId]!.name,
|
||||
value: areas[areaId].value,
|
||||
tooltip: `${formatNumber(areas[areaId].value, this.hass.locale)} kWh`,
|
||||
index: 3,
|
||||
color: computedStyle.getPropertyValue("--primary-color"),
|
||||
});
|
||||
@@ -368,7 +360,6 @@ class HuiEnergySankeyCard
|
||||
"ui.panel.lovelace.cards.energy.energy_devices_detail_graph.untracked_consumption"
|
||||
),
|
||||
value: untrackedConsumption,
|
||||
tooltip: `${formatNumber(untrackedConsumption, this.hass.locale)} kWh`,
|
||||
color: computedStyle.getPropertyValue("--state-unavailable-color"),
|
||||
index: 3 + deviceSections.length,
|
||||
});
|
||||
@@ -378,7 +369,6 @@ class HuiEnergySankeyCard
|
||||
value: untrackedConsumption,
|
||||
});
|
||||
}
|
||||
homeNode.tooltip = `${formatNumber(homeNode.value, this.hass.locale)} kWh`;
|
||||
|
||||
const hasData = nodes.some((node) => node.value > 0);
|
||||
|
||||
@@ -404,7 +394,7 @@ class HuiEnergySankeyCard
|
||||
}
|
||||
|
||||
private _valueFormatter = (value: number) =>
|
||||
`${formatNumber(value, this.hass.locale)} kWh`;
|
||||
`${formatNumber(value, this.hass.locale, value < 0.1 ? { maximumFractionDigits: 3 } : undefined)} kWh`;
|
||||
|
||||
protected _groupByFloorAndArea(deviceNodes: Node[]) {
|
||||
const areas: Record<string, { value: number; devices: Node[] }> = {
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
type TemplateResult,
|
||||
} from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { classMap } from "lit/directives/class-map";
|
||||
import { ifDefined } from "lit/directives/if-defined";
|
||||
import { styleMap } from "lit/directives/style-map";
|
||||
import memoizeOne from "memoize-one";
|
||||
@@ -53,6 +54,19 @@ export const DEVICE_CLASSES = {
|
||||
binary_sensor: ["motion", "moisture"],
|
||||
};
|
||||
|
||||
const SUM_DEVICE_CLASSES = [
|
||||
"power",
|
||||
"apparent_power",
|
||||
"reactive_power",
|
||||
"energy",
|
||||
"reactive_energy",
|
||||
"current",
|
||||
"gas",
|
||||
"monetary",
|
||||
"volume",
|
||||
"water",
|
||||
];
|
||||
|
||||
export interface AreaCardFeatureContext extends LovelaceCardFeatureContext {
|
||||
exclude_entities?: string[];
|
||||
}
|
||||
@@ -84,8 +98,10 @@ export class HuiAreaCard extends LitElement implements LovelaceCard {
|
||||
|
||||
const displayType =
|
||||
config.display_type || (config.show_camera ? "camera" : "picture");
|
||||
const vertical = displayType === "compact" ? config.vertical : false;
|
||||
this._config = {
|
||||
...config,
|
||||
vertical,
|
||||
display_type: displayType,
|
||||
};
|
||||
|
||||
@@ -109,7 +125,7 @@ export class HuiAreaCard extends LitElement implements LovelaceCard {
|
||||
const featuresCount = this._config?.features?.length || 0;
|
||||
return (
|
||||
1 +
|
||||
(displayType === "compact" ? 0 : 2) +
|
||||
(displayType === "compact" ? (this._config?.vertical ? 1 : 0) : 2) +
|
||||
(featuresPosition === "inline" ? 0 : featuresCount)
|
||||
);
|
||||
}
|
||||
@@ -133,6 +149,11 @@ export class HuiAreaCard extends LitElement implements LovelaceCard {
|
||||
|
||||
const displayType = this._config?.display_type || "picture";
|
||||
|
||||
if (this._config?.vertical) {
|
||||
rows++;
|
||||
min_columns = 3;
|
||||
}
|
||||
|
||||
if (displayType !== "compact") {
|
||||
if (featurePosition === "inline" && featuresCount > 0) {
|
||||
rows += 3;
|
||||
@@ -378,9 +399,9 @@ export class HuiAreaCard extends LitElement implements LovelaceCard {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const value =
|
||||
validEntities.reduce((acc, entity) => acc + Number(entity.state), 0) /
|
||||
validEntities.length;
|
||||
const value = SUM_DEVICE_CLASSES.includes(sensorClass)
|
||||
? this._computeSumState(validEntities)
|
||||
: this._computeMedianState(validEntities);
|
||||
|
||||
const formattedAverage = formatNumber(value, this.hass!.locale, {
|
||||
maximumFractionDigits: 1,
|
||||
@@ -397,9 +418,28 @@ export class HuiAreaCard extends LitElement implements LovelaceCard {
|
||||
return sensorStates;
|
||||
}
|
||||
|
||||
private _featurePosition = memoizeOne(
|
||||
(config: AreaCardConfig) => config.features_position || "bottom"
|
||||
);
|
||||
private _computeSumState(entities: HassEntity[]): number {
|
||||
return entities.reduce((acc, entity) => acc + Number(entity.state), 0);
|
||||
}
|
||||
|
||||
private _computeMedianState(entities: HassEntity[]): number {
|
||||
const sortedStates = entities
|
||||
.map((entity) => Number(entity.state))
|
||||
.sort((a, b) => a - b);
|
||||
if (sortedStates.length % 2 === 0) {
|
||||
const medianIndex = sortedStates.length / 2;
|
||||
return (sortedStates[medianIndex] + sortedStates[medianIndex - 1]) / 2;
|
||||
}
|
||||
const medianIndex = Math.floor(sortedStates.length / 2);
|
||||
return sortedStates[medianIndex];
|
||||
}
|
||||
|
||||
private _featurePosition = memoizeOne((config: AreaCardConfig) => {
|
||||
if (config.vertical) {
|
||||
return "bottom";
|
||||
}
|
||||
return config.features_position || "bottom";
|
||||
});
|
||||
|
||||
private _displayedFeatures = memoizeOne((config: AreaCardConfig) => {
|
||||
const features = config.features || [];
|
||||
@@ -439,6 +479,8 @@ export class HuiAreaCard extends LitElement implements LovelaceCard {
|
||||
`;
|
||||
}
|
||||
|
||||
const contentClasses = { vertical: Boolean(this._config.vertical) };
|
||||
|
||||
const icon = area.icon;
|
||||
|
||||
const name = this._config.name || computeAreaName(area);
|
||||
@@ -518,7 +560,7 @@ export class HuiAreaCard extends LitElement implements LovelaceCard {
|
||||
</div>
|
||||
`}
|
||||
<div class="container ${containerOrientationClass}">
|
||||
<div class="content">
|
||||
<div class="content ${classMap(contentClasses)}">
|
||||
<ha-tile-icon>
|
||||
${displayType === "compact"
|
||||
? this._renderAlertSensorBadge()
|
||||
@@ -656,6 +698,16 @@ export class HuiAreaCard extends LitElement implements LovelaceCard {
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.vertical {
|
||||
flex-direction: column;
|
||||
text-align: center;
|
||||
justify-content: center;
|
||||
}
|
||||
.vertical ha-tile-info {
|
||||
width: 100%;
|
||||
flex: none;
|
||||
}
|
||||
|
||||
ha-tile-icon {
|
||||
--tile-icon-color: var(--tile-color);
|
||||
position: relative;
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user