Compare commits
8 Commits
20241010.0
...
column_bre
Author | SHA1 | Date | |
---|---|---|---|
![]() |
1d007a5aaf | ||
![]() |
94c1af7729 | ||
![]() |
d7aaf9bc41 | ||
![]() |
c0aed4325d | ||
![]() |
79a56fabdf | ||
![]() |
14c2b60538 | ||
![]() |
79f3dfdfce | ||
![]() |
3de4dffa02 |
8
.github/workflows/cast_deployment.yaml
vendored
@@ -21,12 +21,12 @@ 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.0
|
||||
uses: actions/checkout@v4.1.7
|
||||
with:
|
||||
ref: dev
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v4.0.4
|
||||
uses: actions/setup-node@v4.0.3
|
||||
with:
|
||||
node-version-file: ".nvmrc"
|
||||
cache: yarn
|
||||
@@ -57,12 +57,12 @@ 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.0
|
||||
uses: actions/checkout@v4.1.7
|
||||
with:
|
||||
ref: master
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v4.0.4
|
||||
uses: actions/setup-node@v4.0.3
|
||||
with:
|
||||
node-version-file: ".nvmrc"
|
||||
cache: yarn
|
||||
|
18
.github/workflows/ci.yaml
vendored
@@ -24,9 +24,9 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check out files from GitHub
|
||||
uses: actions/checkout@v4.2.0
|
||||
uses: actions/checkout@v4.1.7
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v4.0.4
|
||||
uses: actions/setup-node@v4.0.3
|
||||
with:
|
||||
node-version-file: ".nvmrc"
|
||||
cache: yarn
|
||||
@@ -37,7 +37,7 @@ jobs:
|
||||
- name: Build resources
|
||||
run: ./node_modules/.bin/gulp gen-icons-json build-translations build-locale-data gather-gallery-pages
|
||||
- name: Setup lint cache
|
||||
uses: actions/cache@v4.1.0
|
||||
uses: actions/cache@v4.0.2
|
||||
with:
|
||||
path: |
|
||||
node_modules/.cache/prettier
|
||||
@@ -58,9 +58,9 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check out files from GitHub
|
||||
uses: actions/checkout@v4.2.0
|
||||
uses: actions/checkout@v4.1.7
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v4.0.4
|
||||
uses: actions/setup-node@v4.0.3
|
||||
with:
|
||||
node-version-file: ".nvmrc"
|
||||
cache: yarn
|
||||
@@ -76,9 +76,9 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check out files from GitHub
|
||||
uses: actions/checkout@v4.2.0
|
||||
uses: actions/checkout@v4.1.7
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v4.0.4
|
||||
uses: actions/setup-node@v4.0.3
|
||||
with:
|
||||
node-version-file: ".nvmrc"
|
||||
cache: yarn
|
||||
@@ -100,9 +100,9 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check out files from GitHub
|
||||
uses: actions/checkout@v4.2.0
|
||||
uses: actions/checkout@v4.1.7
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v4.0.4
|
||||
uses: actions/setup-node@v4.0.3
|
||||
with:
|
||||
node-version-file: ".nvmrc"
|
||||
cache: yarn
|
||||
|
2
.github/workflows/codeql-analysis.yml
vendored
@@ -23,7 +23,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4.2.0
|
||||
uses: actions/checkout@v4.1.7
|
||||
with:
|
||||
# We must fetch at least the immediate parents so that if this is
|
||||
# a pull request then we can checkout the head.
|
||||
|
8
.github/workflows/demo_deployment.yaml
vendored
@@ -22,12 +22,12 @@ 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.0
|
||||
uses: actions/checkout@v4.1.7
|
||||
with:
|
||||
ref: dev
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v4.0.4
|
||||
uses: actions/setup-node@v4.0.3
|
||||
with:
|
||||
node-version-file: ".nvmrc"
|
||||
cache: yarn
|
||||
@@ -58,12 +58,12 @@ 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.0
|
||||
uses: actions/checkout@v4.1.7
|
||||
with:
|
||||
ref: master
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v4.0.4
|
||||
uses: actions/setup-node@v4.0.3
|
||||
with:
|
||||
node-version-file: ".nvmrc"
|
||||
cache: yarn
|
||||
|
4
.github/workflows/design_deployment.yaml
vendored
@@ -16,10 +16,10 @@ 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.0
|
||||
uses: actions/checkout@v4.1.7
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v4.0.4
|
||||
uses: actions/setup-node@v4.0.3
|
||||
with:
|
||||
node-version-file: ".nvmrc"
|
||||
cache: yarn
|
||||
|
4
.github/workflows/design_preview.yaml
vendored
@@ -21,10 +21,10 @@ 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.0
|
||||
uses: actions/checkout@v4.1.7
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v4.0.4
|
||||
uses: actions/setup-node@v4.0.3
|
||||
with:
|
||||
node-version-file: ".nvmrc"
|
||||
cache: yarn
|
||||
|
4
.github/workflows/nightly.yaml
vendored
@@ -20,7 +20,7 @@ jobs:
|
||||
contents: write
|
||||
steps:
|
||||
- name: Checkout the repository
|
||||
uses: actions/checkout@v4.2.0
|
||||
uses: actions/checkout@v4.1.7
|
||||
|
||||
- name: Set up Python ${{ env.PYTHON_VERSION }}
|
||||
uses: actions/setup-python@v5
|
||||
@@ -28,7 +28,7 @@ jobs:
|
||||
python-version: ${{ env.PYTHON_VERSION }}
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v4.0.4
|
||||
uses: actions/setup-node@v4.0.3
|
||||
with:
|
||||
node-version-file: ".nvmrc"
|
||||
cache: yarn
|
||||
|
4
.github/workflows/release.yaml
vendored
@@ -23,7 +23,7 @@ jobs:
|
||||
contents: write # Required to upload release assets
|
||||
steps:
|
||||
- name: Checkout the repository
|
||||
uses: actions/checkout@v4.2.0
|
||||
uses: actions/checkout@v4.1.7
|
||||
|
||||
- name: Verify version
|
||||
uses: home-assistant/actions/helpers/verify-version@master
|
||||
@@ -34,7 +34,7 @@ jobs:
|
||||
python-version: ${{ env.PYTHON_VERSION }}
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v4.0.4
|
||||
uses: actions/setup-node@v4.0.3
|
||||
with:
|
||||
node-version-file: ".nvmrc"
|
||||
cache: yarn
|
||||
|
2
.github/workflows/translations.yaml
vendored
@@ -13,7 +13,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout the repository
|
||||
uses: actions/checkout@v4.2.0
|
||||
uses: actions/checkout@v4.1.7
|
||||
|
||||
- name: Upload Translations
|
||||
run: |
|
||||
|
@@ -1,7 +1,16 @@
|
||||
diff --git a/modular/sortable.core.esm.js b/modular/sortable.core.esm.js
|
||||
index 8b5e49b011713c8859c669069fbe85ce53974e1d..6a0afc92787157b8a31c38cc5f67dfa526090a00 100644
|
||||
index 93ba17509e2e8583ab241fea6845fbe714c584a2..de0651ddb5dced30d36f7d764da0dd0b441f523f 100644
|
||||
--- a/modular/sortable.core.esm.js
|
||||
+++ b/modular/sortable.core.esm.js
|
||||
@@ -1461,7 +1461,7 @@ Sortable.prototype = /** @lends Sortable.prototype */{
|
||||
}
|
||||
target = parent; // store last element
|
||||
}
|
||||
- /* jshint boss:true */ while (parent = parent.parentNode);
|
||||
+ /* jshint boss:true */ while (parent = parent.parentNode || parent.getRootNode().host);
|
||||
}
|
||||
_unhideGhostForTarget();
|
||||
}
|
||||
@@ -1781,11 +1781,16 @@ Sortable.prototype = /** @lends Sortable.prototype */{
|
||||
}
|
||||
if (_onMove(rootEl, el, dragEl, dragRect, target, targetRect, evt, !!target) !== false) {
|
||||
@@ -24,7 +33,7 @@ index 8b5e49b011713c8859c669069fbe85ce53974e1d..6a0afc92787157b8a31c38cc5f67dfa5
|
||||
}
|
||||
parentEl = el; // actualization
|
||||
|
||||
@@ -1802,7 +1807,12 @@ Sortable.prototype = /** @lends Sortable.prototype */{
|
||||
@@ -1802,7 +1807,13 @@ Sortable.prototype = /** @lends Sortable.prototype */{
|
||||
targetRect = getRect(target);
|
||||
if (_onMove(rootEl, el, dragEl, dragRect, target, targetRect, evt, false) !== false) {
|
||||
capture();
|
||||
@@ -35,10 +44,11 @@ index 8b5e49b011713c8859c669069fbe85ce53974e1d..6a0afc92787157b8a31c38cc5f67dfa5
|
||||
+ catch(err) {
|
||||
+ return completed(false);
|
||||
+ }
|
||||
+
|
||||
parentEl = el; // actualization
|
||||
|
||||
changed();
|
||||
@@ -1849,10 +1859,15 @@ Sortable.prototype = /** @lends Sortable.prototype */{
|
||||
@@ -1849,12 +1860,17 @@ Sortable.prototype = /** @lends Sortable.prototype */{
|
||||
_silent = true;
|
||||
setTimeout(_unsilent, 30);
|
||||
capture();
|
||||
@@ -46,6 +56,8 @@ index 8b5e49b011713c8859c669069fbe85ce53974e1d..6a0afc92787157b8a31c38cc5f67dfa5
|
||||
- el.appendChild(dragEl);
|
||||
- } else {
|
||||
- target.parentNode.insertBefore(dragEl, after ? nextSibling : target);
|
||||
- }
|
||||
|
||||
+ try {
|
||||
+ if (after && !nextSibling) {
|
||||
+ el.appendChild(dragEl);
|
||||
@@ -55,6 +67,7 @@ index 8b5e49b011713c8859c669069fbe85ce53974e1d..6a0afc92787157b8a31c38cc5f67dfa5
|
||||
+ }
|
||||
+ catch(err) {
|
||||
+ return completed(false);
|
||||
}
|
||||
|
||||
+ }
|
||||
// Undo chrome's scroll adjustment (has no effect on other browsers)
|
||||
if (scrolledPastTop) {
|
||||
scrollBy(scrolledPastTop, 0, scrollBefore - scrolledPastTop.scrollTop);
|
@@ -6,4 +6,4 @@ enableGlobalCache: false
|
||||
|
||||
nodeLinker: node-modules
|
||||
|
||||
yarnPath: .yarn/releases/yarn-4.5.0.cjs
|
||||
yarnPath: .yarn/releases/yarn-4.4.1.cjs
|
||||
|
@@ -27,5 +27,3 @@ A complete guide can be found at the following [link](https://www.home-assistant
|
||||
Home Assistant is open-source and Apache 2 licensed. Feel free to browse the repository, learn and reuse parts in your own projects.
|
||||
|
||||
We use [BrowserStack](https://www.browserstack.com) to test Home Assistant on a large variety of devices.
|
||||
|
||||
[](https://www.openhomefoundation.org/)
|
||||
|
@@ -60,12 +60,6 @@ function copyPolyfills(staticDir) {
|
||||
npmPath("@webcomponents/webcomponentsjs/webcomponents-bundle.js.map"),
|
||||
staticPath("polyfills/")
|
||||
);
|
||||
|
||||
// dialog-polyfill css
|
||||
copyFileDir(
|
||||
npmPath("dialog-polyfill/dialog-polyfill.css"),
|
||||
staticPath("polyfills/")
|
||||
);
|
||||
}
|
||||
|
||||
function copyLoaderJS(staticDir) {
|
||||
|
@@ -139,7 +139,7 @@
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="section-header">What does Home Assistant Cast do?</div>
|
||||
<div class="section-header">Wat does Home Assistant Cast do?</div>
|
||||
<div class="card-content">
|
||||
<p>
|
||||
Home Assistant Cast is a receiver application for the Chromecast. When
|
||||
|
@@ -36,7 +36,6 @@ import { HassElement } from "../../../../src/state/hass-element";
|
||||
import { castContext } from "../cast_context";
|
||||
import "./hc-launch-screen";
|
||||
import { getPanelTitleFromUrlPath } from "../../../../src/data/panel";
|
||||
import { checkLovelaceConfig } from "../../../../src/panels/lovelace/common/check-lovelace-config";
|
||||
|
||||
const DEFAULT_CONFIG: LovelaceDashboardStrategyConfig = {
|
||||
strategy: {
|
||||
@@ -366,9 +365,7 @@ export class HcMain extends HassElement {
|
||||
this._urlPath || "lovelace"
|
||||
);
|
||||
castContext.setApplicationState(title || "");
|
||||
this._lovelaceConfig = checkLovelaceConfig(
|
||||
lovelaceConfig
|
||||
) as LovelaceConfig;
|
||||
this._lovelaceConfig = lovelaceConfig;
|
||||
}
|
||||
|
||||
private _handleShowDemo(_msg: ShowDemoMessage) {
|
||||
|
@@ -111,47 +111,9 @@ export const demoEntitiesSections: DemoConfig["entities"] = (localize) =>
|
||||
friendly_name: "Living room Temperature",
|
||||
},
|
||||
},
|
||||
"sensor.living_room_humidity": {
|
||||
entity_id: "sensor.living_room_humidity",
|
||||
state: "57",
|
||||
attributes: {
|
||||
state_class: "measurement",
|
||||
unit_of_measurement: "%",
|
||||
device_class: "humidity",
|
||||
friendly_name: "Living room Humidity",
|
||||
},
|
||||
},
|
||||
"sensor.outdoor_temperature": {
|
||||
entity_id: "sensor.outdoor_temperature",
|
||||
state: "10.5",
|
||||
attributes: {
|
||||
state_class: "measurement",
|
||||
unit_of_measurement: "°C",
|
||||
device_class: "temperature",
|
||||
friendly_name: "Outdoor temperature",
|
||||
},
|
||||
},
|
||||
"sensor.outdoor_humidity": {
|
||||
entity_id: "sensor.outdoor_humidity",
|
||||
state: "70.4",
|
||||
attributes: {
|
||||
state_class: "measurement",
|
||||
unit_of_measurement: "%",
|
||||
device_class: "humidity",
|
||||
friendly_name: "Outdoor humidity",
|
||||
},
|
||||
},
|
||||
"device_tracker.car": {
|
||||
entity_id: "sensor.outdoor_humidity",
|
||||
state: "not_home",
|
||||
attributes: {
|
||||
friendly_name: "Car",
|
||||
icon: "mdi:car",
|
||||
},
|
||||
},
|
||||
"media_player.living_room_nest_mini": {
|
||||
entity_id: "media_player.living_room_nest_mini",
|
||||
state: "playing",
|
||||
state: "on",
|
||||
attributes: {
|
||||
device_class: "speaker",
|
||||
volume_level: 0.18,
|
||||
@@ -199,14 +161,6 @@ export const demoEntitiesSections: DemoConfig["entities"] = (localize) =>
|
||||
supported_features: 32,
|
||||
},
|
||||
},
|
||||
"binary_sensor.kitchen_motion": {
|
||||
entity_id: "light.kitchen_motion",
|
||||
state: "on",
|
||||
attributes: {
|
||||
device_class: "motion",
|
||||
friendly_name: "Kitchen motion",
|
||||
},
|
||||
},
|
||||
"light.worktop_spotlights": {
|
||||
entity_id: "light.worktop_spotlights",
|
||||
state: "off",
|
||||
@@ -441,14 +395,6 @@ export const demoEntitiesSections: DemoConfig["entities"] = (localize) =>
|
||||
supported_features: 64063,
|
||||
},
|
||||
},
|
||||
"switch.in_meeting": {
|
||||
entity_id: "switch.in_meeting",
|
||||
state: "on",
|
||||
attributes: {
|
||||
icon: "mdi:laptop-account",
|
||||
friendly_name: "In a meeting",
|
||||
},
|
||||
},
|
||||
"sensor.standing_desk_height": {
|
||||
entity_id: "sensor.standing_desk_height",
|
||||
state: "72",
|
||||
|
@@ -9,57 +9,17 @@ export const demoLovelaceSections: DemoConfig["lovelace"] = (localize) => ({
|
||||
title: isFrontpageEmbed ? "Home Assistant" : "Demo",
|
||||
path: "home",
|
||||
icon: "mdi:home-assistant",
|
||||
badges: [
|
||||
{
|
||||
type: "entity",
|
||||
entity: "sensor.outdoor_temperature",
|
||||
color: "red",
|
||||
},
|
||||
{
|
||||
type: "entity",
|
||||
entity: "sensor.outdoor_humidity",
|
||||
color: "indigo",
|
||||
},
|
||||
{
|
||||
type: "entity",
|
||||
entity: "device_tracker.car",
|
||||
},
|
||||
],
|
||||
sections: [
|
||||
...(isFrontpageEmbed
|
||||
? []
|
||||
: [
|
||||
{
|
||||
cards: [
|
||||
{
|
||||
type: "heading",
|
||||
heading: `${localize("ui.panel.page-demo.config.sections.titles.welcome")} 👋`,
|
||||
},
|
||||
{ type: "custom:ha-demo-card" },
|
||||
],
|
||||
title: `${localize("ui.panel.page-demo.config.sections.titles.welcome")} 👋`,
|
||||
cards: [{ type: "custom:ha-demo-card" }],
|
||||
},
|
||||
]),
|
||||
{
|
||||
cards: [
|
||||
{
|
||||
type: "heading",
|
||||
heading: localize(
|
||||
"ui.panel.page-demo.config.sections.titles.living_room"
|
||||
),
|
||||
icon: "mdi:sofa",
|
||||
badges: [
|
||||
{
|
||||
type: "entity",
|
||||
entity: "sensor.living_room_temperature",
|
||||
color: "red",
|
||||
},
|
||||
{
|
||||
type: "entity",
|
||||
entity: "sensor.living_room_humidity",
|
||||
color: "indigo",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
type: "tile",
|
||||
entity: "light.floor_lamp",
|
||||
@@ -78,6 +38,13 @@ export const demoLovelaceSections: DemoConfig["lovelace"] = (localize) => ({
|
||||
type: "tile",
|
||||
entity: "light.bar_lamp",
|
||||
},
|
||||
{
|
||||
graph: "line",
|
||||
type: "sensor",
|
||||
entity: "sensor.living_room_temperature",
|
||||
detail: 1,
|
||||
name: "Temperature",
|
||||
},
|
||||
{
|
||||
type: "tile",
|
||||
entity: "cover.living_room_garden_shutter",
|
||||
@@ -88,25 +55,11 @@ export const demoLovelaceSections: DemoConfig["lovelace"] = (localize) => ({
|
||||
entity: "media_player.living_room_nest_mini",
|
||||
},
|
||||
],
|
||||
title: `🛋️ ${localize("ui.panel.page-demo.config.sections.titles.living_room")} `,
|
||||
},
|
||||
{
|
||||
type: "grid",
|
||||
cards: [
|
||||
{
|
||||
type: "heading",
|
||||
heading: localize(
|
||||
"ui.panel.page-demo.config.sections.titles.kitchen"
|
||||
),
|
||||
icon: "mdi:fridge",
|
||||
badges: [
|
||||
{
|
||||
type: "entity",
|
||||
entity: "binary_sensor.kitchen_motion",
|
||||
show_state: false,
|
||||
color: "blue",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
type: "tile",
|
||||
entity: "cover.kitchen_shutter",
|
||||
@@ -137,17 +90,11 @@ export const demoLovelaceSections: DemoConfig["lovelace"] = (localize) => ({
|
||||
entity: "media_player.kitchen_nest_audio",
|
||||
},
|
||||
],
|
||||
title: `👩🍳 ${localize("ui.panel.page-demo.config.sections.titles.kitchen")}`,
|
||||
},
|
||||
{
|
||||
type: "grid",
|
||||
cards: [
|
||||
{
|
||||
type: "heading",
|
||||
heading: localize(
|
||||
"ui.panel.page-demo.config.sections.titles.energy"
|
||||
),
|
||||
icon: "mdi:transmission-tower",
|
||||
},
|
||||
{
|
||||
type: "tile",
|
||||
entity: "binary_sensor.tesla_wall_connector_vehicle_connected",
|
||||
@@ -185,17 +132,11 @@ export const demoLovelaceSections: DemoConfig["lovelace"] = (localize) => ({
|
||||
color: "dark-grey",
|
||||
},
|
||||
],
|
||||
title: `⚡️ ${localize("ui.panel.page-demo.config.sections.titles.energy")}`,
|
||||
},
|
||||
{
|
||||
type: "grid",
|
||||
cards: [
|
||||
{
|
||||
type: "heading",
|
||||
heading: localize(
|
||||
"ui.panel.page-demo.config.sections.titles.climate"
|
||||
),
|
||||
icon: "mdi:thermometer",
|
||||
},
|
||||
{
|
||||
type: "tile",
|
||||
entity: "sun.sun",
|
||||
@@ -228,38 +169,16 @@ export const demoLovelaceSections: DemoConfig["lovelace"] = (localize) => ({
|
||||
state_content: ["preset_mode", "current_temperature"],
|
||||
},
|
||||
],
|
||||
title: `🌤️ ${localize("ui.panel.page-demo.config.sections.titles.climate")}`,
|
||||
},
|
||||
{
|
||||
type: "grid",
|
||||
cards: [
|
||||
{
|
||||
type: "heading",
|
||||
heading: localize(
|
||||
"ui.panel.page-demo.config.sections.titles.study"
|
||||
),
|
||||
icon: "mdi:desk-lamp",
|
||||
badges: [
|
||||
{
|
||||
type: "entity",
|
||||
entity: "switch.in_meeting",
|
||||
state: "on",
|
||||
state_content: "name",
|
||||
visibility: [
|
||||
{
|
||||
condition: "state",
|
||||
state: "on",
|
||||
entity: "switch.in_meeting",
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
type: "tile",
|
||||
entity: "cover.study_shutter",
|
||||
name: "Shutter",
|
||||
},
|
||||
|
||||
{
|
||||
type: "tile",
|
||||
entity: "light.study_spotlights",
|
||||
@@ -276,23 +195,12 @@ export const demoLovelaceSections: DemoConfig["lovelace"] = (localize) => ({
|
||||
color: "brown",
|
||||
icon: "mdi:desk",
|
||||
},
|
||||
{
|
||||
type: "tile",
|
||||
entity: "switch.in_meeting",
|
||||
name: "Meeting mode",
|
||||
},
|
||||
],
|
||||
title: `🧑💻 ${localize("ui.panel.page-demo.config.sections.titles.study")}`,
|
||||
},
|
||||
{
|
||||
type: "grid",
|
||||
cards: [
|
||||
{
|
||||
type: "heading",
|
||||
heading: localize(
|
||||
"ui.panel.page-demo.config.sections.titles.outdoor"
|
||||
),
|
||||
icon: "mdi:tree",
|
||||
},
|
||||
{
|
||||
type: "tile",
|
||||
entity: "light.outdoor_light",
|
||||
@@ -322,17 +230,11 @@ export const demoLovelaceSections: DemoConfig["lovelace"] = (localize) => ({
|
||||
name: "Illuminance",
|
||||
},
|
||||
],
|
||||
title: `🌳 ${localize("ui.panel.page-demo.config.sections.titles.outdoor")}`,
|
||||
},
|
||||
{
|
||||
type: "grid",
|
||||
cards: [
|
||||
{
|
||||
type: "heading",
|
||||
heading: localize(
|
||||
"ui.panel.page-demo.config.sections.titles.updates"
|
||||
),
|
||||
icon: "mdi:update",
|
||||
},
|
||||
{
|
||||
type: "tile",
|
||||
entity: "automation.home_assistant_auto_update",
|
||||
@@ -358,6 +260,7 @@ export const demoLovelaceSections: DemoConfig["lovelace"] = (localize) => ({
|
||||
icon: "mdi:home-assistant",
|
||||
},
|
||||
],
|
||||
title: `🎉 ${localize("ui.panel.page-demo.config.sections.titles.updates")}`,
|
||||
},
|
||||
],
|
||||
},
|
||||
|
@@ -1,9 +0,0 @@
|
||||
import { MockHomeAssistant } from "../../../src/fake_data/provide_hass";
|
||||
|
||||
export const mockConfig = (hass: MockHomeAssistant) => {
|
||||
hass.mockWS("validate_config", () => ({
|
||||
actions: { valid: true },
|
||||
conditions: { valid: true },
|
||||
triggers: { valid: true },
|
||||
}));
|
||||
};
|
@@ -1,6 +0,0 @@
|
||||
import { Tag } from "../../../src/data/tag";
|
||||
import { MockHomeAssistant } from "../../../src/fake_data/provide_hass";
|
||||
|
||||
export const mockTags = (hass: MockHomeAssistant) => {
|
||||
hass.mockWS("tag/list", () => [{ id: "my-tag", name: "My Tag" }] as Tag[]);
|
||||
};
|
@@ -217,22 +217,22 @@ export const basicTrace: DemoTrace = {
|
||||
id: "1615419646544",
|
||||
alias: "Ensure Party mode",
|
||||
description: "",
|
||||
triggers: [
|
||||
trigger: [
|
||||
{
|
||||
trigger: "state",
|
||||
platform: "state",
|
||||
entity_id: "input_boolean.toggle_1",
|
||||
},
|
||||
],
|
||||
conditions: [
|
||||
condition: [
|
||||
{
|
||||
condition: "template",
|
||||
alias: "Test if Paulus is home",
|
||||
value_template: "{{ true }}",
|
||||
},
|
||||
],
|
||||
actions: [
|
||||
action: [
|
||||
{
|
||||
action: "input_boolean.toggle",
|
||||
service: "input_boolean.toggle",
|
||||
target: {
|
||||
entity_id: "input_boolean.toggle_4",
|
||||
},
|
||||
@@ -268,7 +268,7 @@ export const basicTrace: DemoTrace = {
|
||||
],
|
||||
default: [
|
||||
{
|
||||
action: "input_boolean.toggle",
|
||||
service: "input_boolean.toggle",
|
||||
alias: "Toggle 2",
|
||||
target: {
|
||||
entity_id: "input_boolean.toggle_2",
|
||||
@@ -277,7 +277,7 @@ export const basicTrace: DemoTrace = {
|
||||
],
|
||||
},
|
||||
{
|
||||
action: "input_boolean.toggle",
|
||||
service: "input_boolean.toggle",
|
||||
target: {
|
||||
entity_id: "input_boolean.toggle_4",
|
||||
},
|
||||
|
@@ -31,8 +31,8 @@ export const mockDemoTrace = (
|
||||
],
|
||||
},
|
||||
config: {
|
||||
triggers: [],
|
||||
actions: [],
|
||||
trigger: [],
|
||||
action: [],
|
||||
},
|
||||
context: {
|
||||
id: "abcd",
|
||||
|
@@ -133,17 +133,17 @@ export const motionLightTrace: DemoTrace = {
|
||||
config: {
|
||||
mode: "restart",
|
||||
max_exceeded: "silent",
|
||||
triggers: [
|
||||
trigger: [
|
||||
{
|
||||
trigger: "state",
|
||||
platform: "state",
|
||||
entity_id: "binary_sensor.pauluss_macbook_pro_camera_in_use",
|
||||
from: "off",
|
||||
to: "on",
|
||||
},
|
||||
],
|
||||
actions: [
|
||||
action: [
|
||||
{
|
||||
action: "light.turn_on",
|
||||
service: "light.turn_on",
|
||||
target: {
|
||||
entity_id: "light.elgato_key_light_air",
|
||||
},
|
||||
@@ -162,7 +162,7 @@ export const motionLightTrace: DemoTrace = {
|
||||
delay: 0,
|
||||
},
|
||||
{
|
||||
action: "light.turn_off",
|
||||
service: "light.turn_off",
|
||||
target: {
|
||||
entity_id: "light.elgato_key_light_air",
|
||||
},
|
||||
|
@@ -48,7 +48,7 @@ const ACTIONS = [
|
||||
{
|
||||
wait_for_trigger: [
|
||||
{
|
||||
trigger: "state",
|
||||
platform: "state",
|
||||
entity_id: "input_boolean.toggle_1",
|
||||
},
|
||||
],
|
||||
@@ -121,7 +121,7 @@ const ACTIONS = [
|
||||
];
|
||||
|
||||
const initialAction: Action = {
|
||||
action: "light.turn_on",
|
||||
service: "light.turn_on",
|
||||
target: {
|
||||
entity_id: "light.kitchen",
|
||||
},
|
||||
@@ -142,7 +142,7 @@ export class DemoAutomationDescribeAction extends LitElement {
|
||||
<div class="action">
|
||||
<span>
|
||||
${this._action
|
||||
? describeAction(this.hass, [], [], this._action)
|
||||
? describeAction(this.hass, [], [], [], this._action)
|
||||
: "<invalid YAML>"}
|
||||
</span>
|
||||
<ha-yaml-editor
|
||||
@@ -155,7 +155,7 @@ export class DemoAutomationDescribeAction extends LitElement {
|
||||
${ACTIONS.map(
|
||||
(conf) => html`
|
||||
<div class="action">
|
||||
<span>${describeAction(this.hass, [], [], conf as any)}</span>
|
||||
<span>${describeAction(this.hass, [], [], [], conf as any)}</span>
|
||||
<pre>${dump(conf)}</pre>
|
||||
</div>
|
||||
`
|
||||
|
@@ -22,52 +22,46 @@ const ENTITIES = [
|
||||
];
|
||||
|
||||
const triggers = [
|
||||
{ trigger: "state", entity_id: "light.kitchen", from: "off", to: "on" },
|
||||
{ trigger: "mqtt" },
|
||||
{ platform: "state", entity_id: "light.kitchen", from: "off", to: "on" },
|
||||
{ platform: "mqtt" },
|
||||
{
|
||||
trigger: "geo_location",
|
||||
platform: "geo_location",
|
||||
source: "test_source",
|
||||
zone: "zone.home",
|
||||
event: "enter",
|
||||
},
|
||||
{ trigger: "homeassistant", event: "start" },
|
||||
{ platform: "homeassistant", event: "start" },
|
||||
{
|
||||
trigger: "numeric_state",
|
||||
platform: "numeric_state",
|
||||
entity_id: "light.kitchen",
|
||||
attribute: "brightness",
|
||||
below: 80,
|
||||
above: 20,
|
||||
},
|
||||
{ trigger: "sun", event: "sunset" },
|
||||
{ trigger: "time_pattern" },
|
||||
{ trigger: "time_pattern", hours: "*", minutes: "/5", seconds: "10" },
|
||||
{ trigger: "webhook" },
|
||||
{ trigger: "persistent_notification" },
|
||||
{ platform: "sun", event: "sunset" },
|
||||
{ platform: "time_pattern" },
|
||||
{ platform: "time_pattern", hours: "*", minutes: "/5", seconds: "10" },
|
||||
{ platform: "webhook" },
|
||||
{ platform: "persistent_notification" },
|
||||
{
|
||||
trigger: "zone",
|
||||
platform: "zone",
|
||||
entity_id: "person.person",
|
||||
zone: "zone.home",
|
||||
event: "enter",
|
||||
},
|
||||
{ trigger: "tag" },
|
||||
{ trigger: "time", at: "15:32" },
|
||||
{ trigger: "template" },
|
||||
{ trigger: "conversation", command: "Turn on the lights" },
|
||||
{ platform: "tag" },
|
||||
{ platform: "time", at: "15:32" },
|
||||
{ platform: "template" },
|
||||
{ platform: "conversation", command: "Turn on the lights" },
|
||||
{
|
||||
trigger: "conversation",
|
||||
platform: "conversation",
|
||||
command: ["Turn on the lights", "Turn the lights on"],
|
||||
},
|
||||
{ trigger: "event", event_type: "homeassistant_started" },
|
||||
{
|
||||
triggers: [
|
||||
{ trigger: "state", entity_id: "light.kitchen", to: "on" },
|
||||
{ trigger: "state", entity_id: "light.kitchen", to: "off" },
|
||||
],
|
||||
},
|
||||
{ platform: "event", event_type: "homeassistant_started" },
|
||||
];
|
||||
|
||||
const initialTrigger: Trigger = {
|
||||
trigger: "state",
|
||||
platform: "state",
|
||||
entity_id: "light.kitchen",
|
||||
};
|
||||
|
||||
|
@@ -8,9 +8,6 @@ import { mockEntityRegistry } from "../../../../demo/src/stubs/entity_registry";
|
||||
import { mockDeviceRegistry } from "../../../../demo/src/stubs/device_registry";
|
||||
import { mockAreaRegistry } from "../../../../demo/src/stubs/area_registry";
|
||||
import { mockHassioSupervisor } from "../../../../demo/src/stubs/hassio_supervisor";
|
||||
import { mockConfig } from "../../../../demo/src/stubs/config";
|
||||
import { mockTags } from "../../../../demo/src/stubs/tags";
|
||||
import { mockAuth } from "../../../../demo/src/stubs/auth";
|
||||
import type { Trigger } from "../../../../src/data/automation";
|
||||
import { HaGeolocationTrigger } from "../../../../src/panels/config/automation/trigger/types/ha-automation-trigger-geo_location";
|
||||
import { HaEventTrigger } from "../../../../src/panels/config/automation/trigger/types/ha-automation-trigger-event";
|
||||
@@ -29,7 +26,6 @@ import { HaStateTrigger } from "../../../../src/panels/config/automation/trigger
|
||||
import { HaMQTTTrigger } from "../../../../src/panels/config/automation/trigger/types/ha-automation-trigger-mqtt";
|
||||
import "../../../../src/panels/config/automation/trigger/ha-automation-trigger";
|
||||
import { HaConversationTrigger } from "../../../../src/panels/config/automation/trigger/types/ha-automation-trigger-conversation";
|
||||
import { HaTriggerList } from "../../../../src/panels/config/automation/trigger/types/ha-automation-trigger-list";
|
||||
|
||||
const SCHEMAS: { name: string; triggers: Trigger[] }[] = [
|
||||
{
|
||||
@@ -115,15 +111,11 @@ const SCHEMAS: { name: string; triggers: Trigger[] }[] = [
|
||||
triggers: [
|
||||
{ ...HaConversationTrigger.defaultConfig },
|
||||
{
|
||||
trigger: "conversation",
|
||||
platform: "conversation",
|
||||
command: ["Turn on the lights", "Turn the lights on"],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "Trigger list",
|
||||
triggers: [{ ...HaTriggerList.defaultConfig }],
|
||||
},
|
||||
];
|
||||
|
||||
@customElement("demo-automation-editor-trigger")
|
||||
@@ -143,9 +135,6 @@ export class DemoAutomationEditorTrigger extends LitElement {
|
||||
mockDeviceRegistry(hass);
|
||||
mockAreaRegistry(hass);
|
||||
mockHassioSupervisor(hass);
|
||||
mockConfig(hass);
|
||||
mockTags(hass);
|
||||
mockAuth(hass);
|
||||
}
|
||||
|
||||
protected render(): TemplateResult {
|
||||
|
@@ -64,7 +64,6 @@ const DEVICES: DeviceRegistryEntry[] = [
|
||||
labels: [],
|
||||
created_at: 0,
|
||||
modified_at: 0,
|
||||
primary_config_entry: null,
|
||||
},
|
||||
{
|
||||
area_id: "backyard",
|
||||
@@ -87,7 +86,6 @@ const DEVICES: DeviceRegistryEntry[] = [
|
||||
labels: [],
|
||||
created_at: 0,
|
||||
modified_at: 0,
|
||||
primary_config_entry: null,
|
||||
},
|
||||
{
|
||||
area_id: null,
|
||||
@@ -110,7 +108,6 @@ const DEVICES: DeviceRegistryEntry[] = [
|
||||
labels: [],
|
||||
created_at: 0,
|
||||
modified_at: 0,
|
||||
primary_config_entry: null,
|
||||
},
|
||||
];
|
||||
|
||||
|
@@ -64,7 +64,6 @@ const DEVICES: DeviceRegistryEntry[] = [
|
||||
labels: [],
|
||||
created_at: 0,
|
||||
modified_at: 0,
|
||||
primary_config_entry: null,
|
||||
},
|
||||
{
|
||||
area_id: "backyard",
|
||||
@@ -87,7 +86,6 @@ const DEVICES: DeviceRegistryEntry[] = [
|
||||
labels: [],
|
||||
created_at: 0,
|
||||
modified_at: 0,
|
||||
primary_config_entry: null,
|
||||
},
|
||||
{
|
||||
area_id: null,
|
||||
@@ -110,7 +108,6 @@ const DEVICES: DeviceRegistryEntry[] = [
|
||||
labels: [],
|
||||
created_at: 0,
|
||||
modified_at: 0,
|
||||
primary_config_entry: null,
|
||||
},
|
||||
];
|
||||
|
||||
|
@@ -232,7 +232,6 @@ const createDeviceRegistryEntries = (
|
||||
labels: [],
|
||||
created_at: 0,
|
||||
modified_at: 0,
|
||||
primary_config_entry: null,
|
||||
},
|
||||
];
|
||||
|
||||
|
@@ -15,7 +15,6 @@ import { LocalizeFunc } from "../../../src/common/translations/localize";
|
||||
import "../../../src/components/ha-checkbox";
|
||||
import "../../../src/components/ha-formfield";
|
||||
import "../../../src/components/ha-textfield";
|
||||
import "../../../src/components/ha-password-field";
|
||||
import "../../../src/components/ha-radio";
|
||||
import type { HaRadio } from "../../../src/components/ha-radio";
|
||||
import {
|
||||
@@ -262,21 +261,23 @@ export class SupervisorBackupContent extends LitElement {
|
||||
: ""}
|
||||
${this.backupHasPassword
|
||||
? html`
|
||||
<ha-password-field
|
||||
<ha-textfield
|
||||
.label=${this._localize("password")}
|
||||
type="password"
|
||||
name="backupPassword"
|
||||
.value=${this.backupPassword}
|
||||
@change=${this._handleTextValueChanged}
|
||||
>
|
||||
</ha-password-field>
|
||||
</ha-textfield>
|
||||
${!this.backup
|
||||
? html`<ha-password-field
|
||||
? html`<ha-textfield
|
||||
.label=${this._localize("confirm_password")}
|
||||
type="password"
|
||||
name="confirmBackupPassword"
|
||||
.value=${this.confirmBackupPassword}
|
||||
@change=${this._handleTextValueChanged}
|
||||
>
|
||||
</ha-password-field>`
|
||||
</ha-textfield>`
|
||||
: ""}
|
||||
`
|
||||
: ""}
|
||||
|
@@ -13,12 +13,10 @@ import "../../../../src/components/ha-circular-progress";
|
||||
import "../../../../src/components/ha-dialog";
|
||||
import "../../../../src/components/ha-expansion-panel";
|
||||
import "../../../../src/components/ha-formfield";
|
||||
import "../../../../src/components/ha-textfield";
|
||||
import "../../../../src/components/ha-header-bar";
|
||||
import "../../../../src/components/ha-icon-button";
|
||||
import "../../../../src/components/ha-password-field";
|
||||
import "../../../../src/components/ha-radio";
|
||||
import "../../../../src/components/ha-textfield";
|
||||
import type { HaTextField } from "../../../../src/components/ha-textfield";
|
||||
import { extractApiErrorMessage } from "../../../../src/data/hassio/common";
|
||||
import {
|
||||
AccessPoints,
|
||||
@@ -36,6 +34,7 @@ import { HassDialog } from "../../../../src/dialogs/make-dialog-manager";
|
||||
import { haStyleDialog } from "../../../../src/resources/styles";
|
||||
import type { HomeAssistant } from "../../../../src/types";
|
||||
import { HassioNetworkDialogParams } from "./show-dialog-network";
|
||||
import type { HaTextField } from "../../../../src/components/ha-textfield";
|
||||
|
||||
const IP_VERSIONS = ["ipv4", "ipv6"];
|
||||
|
||||
@@ -247,8 +246,9 @@ export class DialogHassioNetwork
|
||||
${this._wifiConfiguration.auth === "wpa-psk" ||
|
||||
this._wifiConfiguration.auth === "wep"
|
||||
? html`
|
||||
<ha-password-field
|
||||
<ha-textfield
|
||||
class="flex-auto"
|
||||
type="password"
|
||||
id="psk"
|
||||
.label=${this.supervisor.localize(
|
||||
"dialog.network.wifi_password"
|
||||
@@ -256,7 +256,7 @@ export class DialogHassioNetwork
|
||||
version="wifi"
|
||||
@change=${this._handleInputValueChangedWifi}
|
||||
>
|
||||
</ha-password-field>
|
||||
</ha-textfield>
|
||||
`
|
||||
: ""}
|
||||
`
|
||||
|
@@ -25,8 +25,8 @@ import type { HomeAssistant } from "../../../../src/types";
|
||||
import { HassioRepositoryDialogParams } from "./show-dialog-repositories";
|
||||
import type { HaTextField } from "../../../../src/components/ha-textfield";
|
||||
import "../../../../src/components/ha-textfield";
|
||||
import "../../../../src/components/ha-md-list";
|
||||
import "../../../../src/components/ha-md-list-item";
|
||||
import "../../../../src/components/ha-list-new";
|
||||
import "../../../../src/components/ha-list-item-new";
|
||||
|
||||
@customElement("dialog-hassio-repositories")
|
||||
class HassioRepositoriesDialog extends LitElement {
|
||||
@@ -107,11 +107,11 @@ class HassioRepositoriesDialog extends LitElement {
|
||||
? html`<ha-alert alert-type="error">${this._error}</ha-alert>`
|
||||
: ""}
|
||||
<div class="form">
|
||||
<ha-md-list>
|
||||
<ha-list-new>
|
||||
${repositories.length
|
||||
? repositories.map(
|
||||
(repo) => html`
|
||||
<ha-md-list-item class="option">
|
||||
<ha-list-item-new class="option">
|
||||
${repo.name}
|
||||
<div slot="supporting-text">
|
||||
<div>${repo.maintainer}</div>
|
||||
@@ -142,11 +142,11 @@ class HassioRepositoriesDialog extends LitElement {
|
||||
)}
|
||||
</simple-tooltip>
|
||||
</div>
|
||||
</ha-md-list-item>
|
||||
</ha-list-item-new>
|
||||
`
|
||||
)
|
||||
: html`<ha-md-list-item> No repositories </ha-md-list-item>`}
|
||||
</ha-md-list>
|
||||
: html`<ha-list-item-new> No repositories </ha-list-item-new>`}
|
||||
</ha-list-new>
|
||||
<div class="layout horizontal bottom">
|
||||
<ha-textfield
|
||||
class="flex-auto"
|
||||
@@ -209,7 +209,7 @@ class HassioRepositoriesDialog extends LitElement {
|
||||
div.delete ha-icon-button {
|
||||
color: var(--error-color);
|
||||
}
|
||||
ha-md-list-item {
|
||||
ha-list-item-new {
|
||||
position: relative;
|
||||
}
|
||||
`,
|
||||
|
@@ -13,11 +13,10 @@
|
||||
<% for (const entry of es5EntryJS) { %>
|
||||
loadES5("<%= entry %>");
|
||||
<% } %>
|
||||
}
|
||||
} else {
|
||||
<% for (const entry of es5EntryJS) { %>
|
||||
loadES5("<%= entry %>");
|
||||
<% } %>
|
||||
}
|
||||
}
|
||||
})();
|
||||
|
82
package.json
@@ -25,15 +25,15 @@
|
||||
"license": "Apache-2.0",
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "7.25.7",
|
||||
"@babel/runtime": "7.25.6",
|
||||
"@braintree/sanitize-url": "7.1.0",
|
||||
"@codemirror/autocomplete": "6.18.1",
|
||||
"@codemirror/commands": "6.7.0",
|
||||
"@codemirror/language": "6.10.3",
|
||||
"@codemirror/autocomplete": "6.18.0",
|
||||
"@codemirror/commands": "6.6.1",
|
||||
"@codemirror/language": "6.10.2",
|
||||
"@codemirror/legacy-modes": "6.4.1",
|
||||
"@codemirror/search": "6.5.6",
|
||||
"@codemirror/state": "6.4.1",
|
||||
"@codemirror/view": "6.34.1",
|
||||
"@codemirror/view": "6.33.0",
|
||||
"@egjs/hammerjs": "2.0.17",
|
||||
"@formatjs/intl-datetimeformat": "6.12.5",
|
||||
"@formatjs/intl-displaynames": "6.6.8",
|
||||
@@ -80,17 +80,16 @@
|
||||
"@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.2.0",
|
||||
"@material/web": "2.1.0",
|
||||
"@mdi/js": "7.4.47",
|
||||
"@mdi/svg": "7.4.47",
|
||||
"@polymer/paper-item": "3.0.1",
|
||||
"@polymer/paper-listbox": "3.0.1",
|
||||
"@polymer/paper-tabs": "3.1.0",
|
||||
"@polymer/polymer": "3.5.1",
|
||||
"@replit/codemirror-indentation-markers": "6.5.3",
|
||||
"@thomasloven/round-slider": "0.6.0",
|
||||
"@vaadin/combo-box": "24.4.11",
|
||||
"@vaadin/vaadin-themable-mixin": "24.4.11",
|
||||
"@vaadin/combo-box": "24.4.7",
|
||||
"@vaadin/vaadin-themable-mixin": "24.4.7",
|
||||
"@vibrant/color": "3.2.1-alpha.1",
|
||||
"@vibrant/core": "3.2.1-alpha.1",
|
||||
"@vibrant/quantizer-mmcq": "3.2.1-alpha.1",
|
||||
@@ -103,11 +102,10 @@
|
||||
"comlink": "4.4.1",
|
||||
"core-js": "3.38.1",
|
||||
"cropperjs": "1.6.2",
|
||||
"date-fns": "4.1.0",
|
||||
"date-fns-tz": "3.2.0",
|
||||
"date-fns": "3.6.0",
|
||||
"date-fns-tz": "3.1.3",
|
||||
"deep-clone-simple": "1.1.1",
|
||||
"deep-freeze": "0.0.1",
|
||||
"dialog-polyfill": "0.5.6",
|
||||
"element-internals-polyfill": "1.3.11",
|
||||
"fuse.js": "7.0.0",
|
||||
"google-timezones-json": "1.2.0",
|
||||
@@ -117,10 +115,10 @@
|
||||
"intl-messageformat": "10.5.14",
|
||||
"js-yaml": "4.1.0",
|
||||
"leaflet": "1.9.4",
|
||||
"leaflet-draw": "patch:leaflet-draw@npm%3A1.0.4#./.yarn/patches/leaflet-draw-npm-1.0.4-0ca0ebcf65.patch",
|
||||
"leaflet-draw": "1.0.4",
|
||||
"lit": "2.8.0",
|
||||
"luxon": "3.5.0",
|
||||
"marked": "14.1.2",
|
||||
"marked": "14.1.0",
|
||||
"memoize-one": "6.0.0",
|
||||
"node-vibrant": "3.2.1-alpha.1",
|
||||
"proxy-polyfill": "0.3.2",
|
||||
@@ -129,13 +127,13 @@
|
||||
"qrcode": "1.5.4",
|
||||
"roboto-fontface": "0.10.0",
|
||||
"rrule": "2.8.1",
|
||||
"sortablejs": "patch:sortablejs@npm%3A1.15.3#~/.yarn/patches/sortablejs-npm-1.15.3-3235a8f83b.patch",
|
||||
"sortablejs": "1.15.2",
|
||||
"stacktrace-js": "2.0.2",
|
||||
"superstruct": "2.0.2",
|
||||
"tinykeys": "3.0.0",
|
||||
"tsparticles-engine": "2.12.0",
|
||||
"tsparticles-preset-links": "2.12.0",
|
||||
"ua-parser-js": "1.0.39",
|
||||
"ua-parser-js": "1.0.38",
|
||||
"unfetch": "5.0.0",
|
||||
"vis-data": "7.1.9",
|
||||
"vis-network": "9.1.9",
|
||||
@@ -151,28 +149,28 @@
|
||||
"xss": "1.0.15"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "7.25.7",
|
||||
"@babel/core": "7.25.2",
|
||||
"@babel/helper-define-polyfill-provider": "0.6.2",
|
||||
"@babel/plugin-proposal-decorators": "7.25.7",
|
||||
"@babel/plugin-transform-runtime": "7.25.7",
|
||||
"@babel/preset-env": "7.25.7",
|
||||
"@babel/preset-typescript": "7.25.7",
|
||||
"@bundle-stats/plugin-webpack-filter": "4.15.1",
|
||||
"@babel/plugin-proposal-decorators": "7.24.7",
|
||||
"@babel/plugin-transform-runtime": "7.25.4",
|
||||
"@babel/preset-env": "7.25.4",
|
||||
"@babel/preset-typescript": "7.24.7",
|
||||
"@bundle-stats/plugin-webpack-filter": "4.15.0",
|
||||
"@koa/cors": "5.0.0",
|
||||
"@lokalise/node-api": "12.7.0",
|
||||
"@octokit/auth-oauth-device": "7.1.1",
|
||||
"@octokit/plugin-retry": "7.1.2",
|
||||
"@octokit/plugin-retry": "7.1.1",
|
||||
"@octokit/rest": "21.0.2",
|
||||
"@open-wc/dev-server-hmr": "0.1.4",
|
||||
"@rollup/plugin-babel": "6.0.4",
|
||||
"@rollup/plugin-commonjs": "26.0.1",
|
||||
"@rollup/plugin-json": "6.1.0",
|
||||
"@rollup/plugin-node-resolve": "15.2.4",
|
||||
"@rollup/plugin-node-resolve": "15.2.3",
|
||||
"@rollup/plugin-replace": "5.0.7",
|
||||
"@types/babel__plugin-transform-runtime": "7.9.5",
|
||||
"@types/chromecast-caf-receiver": "6.0.17",
|
||||
"@types/chromecast-caf-sender": "1.0.10",
|
||||
"@types/color-name": "2.0.0",
|
||||
"@types/color-name": "1.1.4",
|
||||
"@types/glob": "8.1.0",
|
||||
"@types/html-minifier-terser": "7.0.2",
|
||||
"@types/js-yaml": "4.0.9",
|
||||
@@ -191,21 +189,21 @@
|
||||
"@typescript-eslint/parser": "7.18.0",
|
||||
"@web/dev-server": "0.1.38",
|
||||
"@web/dev-server-rollup": "0.4.1",
|
||||
"babel-loader": "9.2.1",
|
||||
"babel-loader": "9.1.3",
|
||||
"babel-plugin-template-html-minifier": "4.1.0",
|
||||
"browserslist-useragent-regexp": "4.1.3",
|
||||
"chai": "5.1.1",
|
||||
"del": "8.0.0",
|
||||
"eslint": "8.57.1",
|
||||
"del": "7.1.0",
|
||||
"eslint": "8.57.0",
|
||||
"eslint-config-airbnb-base": "15.0.0",
|
||||
"eslint-config-airbnb-typescript": "18.0.0",
|
||||
"eslint-config-prettier": "9.1.0",
|
||||
"eslint-import-resolver-webpack": "0.13.9",
|
||||
"eslint-plugin-import": "2.31.0",
|
||||
"eslint-plugin-lit": "1.15.0",
|
||||
"eslint-import-resolver-webpack": "0.13.8",
|
||||
"eslint-plugin-import": "2.29.1",
|
||||
"eslint-plugin-lit": "1.14.0",
|
||||
"eslint-plugin-lit-a11y": "4.1.4",
|
||||
"eslint-plugin-unused-imports": "4.1.4",
|
||||
"eslint-plugin-wc": "2.2.0",
|
||||
"eslint-plugin-unused-imports": "4.1.3",
|
||||
"eslint-plugin-wc": "2.1.1",
|
||||
"fancy-log": "2.0.0",
|
||||
"fs-extra": "11.2.0",
|
||||
"glob": "11.0.0",
|
||||
@@ -215,7 +213,7 @@
|
||||
"gulp-rename": "2.0.0",
|
||||
"gulp-zopfli-green": "6.0.2",
|
||||
"html-minifier-terser": "7.2.0",
|
||||
"husky": "9.1.6",
|
||||
"husky": "9.1.5",
|
||||
"instant-mocha": "1.5.2",
|
||||
"jszip": "3.10.1",
|
||||
"lint-staged": "15.2.10",
|
||||
@@ -229,21 +227,21 @@
|
||||
"open": "10.1.0",
|
||||
"pinst": "3.0.0",
|
||||
"prettier": "3.3.3",
|
||||
"rollup": "2.79.2",
|
||||
"rollup": "2.79.1",
|
||||
"rollup-plugin-string": "3.0.0",
|
||||
"rollup-plugin-terser": "7.0.2",
|
||||
"rollup-plugin-visualizer": "5.12.0",
|
||||
"serve-handler": "6.1.5",
|
||||
"sinon": "19.0.2",
|
||||
"sinon": "18.0.0",
|
||||
"systemjs": "6.15.1",
|
||||
"tar": "7.4.3",
|
||||
"terser-webpack-plugin": "5.3.10",
|
||||
"transform-async-modules-webpack-plugin": "1.1.1",
|
||||
"ts-lit-plugin": "2.0.2",
|
||||
"typescript": "5.6.2",
|
||||
"webpack": "5.95.0",
|
||||
"typescript": "5.5.4",
|
||||
"webpack": "5.94.0",
|
||||
"webpack-cli": "5.1.4",
|
||||
"webpack-dev-server": "5.1.0",
|
||||
"webpack-dev-server": "5.0.4",
|
||||
"webpack-manifest-plugin": "5.0.0",
|
||||
"webpack-stats-plugin": "1.1.3",
|
||||
"webpackbar": "6.0.1",
|
||||
@@ -256,7 +254,9 @@
|
||||
"lit": "2.8.0",
|
||||
"clean-css": "5.3.3",
|
||||
"@lit/reactive-element": "1.6.3",
|
||||
"@fullcalendar/daygrid": "6.1.15"
|
||||
"@fullcalendar/daygrid": "6.1.15",
|
||||
"sortablejs@1.15.2": "patch:sortablejs@npm%3A1.15.2#~/.yarn/patches/sortablejs-npm-1.15.2-73347ae85a.patch",
|
||||
"leaflet-draw@1.0.4": "patch:leaflet-draw@npm%3A1.0.4#./.yarn/patches/leaflet-draw-npm-1.0.4-0ca0ebcf65.patch"
|
||||
},
|
||||
"packageManager": "yarn@4.5.0"
|
||||
"packageManager": "yarn@4.4.1"
|
||||
}
|
||||
|
Before Width: | Height: | Size: 3.9 KiB |
Before Width: | Height: | Size: 4.5 KiB |
Before Width: | Height: | Size: 4.6 KiB |
Before Width: | Height: | Size: 2.3 KiB |
Before Width: | Height: | Size: 4.4 KiB |
Before Width: | Height: | Size: 4.4 KiB |
Before Width: | Height: | Size: 2.9 KiB |
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "home-assistant-frontend"
|
||||
version = "20241010.0"
|
||||
version = "20240904.0"
|
||||
license = {text = "Apache-2.0"}
|
||||
description = "The Home Assistant frontend"
|
||||
readme = "README.md"
|
||||
|
@@ -1,36 +1,36 @@
|
||||
import { theme2hex } from "./convert-color";
|
||||
|
||||
export const COLORS = [
|
||||
"#4269d0",
|
||||
"#f4bd4a",
|
||||
"#ff725c",
|
||||
"#6cc5b0",
|
||||
"#a463f2",
|
||||
"#ff8ab7",
|
||||
"#9c6b4e",
|
||||
"#97bbf5",
|
||||
"#01ab63",
|
||||
"#9498a0",
|
||||
"#094bad",
|
||||
"#c99000",
|
||||
"#d84f3e",
|
||||
"#49a28f",
|
||||
"#048732",
|
||||
"#d96895",
|
||||
"#8043ce",
|
||||
"#7599d1",
|
||||
"#7a4c31",
|
||||
"#74787f",
|
||||
"#6989f4",
|
||||
"#ffd444",
|
||||
"#ff957c",
|
||||
"#8fe9d3",
|
||||
"#62cc71",
|
||||
"#ffadda",
|
||||
"#c884ff",
|
||||
"#badeff",
|
||||
"#bf8b6d",
|
||||
"#b6bac2",
|
||||
"#44739e",
|
||||
"#984ea3",
|
||||
"#00d2d5",
|
||||
"#ff7f00",
|
||||
"#af8d00",
|
||||
"#7f80cd",
|
||||
"#b3e900",
|
||||
"#c42e60",
|
||||
"#a65628",
|
||||
"#f781bf",
|
||||
"#8dd3c7",
|
||||
"#bebada",
|
||||
"#fb8072",
|
||||
"#80b1d3",
|
||||
"#fdb462",
|
||||
"#fccde5",
|
||||
"#bc80bd",
|
||||
"#ffed6f",
|
||||
"#c4eaff",
|
||||
"#cf8c00",
|
||||
"#1b9e77",
|
||||
"#d95f02",
|
||||
"#e7298a",
|
||||
"#e6ab02",
|
||||
"#a6761d",
|
||||
"#0097ff",
|
||||
"#00d067",
|
||||
"#f43600",
|
||||
"#4ba93b",
|
||||
"#5779bb",
|
||||
"#927acc",
|
||||
"#97ee3f",
|
||||
"#bf3947",
|
||||
|
@@ -234,12 +234,7 @@ export const SENSOR_ENTITIES = [
|
||||
"weather",
|
||||
];
|
||||
|
||||
export const ASSIST_ENTITIES = [
|
||||
"assist_satellite",
|
||||
"conversation",
|
||||
"stt",
|
||||
"tts",
|
||||
];
|
||||
export const ASSIST_ENTITIES = ["conversation", "stt", "tts"];
|
||||
|
||||
/** Domains that render an input element instead of a text value when displayed in a row.
|
||||
* Those rows should then not show a cursor pointer when hovered (which would normally
|
||||
|
@@ -20,15 +20,6 @@ function findNestedItem(
|
||||
}, obj);
|
||||
}
|
||||
|
||||
function updateNestedItem(obj: any, path: ItemPath): any {
|
||||
const lastKey = path.pop()!;
|
||||
const parent = findNestedItem(obj, path);
|
||||
parent[lastKey] = Array.isArray(parent[lastKey])
|
||||
? [...parent[lastKey]]
|
||||
: [parent[lastKey]];
|
||||
return obj;
|
||||
}
|
||||
|
||||
export function nestedArrayMove<A>(
|
||||
obj: A,
|
||||
oldIndex: number,
|
||||
@@ -36,18 +27,14 @@ export function nestedArrayMove<A>(
|
||||
oldPath?: ItemPath,
|
||||
newPath?: ItemPath
|
||||
): A {
|
||||
let newObj = (Array.isArray(obj) ? [...obj] : { ...obj }) as A;
|
||||
|
||||
if (oldPath) {
|
||||
newObj = updateNestedItem(newObj, [...oldPath]);
|
||||
}
|
||||
if (newPath) {
|
||||
newObj = updateNestedItem(newObj, [...newPath]);
|
||||
}
|
||||
|
||||
const newObj = (Array.isArray(obj) ? [...obj] : { ...obj }) as A;
|
||||
const from = oldPath ? findNestedItem(newObj, oldPath) : newObj;
|
||||
const to = newPath ? findNestedItem(newObj, newPath, true) : newObj;
|
||||
|
||||
if (!Array.isArray(from) || !Array.isArray(to)) {
|
||||
return obj;
|
||||
}
|
||||
|
||||
const item = from.splice(oldIndex, 1)[0];
|
||||
to.splice(newIndex, 0, item);
|
||||
|
||||
|
@@ -204,29 +204,6 @@ export class HaDataTable extends LitElement {
|
||||
this._checkedRowsChanged();
|
||||
}
|
||||
|
||||
public select(ids: string[], clear?: boolean): void {
|
||||
if (clear) {
|
||||
this._checkedRows = [];
|
||||
}
|
||||
ids.forEach((id) => {
|
||||
const row = this._filteredData.find((data) => data[this.id] === id);
|
||||
if (row?.selectable !== false && !this._checkedRows.includes(id)) {
|
||||
this._checkedRows.push(id);
|
||||
}
|
||||
});
|
||||
this._checkedRowsChanged();
|
||||
}
|
||||
|
||||
public unselect(ids: string[]): void {
|
||||
ids.forEach((id) => {
|
||||
const index = this._checkedRows.indexOf(id);
|
||||
if (index > -1) {
|
||||
this._checkedRows.splice(index, 1);
|
||||
}
|
||||
});
|
||||
this._checkedRowsChanged();
|
||||
}
|
||||
|
||||
public connectedCallback() {
|
||||
super.connectedCallback();
|
||||
if (this._filteredData.length) {
|
||||
@@ -1034,7 +1011,6 @@ export class HaDataTable extends LitElement {
|
||||
/* @noflip */
|
||||
padding-inline-end: initial;
|
||||
width: 60px;
|
||||
min-width: 60px;
|
||||
}
|
||||
|
||||
.mdc-data-table__table {
|
||||
|
@@ -26,7 +26,7 @@ class HaDeviceTriggerPicker extends HaDeviceAutomationPicker<DeviceTrigger> {
|
||||
fetchDeviceTriggers,
|
||||
(deviceId?: string) => ({
|
||||
device_id: deviceId || "",
|
||||
trigger: "device",
|
||||
platform: "device",
|
||||
domain: "",
|
||||
entity_id: "",
|
||||
})
|
||||
|
@@ -1,9 +1,10 @@
|
||||
import type { HassEntity } from "home-assistant-js-websocket";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import { isValidEntityId } from "../../common/entity/valid_entity_id";
|
||||
import type { HomeAssistant, ValueChangedEvent } from "../../types";
|
||||
import type { ValueChangedEvent, HomeAssistant } from "../../types";
|
||||
import "./ha-entity-picker";
|
||||
import type { HaEntityPickerEntityFilterFunc } from "./ha-entity-picker";
|
||||
|
||||
@@ -97,7 +98,10 @@ class HaEntitiesPickerLight extends LitElement {
|
||||
.excludeEntities=${this.excludeEntities}
|
||||
.includeDeviceClasses=${this.includeDeviceClasses}
|
||||
.includeUnitOfMeasurement=${this.includeUnitOfMeasurement}
|
||||
.entityFilter=${this.entityFilter}
|
||||
.entityFilter=${this._getEntityFilter(
|
||||
this.value,
|
||||
this.entityFilter
|
||||
)}
|
||||
.value=${entityId}
|
||||
.label=${this.pickedEntityLabel}
|
||||
.disabled=${this.disabled}
|
||||
@@ -114,13 +118,10 @@ class HaEntitiesPickerLight extends LitElement {
|
||||
.includeDomains=${this.includeDomains}
|
||||
.excludeDomains=${this.excludeDomains}
|
||||
.includeEntities=${this.includeEntities}
|
||||
.excludeEntities=${this._excludeEntities(
|
||||
this.value,
|
||||
this.excludeEntities
|
||||
)}
|
||||
.excludeEntities=${this.excludeEntities}
|
||||
.includeDeviceClasses=${this.includeDeviceClasses}
|
||||
.includeUnitOfMeasurement=${this.includeUnitOfMeasurement}
|
||||
.entityFilter=${this.entityFilter}
|
||||
.entityFilter=${this._getEntityFilter(this.value, this.entityFilter)}
|
||||
.label=${this.pickEntityLabel}
|
||||
.helper=${this.helper}
|
||||
.disabled=${this.disabled}
|
||||
@@ -132,16 +133,14 @@ class HaEntitiesPickerLight extends LitElement {
|
||||
`;
|
||||
}
|
||||
|
||||
private _excludeEntities = memoizeOne(
|
||||
private _getEntityFilter = memoizeOne(
|
||||
(
|
||||
value: string[] | undefined,
|
||||
excludeEntities: string[] | undefined
|
||||
): string[] | undefined => {
|
||||
if (value === undefined) {
|
||||
return excludeEntities;
|
||||
}
|
||||
return [...(excludeEntities || []), ...value];
|
||||
}
|
||||
entityFilter: HaEntityPickerEntityFilterFunc | undefined
|
||||
): HaEntityPickerEntityFilterFunc =>
|
||||
(stateObj: HassEntity) =>
|
||||
(!value || !value.includes(stateObj.entity_id)) &&
|
||||
(!entityFilter || entityFilter(stateObj))
|
||||
);
|
||||
|
||||
private get _currentEntities() {
|
||||
|
@@ -87,7 +87,7 @@ export class HaEntityPicker extends LitElement {
|
||||
public includeUnitOfMeasurement?: string[];
|
||||
|
||||
/**
|
||||
* List of allowed entities to show.
|
||||
* List of allowed entities to show. Will ignore all other filters.
|
||||
* @type {Array}
|
||||
* @attr include-entities
|
||||
*/
|
||||
@@ -220,13 +220,30 @@ export class HaEntityPicker extends LitElement {
|
||||
|
||||
if (includeEntities) {
|
||||
entityIds = entityIds.filter((entityId) =>
|
||||
includeEntities.includes(entityId)
|
||||
this.includeEntities!.includes(entityId)
|
||||
);
|
||||
|
||||
return entityIds
|
||||
.map((key) => {
|
||||
const friendly_name = computeStateName(hass!.states[key]) || key;
|
||||
return {
|
||||
...hass!.states[key],
|
||||
friendly_name,
|
||||
strings: [key, friendly_name],
|
||||
};
|
||||
})
|
||||
.sort((entityA, entityB) =>
|
||||
caseInsensitiveStringCompare(
|
||||
entityA.friendly_name,
|
||||
entityB.friendly_name,
|
||||
this.hass.locale.language
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
if (excludeEntities) {
|
||||
entityIds = entityIds.filter(
|
||||
(entityId) => !excludeEntities.includes(entityId)
|
||||
(entityId) => !excludeEntities!.includes(entityId)
|
||||
);
|
||||
}
|
||||
|
||||
|
@@ -173,7 +173,6 @@ class HaEntityStatePicker extends LitElement {
|
||||
no-style
|
||||
@item-moved=${this._moveItem}
|
||||
.disabled=${this.disabled}
|
||||
filter="button.trailing.action"
|
||||
>
|
||||
<ha-chip-set>
|
||||
${repeat(
|
||||
|
@@ -1,6 +1,6 @@
|
||||
import { mdiTextureBox } from "@mdi/js";
|
||||
import { ComboBoxLitRenderer } from "@vaadin/combo-box/lit";
|
||||
import { HassEntity } from "home-assistant-js-websocket";
|
||||
import { HassEntity, UnsubscribeFunc } from "home-assistant-js-websocket";
|
||||
import { LitElement, PropertyValues, TemplateResult, html, nothing } from "lit";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import { styleMap } from "lit/directives/style-map";
|
||||
@@ -20,7 +20,12 @@ import {
|
||||
getDeviceEntityDisplayLookup,
|
||||
} from "../data/device_registry";
|
||||
import { EntityRegistryDisplayEntry } from "../data/entity_registry";
|
||||
import { FloorRegistryEntry, getFloorAreaLookup } from "../data/floor_registry";
|
||||
import {
|
||||
FloorRegistryEntry,
|
||||
getFloorAreaLookup,
|
||||
subscribeFloorRegistry,
|
||||
} from "../data/floor_registry";
|
||||
import { SubscribeMixin } from "../mixins/subscribe-mixin";
|
||||
import { HomeAssistant, ValueChangedEvent } from "../types";
|
||||
import type { HaDevicePickerDeviceFilterFunc } from "./device/ha-device-picker";
|
||||
import "./ha-combo-box";
|
||||
@@ -45,7 +50,7 @@ interface FloorAreaEntry {
|
||||
}
|
||||
|
||||
@customElement("ha-area-floor-picker")
|
||||
export class HaAreaFloorPicker extends LitElement {
|
||||
export class HaAreaFloorPicker extends SubscribeMixin(LitElement) {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property() public label?: string;
|
||||
@@ -106,12 +111,22 @@ export class HaAreaFloorPicker extends LitElement {
|
||||
|
||||
@property({ type: Boolean }) public required = false;
|
||||
|
||||
@state() private _floors?: FloorRegistryEntry[];
|
||||
|
||||
@state() private _opened?: boolean;
|
||||
|
||||
@query("ha-combo-box", true) public comboBox!: HaComboBox;
|
||||
|
||||
private _init = false;
|
||||
|
||||
protected hassSubscribe(): (UnsubscribeFunc | Promise<UnsubscribeFunc>)[] {
|
||||
return [
|
||||
subscribeFloorRegistry(this.hass.connection, (floors) => {
|
||||
this._floors = floors;
|
||||
}),
|
||||
];
|
||||
}
|
||||
|
||||
public async open() {
|
||||
await this.updateComplete;
|
||||
await this.comboBox?.open();
|
||||
@@ -416,12 +431,12 @@ export class HaAreaFloorPicker extends LitElement {
|
||||
|
||||
protected updated(changedProps: PropertyValues) {
|
||||
if (
|
||||
(!this._init && this.hass) ||
|
||||
(!this._init && this.hass && this._floors) ||
|
||||
(this._init && changedProps.has("_opened") && this._opened)
|
||||
) {
|
||||
this._init = true;
|
||||
const areas = this._getAreas(
|
||||
Object.values(this.hass.floors),
|
||||
this._floors!,
|
||||
Object.values(this.hass.areas),
|
||||
Object.values(this.hass.devices),
|
||||
Object.values(this.hass.entities),
|
||||
|
@@ -6,8 +6,8 @@ import type { HaIconButton } from "./ha-icon-button";
|
||||
import "./ha-menu";
|
||||
import type { HaMenu } from "./ha-menu";
|
||||
|
||||
@customElement("ha-md-button-menu")
|
||||
export class HaMdButtonMenu extends LitElement {
|
||||
@customElement("ha-button-menu-new")
|
||||
export class HaButtonMenuNew extends LitElement {
|
||||
protected readonly [FOCUS_TARGET];
|
||||
|
||||
@property({ type: Boolean }) public disabled = false;
|
||||
@@ -84,6 +84,6 @@ export class HaMdButtonMenu extends LitElement {
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-md-button-menu": HaMdButtonMenu;
|
||||
"ha-button-menu-new": HaButtonMenuNew;
|
||||
}
|
||||
}
|
@@ -124,12 +124,9 @@ export class HaCodeEditor extends ReactiveElement {
|
||||
const transactions: TransactionSpec[] = [];
|
||||
if (changedProps.has("mode")) {
|
||||
transactions.push({
|
||||
effects: [
|
||||
this._loadedCodeMirror!.langCompartment!.reconfigure(this._mode),
|
||||
this._loadedCodeMirror!.foldingCompartment.reconfigure(
|
||||
this._getFoldingExtensions()
|
||||
),
|
||||
],
|
||||
effects: this._loadedCodeMirror!.langCompartment!.reconfigure(
|
||||
this._mode
|
||||
),
|
||||
});
|
||||
}
|
||||
if (changedProps.has("readOnly")) {
|
||||
@@ -180,14 +177,6 @@ export class HaCodeEditor extends ReactiveElement {
|
||||
this._loadedCodeMirror.crosshairCursor(),
|
||||
this._loadedCodeMirror.highlightSelectionMatches(),
|
||||
this._loadedCodeMirror.highlightActiveLine(),
|
||||
this._loadedCodeMirror.indentationMarkers({
|
||||
thickness: 0,
|
||||
activeThickness: 1,
|
||||
colors: {
|
||||
activeLight: "var(--secondary-text-color)",
|
||||
activeDark: "var(--secondary-text-color)",
|
||||
},
|
||||
}),
|
||||
this._loadedCodeMirror.keymap.of([
|
||||
...this._loadedCodeMirror.defaultKeymap,
|
||||
...this._loadedCodeMirror.searchKeymap,
|
||||
@@ -205,9 +194,6 @@ export class HaCodeEditor extends ReactiveElement {
|
||||
this.linewrap ? this._loadedCodeMirror.EditorView.lineWrapping : []
|
||||
),
|
||||
this._loadedCodeMirror.EditorView.updateListener.of(this._onUpdate),
|
||||
this._loadedCodeMirror.foldingCompartment.of(
|
||||
this._getFoldingExtensions()
|
||||
),
|
||||
];
|
||||
|
||||
if (!this.readOnly) {
|
||||
@@ -325,17 +311,6 @@ export class HaCodeEditor extends ReactiveElement {
|
||||
fireEvent(this, "value-changed", { value: this._value });
|
||||
};
|
||||
|
||||
private _getFoldingExtensions = (): Extension => {
|
||||
if (this.mode === "yaml") {
|
||||
return [
|
||||
this._loadedCodeMirror!.foldGutter(),
|
||||
this._loadedCodeMirror!.foldingOnIndent,
|
||||
];
|
||||
}
|
||||
|
||||
return [];
|
||||
};
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return css`
|
||||
:host(.error-state) .cm-gutters {
|
||||
|
@@ -1,16 +1,14 @@
|
||||
import { mdiInvertColorsOff, mdiPalette } from "@mdi/js";
|
||||
import "@material/mwc-list/mwc-list-item";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, query } from "lit/decorators";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { styleMap } from "lit/directives/style-map";
|
||||
import { computeCssColor, THEME_COLORS } from "../common/color/compute-color";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import { stopPropagation } from "../common/dom/stop_propagation";
|
||||
import { LocalizeKeys } from "../common/translations/localize";
|
||||
import { HomeAssistant } from "../types";
|
||||
import "./ha-list-item";
|
||||
import "./ha-md-divider";
|
||||
import "./ha-select";
|
||||
import type { HaSelect } from "./ha-select";
|
||||
import "./ha-list-item";
|
||||
import { HomeAssistant } from "../types";
|
||||
import { LocalizeKeys } from "../common/translations/localize";
|
||||
|
||||
@customElement("ha-color-picker")
|
||||
export class HaColorPicker extends LitElement {
|
||||
@@ -22,97 +20,43 @@ export class HaColorPicker extends LitElement {
|
||||
|
||||
@property() public value?: string;
|
||||
|
||||
@property({ type: String, attribute: "default_color" })
|
||||
public defaultColor?: string;
|
||||
|
||||
@property({ type: Boolean, attribute: "include_state" })
|
||||
public includeState = false;
|
||||
|
||||
@property({ type: Boolean, attribute: "include_none" })
|
||||
public includeNone = false;
|
||||
@property({ type: Boolean }) public defaultColor = false;
|
||||
|
||||
@property({ type: Boolean }) public disabled = false;
|
||||
|
||||
@query("ha-select") private _select?: HaSelect;
|
||||
|
||||
connectedCallback(): void {
|
||||
super.connectedCallback();
|
||||
// Refresh layout options when the field is connected to the DOM to ensure current value displayed
|
||||
this._select?.layoutOptions();
|
||||
}
|
||||
|
||||
private _valueSelected(ev) {
|
||||
ev.stopPropagation();
|
||||
if (!this.isConnected) return;
|
||||
_valueSelected(ev) {
|
||||
const value = ev.target.value;
|
||||
this.value = value === this.defaultColor ? undefined : value;
|
||||
fireEvent(this, "value-changed", {
|
||||
value: this.value,
|
||||
});
|
||||
if (value) {
|
||||
fireEvent(this, "value-changed", {
|
||||
value: value !== "default" ? value : undefined,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const value = this.value || this.defaultColor || "";
|
||||
|
||||
const isCustom = !(
|
||||
THEME_COLORS.has(value) ||
|
||||
value === "none" ||
|
||||
value === "state"
|
||||
);
|
||||
|
||||
return html`
|
||||
<ha-select
|
||||
.icon=${Boolean(value)}
|
||||
.icon=${Boolean(this.value)}
|
||||
.label=${this.label}
|
||||
.value=${value}
|
||||
.value=${this.value || "default"}
|
||||
.helper=${this.helper}
|
||||
.disabled=${this.disabled}
|
||||
@closed=${stopPropagation}
|
||||
@selected=${this._valueSelected}
|
||||
fixedMenuPosition
|
||||
naturalMenuWidth
|
||||
.clearable=${!this.defaultColor}
|
||||
>
|
||||
${value
|
||||
${this.value
|
||||
? html`
|
||||
<span slot="icon">
|
||||
${value === "none"
|
||||
? html`
|
||||
<ha-svg-icon path=${mdiInvertColorsOff}></ha-svg-icon>
|
||||
`
|
||||
: value === "state"
|
||||
? html`<ha-svg-icon path=${mdiPalette}></ha-svg-icon>`
|
||||
: this.renderColorCircle(value || "grey")}
|
||||
${this.renderColorCircle(this.value || "grey")}
|
||||
</span>
|
||||
`
|
||||
: nothing}
|
||||
${this.includeNone
|
||||
? html`
|
||||
<ha-list-item value="none" graphic="icon">
|
||||
${this.hass.localize("ui.components.color-picker.none")}
|
||||
${this.defaultColor === "none"
|
||||
? ` (${this.hass.localize("ui.components.color-picker.default")})`
|
||||
: nothing}
|
||||
<ha-svg-icon
|
||||
slot="graphic"
|
||||
path=${mdiInvertColorsOff}
|
||||
></ha-svg-icon>
|
||||
</ha-list-item>
|
||||
`
|
||||
: nothing}
|
||||
${this.includeState
|
||||
? html`
|
||||
<ha-list-item value="state" graphic="icon">
|
||||
${this.hass.localize("ui.components.color-picker.state")}
|
||||
${this.defaultColor === "state"
|
||||
? ` (${this.hass.localize("ui.components.color-picker.default")})`
|
||||
: nothing}
|
||||
<ha-svg-icon slot="graphic" path=${mdiPalette}></ha-svg-icon>
|
||||
</ha-list-item>
|
||||
`
|
||||
: nothing}
|
||||
${this.includeState || this.includeNone
|
||||
? html`<ha-md-divider role="separator" tabindex="-1"></ha-md-divider>`
|
||||
${this.defaultColor
|
||||
? html` <ha-list-item value="default">
|
||||
${this.hass.localize(`ui.components.color-picker.default_color`)}
|
||||
</ha-list-item>`
|
||||
: nothing}
|
||||
${Array.from(THEME_COLORS).map(
|
||||
(color) => html`
|
||||
@@ -120,21 +64,10 @@ export class HaColorPicker extends LitElement {
|
||||
${this.hass.localize(
|
||||
`ui.components.color-picker.colors.${color}` as LocalizeKeys
|
||||
) || color}
|
||||
${this.defaultColor === color
|
||||
? ` (${this.hass.localize("ui.components.color-picker.default")})`
|
||||
: nothing}
|
||||
<span slot="graphic">${this.renderColorCircle(color)}</span>
|
||||
</ha-list-item>
|
||||
`
|
||||
)}
|
||||
${isCustom
|
||||
? html`
|
||||
<ha-list-item .value=${value} graphic="icon">
|
||||
${value}
|
||||
<span slot="graphic">${this.renderColorCircle(value)}</span>
|
||||
</ha-list-item>
|
||||
`
|
||||
: nothing}
|
||||
</ha-select>
|
||||
`;
|
||||
}
|
||||
@@ -154,11 +87,10 @@ export class HaColorPicker extends LitElement {
|
||||
return css`
|
||||
.circle-color {
|
||||
display: block;
|
||||
background-color: var(--circle-color, var(--divider-color));
|
||||
background-color: var(--circle-color);
|
||||
border-radius: 10px;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
ha-select {
|
||||
width: 100%;
|
||||
|
@@ -10,13 +10,8 @@ export class HaDialogHeader extends LitElement {
|
||||
<section class="header-navigation-icon">
|
||||
<slot name="navigationIcon"></slot>
|
||||
</section>
|
||||
<section class="header-content">
|
||||
<div class="header-title">
|
||||
<slot name="title"></slot>
|
||||
</div>
|
||||
<div class="header-subtitle">
|
||||
<slot name="subtitle"></slot>
|
||||
</div>
|
||||
<section class="header-title">
|
||||
<slot name="title"></slot>
|
||||
</section>
|
||||
<section class="header-action-items">
|
||||
<slot name="actionItems"></slot>
|
||||
@@ -44,24 +39,17 @@ export class HaDialogHeader extends LitElement {
|
||||
padding: 4px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.header-content {
|
||||
.header-title {
|
||||
flex: 1;
|
||||
font-size: 22px;
|
||||
line-height: 28px;
|
||||
font-weight: 400;
|
||||
padding: 10px 4px;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.header-title {
|
||||
font-size: 22px;
|
||||
line-height: 28px;
|
||||
font-weight: 400;
|
||||
}
|
||||
.header-subtitle {
|
||||
font-size: 14px;
|
||||
line-height: 20px;
|
||||
color: var(--secondary-text-color);
|
||||
}
|
||||
@media all and (min-width: 450px) and (min-height: 500px) {
|
||||
.header-bar {
|
||||
padding: 12px;
|
||||
|
@@ -1,5 +1,6 @@
|
||||
import "@material/mwc-menu/mwc-menu-surface";
|
||||
import { mdiFilterVariantRemove, mdiTextureBox } from "@mdi/js";
|
||||
import { UnsubscribeFunc } from "home-assistant-js-websocket";
|
||||
import {
|
||||
CSSResultGroup,
|
||||
LitElement,
|
||||
@@ -14,8 +15,13 @@ import { repeat } from "lit/directives/repeat";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import { computeRTL } from "../common/util/compute_rtl";
|
||||
import { getFloorAreaLookup } from "../data/floor_registry";
|
||||
import {
|
||||
FloorRegistryEntry,
|
||||
getFloorAreaLookup,
|
||||
subscribeFloorRegistry,
|
||||
} from "../data/floor_registry";
|
||||
import { RelatedResult, findRelated } from "../data/search";
|
||||
import { SubscribeMixin } from "../mixins/subscribe-mixin";
|
||||
import { haStyleScrollbar } from "../resources/styles";
|
||||
import type { HomeAssistant } from "../types";
|
||||
import "./ha-check-list-item";
|
||||
@@ -25,7 +31,7 @@ import "./ha-svg-icon";
|
||||
import "./ha-tree-indicator";
|
||||
|
||||
@customElement("ha-filter-floor-areas")
|
||||
export class HaFilterFloorAreas extends LitElement {
|
||||
export class HaFilterFloorAreas extends SubscribeMixin(LitElement) {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public value?: {
|
||||
@@ -41,6 +47,8 @@ export class HaFilterFloorAreas extends LitElement {
|
||||
|
||||
@state() private _shouldRender = false;
|
||||
|
||||
@state() private _floors?: FloorRegistryEntry[];
|
||||
|
||||
public willUpdate(properties: PropertyValues) {
|
||||
super.willUpdate(properties);
|
||||
|
||||
@@ -52,7 +60,7 @@ export class HaFilterFloorAreas extends LitElement {
|
||||
}
|
||||
|
||||
protected render() {
|
||||
const areas = this._areas(this.hass.areas, this.hass.floors);
|
||||
const areas = this._areas(this.hass.areas, this._floors);
|
||||
|
||||
return html`
|
||||
<ha-expansion-panel
|
||||
@@ -181,6 +189,14 @@ export class HaFilterFloorAreas extends LitElement {
|
||||
this._findRelated();
|
||||
}
|
||||
|
||||
protected hassSubscribe(): (UnsubscribeFunc | Promise<UnsubscribeFunc>)[] {
|
||||
return [
|
||||
subscribeFloorRegistry(this.hass.connection, (floors) => {
|
||||
this._floors = floors;
|
||||
}),
|
||||
];
|
||||
}
|
||||
|
||||
protected updated(changed) {
|
||||
if (changed.has("expanded") && this.expanded) {
|
||||
setTimeout(() => {
|
||||
@@ -204,9 +220,9 @@ export class HaFilterFloorAreas extends LitElement {
|
||||
}
|
||||
|
||||
private _areas = memoizeOne(
|
||||
(areaReg: HomeAssistant["areas"], floorReg: HomeAssistant["floors"]) => {
|
||||
(areaReg: HomeAssistant["areas"], floors?: FloorRegistryEntry[]) => {
|
||||
const areas = Object.values(areaReg);
|
||||
const floors = Object.values(floorReg);
|
||||
|
||||
const floorAreaLookup = getFloorAreaLookup(areas);
|
||||
|
||||
const unassisgnedAreas = areas.filter(
|
||||
|
@@ -1,5 +1,5 @@
|
||||
import { ComboBoxLitRenderer } from "@vaadin/combo-box/lit";
|
||||
import { HassEntity } from "home-assistant-js-websocket";
|
||||
import { HassEntity, UnsubscribeFunc } from "home-assistant-js-websocket";
|
||||
import { LitElement, PropertyValues, TemplateResult, html } from "lit";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import { classMap } from "lit/directives/class-map";
|
||||
@@ -24,8 +24,10 @@ import {
|
||||
FloorRegistryEntry,
|
||||
createFloorRegistryEntry,
|
||||
getFloorAreaLookup,
|
||||
subscribeFloorRegistry,
|
||||
} from "../data/floor_registry";
|
||||
import { showAlertDialog } from "../dialogs/generic/show-dialog-box";
|
||||
import { SubscribeMixin } from "../mixins/subscribe-mixin";
|
||||
import { showFloorRegistryDetailDialog } from "../panels/config/areas/show-dialog-floor-registry-detail";
|
||||
import { HomeAssistant, ValueChangedEvent } from "../types";
|
||||
import type { HaDevicePickerDeviceFilterFunc } from "./device/ha-device-picker";
|
||||
@@ -51,7 +53,7 @@ const rowRenderer: ComboBoxLitRenderer<FloorRegistryEntry> = (item) =>
|
||||
</ha-list-item>`;
|
||||
|
||||
@customElement("ha-floor-picker")
|
||||
export class HaFloorPicker extends LitElement {
|
||||
export class HaFloorPicker extends SubscribeMixin(LitElement) {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property() public label?: string;
|
||||
@@ -109,6 +111,8 @@ export class HaFloorPicker extends LitElement {
|
||||
|
||||
@state() private _opened?: boolean;
|
||||
|
||||
@state() private _floors?: FloorRegistryEntry[];
|
||||
|
||||
@query("ha-combo-box", true) public comboBox!: HaComboBox;
|
||||
|
||||
private _suggestion?: string;
|
||||
@@ -125,6 +129,14 @@ export class HaFloorPicker extends LitElement {
|
||||
await this.comboBox?.focus();
|
||||
}
|
||||
|
||||
protected hassSubscribe(): (UnsubscribeFunc | Promise<UnsubscribeFunc>)[] {
|
||||
return [
|
||||
subscribeFloorRegistry(this.hass.connection, (floors) => {
|
||||
this._floors = floors;
|
||||
}),
|
||||
];
|
||||
}
|
||||
|
||||
private _getFloors = memoizeOne(
|
||||
(
|
||||
floors: FloorRegistryEntry[],
|
||||
@@ -308,12 +320,12 @@ export class HaFloorPicker extends LitElement {
|
||||
|
||||
protected updated(changedProps: PropertyValues) {
|
||||
if (
|
||||
(!this._init && this.hass) ||
|
||||
(!this._init && this.hass && this._floors) ||
|
||||
(this._init && changedProps.has("_opened") && this._opened)
|
||||
) {
|
||||
this._init = true;
|
||||
const floors = this._getFloors(
|
||||
Object.values(this.hass.floors),
|
||||
this._floors!,
|
||||
Object.values(this.hass.areas),
|
||||
Object.values(this.hass.devices),
|
||||
Object.values(this.hass.entities),
|
||||
@@ -348,7 +360,8 @@ export class HaFloorPicker extends LitElement {
|
||||
? this.hass.localize("ui.components.floor-picker.floor")
|
||||
: this.label}
|
||||
.placeholder=${this.placeholder
|
||||
? this.hass.floors[this.placeholder]?.name
|
||||
? this._floors?.find((floor) => floor.floor_id === this.placeholder)
|
||||
?.name
|
||||
: undefined}
|
||||
.renderer=${rowRenderer}
|
||||
@filter-changed=${this._filterChanged}
|
||||
@@ -447,7 +460,7 @@ export class HaFloorPicker extends LitElement {
|
||||
floor_id: floor.floor_id,
|
||||
});
|
||||
});
|
||||
const floors = [...Object.values(this.hass.floors), floor];
|
||||
const floors = [...this._floors!, floor];
|
||||
this.comboBox.filteredItems = this._getFloors(
|
||||
floors,
|
||||
Object.values(this.hass.areas)!,
|
||||
|
@@ -95,10 +95,10 @@ export const computeInitialHaFormData = (
|
||||
} else if (
|
||||
"action" in selector ||
|
||||
"trigger" in selector ||
|
||||
"condition" in selector
|
||||
"condition" in selector ||
|
||||
"media" in selector ||
|
||||
"target" in selector
|
||||
) {
|
||||
data[field.name] = [];
|
||||
} else if ("media" in selector || "target" in selector) {
|
||||
data[field.name] = {};
|
||||
} else {
|
||||
throw new Error(
|
||||
|
@@ -30,10 +30,6 @@ export class HaFormExpendable extends LitElement implements HaFormElement {
|
||||
options?: { path?: string[] }
|
||||
) => string;
|
||||
|
||||
@property({ attribute: false }) public localizeValue?: (
|
||||
key: string
|
||||
) => string;
|
||||
|
||||
private _renderDescription() {
|
||||
const description = this.computeHelper?.(this.schema);
|
||||
return description ? html`<p>${description}</p>` : nothing;
|
||||
@@ -90,7 +86,6 @@ export class HaFormExpendable extends LitElement implements HaFormElement {
|
||||
.disabled=${this.disabled}
|
||||
.computeLabel=${this._computeLabel}
|
||||
.computeHelper=${this._computeHelper}
|
||||
.localizeValue=${this.localizeValue}
|
||||
></ha-form>
|
||||
</div>
|
||||
</ha-expansion-panel>
|
||||
|
@@ -35,10 +35,6 @@ export class HaFormGrid extends LitElement implements HaFormElement {
|
||||
schema: HaFormSchema
|
||||
) => string;
|
||||
|
||||
@property({ attribute: false }) public localizeValue?: (
|
||||
key: string
|
||||
) => string;
|
||||
|
||||
public async focus() {
|
||||
await this.updateComplete;
|
||||
this.renderRoot.querySelector("ha-form")?.focus();
|
||||
@@ -69,7 +65,6 @@ export class HaFormGrid extends LitElement implements HaFormElement {
|
||||
.disabled=${this.disabled}
|
||||
.computeLabel=${this.computeLabel}
|
||||
.computeHelper=${this.computeHelper}
|
||||
.localizeValue=${this.localizeValue}
|
||||
></ha-form>
|
||||
`
|
||||
)}
|
||||
|
@@ -73,10 +73,6 @@ export class HaForm extends LitElement implements HaFormElement {
|
||||
schema: any
|
||||
) => string | undefined;
|
||||
|
||||
@property({ attribute: false }) public localizeValue?: (
|
||||
key: string
|
||||
) => string;
|
||||
|
||||
protected getFormProperties(): Record<string, any> {
|
||||
return {};
|
||||
}
|
||||
@@ -149,7 +145,6 @@ export class HaForm extends LitElement implements HaFormElement {
|
||||
.disabled=${item.disabled || this.disabled || false}
|
||||
.placeholder=${item.required ? "" : item.default}
|
||||
.helper=${this._computeHelper(item)}
|
||||
.localizeValue=${this.localizeValue}
|
||||
.required=${item.required || false}
|
||||
.context=${this._generateContext(item)}
|
||||
></ha-selector>`
|
||||
@@ -163,7 +158,6 @@ export class HaForm extends LitElement implements HaFormElement {
|
||||
localize: this.hass?.localize,
|
||||
computeLabel: this.computeLabel,
|
||||
computeHelper: this.computeHelper,
|
||||
localizeValue: this.localizeValue,
|
||||
context: this._generateContext(item),
|
||||
...this.getFormProperties(),
|
||||
})}
|
||||
|
@@ -1,58 +0,0 @@
|
||||
import { css, CSSResultGroup, html, LitElement } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { ifDefined } from "lit/directives/if-defined";
|
||||
|
||||
type HeadingBadgeType = "text" | "button";
|
||||
|
||||
@customElement("ha-heading-badge")
|
||||
export class HaBadge extends LitElement {
|
||||
@property() public type: HeadingBadgeType = "text";
|
||||
|
||||
protected render() {
|
||||
return html`
|
||||
<div
|
||||
class="heading-badge"
|
||||
role=${ifDefined(this.type === "button" ? "button" : undefined)}
|
||||
tabindex=${ifDefined(this.type === "button" ? "0" : undefined)}
|
||||
>
|
||||
<slot name="icon"></slot>
|
||||
<slot></slot>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return css`
|
||||
:host {
|
||||
color: var(--secondary-text-color);
|
||||
}
|
||||
[role="button"] {
|
||||
cursor: pointer;
|
||||
}
|
||||
.heading-badge {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
white-space: nowrap;
|
||||
align-items: center;
|
||||
gap: 3px;
|
||||
font-family: Roboto;
|
||||
font-size: 14px;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: 20px;
|
||||
letter-spacing: 0.1px;
|
||||
--mdc-icon-size: 14px;
|
||||
}
|
||||
::slotted([slot="icon"]) {
|
||||
--ha-icon-display: block;
|
||||
color: var(--icon-color, inherit);
|
||||
}
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-heading-badge": HaBadge;
|
||||
}
|
||||
}
|
@@ -2,8 +2,8 @@ import { MdListItem } from "@material/web/list/list-item";
|
||||
import { css } from "lit";
|
||||
import { customElement } from "lit/decorators";
|
||||
|
||||
@customElement("ha-md-list-item")
|
||||
export class HaMdListItem extends MdListItem {
|
||||
@customElement("ha-list-item-new")
|
||||
export class HaListItemNew extends MdListItem {
|
||||
static override styles = [
|
||||
...super.styles,
|
||||
css`
|
||||
@@ -21,6 +21,6 @@ export class HaMdListItem extends MdListItem {
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-md-list-item": HaMdListItem;
|
||||
"ha-list-item-new": HaListItemNew;
|
||||
}
|
||||
}
|
@@ -2,8 +2,8 @@ import { MdList } from "@material/web/list/list";
|
||||
import { css } from "lit";
|
||||
import { customElement } from "lit/decorators";
|
||||
|
||||
@customElement("ha-md-list")
|
||||
export class HaMdList extends MdList {
|
||||
@customElement("ha-list-new")
|
||||
export class HaListNew extends MdList {
|
||||
static override styles = [
|
||||
...super.styles,
|
||||
css`
|
||||
@@ -16,6 +16,6 @@ export class HaMdList extends MdList {
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-md-list": HaMdList;
|
||||
"ha-list-new": HaListNew;
|
||||
}
|
||||
}
|
@@ -1,250 +0,0 @@
|
||||
import { MdDialog } from "@material/web/dialog/dialog";
|
||||
import {
|
||||
type DialogAnimation,
|
||||
DIALOG_DEFAULT_CLOSE_ANIMATION,
|
||||
DIALOG_DEFAULT_OPEN_ANIMATION,
|
||||
} from "@material/web/dialog/internal/animations";
|
||||
import { css } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
|
||||
// workaround to be able to overlay an dialog with another dialog
|
||||
MdDialog.addInitializer(async (instance) => {
|
||||
await instance.updateComplete;
|
||||
|
||||
const dialogInstance = instance as MdDialog;
|
||||
|
||||
// @ts-expect-error dialog is private
|
||||
dialogInstance.dialog.prepend(dialogInstance.scrim);
|
||||
// @ts-expect-error scrim is private
|
||||
dialogInstance.scrim.style.inset = 0;
|
||||
// @ts-expect-error scrim is private
|
||||
dialogInstance.scrim.style.zIndex = 0;
|
||||
|
||||
const { getOpenAnimation, getCloseAnimation } = dialogInstance;
|
||||
dialogInstance.getOpenAnimation = () => {
|
||||
const animations = getOpenAnimation.call(this);
|
||||
animations.container = [
|
||||
...(animations.container ?? []),
|
||||
...(animations.dialog ?? []),
|
||||
];
|
||||
animations.dialog = [];
|
||||
return animations;
|
||||
};
|
||||
dialogInstance.getCloseAnimation = () => {
|
||||
const animations = getCloseAnimation.call(this);
|
||||
animations.container = [
|
||||
...(animations.container ?? []),
|
||||
...(animations.dialog ?? []),
|
||||
];
|
||||
animations.dialog = [];
|
||||
return animations;
|
||||
};
|
||||
});
|
||||
|
||||
let DIALOG_POLYFILL: Promise<typeof import("dialog-polyfill")>;
|
||||
|
||||
/**
|
||||
* Based on the home assistant design: https://design.home-assistant.io/#components/ha-dialogs
|
||||
*
|
||||
*/
|
||||
@customElement("ha-md-dialog")
|
||||
export class HaMdDialog extends MdDialog {
|
||||
/**
|
||||
* When true the dialog will not close when the user presses the esc key or press out of the dialog.
|
||||
*/
|
||||
@property({ attribute: "disable-cancel-action", type: Boolean })
|
||||
public disableCancelAction = false;
|
||||
|
||||
private _polyfillDialogRegistered = false;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.addEventListener("cancel", this._handleCancel);
|
||||
|
||||
if (typeof HTMLDialogElement !== "function") {
|
||||
this.addEventListener("open", this._handleOpen);
|
||||
|
||||
if (!DIALOG_POLYFILL) {
|
||||
DIALOG_POLYFILL = import("dialog-polyfill");
|
||||
}
|
||||
}
|
||||
|
||||
// if browser doesn't support animate API disable open/close animations
|
||||
if (this.animate === undefined) {
|
||||
this.quick = true;
|
||||
}
|
||||
|
||||
// if browser doesn't support animate API disable open/close animations
|
||||
if (this.animate === undefined) {
|
||||
this.quick = true;
|
||||
}
|
||||
}
|
||||
|
||||
// prevent open in older browsers and wait for polyfill to load
|
||||
private async _handleOpen(openEvent: Event) {
|
||||
openEvent.preventDefault();
|
||||
|
||||
if (this._polyfillDialogRegistered) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._polyfillDialogRegistered = true;
|
||||
this._loadPolyfillStylesheet("/static/polyfills/dialog-polyfill.css");
|
||||
const dialog = this.shadowRoot?.querySelector(
|
||||
"dialog"
|
||||
) as HTMLDialogElement;
|
||||
|
||||
const dialogPolyfill = await DIALOG_POLYFILL;
|
||||
dialogPolyfill.default.registerDialog(dialog);
|
||||
this.removeEventListener("open", this._handleOpen);
|
||||
|
||||
this.show();
|
||||
}
|
||||
|
||||
private async _loadPolyfillStylesheet(href) {
|
||||
const link = document.createElement("link");
|
||||
link.rel = "stylesheet";
|
||||
link.href = href;
|
||||
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
link.onload = () => resolve();
|
||||
link.onerror = () =>
|
||||
reject(new Error(`Stylesheet failed to load: ${href}`));
|
||||
|
||||
this.shadowRoot?.appendChild(link);
|
||||
});
|
||||
}
|
||||
|
||||
_handleCancel(closeEvent: Event) {
|
||||
if (this.disableCancelAction) {
|
||||
closeEvent.preventDefault();
|
||||
const dialogElement = this.shadowRoot?.querySelector("dialog .container");
|
||||
if (this.animate !== undefined) {
|
||||
dialogElement?.animate(
|
||||
[
|
||||
{
|
||||
transform: "rotate(-1deg)",
|
||||
"animation-timing-function": "ease-in",
|
||||
},
|
||||
{
|
||||
transform: "rotate(1.5deg)",
|
||||
"animation-timing-function": "ease-out",
|
||||
},
|
||||
{
|
||||
transform: "rotate(0deg)",
|
||||
"animation-timing-function": "ease-in",
|
||||
},
|
||||
],
|
||||
{
|
||||
duration: 200,
|
||||
iterations: 2,
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static override styles = [
|
||||
...super.styles,
|
||||
css`
|
||||
:host {
|
||||
--md-dialog-container-color: var(--card-background-color);
|
||||
--md-dialog-headline-color: var(--primary-text-color);
|
||||
--md-dialog-supporting-text-color: var(--primary-text-color);
|
||||
--md-sys-color-scrim: #000000;
|
||||
|
||||
--md-dialog-headline-weight: 400;
|
||||
--md-dialog-headline-size: 1.574rem;
|
||||
--md-dialog-supporting-text-size: 1rem;
|
||||
--md-dialog-supporting-text-line-height: 1.5rem;
|
||||
}
|
||||
|
||||
:host([type="alert"]) {
|
||||
min-width: 320px;
|
||||
}
|
||||
|
||||
:host(:not([type="alert"])) {
|
||||
@media all and (max-width: 450px), all and (max-height: 500px) {
|
||||
min-width: calc(
|
||||
100vw - env(safe-area-inset-right) - env(safe-area-inset-left)
|
||||
);
|
||||
max-width: calc(
|
||||
100vw - env(safe-area-inset-right) - env(safe-area-inset-left)
|
||||
);
|
||||
min-height: 100%;
|
||||
max-height: 100%;
|
||||
--md-dialog-container-shape: 0;
|
||||
}
|
||||
}
|
||||
|
||||
:host ::slotted(ha-dialog-header) {
|
||||
display: contents;
|
||||
}
|
||||
|
||||
slot[name="content"]::slotted(*) {
|
||||
padding: var(--dialog-content-padding, 24px);
|
||||
}
|
||||
.scrim {
|
||||
z-index: 10; // overlay navigation
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
||||
// by default the dialog open/close animation will be from/to the top
|
||||
// but if we have a special mobile dialog which is at the bottom of the screen, an from bottom animation can be used:
|
||||
const OPEN_FROM_BOTTOM_ANIMATION: DialogAnimation = {
|
||||
...DIALOG_DEFAULT_OPEN_ANIMATION,
|
||||
dialog: [
|
||||
[
|
||||
// Dialog slide up
|
||||
[{ transform: "translateY(50px)" }, { transform: "translateY(0)" }],
|
||||
{ duration: 500, easing: "cubic-bezier(.3,0,0,1)" },
|
||||
],
|
||||
],
|
||||
container: [
|
||||
[
|
||||
// Container fade in
|
||||
[{ opacity: 0 }, { opacity: 1 }],
|
||||
{ duration: 50, easing: "linear", pseudoElement: "::before" },
|
||||
],
|
||||
],
|
||||
};
|
||||
|
||||
const CLOSE_TO_BOTTOM_ANIMATION: DialogAnimation = {
|
||||
...DIALOG_DEFAULT_CLOSE_ANIMATION,
|
||||
dialog: [
|
||||
[
|
||||
// Dialog slide down
|
||||
[{ transform: "translateY(0)" }, { transform: "translateY(50px)" }],
|
||||
{ duration: 150, easing: "cubic-bezier(.3,0,0,1)" },
|
||||
],
|
||||
],
|
||||
container: [
|
||||
[
|
||||
// Container fade out
|
||||
[{ opacity: "1" }, { opacity: "0" }],
|
||||
{ delay: 100, duration: 50, easing: "linear", pseudoElement: "::before" },
|
||||
],
|
||||
],
|
||||
};
|
||||
|
||||
export const getMobileOpenFromBottomAnimation = () => {
|
||||
const matches = window.matchMedia(
|
||||
"all and (max-width: 450px), all and (max-height: 500px)"
|
||||
).matches;
|
||||
return matches ? OPEN_FROM_BOTTOM_ANIMATION : DIALOG_DEFAULT_OPEN_ANIMATION;
|
||||
};
|
||||
|
||||
export const getMobileCloseToBottomAnimation = () => {
|
||||
const matches = window.matchMedia(
|
||||
"all and (max-width: 450px), all and (max-height: 500px)"
|
||||
).matches;
|
||||
return matches ? CLOSE_TO_BOTTOM_ANIMATION : DIALOG_DEFAULT_CLOSE_ANIMATION;
|
||||
};
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-md-dialog": HaMdDialog;
|
||||
}
|
||||
}
|
@@ -1,21 +0,0 @@
|
||||
import { MdDivider } from "@material/web/divider/divider";
|
||||
import { css } from "lit";
|
||||
import { customElement } from "lit/decorators";
|
||||
|
||||
@customElement("ha-md-divider")
|
||||
export class HaMdDivider extends MdDivider {
|
||||
static override styles = [
|
||||
...super.styles,
|
||||
css`
|
||||
:host {
|
||||
--md-divider-color: var(--divider-color);
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-md-divider": HaMdDivider;
|
||||
}
|
||||
}
|
@@ -2,8 +2,8 @@ import { MdMenuItem } from "@material/web/menu/menu-item";
|
||||
import { css } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
|
||||
@customElement("ha-md-menu-item")
|
||||
export class HaMdMenuItem extends MdMenuItem {
|
||||
@customElement("ha-menu-item")
|
||||
export class HaMenuItem extends MdMenuItem {
|
||||
@property({ attribute: false }) clickAction?: (item?: HTMLElement) => void;
|
||||
|
||||
static override styles = [
|
||||
@@ -41,6 +41,6 @@ export class HaMdMenuItem extends MdMenuItem {
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-md-menu-item": HaMdMenuItem;
|
||||
"ha-menu-item": HaMenuItem;
|
||||
}
|
||||
}
|
@@ -6,7 +6,7 @@ import {
|
||||
} from "@material/web/menu/internal/controllers/shared";
|
||||
import { css } from "lit";
|
||||
import { customElement } from "lit/decorators";
|
||||
import type { HaMdMenuItem } from "./ha-md-menu-item";
|
||||
import type { HaMenuItem } from "./ha-menu-item";
|
||||
|
||||
@customElement("ha-menu")
|
||||
export class HaMenu extends MdMenu {
|
||||
@@ -22,7 +22,7 @@ export class HaMenu extends MdMenu {
|
||||
) {
|
||||
return;
|
||||
}
|
||||
(ev.detail.initiator as HaMdMenuItem).clickAction?.(ev.detail.initiator);
|
||||
(ev.detail.initiator as HaMenuItem).clickAction?.(ev.detail.initiator);
|
||||
}
|
||||
|
||||
static override styles = [
|
||||
|
@@ -24,11 +24,9 @@ export class HaOutlinedField extends MdOutlinedField {
|
||||
}
|
||||
.with-start .start {
|
||||
margin-inline-end: var(--ha-outlined-field-start-margin, 4px);
|
||||
margin-inline-start: initial;
|
||||
}
|
||||
.with-end .end {
|
||||
margin-inline-start: var(--ha-outlined-field-end-margin, 4px);
|
||||
margin-inline-end: initial;
|
||||
}
|
||||
`,
|
||||
];
|
||||
|
@@ -1,185 +0,0 @@
|
||||
import { TextAreaCharCounter } from "@material/mwc-textfield/mwc-textfield-base";
|
||||
import { mdiEye, mdiEyeOff } from "@mdi/js";
|
||||
import { LitElement, css, html } from "lit";
|
||||
import {
|
||||
customElement,
|
||||
eventOptions,
|
||||
property,
|
||||
query,
|
||||
state,
|
||||
} from "lit/decorators";
|
||||
import { HomeAssistant } from "../types";
|
||||
import "./ha-icon-button";
|
||||
import "./ha-textfield";
|
||||
import type { HaTextField } from "./ha-textfield";
|
||||
|
||||
@customElement("ha-password-field")
|
||||
export class HaPasswordField extends LitElement {
|
||||
@property({ attribute: false }) public hass?: HomeAssistant;
|
||||
|
||||
@property({ type: Boolean }) public invalid?: boolean;
|
||||
|
||||
@property({ attribute: "error-message" }) public errorMessage?: string;
|
||||
|
||||
@property({ type: Boolean }) public icon = false;
|
||||
|
||||
@property({ type: Boolean }) public iconTrailing = false;
|
||||
|
||||
@property() public autocomplete?: string;
|
||||
|
||||
@property() public autocorrect?: string;
|
||||
|
||||
@property({ attribute: "input-spellcheck" })
|
||||
public inputSpellcheck?: string;
|
||||
|
||||
@property({ type: String }) value = "";
|
||||
|
||||
@property({ type: String }) placeholder = "";
|
||||
|
||||
@property({ type: String }) label = "";
|
||||
|
||||
@property({ type: Boolean, reflect: true }) disabled = false;
|
||||
|
||||
@property({ type: Boolean }) required = false;
|
||||
|
||||
@property({ type: Number }) minLength = -1;
|
||||
|
||||
@property({ type: Number }) maxLength = -1;
|
||||
|
||||
@property({ type: Boolean, reflect: true }) outlined = false;
|
||||
|
||||
@property({ type: String }) helper = "";
|
||||
|
||||
@property({ type: Boolean }) validateOnInitialRender = false;
|
||||
|
||||
@property({ type: String }) validationMessage = "";
|
||||
|
||||
@property({ type: Boolean }) autoValidate = false;
|
||||
|
||||
@property({ type: String }) pattern = "";
|
||||
|
||||
@property({ type: Number }) size: number | null = null;
|
||||
|
||||
@property({ type: Boolean }) helperPersistent = false;
|
||||
|
||||
@property({ type: Boolean }) charCounter: boolean | TextAreaCharCounter =
|
||||
false;
|
||||
|
||||
@property({ type: Boolean }) endAligned = false;
|
||||
|
||||
@property({ type: String }) prefix = "";
|
||||
|
||||
@property({ type: String }) suffix = "";
|
||||
|
||||
@property({ type: String }) name = "";
|
||||
|
||||
@property({ type: String, attribute: "input-mode" })
|
||||
inputMode!: string;
|
||||
|
||||
@property({ type: Boolean }) readOnly = false;
|
||||
|
||||
@property({ type: String }) autocapitalize = "";
|
||||
|
||||
@state() private _unmaskedPassword = false;
|
||||
|
||||
@query("ha-textfield") private _textField!: HaTextField;
|
||||
|
||||
protected render() {
|
||||
return html`<ha-textfield
|
||||
.invalid=${this.invalid}
|
||||
.errorMessage=${this.errorMessage}
|
||||
.icon=${this.icon}
|
||||
.iconTrailing=${this.iconTrailing}
|
||||
.autocomplete=${this.autocomplete}
|
||||
.autocorrect=${this.autocorrect}
|
||||
.inputSpellcheck=${this.inputSpellcheck}
|
||||
.value=${this.value}
|
||||
.placeholder=${this.placeholder}
|
||||
.label=${this.label}
|
||||
.disabled=${this.disabled}
|
||||
.required=${this.required}
|
||||
.minLength=${this.minLength}
|
||||
.maxLength=${this.maxLength}
|
||||
.outlined=${this.outlined}
|
||||
.helper=${this.helper}
|
||||
.validateOnInitialRender=${this.validateOnInitialRender}
|
||||
.validationMessage=${this.validationMessage}
|
||||
.autoValidate=${this.autoValidate}
|
||||
.pattern=${this.pattern}
|
||||
.size=${this.size}
|
||||
.helperPersistent=${this.helperPersistent}
|
||||
.charCounter=${this.charCounter}
|
||||
.endAligned=${this.endAligned}
|
||||
.prefix=${this.prefix}
|
||||
.name=${this.name}
|
||||
.inputMode=${this.inputMode}
|
||||
.readOnly=${this.readOnly}
|
||||
.autocapitalize=${this.autocapitalize}
|
||||
.type=${this._unmaskedPassword ? "text" : "password"}
|
||||
.suffix=${html`<div style="width: 24px"></div>`}
|
||||
@input=${this._handleInputChange}
|
||||
></ha-textfield>
|
||||
<ha-icon-button
|
||||
toggles
|
||||
.label=${this.hass?.localize(
|
||||
this._unmaskedPassword
|
||||
? "ui.components.selectors.text.hide_password"
|
||||
: "ui.components.selectors.text.show_password"
|
||||
) || (this._unmaskedPassword ? "Hide password" : "Show password")}
|
||||
@click=${this._toggleUnmaskedPassword}
|
||||
.path=${this._unmaskedPassword ? mdiEyeOff : mdiEye}
|
||||
></ha-icon-button>`;
|
||||
}
|
||||
|
||||
public checkValidity(): boolean {
|
||||
return this._textField.checkValidity();
|
||||
}
|
||||
|
||||
public reportValidity(): boolean {
|
||||
return this._textField.reportValidity();
|
||||
}
|
||||
|
||||
public setCustomValidity(message: string): void {
|
||||
return this._textField.setCustomValidity(message);
|
||||
}
|
||||
|
||||
public layout(): Promise<void> {
|
||||
return this._textField.layout();
|
||||
}
|
||||
|
||||
private _toggleUnmaskedPassword(): void {
|
||||
this._unmaskedPassword = !this._unmaskedPassword;
|
||||
}
|
||||
|
||||
@eventOptions({ passive: true })
|
||||
private _handleInputChange(ev) {
|
||||
this.value = ev.target.value;
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
:host {
|
||||
display: block;
|
||||
position: relative;
|
||||
}
|
||||
ha-textfield {
|
||||
width: 100%;
|
||||
}
|
||||
ha-icon-button {
|
||||
position: absolute;
|
||||
top: 8px;
|
||||
right: 8px;
|
||||
inset-inline-start: initial;
|
||||
inset-inline-end: 8px;
|
||||
--mdc-icon-button-size: 40px;
|
||||
--mdc-icon-size: 20px;
|
||||
color: var(--secondary-text-color);
|
||||
direction: var(--direction);
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-password-field": HaPasswordField;
|
||||
}
|
||||
}
|
@@ -1,7 +1,6 @@
|
||||
import { css, CSSResultGroup, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { Action, migrateAutomationAction } from "../../data/script";
|
||||
import { Action } from "../../data/script";
|
||||
import { ActionSelector } from "../../data/selector";
|
||||
import "../../panels/config/automation/action/ha-automation-action";
|
||||
import { HomeAssistant } from "../../types";
|
||||
@@ -18,19 +17,12 @@ export class HaActionSelector extends LitElement {
|
||||
|
||||
@property({ type: Boolean, reflect: true }) public disabled = false;
|
||||
|
||||
private _actions = memoizeOne((action: Action | undefined) => {
|
||||
if (!action) {
|
||||
return [];
|
||||
}
|
||||
return migrateAutomationAction(action);
|
||||
});
|
||||
|
||||
protected render() {
|
||||
return html`
|
||||
${this.label ? html`<label>${this.label}</label>` : nothing}
|
||||
<ha-automation-action
|
||||
.disabled=${this.disabled}
|
||||
.actions=${this._actions(this.value)}
|
||||
.actions=${this.value || []}
|
||||
.hass=${this.hass}
|
||||
.path=${this.selector.action?.path}
|
||||
></ha-automation-action>
|
||||
|
@@ -31,7 +31,7 @@ export class HaColorRGBSelector extends LitElement {
|
||||
.label=${this.label || ""}
|
||||
.required=${this.required}
|
||||
.helper=${this.helper}
|
||||
.disabled=${this.disabled}
|
||||
.disalbled=${this.disabled}
|
||||
@change=${this._valueChanged}
|
||||
></ha-textfield>
|
||||
`;
|
||||
|
@@ -7,7 +7,12 @@ import "../ha-code-editor";
|
||||
import "../ha-input-helper-text";
|
||||
import "../ha-alert";
|
||||
|
||||
const WARNING_STRINGS = ["template:", "sensor:", "state:", "trigger: template"];
|
||||
const WARNING_STRINGS = [
|
||||
"template:",
|
||||
"sensor:",
|
||||
"state:",
|
||||
"platform: template",
|
||||
];
|
||||
|
||||
@customElement("ha-selector-template")
|
||||
export class HaTemplateSelector extends LitElement {
|
||||
|
@@ -1,7 +1,6 @@
|
||||
import { css, CSSResultGroup, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { migrateAutomationTrigger, Trigger } from "../../data/automation";
|
||||
import { Trigger } from "../../data/automation";
|
||||
import { TriggerSelector } from "../../data/selector";
|
||||
import "../../panels/config/automation/trigger/ha-automation-trigger";
|
||||
import { HomeAssistant } from "../../types";
|
||||
@@ -18,19 +17,12 @@ export class HaTriggerSelector extends LitElement {
|
||||
|
||||
@property({ type: Boolean, reflect: true }) public disabled = false;
|
||||
|
||||
private _triggers = memoizeOne((trigger: Trigger | undefined) => {
|
||||
if (!trigger) {
|
||||
return [];
|
||||
}
|
||||
return migrateAutomationTrigger(trigger);
|
||||
});
|
||||
|
||||
protected render() {
|
||||
return html`
|
||||
${this.label ? html`<label>${this.label}</label>` : nothing}
|
||||
<ha-automation-trigger
|
||||
.disabled=${this.disabled}
|
||||
.triggers=${this._triggers(this.value)}
|
||||
.triggers=${this.value || []}
|
||||
.hass=${this.hass}
|
||||
.path=${this.selector.trigger?.path}
|
||||
></ha-automation-trigger>
|
||||
|
@@ -24,8 +24,6 @@ export class HaSelectorUiColor extends LitElement {
|
||||
.hass=${this.hass}
|
||||
.value=${this.value}
|
||||
.helper=${this.helper}
|
||||
.includeNone=${this.selector.ui_color?.include_none}
|
||||
.includeState=${this.selector.ui_color?.include_state}
|
||||
.defaultColor=${this.selector.ui_color?.default_color}
|
||||
@value-changed=${this._valueChanged}
|
||||
></ha-color-picker>
|
||||
|
@@ -240,24 +240,12 @@ export class HaServiceControl extends LitElement {
|
||||
...value,
|
||||
selector: value.selector as Selector | undefined,
|
||||
}));
|
||||
|
||||
const hasSelector: string[] = [];
|
||||
fields.forEach((field) => {
|
||||
if ((field as any).fields) {
|
||||
Object.entries((field as any).fields).forEach(([key, subField]) => {
|
||||
if ((subField as any).selector) {
|
||||
hasSelector.push(key);
|
||||
}
|
||||
});
|
||||
} else if (field.selector) {
|
||||
hasSelector.push(field.key);
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
...serviceDomains[domain][serviceName],
|
||||
fields,
|
||||
hasSelector,
|
||||
hasSelector: fields.length
|
||||
? fields.filter((field) => field.selector).map((field) => field.key)
|
||||
: [],
|
||||
};
|
||||
}
|
||||
);
|
||||
@@ -805,8 +793,7 @@ export class HaServiceControl extends LitElement {
|
||||
const value = ev.detail.value;
|
||||
if (
|
||||
this._value?.data?.[key] === value ||
|
||||
((!this._value?.data || !(key in this._value.data)) &&
|
||||
(value === "" || value === undefined))
|
||||
(!this._value?.data?.[key] && (value === "" || value === undefined))
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
@@ -43,13 +43,6 @@ export class HaSortable extends LitElement {
|
||||
@property({ type: String, attribute: "handle-selector" })
|
||||
public handleSelector?: string;
|
||||
|
||||
/**
|
||||
* Selectors that do not lead to dragging (String or Function)
|
||||
* https://github.com/SortableJS/Sortable?tab=readme-ov-file#filter-option
|
||||
* */
|
||||
@property({ type: String, attribute: "filter" })
|
||||
public filter?: string;
|
||||
|
||||
@property({ type: String })
|
||||
public group?: string | SortableInstance.GroupOptions;
|
||||
|
||||
@@ -152,9 +145,6 @@ export class HaSortable extends LitElement {
|
||||
if (this.group) {
|
||||
options.group = this.group;
|
||||
}
|
||||
if (this.filter) {
|
||||
options.filter = this.filter;
|
||||
}
|
||||
|
||||
this._sortable = new Sortable(container, options);
|
||||
}
|
||||
|
@@ -35,6 +35,10 @@ import {
|
||||
computeDeviceName,
|
||||
} from "../data/device_registry";
|
||||
import { EntityRegistryDisplayEntry } from "../data/entity_registry";
|
||||
import {
|
||||
FloorRegistryEntry,
|
||||
subscribeFloorRegistry,
|
||||
} from "../data/floor_registry";
|
||||
import {
|
||||
LabelRegistryEntry,
|
||||
subscribeLabelRegistry,
|
||||
@@ -99,12 +103,17 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
|
||||
|
||||
@query(".add-container", true) private _addContainer?: HTMLDivElement;
|
||||
|
||||
@state() private _floors?: FloorRegistryEntry[];
|
||||
|
||||
@state() private _labels?: LabelRegistryEntry[];
|
||||
|
||||
private _opened = false;
|
||||
|
||||
protected hassSubscribe(): (UnsubscribeFunc | Promise<UnsubscribeFunc>)[] {
|
||||
return [
|
||||
subscribeFloorRegistry(this.hass.connection, (floors) => {
|
||||
this._floors = floors;
|
||||
}),
|
||||
subscribeLabelRegistry(this.hass.connection, (labels) => {
|
||||
this._labels = labels;
|
||||
}),
|
||||
@@ -123,7 +132,9 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
|
||||
<div class="mdc-chip-set items">
|
||||
${this.value?.floor_id
|
||||
? ensureArray(this.value.floor_id).map((floor_id) => {
|
||||
const floor = this.hass.floors[floor_id];
|
||||
const floor = this._floors?.find(
|
||||
(flr) => flr.floor_id === floor_id
|
||||
);
|
||||
return this._renderChip(
|
||||
"floor_id",
|
||||
floor_id,
|
||||
|
@@ -6,7 +6,7 @@ import { mainWindow } from "../common/dom/get_main_window";
|
||||
|
||||
@customElement("ha-textfield")
|
||||
export class HaTextField extends TextFieldBase {
|
||||
@property({ type: Boolean }) public invalid?: boolean;
|
||||
@property({ type: Boolean }) public invalid = false;
|
||||
|
||||
@property({ attribute: "error-message" }) public errorMessage?: string;
|
||||
|
||||
@@ -28,24 +28,14 @@ export class HaTextField extends TextFieldBase {
|
||||
override updated(changedProperties: PropertyValues) {
|
||||
super.updated(changedProperties);
|
||||
if (
|
||||
changedProperties.has("invalid") ||
|
||||
(changedProperties.has("invalid") &&
|
||||
(this.invalid || changedProperties.get("invalid") !== undefined)) ||
|
||||
changedProperties.has("errorMessage")
|
||||
) {
|
||||
this.setCustomValidity(
|
||||
this.invalid
|
||||
? this.errorMessage || this.validationMessage || "Invalid"
|
||||
: ""
|
||||
this.invalid ? this.errorMessage || "Invalid" : ""
|
||||
);
|
||||
if (
|
||||
this.invalid ||
|
||||
this.validateOnInitialRender ||
|
||||
(changedProperties.has("invalid") &&
|
||||
changedProperties.get("invalid") !== undefined)
|
||||
) {
|
||||
// Only report validity if the field is invalid or the invalid state has changed from
|
||||
// true to false to prevent setting empty required fields to invalid on first render
|
||||
this.reportValidity();
|
||||
}
|
||||
this.reportValidity();
|
||||
}
|
||||
if (changedProperties.has("autocomplete")) {
|
||||
if (this.autocomplete) {
|
||||
@@ -119,7 +109,7 @@ export class HaTextField extends TextFieldBase {
|
||||
color: var(--secondary-text-color);
|
||||
}
|
||||
|
||||
.mdc-text-field:not(.mdc-text-field--disabled) .mdc-text-field__icon {
|
||||
.mdc-text-field__icon {
|
||||
color: var(--secondary-text-color);
|
||||
}
|
||||
|
||||
|
@@ -1,4 +1,3 @@
|
||||
/* eslint-disable no-console */
|
||||
import {
|
||||
css,
|
||||
CSSResultGroup,
|
||||
@@ -7,14 +6,11 @@ import {
|
||||
PropertyValues,
|
||||
TemplateResult,
|
||||
} from "lit";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import { ifDefined } from "lit/directives/if-defined";
|
||||
import { customElement, property, state, query } from "lit/decorators";
|
||||
import { isComponentLoaded } from "../common/config/is_component_loaded";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import {
|
||||
fetchWebRtcClientConfiguration,
|
||||
handleWebRtcOffer,
|
||||
WebRtcAnswer,
|
||||
} from "../data/camera";
|
||||
import { handleWebRtcOffer, WebRtcAnswer } from "../data/camera";
|
||||
import { fetchWebRtcSettings } from "../data/rtsp_to_webrtc";
|
||||
import type { HomeAssistant } from "../types";
|
||||
import "./ha-alert";
|
||||
|
||||
@@ -41,11 +37,12 @@ class HaWebRtcPlayer extends LitElement {
|
||||
@property({ type: Boolean, attribute: "playsinline" })
|
||||
public playsInline = false;
|
||||
|
||||
@property({ attribute: "poster-url" }) public posterUrl?: string;
|
||||
@property() public posterUrl!: string;
|
||||
|
||||
@state() private _error?: string;
|
||||
|
||||
@query("#remote-stream", true) private _videoEl!: HTMLVideoElement;
|
||||
// don't cache this, as we remove it on disconnects
|
||||
@query("#remote-stream") private _videoEl!: HTMLVideoElement;
|
||||
|
||||
private _peerConnection?: RTCPeerConnection;
|
||||
|
||||
@@ -62,7 +59,7 @@ class HaWebRtcPlayer extends LitElement {
|
||||
.muted=${this.muted}
|
||||
?playsinline=${this.playsInline}
|
||||
?controls=${this.controls}
|
||||
poster=${ifDefined(this.posterUrl)}
|
||||
.poster=${this.posterUrl}
|
||||
@loadeddata=${this._loadedData}
|
||||
></video>
|
||||
`;
|
||||
@@ -84,30 +81,20 @@ class HaWebRtcPlayer extends LitElement {
|
||||
if (!changedProperties.has("entityid")) {
|
||||
return;
|
||||
}
|
||||
if (!this._videoEl) {
|
||||
return;
|
||||
}
|
||||
this._startWebRtc();
|
||||
}
|
||||
|
||||
private async _startWebRtc(): Promise<void> {
|
||||
console.time("WebRTC");
|
||||
|
||||
this._error = undefined;
|
||||
|
||||
console.timeLog("WebRTC", "start clientConfig");
|
||||
|
||||
const clientConfig = await fetchWebRtcClientConfiguration(
|
||||
this.hass,
|
||||
this.entityid
|
||||
);
|
||||
|
||||
console.timeLog("WebRTC", "end clientConfig", clientConfig);
|
||||
|
||||
const peerConnection = new RTCPeerConnection(clientConfig.configuration);
|
||||
|
||||
if (clientConfig.dataChannel) {
|
||||
// Some cameras (such as nest) require a data channel to establish a stream
|
||||
// however, not used by any integrations.
|
||||
peerConnection.createDataChannel(clientConfig.dataChannel);
|
||||
}
|
||||
const configuration = await this._fetchPeerConfiguration();
|
||||
const peerConnection = new RTCPeerConnection(configuration);
|
||||
// Some cameras (such as nest) require a data channel to establish a stream
|
||||
// however, not used by any integrations.
|
||||
peerConnection.createDataChannel("dataSendChannel");
|
||||
peerConnection.addTransceiver("audio", { direction: "recvonly" });
|
||||
peerConnection.addTransceiver("video", { direction: "recvonly" });
|
||||
|
||||
@@ -115,48 +102,30 @@ class HaWebRtcPlayer extends LitElement {
|
||||
offerToReceiveAudio: true,
|
||||
offerToReceiveVideo: true,
|
||||
};
|
||||
|
||||
console.timeLog("WebRTC", "start createOffer", offerOptions);
|
||||
|
||||
const offer: RTCSessionDescriptionInit =
|
||||
await peerConnection.createOffer(offerOptions);
|
||||
|
||||
console.timeLog("WebRTC", "end createOffer", offer);
|
||||
|
||||
console.timeLog("WebRTC", "start setLocalDescription");
|
||||
|
||||
await peerConnection.setLocalDescription(offer);
|
||||
|
||||
console.timeLog("WebRTC", "end setLocalDescription");
|
||||
|
||||
console.timeLog("WebRTC", "start iceResolver");
|
||||
|
||||
let candidates = ""; // Build an Offer SDP string with ice candidates
|
||||
const iceResolver = new Promise<void>((resolve) => {
|
||||
peerConnection.addEventListener("icecandidate", (event) => {
|
||||
if (!event.candidate?.candidate) {
|
||||
peerConnection.addEventListener("icecandidate", async (event) => {
|
||||
if (!event.candidate) {
|
||||
resolve(); // Gathering complete
|
||||
return;
|
||||
}
|
||||
console.timeLog("WebRTC", "iceResolver candidate", event.candidate);
|
||||
candidates += `a=${event.candidate.candidate}\r\n`;
|
||||
});
|
||||
});
|
||||
await iceResolver;
|
||||
|
||||
console.timeLog("WebRTC", "end iceResolver", candidates);
|
||||
|
||||
const offer_sdp = offer.sdp! + candidates;
|
||||
|
||||
let webRtcAnswer: WebRtcAnswer;
|
||||
try {
|
||||
console.timeLog("WebRTC", "start WebRTCOffer", offer_sdp);
|
||||
webRtcAnswer = await handleWebRtcOffer(
|
||||
this.hass,
|
||||
this.entityid,
|
||||
offer_sdp
|
||||
);
|
||||
console.timeLog("WebRTC", "end webRtcOffer", webRtcAnswer);
|
||||
} catch (err: any) {
|
||||
this._error = "Failed to start WebRTC stream: " + err.message;
|
||||
peerConnection.close();
|
||||
@@ -166,7 +135,6 @@ class HaWebRtcPlayer extends LitElement {
|
||||
// Setup callbacks to render remote stream once media tracks are discovered.
|
||||
const remoteStream = new MediaStream();
|
||||
peerConnection.addEventListener("track", (event) => {
|
||||
console.timeLog("WebRTC", "track", event);
|
||||
remoteStream.addTrack(event.track);
|
||||
this._videoEl.srcObject = remoteStream;
|
||||
});
|
||||
@@ -178,9 +146,7 @@ class HaWebRtcPlayer extends LitElement {
|
||||
sdp: webRtcAnswer.answer,
|
||||
});
|
||||
try {
|
||||
console.timeLog("WebRTC", "start setRemoteDescription", remoteDesc);
|
||||
await peerConnection.setRemoteDescription(remoteDesc);
|
||||
console.timeLog("WebRTC", "end setRemoteDescription");
|
||||
} catch (err: any) {
|
||||
this._error = "Failed to connect WebRTC stream: " + err.message;
|
||||
peerConnection.close();
|
||||
@@ -189,6 +155,23 @@ class HaWebRtcPlayer extends LitElement {
|
||||
this._peerConnection = peerConnection;
|
||||
}
|
||||
|
||||
private async _fetchPeerConfiguration(): Promise<RTCConfiguration> {
|
||||
if (!isComponentLoaded(this.hass!, "rtsp_to_webrtc")) {
|
||||
return {};
|
||||
}
|
||||
const settings = await fetchWebRtcSettings(this.hass!);
|
||||
if (!settings || !settings.stun_server) {
|
||||
return {};
|
||||
}
|
||||
return {
|
||||
iceServers: [
|
||||
{
|
||||
urls: [`stun:${settings.stun_server!}`],
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
private _cleanUp() {
|
||||
if (this._remoteStream) {
|
||||
this._remoteStream.getTracks().forEach((track) => {
|
||||
@@ -207,8 +190,6 @@ class HaWebRtcPlayer extends LitElement {
|
||||
}
|
||||
|
||||
private _loadedData() {
|
||||
console.timeLog("WebRTC", "loadedData");
|
||||
console.timeEnd("WebRTC");
|
||||
// @ts-ignore
|
||||
fireEvent(this, "load");
|
||||
}
|
||||
|
@@ -7,18 +7,16 @@ import {
|
||||
nothing,
|
||||
PropertyValues,
|
||||
} from "lit";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import type { HomeAssistant } from "../types";
|
||||
import { haStyle } from "../resources/styles";
|
||||
import "./ha-code-editor";
|
||||
import { showToast } from "../util/toast";
|
||||
import { copyToClipboard } from "../common/util/copy-clipboard";
|
||||
import type { HaCodeEditor } from "./ha-code-editor";
|
||||
import "./ha-button";
|
||||
|
||||
const isEmpty = (obj: Record<string, unknown>): boolean => {
|
||||
if (typeof obj !== "object" || obj === null) {
|
||||
if (typeof obj !== "object") {
|
||||
return false;
|
||||
}
|
||||
for (const key in obj) {
|
||||
@@ -55,17 +53,16 @@ export class HaYamlEditor extends LitElement {
|
||||
|
||||
@state() private _yaml = "";
|
||||
|
||||
@query("ha-code-editor") _codeEditor?: HaCodeEditor;
|
||||
|
||||
public setValue(value): void {
|
||||
try {
|
||||
this._yaml = !isEmpty(value)
|
||||
? dump(value, {
|
||||
schema: this.yamlSchema,
|
||||
quotingType: '"',
|
||||
noRefs: true,
|
||||
})
|
||||
: "";
|
||||
this._yaml =
|
||||
value && !isEmpty(value)
|
||||
? dump(value, {
|
||||
schema: this.yamlSchema,
|
||||
quotingType: '"',
|
||||
noRefs: true,
|
||||
})
|
||||
: "";
|
||||
} catch (err: any) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error(err, value);
|
||||
@@ -74,7 +71,7 @@ export class HaYamlEditor extends LitElement {
|
||||
}
|
||||
|
||||
protected firstUpdated(): void {
|
||||
if (this.defaultValue !== undefined) {
|
||||
if (this.defaultValue) {
|
||||
this.setValue(this.defaultValue);
|
||||
}
|
||||
}
|
||||
@@ -86,12 +83,6 @@ export class HaYamlEditor extends LitElement {
|
||||
}
|
||||
}
|
||||
|
||||
public focus(): void {
|
||||
if (this._codeEditor?.codemirror) {
|
||||
this._codeEditor?.codemirror.focus();
|
||||
}
|
||||
}
|
||||
|
||||
protected render() {
|
||||
if (this._yaml === undefined) {
|
||||
return nothing;
|
||||
@@ -99,7 +90,7 @@ export class HaYamlEditor extends LitElement {
|
||||
return html`
|
||||
${this.label
|
||||
? html`<p>${this.label}${this.required ? " *" : ""}</p>`
|
||||
: nothing}
|
||||
: ""}
|
||||
<ha-code-editor
|
||||
.hass=${this.hass}
|
||||
.value=${this._yaml}
|
||||
@@ -112,20 +103,16 @@ export class HaYamlEditor extends LitElement {
|
||||
dir="ltr"
|
||||
></ha-code-editor>
|
||||
${this.copyClipboard || this.hasExtraActions
|
||||
? html`
|
||||
<div class="card-actions">
|
||||
${this.copyClipboard
|
||||
? html`
|
||||
<ha-button @click=${this._copyYaml}>
|
||||
${this.hass.localize(
|
||||
"ui.components.yaml-editor.copy_to_clipboard"
|
||||
)}
|
||||
</ha-button>
|
||||
`
|
||||
: nothing}
|
||||
<slot name="extra-actions"></slot>
|
||||
</div>
|
||||
`
|
||||
? html`<div class="card-actions">
|
||||
${this.copyClipboard
|
||||
? html` <mwc-button @click=${this._copyYaml}>
|
||||
${this.hass.localize(
|
||||
"ui.components.yaml-editor.copy_to_clipboard"
|
||||
)}
|
||||
</mwc-button>`
|
||||
: nothing}
|
||||
<slot name="extra-actions"></slot>
|
||||
</div>`
|
||||
: nothing}
|
||||
`;
|
||||
}
|
||||
|
@@ -22,7 +22,7 @@ import { LitElement, PropertyValues, css, html, nothing } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { ensureArray } from "../../common/array/ensure-array";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import { Condition, Trigger, flattenTriggers } from "../../data/automation";
|
||||
import { Condition, Trigger } from "../../data/automation";
|
||||
import {
|
||||
Action,
|
||||
ChooseAction,
|
||||
@@ -94,7 +94,7 @@ export class HatScriptGraph extends LitElement {
|
||||
@focus=${this.selectNode(config, path)}
|
||||
?active=${this.selected === path}
|
||||
.iconPath=${mdiAsterisk}
|
||||
.notEnabled=${"enabled" in config && config.enabled === false}
|
||||
.notEnabled=${config.enabled === false}
|
||||
.error=${this.trace.trace[path]?.some((tr) => tr.error)}
|
||||
tabindex=${track ? "0" : "-1"}
|
||||
></hat-graph-node>
|
||||
@@ -569,16 +569,11 @@ export class HatScriptGraph extends LitElement {
|
||||
}
|
||||
|
||||
protected render() {
|
||||
const triggerKey = "triggers" in this.trace.config ? "triggers" : "trigger";
|
||||
const conditionKey =
|
||||
"conditions" in this.trace.config ? "conditions" : "condition";
|
||||
const actionKey = "actions" in this.trace.config ? "actions" : "action";
|
||||
|
||||
const paths = Object.keys(this.trackedNodes);
|
||||
const trigger_nodes =
|
||||
triggerKey in this.trace.config
|
||||
? flattenTriggers(ensureArray(this.trace.config[triggerKey])).map(
|
||||
(trigger, i) => this.render_trigger(trigger, i)
|
||||
"trigger" in this.trace.config
|
||||
? ensureArray(this.trace.config.trigger).map((trigger, i) =>
|
||||
this.render_trigger(trigger, i)
|
||||
)
|
||||
: undefined;
|
||||
try {
|
||||
@@ -589,14 +584,14 @@ export class HatScriptGraph extends LitElement {
|
||||
${trigger_nodes}
|
||||
</hat-graph-branch>`
|
||||
: ""}
|
||||
${conditionKey in this.trace.config
|
||||
? html`${ensureArray(this.trace.config[conditionKey])?.map(
|
||||
${"condition" in this.trace.config
|
||||
? html`${ensureArray(this.trace.config.condition)?.map(
|
||||
(condition, i) => this.render_condition(condition, i)
|
||||
)}`
|
||||
: ""}
|
||||
${actionKey in this.trace.config
|
||||
? html`${ensureArray(this.trace.config[actionKey]).map(
|
||||
(action, i) => this.render_action_node(action, `action/${i}`)
|
||||
${"action" in this.trace.config
|
||||
? html`${ensureArray(this.trace.config.action).map((action, i) =>
|
||||
this.render_action_node(action, `action/${i}`)
|
||||
)}`
|
||||
: ""}
|
||||
${"sequence" in this.trace.config
|
||||
|
@@ -22,8 +22,13 @@ import { formatDateTimeWithSeconds } from "../../common/datetime/format_date_tim
|
||||
import { relativeTime } from "../../common/datetime/relative_time";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import { toggleAttribute } from "../../common/dom/toggle_attribute";
|
||||
import { fullEntitiesContext, labelsContext } from "../../data/context";
|
||||
import {
|
||||
floorsContext,
|
||||
fullEntitiesContext,
|
||||
labelsContext,
|
||||
} from "../../data/context";
|
||||
import { EntityRegistryEntry } from "../../data/entity_registry";
|
||||
import { FloorRegistryEntry } from "../../data/floor_registry";
|
||||
import { LabelRegistryEntry } from "../../data/label_registry";
|
||||
import { LogbookEntry } from "../../data/logbook";
|
||||
import {
|
||||
@@ -201,6 +206,7 @@ class ActionRenderer {
|
||||
private hass: HomeAssistant,
|
||||
private entityReg: EntityRegistryEntry[],
|
||||
private labelReg: LabelRegistryEntry[],
|
||||
private floorReg: FloorRegistryEntry[],
|
||||
private entries: TemplateResult[],
|
||||
private trace: AutomationTraceExtended,
|
||||
private logbookRenderer: LogbookRenderer,
|
||||
@@ -319,6 +325,7 @@ class ActionRenderer {
|
||||
this.hass,
|
||||
this.entityReg,
|
||||
this.labelReg,
|
||||
this.floorReg,
|
||||
data,
|
||||
actionType
|
||||
),
|
||||
@@ -486,7 +493,13 @@ class ActionRenderer {
|
||||
|
||||
const name =
|
||||
repeatConfig.alias ||
|
||||
describeAction(this.hass, this.entityReg, this.labelReg, repeatConfig);
|
||||
describeAction(
|
||||
this.hass,
|
||||
this.entityReg,
|
||||
this.labelReg,
|
||||
this.floorReg,
|
||||
repeatConfig
|
||||
);
|
||||
|
||||
this._renderEntry(repeatPath, name, undefined, disabled);
|
||||
|
||||
@@ -584,6 +597,7 @@ class ActionRenderer {
|
||||
this.hass,
|
||||
this.entityReg,
|
||||
this.labelReg,
|
||||
this.floorReg,
|
||||
sequenceConfig,
|
||||
"sequence"
|
||||
),
|
||||
@@ -680,6 +694,10 @@ export class HaAutomationTracer extends LitElement {
|
||||
@consume({ context: labelsContext, subscribe: true })
|
||||
_labelReg!: LabelRegistryEntry[];
|
||||
|
||||
@state()
|
||||
@consume({ context: floorsContext, subscribe: true })
|
||||
_floorReg!: FloorRegistryEntry[];
|
||||
|
||||
protected render() {
|
||||
if (!this.trace) {
|
||||
return nothing;
|
||||
@@ -697,6 +715,7 @@ export class HaAutomationTracer extends LitElement {
|
||||
this.hass,
|
||||
this._entityReg,
|
||||
this._labelReg,
|
||||
this._floorReg,
|
||||
entries,
|
||||
this.trace,
|
||||
logbookRenderer,
|
||||
|
@@ -1,81 +0,0 @@
|
||||
import { HassEntity } from "home-assistant-js-websocket";
|
||||
import { HomeAssistant } from "../types";
|
||||
import { supportsFeature } from "../common/entity/supports-feature";
|
||||
import { UNAVAILABLE } from "./entity";
|
||||
|
||||
export const enum AssistSatelliteEntityFeature {
|
||||
ANNOUNCE = 1,
|
||||
}
|
||||
|
||||
export interface WakeWordInterceptMessage {
|
||||
wake_word_phrase: string;
|
||||
}
|
||||
|
||||
export interface WakeWordOption {
|
||||
id: string;
|
||||
wake_word: string;
|
||||
trained_languages: string[];
|
||||
}
|
||||
|
||||
export interface AssistSatelliteConfiguration {
|
||||
active_wake_words: string[];
|
||||
available_wake_words: WakeWordOption[];
|
||||
max_active_wake_words: number;
|
||||
pipeline_entity_id: string;
|
||||
vad_entity_id: string;
|
||||
}
|
||||
|
||||
export const interceptWakeWord = (
|
||||
hass: HomeAssistant,
|
||||
entity_id: string,
|
||||
callback: (result: WakeWordInterceptMessage) => void
|
||||
) =>
|
||||
hass.connection.subscribeMessage(callback, {
|
||||
type: "assist_satellite/intercept_wake_word",
|
||||
entity_id,
|
||||
});
|
||||
|
||||
export const testAssistSatelliteConnection = (
|
||||
hass: HomeAssistant,
|
||||
entity_id: string
|
||||
) =>
|
||||
hass.callWS<{
|
||||
status: "success" | "timeout";
|
||||
}>({
|
||||
type: "assist_satellite/test_connection",
|
||||
entity_id,
|
||||
});
|
||||
|
||||
export const assistSatelliteAnnounce = (
|
||||
hass: HomeAssistant,
|
||||
entity_id: string,
|
||||
message: string
|
||||
) =>
|
||||
hass.callService("assist_satellite", "announce", { message }, { entity_id });
|
||||
|
||||
export const fetchAssistSatelliteConfiguration = (
|
||||
hass: HomeAssistant,
|
||||
entity_id: string
|
||||
) =>
|
||||
hass.callWS<AssistSatelliteConfiguration>({
|
||||
type: "assist_satellite/get_configuration",
|
||||
entity_id,
|
||||
});
|
||||
|
||||
export const setWakeWords = (
|
||||
hass: HomeAssistant,
|
||||
entity_id: string,
|
||||
wake_word_ids: string[]
|
||||
) =>
|
||||
hass.callWS({
|
||||
type: "assist_satellite/set_wake_words",
|
||||
entity_id,
|
||||
wake_word_ids,
|
||||
});
|
||||
|
||||
export const assistSatelliteSupportsSetupFlow = (
|
||||
assistSatelliteEntity: HassEntity | undefined
|
||||
) =>
|
||||
assistSatelliteEntity &&
|
||||
assistSatelliteEntity.state !== UNAVAILABLE &&
|
||||
supportsFeature(assistSatelliteEntity, AssistSatelliteEntityFeature.ANNOUNCE);
|
@@ -3,12 +3,10 @@ import {
|
||||
HassEntityBase,
|
||||
} from "home-assistant-js-websocket";
|
||||
import { navigate } from "../common/navigate";
|
||||
import { ensureArray } from "../common/array/ensure-array";
|
||||
import { Context, HomeAssistant } from "../types";
|
||||
import { BlueprintInput } from "./blueprint";
|
||||
import { DeviceCondition, DeviceTrigger } from "./device_automation";
|
||||
import { Action, MODES, migrateAutomationAction } from "./script";
|
||||
import { createSearchParam } from "../common/url/search-params";
|
||||
|
||||
export const AUTOMATION_DEFAULT_MODE: (typeof MODES)[number] = "single";
|
||||
export const AUTOMATION_DEFAULT_MAX = 10;
|
||||
@@ -28,14 +26,8 @@ export interface ManualAutomationConfig {
|
||||
id?: string;
|
||||
alias?: string;
|
||||
description?: string;
|
||||
triggers: Trigger | Trigger[];
|
||||
/** @deprecated Use `triggers` instead */
|
||||
trigger?: Trigger | Trigger[];
|
||||
conditions?: Condition | Condition[];
|
||||
/** @deprecated Use `conditions` instead */
|
||||
trigger: Trigger | Trigger[];
|
||||
condition?: Condition | Condition[];
|
||||
actions: Action | Action[];
|
||||
/** @deprecated Use `actions` instead */
|
||||
action?: Action | Action[];
|
||||
mode?: (typeof MODES)[number];
|
||||
max?: number;
|
||||
@@ -70,22 +62,16 @@ export interface ContextConstraint {
|
||||
user_id?: string | string[];
|
||||
}
|
||||
|
||||
export interface TriggerList {
|
||||
triggers: Trigger | Trigger[] | undefined;
|
||||
}
|
||||
|
||||
export interface BaseTrigger {
|
||||
alias?: string;
|
||||
/** @deprecated Use `trigger` instead */
|
||||
platform?: string;
|
||||
trigger: string;
|
||||
platform: string;
|
||||
id?: string;
|
||||
variables?: Record<string, unknown>;
|
||||
enabled?: boolean;
|
||||
}
|
||||
|
||||
export interface StateTrigger extends BaseTrigger {
|
||||
trigger: "state";
|
||||
platform: "state";
|
||||
entity_id: string | string[];
|
||||
attribute?: string;
|
||||
from?: string | string[];
|
||||
@@ -94,25 +80,25 @@ export interface StateTrigger extends BaseTrigger {
|
||||
}
|
||||
|
||||
export interface MqttTrigger extends BaseTrigger {
|
||||
trigger: "mqtt";
|
||||
platform: "mqtt";
|
||||
topic: string;
|
||||
payload?: string;
|
||||
}
|
||||
|
||||
export interface GeoLocationTrigger extends BaseTrigger {
|
||||
trigger: "geo_location";
|
||||
platform: "geo_location";
|
||||
source: string;
|
||||
zone: string;
|
||||
event: "enter" | "leave";
|
||||
}
|
||||
|
||||
export interface HassTrigger extends BaseTrigger {
|
||||
trigger: "homeassistant";
|
||||
platform: "homeassistant";
|
||||
event: "start" | "shutdown";
|
||||
}
|
||||
|
||||
export interface NumericStateTrigger extends BaseTrigger {
|
||||
trigger: "numeric_state";
|
||||
platform: "numeric_state";
|
||||
entity_id: string | string[];
|
||||
attribute?: string;
|
||||
above?: number;
|
||||
@@ -122,69 +108,69 @@ export interface NumericStateTrigger extends BaseTrigger {
|
||||
}
|
||||
|
||||
export interface ConversationTrigger extends BaseTrigger {
|
||||
trigger: "conversation";
|
||||
platform: "conversation";
|
||||
command: string | string[];
|
||||
}
|
||||
|
||||
export interface SunTrigger extends BaseTrigger {
|
||||
trigger: "sun";
|
||||
platform: "sun";
|
||||
offset: number;
|
||||
event: "sunrise" | "sunset";
|
||||
}
|
||||
|
||||
export interface TimePatternTrigger extends BaseTrigger {
|
||||
trigger: "time_pattern";
|
||||
platform: "time_pattern";
|
||||
hours?: number | string;
|
||||
minutes?: number | string;
|
||||
seconds?: number | string;
|
||||
}
|
||||
|
||||
export interface WebhookTrigger extends BaseTrigger {
|
||||
trigger: "webhook";
|
||||
platform: "webhook";
|
||||
webhook_id: string;
|
||||
allowed_methods?: string[];
|
||||
local_only?: boolean;
|
||||
}
|
||||
|
||||
export interface PersistentNotificationTrigger extends BaseTrigger {
|
||||
trigger: "persistent_notification";
|
||||
platform: "persistent_notification";
|
||||
notification_id?: string;
|
||||
update_type?: string[];
|
||||
}
|
||||
|
||||
export interface ZoneTrigger extends BaseTrigger {
|
||||
trigger: "zone";
|
||||
platform: "zone";
|
||||
entity_id: string;
|
||||
zone: string;
|
||||
event: "enter" | "leave";
|
||||
}
|
||||
|
||||
export interface TagTrigger extends BaseTrigger {
|
||||
trigger: "tag";
|
||||
platform: "tag";
|
||||
tag_id: string;
|
||||
device_id?: string;
|
||||
}
|
||||
|
||||
export interface TimeTrigger extends BaseTrigger {
|
||||
trigger: "time";
|
||||
platform: "time";
|
||||
at: string;
|
||||
}
|
||||
|
||||
export interface TemplateTrigger extends BaseTrigger {
|
||||
trigger: "template";
|
||||
platform: "template";
|
||||
value_template: string;
|
||||
for?: string | number | ForDict;
|
||||
}
|
||||
|
||||
export interface EventTrigger extends BaseTrigger {
|
||||
trigger: "event";
|
||||
platform: "event";
|
||||
event_type: string;
|
||||
event_data?: any;
|
||||
context?: ContextConstraint;
|
||||
}
|
||||
|
||||
export interface CalendarTrigger extends BaseTrigger {
|
||||
trigger: "calendar";
|
||||
platform: "calendar";
|
||||
event: "start" | "end";
|
||||
entity_id: string;
|
||||
offset: string;
|
||||
@@ -207,8 +193,7 @@ export type Trigger =
|
||||
| TemplateTrigger
|
||||
| EventTrigger
|
||||
| DeviceTrigger
|
||||
| CalendarTrigger
|
||||
| TriggerList;
|
||||
| CalendarTrigger;
|
||||
|
||||
interface BaseCondition {
|
||||
condition: string;
|
||||
@@ -372,104 +357,25 @@ export const normalizeAutomationConfig = <
|
||||
>(
|
||||
config: T
|
||||
): T => {
|
||||
config = migrateAutomationConfig(config);
|
||||
|
||||
// Normalize data: ensure triggers, actions and conditions are lists
|
||||
// Happens when people copy paste their automations into the config
|
||||
for (const key of ["triggers", "conditions", "actions"]) {
|
||||
for (const key of ["trigger", "condition", "action"]) {
|
||||
const value = config[key];
|
||||
if (value && !Array.isArray(value)) {
|
||||
config[key] = [value];
|
||||
}
|
||||
}
|
||||
|
||||
return config;
|
||||
};
|
||||
|
||||
export const migrateAutomationConfig = <
|
||||
T extends Partial<AutomationConfig> | AutomationConfig,
|
||||
>(
|
||||
config: T
|
||||
) => {
|
||||
if ("trigger" in config) {
|
||||
if (!("triggers" in config)) {
|
||||
config.triggers = config.trigger;
|
||||
}
|
||||
delete config.trigger;
|
||||
}
|
||||
if ("condition" in config) {
|
||||
if (!("conditions" in config)) {
|
||||
config.conditions = config.condition;
|
||||
}
|
||||
delete config.condition;
|
||||
}
|
||||
if ("action" in config) {
|
||||
if (!("actions" in config)) {
|
||||
config.actions = config.action;
|
||||
}
|
||||
delete config.action;
|
||||
}
|
||||
|
||||
if (config.triggers) {
|
||||
config.triggers = migrateAutomationTrigger(config.triggers);
|
||||
}
|
||||
|
||||
if (config.actions) {
|
||||
config.actions = migrateAutomationAction(config.actions);
|
||||
if (config.action) {
|
||||
config.action = migrateAutomationAction(config.action);
|
||||
}
|
||||
|
||||
return config;
|
||||
};
|
||||
|
||||
export const migrateAutomationTrigger = (
|
||||
trigger: Trigger | Trigger[]
|
||||
): Trigger | Trigger[] => {
|
||||
if (Array.isArray(trigger)) {
|
||||
return trigger.map(migrateAutomationTrigger) as Trigger[];
|
||||
}
|
||||
|
||||
if ("triggers" in trigger && trigger.triggers) {
|
||||
trigger.triggers = migrateAutomationTrigger(trigger.triggers);
|
||||
}
|
||||
|
||||
if ("platform" in trigger) {
|
||||
if (!("trigger" in trigger)) {
|
||||
// @ts-ignore
|
||||
trigger.trigger = trigger.platform;
|
||||
}
|
||||
delete trigger.platform;
|
||||
}
|
||||
return trigger;
|
||||
};
|
||||
|
||||
export const flattenTriggers = (
|
||||
triggers: undefined | Trigger | Trigger[]
|
||||
): Trigger[] => {
|
||||
if (!triggers) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const flatTriggers: Trigger[] = [];
|
||||
|
||||
ensureArray(triggers).forEach((t) => {
|
||||
if ("triggers" in t) {
|
||||
if (t.triggers) {
|
||||
flatTriggers.push(...flattenTriggers(t.triggers));
|
||||
}
|
||||
} else {
|
||||
flatTriggers.push(t);
|
||||
}
|
||||
});
|
||||
return flatTriggers;
|
||||
};
|
||||
|
||||
export const showAutomationEditor = (
|
||||
data?: Partial<AutomationConfig>,
|
||||
expanded?: boolean
|
||||
) => {
|
||||
export const showAutomationEditor = (data?: Partial<AutomationConfig>) => {
|
||||
initialAutomationEditorData = data;
|
||||
const params = expanded ? `?${createSearchParam({ expanded: "1" })}` : "";
|
||||
navigate(`/config/automation/edit/new${params}`);
|
||||
navigate("/config/automation/edit/new");
|
||||
};
|
||||
|
||||
export const duplicateAutomation = (config: AutomationConfig) => {
|
||||
|
@@ -22,7 +22,6 @@ import {
|
||||
formatListWithAnds,
|
||||
formatListWithOrs,
|
||||
} from "../common/string/format-list";
|
||||
import { isTriggerList } from "./trigger";
|
||||
|
||||
const triggerTranslationBaseKey =
|
||||
"ui.panel.config.automation.editor.triggers.type";
|
||||
@@ -69,18 +68,9 @@ export const describeTrigger = (
|
||||
hass: HomeAssistant,
|
||||
entityRegistry: EntityRegistryEntry[],
|
||||
ignoreAlias = false
|
||||
): string => {
|
||||
) => {
|
||||
try {
|
||||
const description = tryDescribeTrigger(
|
||||
trigger,
|
||||
hass,
|
||||
entityRegistry,
|
||||
ignoreAlias
|
||||
);
|
||||
if (typeof description !== "string") {
|
||||
throw new Error(String(description));
|
||||
}
|
||||
return description;
|
||||
return tryDescribeTrigger(trigger, hass, entityRegistry, ignoreAlias);
|
||||
} catch (error: any) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error(error);
|
||||
@@ -99,26 +89,12 @@ const tryDescribeTrigger = (
|
||||
entityRegistry: EntityRegistryEntry[],
|
||||
ignoreAlias = false
|
||||
) => {
|
||||
if (isTriggerList(trigger)) {
|
||||
const triggers = ensureArray(trigger.triggers);
|
||||
|
||||
if (!triggers || triggers.length === 0) {
|
||||
return hass.localize(
|
||||
`${triggerTranslationBaseKey}.list.description.no_trigger`
|
||||
);
|
||||
}
|
||||
const count = triggers.length;
|
||||
return hass.localize(`${triggerTranslationBaseKey}.list.description.full`, {
|
||||
count: count,
|
||||
});
|
||||
}
|
||||
|
||||
if (trigger.alias && !ignoreAlias) {
|
||||
return trigger.alias;
|
||||
}
|
||||
|
||||
// Event Trigger
|
||||
if (trigger.trigger === "event" && trigger.event_type) {
|
||||
if (trigger.platform === "event" && trigger.event_type) {
|
||||
const eventTypes: string[] = [];
|
||||
|
||||
if (Array.isArray(trigger.event_type)) {
|
||||
@@ -137,7 +113,7 @@ const tryDescribeTrigger = (
|
||||
}
|
||||
|
||||
// Home Assistant Trigger
|
||||
if (trigger.trigger === "homeassistant" && trigger.event) {
|
||||
if (trigger.platform === "homeassistant" && trigger.event) {
|
||||
return hass.localize(
|
||||
trigger.event === "start"
|
||||
? `${triggerTranslationBaseKey}.homeassistant.description.started`
|
||||
@@ -146,7 +122,7 @@ const tryDescribeTrigger = (
|
||||
}
|
||||
|
||||
// Numeric State Trigger
|
||||
if (trigger.trigger === "numeric_state" && trigger.entity_id) {
|
||||
if (trigger.platform === "numeric_state" && trigger.entity_id) {
|
||||
const entities: string[] = [];
|
||||
const states = hass.states;
|
||||
|
||||
@@ -221,7 +197,7 @@ const tryDescribeTrigger = (
|
||||
}
|
||||
|
||||
// State Trigger
|
||||
if (trigger.trigger === "state") {
|
||||
if (trigger.platform === "state") {
|
||||
const entities: string[] = [];
|
||||
const states = hass.states;
|
||||
|
||||
@@ -344,7 +320,7 @@ const tryDescribeTrigger = (
|
||||
}
|
||||
|
||||
// Sun Trigger
|
||||
if (trigger.trigger === "sun" && trigger.event) {
|
||||
if (trigger.platform === "sun" && trigger.event) {
|
||||
let duration = "";
|
||||
if (trigger.offset) {
|
||||
if (typeof trigger.offset === "number") {
|
||||
@@ -365,12 +341,12 @@ const tryDescribeTrigger = (
|
||||
}
|
||||
|
||||
// Tag Trigger
|
||||
if (trigger.trigger === "tag") {
|
||||
if (trigger.platform === "tag") {
|
||||
return hass.localize(`${triggerTranslationBaseKey}.tag.description.full`);
|
||||
}
|
||||
|
||||
// Time Trigger
|
||||
if (trigger.trigger === "time" && trigger.at) {
|
||||
if (trigger.platform === "time" && trigger.at) {
|
||||
const result = ensureArray(trigger.at).map((at) =>
|
||||
typeof at !== "string"
|
||||
? at
|
||||
@@ -385,7 +361,7 @@ const tryDescribeTrigger = (
|
||||
}
|
||||
|
||||
// Time Pattern Trigger
|
||||
if (trigger.trigger === "time_pattern") {
|
||||
if (trigger.platform === "time_pattern") {
|
||||
if (!trigger.seconds && !trigger.minutes && !trigger.hours) {
|
||||
return hass.localize(
|
||||
`${triggerTranslationBaseKey}.time_pattern.description.initial`
|
||||
@@ -562,7 +538,7 @@ const tryDescribeTrigger = (
|
||||
}
|
||||
|
||||
// Zone Trigger
|
||||
if (trigger.trigger === "zone" && trigger.entity_id && trigger.zone) {
|
||||
if (trigger.platform === "zone" && trigger.entity_id && trigger.zone) {
|
||||
const entities: string[] = [];
|
||||
const zones: string[] = [];
|
||||
|
||||
@@ -605,7 +581,7 @@ const tryDescribeTrigger = (
|
||||
}
|
||||
|
||||
// Geo Location Trigger
|
||||
if (trigger.trigger === "geo_location" && trigger.source && trigger.zone) {
|
||||
if (trigger.platform === "geo_location" && trigger.source && trigger.zone) {
|
||||
const sources: string[] = [];
|
||||
const zones: string[] = [];
|
||||
const states = hass.states;
|
||||
@@ -644,12 +620,12 @@ const tryDescribeTrigger = (
|
||||
}
|
||||
|
||||
// MQTT Trigger
|
||||
if (trigger.trigger === "mqtt") {
|
||||
if (trigger.platform === "mqtt") {
|
||||
return hass.localize(`${triggerTranslationBaseKey}.mqtt.description.full`);
|
||||
}
|
||||
|
||||
// Template Trigger
|
||||
if (trigger.trigger === "template") {
|
||||
if (trigger.platform === "template") {
|
||||
let duration = "";
|
||||
if (trigger.for) {
|
||||
duration = describeDuration(hass.locale, trigger.for) ?? "";
|
||||
@@ -662,14 +638,14 @@ const tryDescribeTrigger = (
|
||||
}
|
||||
|
||||
// Webhook Trigger
|
||||
if (trigger.trigger === "webhook") {
|
||||
if (trigger.platform === "webhook") {
|
||||
return hass.localize(
|
||||
`${triggerTranslationBaseKey}.webhook.description.full`
|
||||
);
|
||||
}
|
||||
|
||||
// Conversation Trigger
|
||||
if (trigger.trigger === "conversation") {
|
||||
if (trigger.platform === "conversation") {
|
||||
if (!trigger.command) {
|
||||
return hass.localize(
|
||||
`${triggerTranslationBaseKey}.conversation.description.empty`
|
||||
@@ -688,14 +664,14 @@ const tryDescribeTrigger = (
|
||||
}
|
||||
|
||||
// Persistent Notification Trigger
|
||||
if (trigger.trigger === "persistent_notification") {
|
||||
if (trigger.platform === "persistent_notification") {
|
||||
return hass.localize(
|
||||
`${triggerTranslationBaseKey}.persistent_notification.description.full`
|
||||
);
|
||||
}
|
||||
|
||||
// Device Trigger
|
||||
if (trigger.trigger === "device" && trigger.device_id) {
|
||||
if (trigger.platform === "device" && trigger.device_id) {
|
||||
const config = trigger as DeviceTrigger;
|
||||
const localized = localizeDeviceAutomationTrigger(
|
||||
hass,
|
||||
@@ -713,7 +689,7 @@ const tryDescribeTrigger = (
|
||||
|
||||
return (
|
||||
hass.localize(
|
||||
`ui.panel.config.automation.editor.triggers.type.${trigger.trigger}.label`
|
||||
`ui.panel.config.automation.editor.triggers.type.${trigger.platform}.label`
|
||||
) ||
|
||||
hass.localize(`ui.panel.config.automation.editor.triggers.unknown_trigger`)
|
||||
);
|
||||
@@ -724,18 +700,9 @@ export const describeCondition = (
|
||||
hass: HomeAssistant,
|
||||
entityRegistry: EntityRegistryEntry[],
|
||||
ignoreAlias = false
|
||||
): string => {
|
||||
) => {
|
||||
try {
|
||||
const description = tryDescribeCondition(
|
||||
condition,
|
||||
hass,
|
||||
entityRegistry,
|
||||
ignoreAlias
|
||||
);
|
||||
if (typeof description !== "string") {
|
||||
throw new Error(String(description));
|
||||
}
|
||||
return description;
|
||||
return tryDescribeCondition(condition, hass, entityRegistry, ignoreAlias);
|
||||
} catch (error: any) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error(error);
|
||||
@@ -922,14 +889,8 @@ const tryDescribeCondition = (
|
||||
|
||||
// Numeric State Condition
|
||||
if (condition.condition === "numeric_state" && condition.entity_id) {
|
||||
const entity_ids = ensureArray(condition.entity_id);
|
||||
const stateObj = hass.states[entity_ids[0]];
|
||||
const entity = formatListWithAnds(
|
||||
hass.locale,
|
||||
entity_ids.map((id) =>
|
||||
hass.states[id] ? computeStateName(hass.states[id]) : id || ""
|
||||
)
|
||||
);
|
||||
const stateObj = hass.states[condition.entity_id];
|
||||
const entity = stateObj ? computeStateName(stateObj) : condition.entity_id;
|
||||
|
||||
const attribute = condition.attribute
|
||||
? computeAttributeNameDisplay(
|
||||
@@ -944,9 +905,8 @@ const tryDescribeCondition = (
|
||||
return hass.localize(
|
||||
`${conditionsTranslationBaseKey}.numeric_state.description.above-below`,
|
||||
{
|
||||
attribute,
|
||||
entity,
|
||||
numberOfEntities: entity_ids.length,
|
||||
attribute: attribute,
|
||||
entity: entity,
|
||||
above: condition.above,
|
||||
below: condition.below,
|
||||
}
|
||||
@@ -956,9 +916,8 @@ const tryDescribeCondition = (
|
||||
return hass.localize(
|
||||
`${conditionsTranslationBaseKey}.numeric_state.description.above`,
|
||||
{
|
||||
attribute,
|
||||
entity,
|
||||
numberOfEntities: entity_ids.length,
|
||||
attribute: attribute,
|
||||
entity: entity,
|
||||
above: condition.above,
|
||||
}
|
||||
);
|
||||
@@ -967,9 +926,8 @@ const tryDescribeCondition = (
|
||||
return hass.localize(
|
||||
`${conditionsTranslationBaseKey}.numeric_state.description.below`,
|
||||
{
|
||||
attribute,
|
||||
entity,
|
||||
numberOfEntities: entity_ids.length,
|
||||
attribute: attribute,
|
||||
entity: entity,
|
||||
below: condition.below,
|
||||
}
|
||||
);
|
||||
|
@@ -133,17 +133,3 @@ export const isCameraMediaSource = (mediaContentId: string) =>
|
||||
|
||||
export const getEntityIdFromCameraMediaSource = (mediaContentId: string) =>
|
||||
mediaContentId.substring(CAMERA_MEDIA_SOURCE_PREFIX.length);
|
||||
|
||||
export interface WebRTCClientConfiguration {
|
||||
configuration: RTCConfiguration;
|
||||
dataChannel?: string;
|
||||
}
|
||||
|
||||
export const fetchWebRtcClientConfiguration = async (
|
||||
hass: HomeAssistant,
|
||||
entityId: string
|
||||
) =>
|
||||
hass.callWS<WebRTCClientConfiguration>({
|
||||
type: "camera/webrtc/get_client_config",
|
||||
entity_id: entityId,
|
||||
});
|
||||
|
@@ -10,7 +10,7 @@ interface InvalidConfig {
|
||||
error: string;
|
||||
}
|
||||
|
||||
type ValidKeys = "triggers" | "actions" | "conditions";
|
||||
type ValidKeys = "trigger" | "action" | "condition";
|
||||
|
||||
export const validateConfig = <
|
||||
T extends Partial<{ [key in ValidKeys]: unknown }>,
|
||||
|
@@ -1,6 +1,6 @@
|
||||
import type { UnsubscribeFunc } from "home-assistant-js-websocket";
|
||||
import type { HomeAssistant } from "../types";
|
||||
import type { IntegrationType } from "./integration";
|
||||
import type { IntegrationManifest, IntegrationType } from "./integration";
|
||||
|
||||
export interface ConfigEntry {
|
||||
entry_id: string;
|
||||
@@ -149,19 +149,20 @@ export const enableConfigEntry = (hass: HomeAssistant, configEntryId: string) =>
|
||||
|
||||
export const sortConfigEntries = (
|
||||
configEntries: ConfigEntry[],
|
||||
primaryConfigEntry: string | null
|
||||
manifestLookup: { [domain: string]: IntegrationManifest }
|
||||
): ConfigEntry[] => {
|
||||
if (!primaryConfigEntry) {
|
||||
return configEntries;
|
||||
}
|
||||
const primaryEntry = configEntries.find(
|
||||
(e) => e.entry_id === primaryConfigEntry
|
||||
);
|
||||
if (!primaryEntry) {
|
||||
return configEntries;
|
||||
}
|
||||
const otherEntries = configEntries.filter(
|
||||
(e) => e.entry_id !== primaryConfigEntry
|
||||
);
|
||||
return [primaryEntry, ...otherEntries];
|
||||
const sortedConfigEntries = [...configEntries];
|
||||
|
||||
const getScore = (entry: ConfigEntry) => {
|
||||
const manifest = manifestLookup[entry.domain] as
|
||||
| IntegrationManifest
|
||||
| undefined;
|
||||
const isHelper = manifest?.integration_type === "helper";
|
||||
return isHelper ? -1 : 1;
|
||||
};
|
||||
|
||||
const configEntriesCompare = (a: ConfigEntry, b: ConfigEntry) =>
|
||||
getScore(b) - getScore(a);
|
||||
|
||||
return sortedConfigEntries.sort(configEntriesCompare);
|
||||
};
|
||||
|
@@ -2,6 +2,7 @@ import { createContext } from "@lit-labs/context";
|
||||
import { HassConfig } from "home-assistant-js-websocket";
|
||||
import { HomeAssistant } from "../types";
|
||||
import { EntityRegistryEntry } from "./entity_registry";
|
||||
import { FloorRegistryEntry } from "./floor_registry";
|
||||
import { LabelRegistryEntry } from "./label_registry";
|
||||
|
||||
export const connectionContext =
|
||||
@@ -27,4 +28,6 @@ export const panelsContext = createContext<HomeAssistant["panels"]>("panels");
|
||||
export const fullEntitiesContext =
|
||||
createContext<EntityRegistryEntry[]>("extendedEntities");
|
||||
|
||||
export const floorsContext = createContext<FloorRegistryEntry[]>("floors");
|
||||
|
||||
export const labelsContext = createContext<LabelRegistryEntry[]>("labels");
|
||||
|
@@ -1,20 +1,10 @@
|
||||
export interface DataTableFilters {
|
||||
[key: string]: {
|
||||
value: DataTableFiltersValue;
|
||||
value: string[] | { key: string[] } | undefined;
|
||||
items: Set<string> | undefined;
|
||||
};
|
||||
}
|
||||
|
||||
export type DataTableFiltersValue = string[] | { key: string[] } | undefined;
|
||||
|
||||
export interface DataTableFiltersValues {
|
||||
[key: string]: DataTableFiltersValue;
|
||||
}
|
||||
|
||||
export interface DataTableFiltersItems {
|
||||
[key: string]: Set<string> | undefined;
|
||||
}
|
||||
|
||||
export const serializeFilters = (value: DataTableFilters) => {
|
||||
const serializedValue = {};
|
||||
Object.entries(value).forEach(([key, val]) => {
|
||||
|
@@ -1,7 +1,7 @@
|
||||
import { computeStateName } from "../common/entity/compute_state_name";
|
||||
import type { HaFormSchema } from "../components/ha-form/types";
|
||||
import { HomeAssistant } from "../types";
|
||||
import { BaseTrigger, migrateAutomationTrigger } from "./automation";
|
||||
import { BaseTrigger } from "./automation";
|
||||
import {
|
||||
computeEntityRegistryName,
|
||||
entityRegistryByEntityId,
|
||||
@@ -31,7 +31,7 @@ export interface DeviceCondition extends DeviceAutomation {
|
||||
|
||||
export type DeviceTrigger = DeviceAutomation &
|
||||
BaseTrigger & {
|
||||
trigger: "device";
|
||||
platform: "device";
|
||||
};
|
||||
|
||||
export interface DeviceCapabilities {
|
||||
@@ -51,12 +51,10 @@ export const fetchDeviceConditions = (hass: HomeAssistant, deviceId: string) =>
|
||||
});
|
||||
|
||||
export const fetchDeviceTriggers = (hass: HomeAssistant, deviceId: string) =>
|
||||
hass
|
||||
.callWS<DeviceTrigger[]>({
|
||||
type: "device_automation/trigger/list",
|
||||
device_id: deviceId,
|
||||
})
|
||||
.then((triggers) => migrateAutomationTrigger(triggers) as DeviceTrigger[]);
|
||||
hass.callWS<DeviceTrigger[]>({
|
||||
type: "device_automation/trigger/list",
|
||||
device_id: deviceId,
|
||||
});
|
||||
|
||||
export const fetchDeviceActionCapabilities = (
|
||||
hass: HomeAssistant,
|
||||
@@ -93,7 +91,7 @@ const deviceAutomationIdentifiers = [
|
||||
"subtype",
|
||||
"event",
|
||||
"condition",
|
||||
"trigger",
|
||||
"platform",
|
||||
];
|
||||
|
||||
export const deviceAutomationsEqual = (
|
||||
|
@@ -33,7 +33,6 @@ export interface DeviceRegistryEntry extends RegistryEntry {
|
||||
entry_type: "service" | null;
|
||||
disabled_by: "user" | "integration" | "config_entry" | null;
|
||||
configuration_url: string | null;
|
||||
primary_config_entry: string | null;
|
||||
}
|
||||
|
||||
export interface DeviceEntityDisplayLookup {
|
||||
|
@@ -1,4 +1,7 @@
|
||||
import { Connection, createCollection } from "home-assistant-js-websocket";
|
||||
import { Store } from "home-assistant-js-websocket/dist/store";
|
||||
import { stringCompare } from "../common/string/compare";
|
||||
import { debounce } from "../common/util/debounce";
|
||||
import { HomeAssistant } from "../types";
|
||||
import { AreaRegistryEntry } from "./area_registry";
|
||||
import { RegistryEntry } from "./registry";
|
||||
@@ -24,6 +27,48 @@ export interface FloorRegistryEntryMutableParams {
|
||||
aliases?: string[];
|
||||
}
|
||||
|
||||
const fetchFloorRegistry = (conn: Connection) =>
|
||||
conn
|
||||
.sendMessagePromise({
|
||||
type: "config/floor_registry/list",
|
||||
})
|
||||
.then((floors) =>
|
||||
(floors as FloorRegistryEntry[]).sort((ent1, ent2) => {
|
||||
if (ent1.level !== ent2.level) {
|
||||
return (ent1.level ?? 9999) - (ent2.level ?? 9999);
|
||||
}
|
||||
return stringCompare(ent1.name, ent2.name);
|
||||
})
|
||||
);
|
||||
|
||||
const subscribeFloorRegistryUpdates = (
|
||||
conn: Connection,
|
||||
store: Store<FloorRegistryEntry[]>
|
||||
) =>
|
||||
conn.subscribeEvents(
|
||||
debounce(
|
||||
() =>
|
||||
fetchFloorRegistry(conn).then((areas: FloorRegistryEntry[]) =>
|
||||
store.setState(areas, true)
|
||||
),
|
||||
500,
|
||||
true
|
||||
),
|
||||
"floor_registry_updated"
|
||||
);
|
||||
|
||||
export const subscribeFloorRegistry = (
|
||||
conn: Connection,
|
||||
onChange: (floors: FloorRegistryEntry[]) => void
|
||||
) =>
|
||||
createCollection<FloorRegistryEntry[]>(
|
||||
"_floorRegistry",
|
||||
fetchFloorRegistry,
|
||||
subscribeFloorRegistryUpdates,
|
||||
conn,
|
||||
onChange
|
||||
);
|
||||
|
||||
export const createFloorRegistryEntry = (
|
||||
hass: HomeAssistant,
|
||||
values: FloorRegistryEntryMutableParams
|
||||
|
@@ -28,7 +28,6 @@ export interface UrlActionConfig extends BaseActionConfig {
|
||||
|
||||
export interface MoreInfoActionConfig extends BaseActionConfig {
|
||||
action: "more-info";
|
||||
entity_id?: string;
|
||||
}
|
||||
|
||||
export interface AssistActionConfig extends BaseActionConfig {
|
||||
|
@@ -3,13 +3,10 @@ import type { LovelaceCardConfig } from "./card";
|
||||
import type { LovelaceStrategyConfig } from "./strategy";
|
||||
|
||||
export interface LovelaceBaseSectionConfig {
|
||||
title?: string;
|
||||
visibility?: Condition[];
|
||||
column_span?: number;
|
||||
row_span?: number;
|
||||
/**
|
||||
* @deprecated Use heading card instead.
|
||||
*/
|
||||
title?: string;
|
||||
}
|
||||
|
||||
export interface LovelaceSectionConfig extends LovelaceBaseSectionConfig {
|
||||
|
@@ -25,6 +25,8 @@ export interface LovelaceBaseViewConfig {
|
||||
// Only used for section view, it should move to a section view config type when the views will have dedicated editor.
|
||||
max_columns?: number;
|
||||
dense_section_placement?: boolean;
|
||||
column_breakpoints?: Record<string, number>;
|
||||
experimental_breakpoints?: boolean;
|
||||
}
|
||||
|
||||
export interface LovelaceViewConfig extends LovelaceBaseViewConfig {
|
||||
|
@@ -2,8 +2,6 @@ import { UnsubscribeFunc } from "home-assistant-js-websocket";
|
||||
import { navigate } from "../common/navigate";
|
||||
import { HomeAssistant } from "../types";
|
||||
import { subscribeDeviceRegistry } from "./device_registry";
|
||||
import { isComponentLoaded } from "../common/config/is_component_loaded";
|
||||
import { getThreadDataSetTLV, listThreadDataSets } from "./thread";
|
||||
|
||||
export enum NetworkType {
|
||||
THREAD = "thread",
|
||||
@@ -53,31 +51,10 @@ export interface MatterCommissioningParameters {
|
||||
export const canCommissionMatterExternal = (hass: HomeAssistant) =>
|
||||
hass.auth.external?.config.canCommissionMatter;
|
||||
|
||||
export const startExternalCommissioning = async (hass: HomeAssistant) => {
|
||||
if (isComponentLoaded(hass, "thread")) {
|
||||
const datasets = await listThreadDataSets(hass);
|
||||
const preferredDataset = datasets.datasets.find(
|
||||
(dataset) => dataset.preferred
|
||||
);
|
||||
if (preferredDataset) {
|
||||
return hass.auth.external!.fireMessage({
|
||||
type: "matter/commission",
|
||||
payload: {
|
||||
active_operational_dataset: (
|
||||
await getThreadDataSetTLV(hass, preferredDataset.dataset_id)
|
||||
).tlv,
|
||||
border_agent_id: preferredDataset.preferred_border_agent_id,
|
||||
mac_extended_address: preferredDataset.preferred_extended_address,
|
||||
extended_pan_id: preferredDataset.extended_pan_id,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return hass.auth.external!.fireMessage({
|
||||
export const startExternalCommissioning = (hass: HomeAssistant) =>
|
||||
hass.auth.external!.fireMessage({
|
||||
type: "matter/commission",
|
||||
});
|
||||
};
|
||||
|
||||
export const redirectOnNewMatterDevice = (
|
||||
hass: HomeAssistant,
|
||||
|
@@ -245,8 +245,7 @@ export const computeMediaDescription = (
|
||||
secondaryTitle = stateObj.attributes.media_artist!;
|
||||
break;
|
||||
case "playlist":
|
||||
secondaryTitle =
|
||||
stateObj.attributes.media_playlist || stateObj.attributes.media_artist!;
|
||||
secondaryTitle = stateObj.attributes.media_playlist!;
|
||||
break;
|
||||
case "tvshow":
|
||||
secondaryTitle = stateObj.attributes.media_series_title!;
|
||||
|
@@ -47,19 +47,11 @@ export interface StatisticsMetaData {
|
||||
unit_class: string | null;
|
||||
}
|
||||
|
||||
export const STATISTIC_TYPES: StatisticsValidationResult["type"][] = [
|
||||
"entity_not_recorded",
|
||||
"entity_no_longer_recorded",
|
||||
"state_class_removed",
|
||||
"units_changed",
|
||||
"no_state",
|
||||
];
|
||||
|
||||
export type StatisticsValidationResult =
|
||||
| StatisticsValidationResultNoState
|
||||
| StatisticsValidationResultEntityNotRecorded
|
||||
| StatisticsValidationResultEntityNoLongerRecorded
|
||||
| StatisticsValidationResultStateClassRemoved
|
||||
| StatisticsValidationResultUnsupportedStateClass
|
||||
| StatisticsValidationResultUnitsChanged;
|
||||
|
||||
export interface StatisticsValidationResultNoState {
|
||||
@@ -77,9 +69,9 @@ export interface StatisticsValidationResultEntityNotRecorded {
|
||||
data: { statistic_id: string };
|
||||
}
|
||||
|
||||
export interface StatisticsValidationResultStateClassRemoved {
|
||||
type: "state_class_removed";
|
||||
data: { statistic_id: string };
|
||||
export interface StatisticsValidationResultUnsupportedStateClass {
|
||||
type: "unsupported_state_class";
|
||||
data: { statistic_id: string; state_class: string };
|
||||
}
|
||||
|
||||
export interface StatisticsValidationResultUnitsChanged {
|
||||
@@ -332,6 +324,3 @@ export const getDisplayUnit = (
|
||||
|
||||
export const isExternalStatistic = (statisticsId: string): boolean =>
|
||||
statisticsId.includes(":");
|
||||
|
||||
export const updateStatisticsIssues = (hass: HomeAssistant) =>
|
||||
hass.callWS({ type: "recorder/update_statistics_issues" });
|
||||
|