Compare commits

..

8 Commits

Author SHA1 Message Date
Paul Bottein
1d007a5aaf After rebase clean 2024-09-05 15:52:59 +02:00
Paul Bottein
94c1af7729 Use breakpoints to define number of column in dashboard view 2024-09-05 15:49:13 +02:00
Paul Bottein
d7aaf9bc41 Use old logic to calculate the number of columns 2024-09-05 15:46:28 +02:00
Paul Bottein
c0aed4325d Make column breakpoints optional 2024-09-05 15:44:49 +02:00
Paul Bottein
79a56fabdf Remove column min width 2024-09-05 15:44:21 +02:00
Paul Bottein
14c2b60538 Rename variables 2024-09-05 11:36:18 +02:00
Paul Bottein
79f3dfdfce Use listeners 2024-09-05 11:33:54 +02:00
Paul Bottein
3de4dffa02 WIP: Allow to resize section 2024-09-05 11:30:07 +02:00
336 changed files with 6801 additions and 12938 deletions

View File

@@ -21,12 +21,12 @@ jobs:
url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }} url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }}
steps: steps:
- name: Check out files from GitHub - name: Check out files from GitHub
uses: actions/checkout@v4.2.1 uses: actions/checkout@v4.1.7
with: with:
ref: dev ref: dev
- name: Setup Node - name: Setup Node
uses: actions/setup-node@v4.0.4 uses: actions/setup-node@v4.0.3
with: with:
node-version-file: ".nvmrc" node-version-file: ".nvmrc"
cache: yarn cache: yarn
@@ -57,12 +57,12 @@ jobs:
url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }} url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }}
steps: steps:
- name: Check out files from GitHub - name: Check out files from GitHub
uses: actions/checkout@v4.2.1 uses: actions/checkout@v4.1.7
with: with:
ref: master ref: master
- name: Setup Node - name: Setup Node
uses: actions/setup-node@v4.0.4 uses: actions/setup-node@v4.0.3
with: with:
node-version-file: ".nvmrc" node-version-file: ".nvmrc"
cache: yarn cache: yarn

View File

@@ -24,9 +24,9 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Check out files from GitHub - name: Check out files from GitHub
uses: actions/checkout@v4.2.1 uses: actions/checkout@v4.1.7
- name: Setup Node - name: Setup Node
uses: actions/setup-node@v4.0.4 uses: actions/setup-node@v4.0.3
with: with:
node-version-file: ".nvmrc" node-version-file: ".nvmrc"
cache: yarn cache: yarn
@@ -37,7 +37,7 @@ jobs:
- name: Build resources - name: Build resources
run: ./node_modules/.bin/gulp gen-icons-json build-translations build-locale-data gather-gallery-pages run: ./node_modules/.bin/gulp gen-icons-json build-translations build-locale-data gather-gallery-pages
- name: Setup lint cache - name: Setup lint cache
uses: actions/cache@v4.1.1 uses: actions/cache@v4.0.2
with: with:
path: | path: |
node_modules/.cache/prettier node_modules/.cache/prettier
@@ -58,9 +58,9 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Check out files from GitHub - name: Check out files from GitHub
uses: actions/checkout@v4.2.1 uses: actions/checkout@v4.1.7
- name: Setup Node - name: Setup Node
uses: actions/setup-node@v4.0.4 uses: actions/setup-node@v4.0.3
with: with:
node-version-file: ".nvmrc" node-version-file: ".nvmrc"
cache: yarn cache: yarn
@@ -76,9 +76,9 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Check out files from GitHub - name: Check out files from GitHub
uses: actions/checkout@v4.2.1 uses: actions/checkout@v4.1.7
- name: Setup Node - name: Setup Node
uses: actions/setup-node@v4.0.4 uses: actions/setup-node@v4.0.3
with: with:
node-version-file: ".nvmrc" node-version-file: ".nvmrc"
cache: yarn cache: yarn
@@ -89,7 +89,7 @@ jobs:
env: env:
IS_TEST: "true" IS_TEST: "true"
- name: Upload bundle stats - name: Upload bundle stats
uses: actions/upload-artifact@v4.4.3 uses: actions/upload-artifact@v4.4.0
with: with:
name: frontend-bundle-stats name: frontend-bundle-stats
path: build/stats/*.json path: build/stats/*.json
@@ -100,9 +100,9 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Check out files from GitHub - name: Check out files from GitHub
uses: actions/checkout@v4.2.1 uses: actions/checkout@v4.1.7
- name: Setup Node - name: Setup Node
uses: actions/setup-node@v4.0.4 uses: actions/setup-node@v4.0.3
with: with:
node-version-file: ".nvmrc" node-version-file: ".nvmrc"
cache: yarn cache: yarn
@@ -113,7 +113,7 @@ jobs:
env: env:
IS_TEST: "true" IS_TEST: "true"
- name: Upload bundle stats - name: Upload bundle stats
uses: actions/upload-artifact@v4.4.3 uses: actions/upload-artifact@v4.4.0
with: with:
name: supervisor-bundle-stats name: supervisor-bundle-stats
path: build/stats/*.json path: build/stats/*.json

View File

@@ -23,7 +23,7 @@ jobs:
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v4.2.1 uses: actions/checkout@v4.1.7
with: with:
# We must fetch at least the immediate parents so that if this is # We must fetch at least the immediate parents so that if this is
# a pull request then we can checkout the head. # a pull request then we can checkout the head.

View File

@@ -22,12 +22,12 @@ jobs:
url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }} url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }}
steps: steps:
- name: Check out files from GitHub - name: Check out files from GitHub
uses: actions/checkout@v4.2.1 uses: actions/checkout@v4.1.7
with: with:
ref: dev ref: dev
- name: Setup Node - name: Setup Node
uses: actions/setup-node@v4.0.4 uses: actions/setup-node@v4.0.3
with: with:
node-version-file: ".nvmrc" node-version-file: ".nvmrc"
cache: yarn cache: yarn
@@ -58,12 +58,12 @@ jobs:
url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }} url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }}
steps: steps:
- name: Check out files from GitHub - name: Check out files from GitHub
uses: actions/checkout@v4.2.1 uses: actions/checkout@v4.1.7
with: with:
ref: master ref: master
- name: Setup Node - name: Setup Node
uses: actions/setup-node@v4.0.4 uses: actions/setup-node@v4.0.3
with: with:
node-version-file: ".nvmrc" node-version-file: ".nvmrc"
cache: yarn cache: yarn

View File

@@ -16,10 +16,10 @@ jobs:
url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }} url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }}
steps: steps:
- name: Check out files from GitHub - name: Check out files from GitHub
uses: actions/checkout@v4.2.1 uses: actions/checkout@v4.1.7
- name: Setup Node - name: Setup Node
uses: actions/setup-node@v4.0.4 uses: actions/setup-node@v4.0.3
with: with:
node-version-file: ".nvmrc" node-version-file: ".nvmrc"
cache: yarn cache: yarn

View File

@@ -21,10 +21,10 @@ jobs:
if: github.repository == 'home-assistant/frontend' && contains(github.event.pull_request.labels.*.name, 'needs design preview') if: github.repository == 'home-assistant/frontend' && contains(github.event.pull_request.labels.*.name, 'needs design preview')
steps: steps:
- name: Check out files from GitHub - name: Check out files from GitHub
uses: actions/checkout@v4.2.1 uses: actions/checkout@v4.1.7
- name: Setup Node - name: Setup Node
uses: actions/setup-node@v4.0.4 uses: actions/setup-node@v4.0.3
with: with:
node-version-file: ".nvmrc" node-version-file: ".nvmrc"
cache: yarn cache: yarn

View File

@@ -20,7 +20,7 @@ jobs:
contents: write contents: write
steps: steps:
- name: Checkout the repository - name: Checkout the repository
uses: actions/checkout@v4.2.1 uses: actions/checkout@v4.1.7
- name: Set up Python ${{ env.PYTHON_VERSION }} - name: Set up Python ${{ env.PYTHON_VERSION }}
uses: actions/setup-python@v5 uses: actions/setup-python@v5
@@ -28,7 +28,7 @@ jobs:
python-version: ${{ env.PYTHON_VERSION }} python-version: ${{ env.PYTHON_VERSION }}
- name: Setup Node - name: Setup Node
uses: actions/setup-node@v4.0.4 uses: actions/setup-node@v4.0.3
with: with:
node-version-file: ".nvmrc" node-version-file: ".nvmrc"
cache: yarn cache: yarn
@@ -57,14 +57,14 @@ jobs:
run: tar -czvf translations.tar.gz translations run: tar -czvf translations.tar.gz translations
- name: Upload build artifacts - name: Upload build artifacts
uses: actions/upload-artifact@v4.4.3 uses: actions/upload-artifact@v4.4.0
with: with:
name: wheels name: wheels
path: dist/home_assistant_frontend*.whl path: dist/home_assistant_frontend*.whl
if-no-files-found: error if-no-files-found: error
- name: Upload translations - name: Upload translations
uses: actions/upload-artifact@v4.4.3 uses: actions/upload-artifact@v4.4.0
with: with:
name: translations name: translations
path: translations.tar.gz path: translations.tar.gz

View File

@@ -23,7 +23,7 @@ jobs:
contents: write # Required to upload release assets contents: write # Required to upload release assets
steps: steps:
- name: Checkout the repository - name: Checkout the repository
uses: actions/checkout@v4.2.1 uses: actions/checkout@v4.1.7
- name: Verify version - name: Verify version
uses: home-assistant/actions/helpers/verify-version@master uses: home-assistant/actions/helpers/verify-version@master
@@ -34,7 +34,7 @@ jobs:
python-version: ${{ env.PYTHON_VERSION }} python-version: ${{ env.PYTHON_VERSION }}
- name: Setup Node - name: Setup Node
uses: actions/setup-node@v4.0.4 uses: actions/setup-node@v4.0.3
with: with:
node-version-file: ".nvmrc" node-version-file: ".nvmrc"
cache: yarn cache: yarn

View File

@@ -13,7 +13,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout the repository - name: Checkout the repository
uses: actions/checkout@v4.2.1 uses: actions/checkout@v4.1.7
- name: Upload Translations - name: Upload Translations
run: | run: |

View File

@@ -1,7 +1,16 @@
diff --git a/modular/sortable.core.esm.js b/modular/sortable.core.esm.js 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 --- a/modular/sortable.core.esm.js
+++ b/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 */{ @@ -1781,11 +1781,16 @@ Sortable.prototype = /** @lends Sortable.prototype */{
} }
if (_onMove(rootEl, el, dragEl, dragRect, target, targetRect, evt, !!target) !== false) { if (_onMove(rootEl, el, dragEl, dragRect, target, targetRect, evt, !!target) !== false) {
@@ -24,7 +33,7 @@ index 8b5e49b011713c8859c669069fbe85ce53974e1d..6a0afc92787157b8a31c38cc5f67dfa5
} }
parentEl = el; // actualization parentEl = el; // actualization
@@ -1802,7 +1807,12 @@ Sortable.prototype = /** @lends Sortable.prototype */{ @@ -1802,7 +1807,13 @@ Sortable.prototype = /** @lends Sortable.prototype */{
targetRect = getRect(target); targetRect = getRect(target);
if (_onMove(rootEl, el, dragEl, dragRect, target, targetRect, evt, false) !== false) { if (_onMove(rootEl, el, dragEl, dragRect, target, targetRect, evt, false) !== false) {
capture(); capture();
@@ -35,10 +44,11 @@ index 8b5e49b011713c8859c669069fbe85ce53974e1d..6a0afc92787157b8a31c38cc5f67dfa5
+ catch(err) { + catch(err) {
+ return completed(false); + return completed(false);
+ } + }
+
parentEl = el; // actualization parentEl = el; // actualization
changed(); changed();
@@ -1849,10 +1859,15 @@ Sortable.prototype = /** @lends Sortable.prototype */{ @@ -1849,12 +1860,17 @@ Sortable.prototype = /** @lends Sortable.prototype */{
_silent = true; _silent = true;
setTimeout(_unsilent, 30); setTimeout(_unsilent, 30);
capture(); capture();
@@ -46,6 +56,8 @@ index 8b5e49b011713c8859c669069fbe85ce53974e1d..6a0afc92787157b8a31c38cc5f67dfa5
- el.appendChild(dragEl); - el.appendChild(dragEl);
- } else { - } else {
- target.parentNode.insertBefore(dragEl, after ? nextSibling : target); - target.parentNode.insertBefore(dragEl, after ? nextSibling : target);
- }
+ try { + try {
+ if (after && !nextSibling) { + if (after && !nextSibling) {
+ el.appendChild(dragEl); + el.appendChild(dragEl);
@@ -55,6 +67,7 @@ index 8b5e49b011713c8859c669069fbe85ce53974e1d..6a0afc92787157b8a31c38cc5f67dfa5
+ } + }
+ catch(err) { + catch(err) {
+ return completed(false); + return completed(false);
} + }
// Undo chrome's scroll adjustment (has no effect on other browsers) // Undo chrome's scroll adjustment (has no effect on other browsers)
if (scrolledPastTop) {
scrollBy(scrolledPastTop, 0, scrollBefore - scrolledPastTop.scrollTop);

File diff suppressed because one or more lines are too long

View File

@@ -6,4 +6,4 @@ enableGlobalCache: false
nodeLinker: node-modules nodeLinker: node-modules
yarnPath: .yarn/releases/yarn-4.5.1.cjs yarnPath: .yarn/releases/yarn-4.4.1.cjs

View File

@@ -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. 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. We use [BrowserStack](https://www.browserstack.com) to test Home Assistant on a large variety of devices.
[![Home Assistant - A project from the Open Home Foundation](https://www.openhomefoundation.org/badges/home-assistant.png)](https://www.openhomefoundation.org/)

View File

@@ -60,12 +60,6 @@ function copyPolyfills(staticDir) {
npmPath("@webcomponents/webcomponentsjs/webcomponents-bundle.js.map"), npmPath("@webcomponents/webcomponentsjs/webcomponents-bundle.js.map"),
staticPath("polyfills/") staticPath("polyfills/")
); );
// dialog-polyfill css
copyFileDir(
npmPath("dialog-polyfill/dialog-polyfill.css"),
staticPath("polyfills/")
);
} }
function copyLoaderJS(staticDir) { function copyLoaderJS(staticDir) {

View File

@@ -139,7 +139,7 @@
</p> </p>
</div> </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"> <div class="card-content">
<p> <p>
Home Assistant Cast is a receiver application for the Chromecast. When Home Assistant Cast is a receiver application for the Chromecast. When

View File

@@ -45,7 +45,6 @@ class HcLovelace extends LitElement {
saveConfig: async () => undefined, saveConfig: async () => undefined,
deleteConfig: async () => undefined, deleteConfig: async () => undefined,
setEditMode: () => undefined, setEditMode: () => undefined,
showToast: () => undefined,
}; };
return html` return html`
<hui-view <hui-view

View File

@@ -36,7 +36,6 @@ import { HassElement } from "../../../../src/state/hass-element";
import { castContext } from "../cast_context"; import { castContext } from "../cast_context";
import "./hc-launch-screen"; import "./hc-launch-screen";
import { getPanelTitleFromUrlPath } from "../../../../src/data/panel"; import { getPanelTitleFromUrlPath } from "../../../../src/data/panel";
import { checkLovelaceConfig } from "../../../../src/panels/lovelace/common/check-lovelace-config";
const DEFAULT_CONFIG: LovelaceDashboardStrategyConfig = { const DEFAULT_CONFIG: LovelaceDashboardStrategyConfig = {
strategy: { strategy: {
@@ -366,9 +365,7 @@ export class HcMain extends HassElement {
this._urlPath || "lovelace" this._urlPath || "lovelace"
); );
castContext.setApplicationState(title || ""); castContext.setApplicationState(title || "");
this._lovelaceConfig = checkLovelaceConfig( this._lovelaceConfig = lovelaceConfig;
lovelaceConfig
) as LovelaceConfig;
} }
private _handleShowDemo(_msg: ShowDemoMessage) { private _handleShowDemo(_msg: ShowDemoMessage) {

View File

@@ -111,47 +111,9 @@ export const demoEntitiesSections: DemoConfig["entities"] = (localize) =>
friendly_name: "Living room Temperature", 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": { "media_player.living_room_nest_mini": {
entity_id: "media_player.living_room_nest_mini", entity_id: "media_player.living_room_nest_mini",
state: "playing", state: "on",
attributes: { attributes: {
device_class: "speaker", device_class: "speaker",
volume_level: 0.18, volume_level: 0.18,
@@ -199,14 +161,6 @@ export const demoEntitiesSections: DemoConfig["entities"] = (localize) =>
supported_features: 32, 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": { "light.worktop_spotlights": {
entity_id: "light.worktop_spotlights", entity_id: "light.worktop_spotlights",
state: "off", state: "off",
@@ -441,14 +395,6 @@ export const demoEntitiesSections: DemoConfig["entities"] = (localize) =>
supported_features: 64063, 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": { "sensor.standing_desk_height": {
entity_id: "sensor.standing_desk_height", entity_id: "sensor.standing_desk_height",
state: "72", state: "72",

View File

@@ -9,57 +9,17 @@ export const demoLovelaceSections: DemoConfig["lovelace"] = (localize) => ({
title: isFrontpageEmbed ? "Home Assistant" : "Demo", title: isFrontpageEmbed ? "Home Assistant" : "Demo",
path: "home", path: "home",
icon: "mdi:home-assistant", 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: [ sections: [
...(isFrontpageEmbed ...(isFrontpageEmbed
? [] ? []
: [ : [
{ {
cards: [ title: `${localize("ui.panel.page-demo.config.sections.titles.welcome")} 👋`,
{ cards: [{ type: "custom:ha-demo-card" }],
type: "heading",
heading: `${localize("ui.panel.page-demo.config.sections.titles.welcome")} 👋`,
},
{ type: "custom:ha-demo-card" },
],
}, },
]), ]),
{ {
cards: [ 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", type: "tile",
entity: "light.floor_lamp", entity: "light.floor_lamp",
@@ -78,6 +38,13 @@ export const demoLovelaceSections: DemoConfig["lovelace"] = (localize) => ({
type: "tile", type: "tile",
entity: "light.bar_lamp", entity: "light.bar_lamp",
}, },
{
graph: "line",
type: "sensor",
entity: "sensor.living_room_temperature",
detail: 1,
name: "Temperature",
},
{ {
type: "tile", type: "tile",
entity: "cover.living_room_garden_shutter", entity: "cover.living_room_garden_shutter",
@@ -88,25 +55,11 @@ export const demoLovelaceSections: DemoConfig["lovelace"] = (localize) => ({
entity: "media_player.living_room_nest_mini", entity: "media_player.living_room_nest_mini",
}, },
], ],
title: `🛋️ ${localize("ui.panel.page-demo.config.sections.titles.living_room")} `,
}, },
{ {
type: "grid", type: "grid",
cards: [ 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", type: "tile",
entity: "cover.kitchen_shutter", entity: "cover.kitchen_shutter",
@@ -137,17 +90,11 @@ export const demoLovelaceSections: DemoConfig["lovelace"] = (localize) => ({
entity: "media_player.kitchen_nest_audio", entity: "media_player.kitchen_nest_audio",
}, },
], ],
title: `👩‍🍳 ${localize("ui.panel.page-demo.config.sections.titles.kitchen")}`,
}, },
{ {
type: "grid", type: "grid",
cards: [ cards: [
{
type: "heading",
heading: localize(
"ui.panel.page-demo.config.sections.titles.energy"
),
icon: "mdi:transmission-tower",
},
{ {
type: "tile", type: "tile",
entity: "binary_sensor.tesla_wall_connector_vehicle_connected", entity: "binary_sensor.tesla_wall_connector_vehicle_connected",
@@ -185,17 +132,11 @@ export const demoLovelaceSections: DemoConfig["lovelace"] = (localize) => ({
color: "dark-grey", color: "dark-grey",
}, },
], ],
title: `⚡️ ${localize("ui.panel.page-demo.config.sections.titles.energy")}`,
}, },
{ {
type: "grid", type: "grid",
cards: [ cards: [
{
type: "heading",
heading: localize(
"ui.panel.page-demo.config.sections.titles.climate"
),
icon: "mdi:thermometer",
},
{ {
type: "tile", type: "tile",
entity: "sun.sun", entity: "sun.sun",
@@ -228,38 +169,16 @@ export const demoLovelaceSections: DemoConfig["lovelace"] = (localize) => ({
state_content: ["preset_mode", "current_temperature"], state_content: ["preset_mode", "current_temperature"],
}, },
], ],
title: `🌤️ ${localize("ui.panel.page-demo.config.sections.titles.climate")}`,
}, },
{ {
type: "grid", type: "grid",
cards: [ 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", type: "tile",
entity: "cover.study_shutter", entity: "cover.study_shutter",
name: "Shutter", name: "Shutter",
}, },
{ {
type: "tile", type: "tile",
entity: "light.study_spotlights", entity: "light.study_spotlights",
@@ -276,23 +195,12 @@ export const demoLovelaceSections: DemoConfig["lovelace"] = (localize) => ({
color: "brown", color: "brown",
icon: "mdi:desk", icon: "mdi:desk",
}, },
{
type: "tile",
entity: "switch.in_meeting",
name: "Meeting mode",
},
], ],
title: `🧑‍💻 ${localize("ui.panel.page-demo.config.sections.titles.study")}`,
}, },
{ {
type: "grid", type: "grid",
cards: [ cards: [
{
type: "heading",
heading: localize(
"ui.panel.page-demo.config.sections.titles.outdoor"
),
icon: "mdi:tree",
},
{ {
type: "tile", type: "tile",
entity: "light.outdoor_light", entity: "light.outdoor_light",
@@ -322,17 +230,11 @@ export const demoLovelaceSections: DemoConfig["lovelace"] = (localize) => ({
name: "Illuminance", name: "Illuminance",
}, },
], ],
title: `🌳 ${localize("ui.panel.page-demo.config.sections.titles.outdoor")}`,
}, },
{ {
type: "grid", type: "grid",
cards: [ cards: [
{
type: "heading",
heading: localize(
"ui.panel.page-demo.config.sections.titles.updates"
),
icon: "mdi:update",
},
{ {
type: "tile", type: "tile",
entity: "automation.home_assistant_auto_update", entity: "automation.home_assistant_auto_update",
@@ -358,6 +260,7 @@ export const demoLovelaceSections: DemoConfig["lovelace"] = (localize) => ({
icon: "mdi:home-assistant", icon: "mdi:home-assistant",
}, },
], ],
title: `🎉 ${localize("ui.panel.page-demo.config.sections.titles.updates")}`,
}, },
], ],
}, },

View File

@@ -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 },
}));
};

View File

@@ -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[]);
};

View File

@@ -217,22 +217,22 @@ export const basicTrace: DemoTrace = {
id: "1615419646544", id: "1615419646544",
alias: "Ensure Party mode", alias: "Ensure Party mode",
description: "", description: "",
triggers: [ trigger: [
{ {
trigger: "state", platform: "state",
entity_id: "input_boolean.toggle_1", entity_id: "input_boolean.toggle_1",
}, },
], ],
conditions: [ condition: [
{ {
condition: "template", condition: "template",
alias: "Test if Paulus is home", alias: "Test if Paulus is home",
value_template: "{{ true }}", value_template: "{{ true }}",
}, },
], ],
actions: [ action: [
{ {
action: "input_boolean.toggle", service: "input_boolean.toggle",
target: { target: {
entity_id: "input_boolean.toggle_4", entity_id: "input_boolean.toggle_4",
}, },
@@ -268,7 +268,7 @@ export const basicTrace: DemoTrace = {
], ],
default: [ default: [
{ {
action: "input_boolean.toggle", service: "input_boolean.toggle",
alias: "Toggle 2", alias: "Toggle 2",
target: { target: {
entity_id: "input_boolean.toggle_2", entity_id: "input_boolean.toggle_2",
@@ -277,7 +277,7 @@ export const basicTrace: DemoTrace = {
], ],
}, },
{ {
action: "input_boolean.toggle", service: "input_boolean.toggle",
target: { target: {
entity_id: "input_boolean.toggle_4", entity_id: "input_boolean.toggle_4",
}, },

View File

@@ -31,8 +31,8 @@ export const mockDemoTrace = (
], ],
}, },
config: { config: {
triggers: [], trigger: [],
actions: [], action: [],
}, },
context: { context: {
id: "abcd", id: "abcd",

View File

@@ -133,17 +133,17 @@ export const motionLightTrace: DemoTrace = {
config: { config: {
mode: "restart", mode: "restart",
max_exceeded: "silent", max_exceeded: "silent",
triggers: [ trigger: [
{ {
trigger: "state", platform: "state",
entity_id: "binary_sensor.pauluss_macbook_pro_camera_in_use", entity_id: "binary_sensor.pauluss_macbook_pro_camera_in_use",
from: "off", from: "off",
to: "on", to: "on",
}, },
], ],
actions: [ action: [
{ {
action: "light.turn_on", service: "light.turn_on",
target: { target: {
entity_id: "light.elgato_key_light_air", entity_id: "light.elgato_key_light_air",
}, },
@@ -162,7 +162,7 @@ export const motionLightTrace: DemoTrace = {
delay: 0, delay: 0,
}, },
{ {
action: "light.turn_off", service: "light.turn_off",
target: { target: {
entity_id: "light.elgato_key_light_air", entity_id: "light.elgato_key_light_air",
}, },

View File

@@ -48,7 +48,7 @@ const ACTIONS = [
{ {
wait_for_trigger: [ wait_for_trigger: [
{ {
trigger: "state", platform: "state",
entity_id: "input_boolean.toggle_1", entity_id: "input_boolean.toggle_1",
}, },
], ],
@@ -121,7 +121,7 @@ const ACTIONS = [
]; ];
const initialAction: Action = { const initialAction: Action = {
action: "light.turn_on", service: "light.turn_on",
target: { target: {
entity_id: "light.kitchen", entity_id: "light.kitchen",
}, },
@@ -142,7 +142,7 @@ export class DemoAutomationDescribeAction extends LitElement {
<div class="action"> <div class="action">
<span> <span>
${this._action ${this._action
? describeAction(this.hass, [], [], this._action) ? describeAction(this.hass, [], [], [], this._action)
: "<invalid YAML>"} : "<invalid YAML>"}
</span> </span>
<ha-yaml-editor <ha-yaml-editor
@@ -155,7 +155,7 @@ export class DemoAutomationDescribeAction extends LitElement {
${ACTIONS.map( ${ACTIONS.map(
(conf) => html` (conf) => html`
<div class="action"> <div class="action">
<span>${describeAction(this.hass, [], [], conf as any)}</span> <span>${describeAction(this.hass, [], [], [], conf as any)}</span>
<pre>${dump(conf)}</pre> <pre>${dump(conf)}</pre>
</div> </div>
` `

View File

@@ -22,52 +22,46 @@ const ENTITIES = [
]; ];
const triggers = [ const triggers = [
{ trigger: "state", entity_id: "light.kitchen", from: "off", to: "on" }, { platform: "state", entity_id: "light.kitchen", from: "off", to: "on" },
{ trigger: "mqtt" }, { platform: "mqtt" },
{ {
trigger: "geo_location", platform: "geo_location",
source: "test_source", source: "test_source",
zone: "zone.home", zone: "zone.home",
event: "enter", event: "enter",
}, },
{ trigger: "homeassistant", event: "start" }, { platform: "homeassistant", event: "start" },
{ {
trigger: "numeric_state", platform: "numeric_state",
entity_id: "light.kitchen", entity_id: "light.kitchen",
attribute: "brightness", attribute: "brightness",
below: 80, below: 80,
above: 20, above: 20,
}, },
{ trigger: "sun", event: "sunset" }, { platform: "sun", event: "sunset" },
{ trigger: "time_pattern" }, { platform: "time_pattern" },
{ trigger: "time_pattern", hours: "*", minutes: "/5", seconds: "10" }, { platform: "time_pattern", hours: "*", minutes: "/5", seconds: "10" },
{ trigger: "webhook" }, { platform: "webhook" },
{ trigger: "persistent_notification" }, { platform: "persistent_notification" },
{ {
trigger: "zone", platform: "zone",
entity_id: "person.person", entity_id: "person.person",
zone: "zone.home", zone: "zone.home",
event: "enter", event: "enter",
}, },
{ trigger: "tag" }, { platform: "tag" },
{ trigger: "time", at: "15:32" }, { platform: "time", at: "15:32" },
{ trigger: "template" }, { platform: "template" },
{ trigger: "conversation", command: "Turn on the lights" }, { platform: "conversation", command: "Turn on the lights" },
{ {
trigger: "conversation", platform: "conversation",
command: ["Turn on the lights", "Turn the lights on"], command: ["Turn on the lights", "Turn the lights on"],
}, },
{ trigger: "event", event_type: "homeassistant_started" }, { platform: "event", event_type: "homeassistant_started" },
{
triggers: [
{ trigger: "state", entity_id: "light.kitchen", to: "on" },
{ trigger: "state", entity_id: "light.kitchen", to: "off" },
],
},
]; ];
const initialTrigger: Trigger = { const initialTrigger: Trigger = {
trigger: "state", platform: "state",
entity_id: "light.kitchen", entity_id: "light.kitchen",
}; };

View File

@@ -8,9 +8,6 @@ import { mockEntityRegistry } from "../../../../demo/src/stubs/entity_registry";
import { mockDeviceRegistry } from "../../../../demo/src/stubs/device_registry"; import { mockDeviceRegistry } from "../../../../demo/src/stubs/device_registry";
import { mockAreaRegistry } from "../../../../demo/src/stubs/area_registry"; import { mockAreaRegistry } from "../../../../demo/src/stubs/area_registry";
import { mockHassioSupervisor } from "../../../../demo/src/stubs/hassio_supervisor"; 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 type { Trigger } from "../../../../src/data/automation";
import { HaGeolocationTrigger } from "../../../../src/panels/config/automation/trigger/types/ha-automation-trigger-geo_location"; 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"; 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 { HaMQTTTrigger } from "../../../../src/panels/config/automation/trigger/types/ha-automation-trigger-mqtt";
import "../../../../src/panels/config/automation/trigger/ha-automation-trigger"; import "../../../../src/panels/config/automation/trigger/ha-automation-trigger";
import { HaConversationTrigger } from "../../../../src/panels/config/automation/trigger/types/ha-automation-trigger-conversation"; 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[] }[] = [ const SCHEMAS: { name: string; triggers: Trigger[] }[] = [
{ {
@@ -115,15 +111,11 @@ const SCHEMAS: { name: string; triggers: Trigger[] }[] = [
triggers: [ triggers: [
{ ...HaConversationTrigger.defaultConfig }, { ...HaConversationTrigger.defaultConfig },
{ {
trigger: "conversation", platform: "conversation",
command: ["Turn on the lights", "Turn the lights on"], command: ["Turn on the lights", "Turn the lights on"],
}, },
], ],
}, },
{
name: "Trigger list",
triggers: [{ ...HaTriggerList.defaultConfig }],
},
]; ];
@customElement("demo-automation-editor-trigger") @customElement("demo-automation-editor-trigger")
@@ -143,9 +135,6 @@ export class DemoAutomationEditorTrigger extends LitElement {
mockDeviceRegistry(hass); mockDeviceRegistry(hass);
mockAreaRegistry(hass); mockAreaRegistry(hass);
mockHassioSupervisor(hass); mockHassioSupervisor(hass);
mockConfig(hass);
mockTags(hass);
mockAuth(hass);
} }
protected render(): TemplateResult { protected render(): TemplateResult {

View File

@@ -64,7 +64,6 @@ const DEVICES: DeviceRegistryEntry[] = [
labels: [], labels: [],
created_at: 0, created_at: 0,
modified_at: 0, modified_at: 0,
primary_config_entry: null,
}, },
{ {
area_id: "backyard", area_id: "backyard",
@@ -87,7 +86,6 @@ const DEVICES: DeviceRegistryEntry[] = [
labels: [], labels: [],
created_at: 0, created_at: 0,
modified_at: 0, modified_at: 0,
primary_config_entry: null,
}, },
{ {
area_id: null, area_id: null,
@@ -110,7 +108,6 @@ const DEVICES: DeviceRegistryEntry[] = [
labels: [], labels: [],
created_at: 0, created_at: 0,
modified_at: 0, modified_at: 0,
primary_config_entry: null,
}, },
]; ];

View File

@@ -64,7 +64,6 @@ const DEVICES: DeviceRegistryEntry[] = [
labels: [], labels: [],
created_at: 0, created_at: 0,
modified_at: 0, modified_at: 0,
primary_config_entry: null,
}, },
{ {
area_id: "backyard", area_id: "backyard",
@@ -87,7 +86,6 @@ const DEVICES: DeviceRegistryEntry[] = [
labels: [], labels: [],
created_at: 0, created_at: 0,
modified_at: 0, modified_at: 0,
primary_config_entry: null,
}, },
{ {
area_id: null, area_id: null,
@@ -110,7 +108,6 @@ const DEVICES: DeviceRegistryEntry[] = [
labels: [], labels: [],
created_at: 0, created_at: 0,
modified_at: 0, modified_at: 0,
primary_config_entry: null,
}, },
]; ];

View File

@@ -232,7 +232,6 @@ const createDeviceRegistryEntries = (
labels: [], labels: [],
created_at: 0, created_at: 0,
modified_at: 0, modified_at: 0,
primary_config_entry: null,
}, },
]; ];

View File

@@ -2,8 +2,7 @@ import "@material/mwc-button";
import { import {
mdiCheckCircle, mdiCheckCircle,
mdiChip, mdiChip,
mdiPlayCircle, mdiCircle,
mdiCircleOffOutline,
mdiCursorDefaultClickOutline, mdiCursorDefaultClickOutline,
mdiDocker, mdiDocker,
mdiExclamationThick, mdiExclamationThick,
@@ -199,7 +198,7 @@ class HassioAddonInfo extends LitElement {
"dashboard.addon_running" "dashboard.addon_running"
)} )}
class="running" class="running"
.path=${mdiPlayCircle} .path=${mdiCircle}
></ha-svg-icon> ></ha-svg-icon>
` `
: html` : html`
@@ -208,7 +207,7 @@ class HassioAddonInfo extends LitElement {
"dashboard.addon_stopped" "dashboard.addon_stopped"
)} )}
class="stopped" class="stopped"
.path=${mdiCircleOffOutline} .path=${mdiCircle}
></ha-svg-icon> ></ha-svg-icon>
`} `}
` `

View File

@@ -15,7 +15,6 @@ import { LocalizeFunc } from "../../../src/common/translations/localize";
import "../../../src/components/ha-checkbox"; import "../../../src/components/ha-checkbox";
import "../../../src/components/ha-formfield"; import "../../../src/components/ha-formfield";
import "../../../src/components/ha-textfield"; import "../../../src/components/ha-textfield";
import "../../../src/components/ha-password-field";
import "../../../src/components/ha-radio"; import "../../../src/components/ha-radio";
import type { HaRadio } from "../../../src/components/ha-radio"; import type { HaRadio } from "../../../src/components/ha-radio";
import { import {
@@ -262,21 +261,23 @@ export class SupervisorBackupContent extends LitElement {
: ""} : ""}
${this.backupHasPassword ${this.backupHasPassword
? html` ? html`
<ha-password-field <ha-textfield
.label=${this._localize("password")} .label=${this._localize("password")}
type="password"
name="backupPassword" name="backupPassword"
.value=${this.backupPassword} .value=${this.backupPassword}
@change=${this._handleTextValueChanged} @change=${this._handleTextValueChanged}
> >
</ha-password-field> </ha-textfield>
${!this.backup ${!this.backup
? html`<ha-password-field ? html`<ha-textfield
.label=${this._localize("confirm_password")} .label=${this._localize("confirm_password")}
type="password"
name="confirmBackupPassword" name="confirmBackupPassword"
.value=${this.confirmBackupPassword} .value=${this.confirmBackupPassword}
@change=${this._handleTextValueChanged} @change=${this._handleTextValueChanged}
> >
</ha-password-field>` </ha-textfield>`
: ""} : ""}
` `
: ""} : ""}

View File

@@ -13,12 +13,10 @@ import "../../../../src/components/ha-circular-progress";
import "../../../../src/components/ha-dialog"; import "../../../../src/components/ha-dialog";
import "../../../../src/components/ha-expansion-panel"; import "../../../../src/components/ha-expansion-panel";
import "../../../../src/components/ha-formfield"; import "../../../../src/components/ha-formfield";
import "../../../../src/components/ha-textfield";
import "../../../../src/components/ha-header-bar"; import "../../../../src/components/ha-header-bar";
import "../../../../src/components/ha-icon-button"; import "../../../../src/components/ha-icon-button";
import "../../../../src/components/ha-password-field";
import "../../../../src/components/ha-radio"; 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 { extractApiErrorMessage } from "../../../../src/data/hassio/common";
import { import {
AccessPoints, AccessPoints,
@@ -36,6 +34,7 @@ import { HassDialog } from "../../../../src/dialogs/make-dialog-manager";
import { haStyleDialog } from "../../../../src/resources/styles"; import { haStyleDialog } from "../../../../src/resources/styles";
import type { HomeAssistant } from "../../../../src/types"; import type { HomeAssistant } from "../../../../src/types";
import { HassioNetworkDialogParams } from "./show-dialog-network"; import { HassioNetworkDialogParams } from "./show-dialog-network";
import type { HaTextField } from "../../../../src/components/ha-textfield";
const IP_VERSIONS = ["ipv4", "ipv6"]; const IP_VERSIONS = ["ipv4", "ipv6"];
@@ -247,8 +246,9 @@ export class DialogHassioNetwork
${this._wifiConfiguration.auth === "wpa-psk" || ${this._wifiConfiguration.auth === "wpa-psk" ||
this._wifiConfiguration.auth === "wep" this._wifiConfiguration.auth === "wep"
? html` ? html`
<ha-password-field <ha-textfield
class="flex-auto" class="flex-auto"
type="password"
id="psk" id="psk"
.label=${this.supervisor.localize( .label=${this.supervisor.localize(
"dialog.network.wifi_password" "dialog.network.wifi_password"
@@ -256,7 +256,7 @@ export class DialogHassioNetwork
version="wifi" version="wifi"
@change=${this._handleInputValueChangedWifi} @change=${this._handleInputValueChangedWifi}
> >
</ha-password-field> </ha-textfield>
` `
: ""} : ""}
` `

View File

@@ -25,8 +25,8 @@ import type { HomeAssistant } from "../../../../src/types";
import { HassioRepositoryDialogParams } from "./show-dialog-repositories"; import { HassioRepositoryDialogParams } from "./show-dialog-repositories";
import type { HaTextField } from "../../../../src/components/ha-textfield"; import type { HaTextField } from "../../../../src/components/ha-textfield";
import "../../../../src/components/ha-textfield"; import "../../../../src/components/ha-textfield";
import "../../../../src/components/ha-md-list"; import "../../../../src/components/ha-list-new";
import "../../../../src/components/ha-md-list-item"; import "../../../../src/components/ha-list-item-new";
@customElement("dialog-hassio-repositories") @customElement("dialog-hassio-repositories")
class HassioRepositoriesDialog extends LitElement { class HassioRepositoriesDialog extends LitElement {
@@ -107,11 +107,11 @@ class HassioRepositoriesDialog extends LitElement {
? html`<ha-alert alert-type="error">${this._error}</ha-alert>` ? html`<ha-alert alert-type="error">${this._error}</ha-alert>`
: ""} : ""}
<div class="form"> <div class="form">
<ha-md-list> <ha-list-new>
${repositories.length ${repositories.length
? repositories.map( ? repositories.map(
(repo) => html` (repo) => html`
<ha-md-list-item class="option"> <ha-list-item-new class="option">
${repo.name} ${repo.name}
<div slot="supporting-text"> <div slot="supporting-text">
<div>${repo.maintainer}</div> <div>${repo.maintainer}</div>
@@ -142,11 +142,11 @@ class HassioRepositoriesDialog extends LitElement {
)} )}
</simple-tooltip> </simple-tooltip>
</div> </div>
</ha-md-list-item> </ha-list-item-new>
` `
) )
: html`<ha-md-list-item> No repositories </ha-md-list-item>`} : html`<ha-list-item-new> No repositories </ha-list-item-new>`}
</ha-md-list> </ha-list-new>
<div class="layout horizontal bottom"> <div class="layout horizontal bottom">
<ha-textfield <ha-textfield
class="flex-auto" class="flex-auto"
@@ -209,7 +209,7 @@ class HassioRepositoriesDialog extends LitElement {
div.delete ha-icon-button { div.delete ha-icon-button {
color: var(--error-color); color: var(--error-color);
} }
ha-md-list-item { ha-list-item-new {
position: relative; position: relative;
} }
`, `,

View File

@@ -13,11 +13,10 @@
<% for (const entry of es5EntryJS) { %> <% for (const entry of es5EntryJS) { %>
loadES5("<%= entry %>"); loadES5("<%= entry %>");
<% } %> <% } %>
}
} else { } else {
<% for (const entry of es5EntryJS) { %> <% for (const entry of es5EntryJS) { %>
loadES5("<%= entry %>"); loadES5("<%= entry %>");
<% } %> <% } %>
} }
}
})(); })();

View File

@@ -25,24 +25,24 @@
"license": "Apache-2.0", "license": "Apache-2.0",
"type": "module", "type": "module",
"dependencies": { "dependencies": {
"@babel/runtime": "7.25.7", "@babel/runtime": "7.25.6",
"@braintree/sanitize-url": "7.1.0", "@braintree/sanitize-url": "7.1.0",
"@codemirror/autocomplete": "6.18.1", "@codemirror/autocomplete": "6.18.0",
"@codemirror/commands": "6.7.0", "@codemirror/commands": "6.6.1",
"@codemirror/language": "6.10.3", "@codemirror/language": "6.10.2",
"@codemirror/legacy-modes": "6.4.1", "@codemirror/legacy-modes": "6.4.1",
"@codemirror/search": "6.5.6", "@codemirror/search": "6.5.6",
"@codemirror/state": "6.4.1", "@codemirror/state": "6.4.1",
"@codemirror/view": "6.34.1", "@codemirror/view": "6.33.0",
"@egjs/hammerjs": "2.0.17", "@egjs/hammerjs": "2.0.17",
"@formatjs/intl-datetimeformat": "6.14.0", "@formatjs/intl-datetimeformat": "6.12.5",
"@formatjs/intl-displaynames": "6.6.10", "@formatjs/intl-displaynames": "6.6.8",
"@formatjs/intl-getcanonicallocales": "2.3.1", "@formatjs/intl-getcanonicallocales": "2.3.0",
"@formatjs/intl-listformat": "7.5.9", "@formatjs/intl-listformat": "7.5.7",
"@formatjs/intl-locale": "4.0.2", "@formatjs/intl-locale": "4.0.0",
"@formatjs/intl-numberformat": "8.12.0", "@formatjs/intl-numberformat": "8.10.3",
"@formatjs/intl-pluralrules": "5.2.16", "@formatjs/intl-pluralrules": "5.2.14",
"@formatjs/intl-relativetimeformat": "11.2.16", "@formatjs/intl-relativetimeformat": "11.2.14",
"@fullcalendar/core": "6.1.15", "@fullcalendar/core": "6.1.15",
"@fullcalendar/daygrid": "6.1.15", "@fullcalendar/daygrid": "6.1.15",
"@fullcalendar/interaction": "6.1.15", "@fullcalendar/interaction": "6.1.15",
@@ -80,17 +80,16 @@
"@material/mwc-top-app-bar": "0.27.0", "@material/mwc-top-app-bar": "0.27.0",
"@material/mwc-top-app-bar-fixed": "0.27.0", "@material/mwc-top-app-bar-fixed": "0.27.0",
"@material/top-app-bar": "=14.0.0-canary.53b3cad2f.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/js": "7.4.47",
"@mdi/svg": "7.4.47", "@mdi/svg": "7.4.47",
"@polymer/paper-item": "3.0.1", "@polymer/paper-item": "3.0.1",
"@polymer/paper-listbox": "3.0.1", "@polymer/paper-listbox": "3.0.1",
"@polymer/paper-tabs": "3.1.0", "@polymer/paper-tabs": "3.1.0",
"@polymer/polymer": "3.5.2", "@polymer/polymer": "3.5.1",
"@replit/codemirror-indentation-markers": "6.5.3",
"@thomasloven/round-slider": "0.6.0", "@thomasloven/round-slider": "0.6.0",
"@vaadin/combo-box": "24.5.0", "@vaadin/combo-box": "24.4.7",
"@vaadin/vaadin-themable-mixin": "24.5.0", "@vaadin/vaadin-themable-mixin": "24.4.7",
"@vibrant/color": "3.2.1-alpha.1", "@vibrant/color": "3.2.1-alpha.1",
"@vibrant/core": "3.2.1-alpha.1", "@vibrant/core": "3.2.1-alpha.1",
"@vibrant/quantizer-mmcq": "3.2.1-alpha.1", "@vibrant/quantizer-mmcq": "3.2.1-alpha.1",
@@ -98,29 +97,28 @@
"@webcomponents/scoped-custom-element-registry": "0.0.9", "@webcomponents/scoped-custom-element-registry": "0.0.9",
"@webcomponents/webcomponentsjs": "2.8.0", "@webcomponents/webcomponentsjs": "2.8.0",
"app-datepicker": "5.1.1", "app-datepicker": "5.1.1",
"chart.js": "4.4.5", "chart.js": "4.4.4",
"color-name": "2.0.0", "color-name": "2.0.0",
"comlink": "4.4.1", "comlink": "4.4.1",
"core-js": "3.38.1", "core-js": "3.38.1",
"cropperjs": "1.6.2", "cropperjs": "1.6.2",
"date-fns": "4.1.0", "date-fns": "3.6.0",
"date-fns-tz": "3.2.0", "date-fns-tz": "3.1.3",
"deep-clone-simple": "1.1.1", "deep-clone-simple": "1.1.1",
"deep-freeze": "0.0.1", "deep-freeze": "0.0.1",
"dialog-polyfill": "0.5.6",
"element-internals-polyfill": "1.3.11", "element-internals-polyfill": "1.3.11",
"fuse.js": "7.0.0", "fuse.js": "7.0.0",
"google-timezones-json": "1.2.0", "google-timezones-json": "1.2.0",
"hls.js": "patch:hls.js@npm%3A1.5.7#~/.yarn/patches/hls.js-npm-1.5.7-f5bbd3d060.patch", "hls.js": "patch:hls.js@npm%3A1.5.7#~/.yarn/patches/hls.js-npm-1.5.7-f5bbd3d060.patch",
"home-assistant-js-websocket": "9.4.0", "home-assistant-js-websocket": "9.4.0",
"idb-keyval": "6.2.1", "idb-keyval": "6.2.1",
"intl-messageformat": "10.7.0", "intl-messageformat": "10.5.14",
"js-yaml": "4.1.0", "js-yaml": "4.1.0",
"leaflet": "1.9.4", "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", "lit": "2.8.0",
"luxon": "3.5.0", "luxon": "3.5.0",
"marked": "14.1.3", "marked": "14.1.0",
"memoize-one": "6.0.0", "memoize-one": "6.0.0",
"node-vibrant": "3.2.1-alpha.1", "node-vibrant": "3.2.1-alpha.1",
"proxy-polyfill": "0.3.2", "proxy-polyfill": "0.3.2",
@@ -129,13 +127,13 @@
"qrcode": "1.5.4", "qrcode": "1.5.4",
"roboto-fontface": "0.10.0", "roboto-fontface": "0.10.0",
"rrule": "2.8.1", "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", "stacktrace-js": "2.0.2",
"superstruct": "2.0.2", "superstruct": "2.0.2",
"tinykeys": "3.0.0", "tinykeys": "3.0.0",
"tsparticles-engine": "2.12.0", "tsparticles-engine": "2.12.0",
"tsparticles-preset-links": "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", "unfetch": "5.0.0",
"vis-data": "7.1.9", "vis-data": "7.1.9",
"vis-network": "9.1.9", "vis-network": "9.1.9",
@@ -151,36 +149,36 @@
"xss": "1.0.15" "xss": "1.0.15"
}, },
"devDependencies": { "devDependencies": {
"@babel/core": "7.25.8", "@babel/core": "7.25.2",
"@babel/helper-define-polyfill-provider": "0.6.2", "@babel/helper-define-polyfill-provider": "0.6.2",
"@babel/plugin-proposal-decorators": "7.25.7", "@babel/plugin-proposal-decorators": "7.24.7",
"@babel/plugin-transform-runtime": "7.25.7", "@babel/plugin-transform-runtime": "7.25.4",
"@babel/preset-env": "7.25.8", "@babel/preset-env": "7.25.4",
"@babel/preset-typescript": "7.25.7", "@babel/preset-typescript": "7.24.7",
"@bundle-stats/plugin-webpack-filter": "4.16.0", "@bundle-stats/plugin-webpack-filter": "4.15.0",
"@koa/cors": "5.0.0", "@koa/cors": "5.0.0",
"@lokalise/node-api": "12.8.0", "@lokalise/node-api": "12.7.0",
"@octokit/auth-oauth-device": "7.1.1", "@octokit/auth-oauth-device": "7.1.1",
"@octokit/plugin-retry": "7.1.2", "@octokit/plugin-retry": "7.1.1",
"@octokit/rest": "21.0.2", "@octokit/rest": "21.0.2",
"@open-wc/dev-server-hmr": "0.1.4", "@open-wc/dev-server-hmr": "0.1.4",
"@rollup/plugin-babel": "6.0.4", "@rollup/plugin-babel": "6.0.4",
"@rollup/plugin-commonjs": "26.0.1", "@rollup/plugin-commonjs": "26.0.1",
"@rollup/plugin-json": "6.1.0", "@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", "@rollup/plugin-replace": "5.0.7",
"@types/babel__plugin-transform-runtime": "7.9.5", "@types/babel__plugin-transform-runtime": "7.9.5",
"@types/chromecast-caf-receiver": "6.0.17", "@types/chromecast-caf-receiver": "6.0.17",
"@types/chromecast-caf-sender": "1.0.10", "@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/glob": "8.1.0",
"@types/html-minifier-terser": "7.0.2", "@types/html-minifier-terser": "7.0.2",
"@types/js-yaml": "4.0.9", "@types/js-yaml": "4.0.9",
"@types/leaflet": "1.9.13", "@types/leaflet": "1.9.12",
"@types/leaflet-draw": "1.0.11", "@types/leaflet-draw": "1.0.11",
"@types/lodash.merge": "4.6.9", "@types/lodash.merge": "4.6.9",
"@types/luxon": "3.4.2", "@types/luxon": "3.4.2",
"@types/mocha": "10.0.9", "@types/mocha": "10.0.7",
"@types/qrcode": "1.5.5", "@types/qrcode": "1.5.5",
"@types/serve-handler": "6.1.4", "@types/serve-handler": "6.1.4",
"@types/sortablejs": "1.15.8", "@types/sortablejs": "1.15.8",
@@ -191,21 +189,21 @@
"@typescript-eslint/parser": "7.18.0", "@typescript-eslint/parser": "7.18.0",
"@web/dev-server": "0.1.38", "@web/dev-server": "0.1.38",
"@web/dev-server-rollup": "0.4.1", "@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", "babel-plugin-template-html-minifier": "4.1.0",
"browserslist-useragent-regexp": "4.1.3", "browserslist-useragent-regexp": "4.1.3",
"chai": "5.1.1", "chai": "5.1.1",
"del": "8.0.0", "del": "7.1.0",
"eslint": "8.57.1", "eslint": "8.57.0",
"eslint-config-airbnb-base": "15.0.0", "eslint-config-airbnb-base": "15.0.0",
"eslint-config-airbnb-typescript": "18.0.0", "eslint-config-airbnb-typescript": "18.0.0",
"eslint-config-prettier": "9.1.0", "eslint-config-prettier": "9.1.0",
"eslint-import-resolver-webpack": "0.13.9", "eslint-import-resolver-webpack": "0.13.8",
"eslint-plugin-import": "2.31.0", "eslint-plugin-import": "2.29.1",
"eslint-plugin-lit": "1.15.0", "eslint-plugin-lit": "1.14.0",
"eslint-plugin-lit-a11y": "4.1.4", "eslint-plugin-lit-a11y": "4.1.4",
"eslint-plugin-unused-imports": "4.1.4", "eslint-plugin-unused-imports": "4.1.3",
"eslint-plugin-wc": "2.2.0", "eslint-plugin-wc": "2.1.1",
"fancy-log": "2.0.0", "fancy-log": "2.0.0",
"fs-extra": "11.2.0", "fs-extra": "11.2.0",
"glob": "11.0.0", "glob": "11.0.0",
@@ -215,35 +213,35 @@
"gulp-rename": "2.0.0", "gulp-rename": "2.0.0",
"gulp-zopfli-green": "6.0.2", "gulp-zopfli-green": "6.0.2",
"html-minifier-terser": "7.2.0", "html-minifier-terser": "7.2.0",
"husky": "9.1.6", "husky": "9.1.5",
"instant-mocha": "1.5.3", "instant-mocha": "1.5.2",
"jszip": "3.10.1", "jszip": "3.10.1",
"lint-staged": "15.2.10", "lint-staged": "15.2.10",
"lit-analyzer": "2.0.3", "lit-analyzer": "2.0.3",
"lodash.merge": "4.6.2", "lodash.merge": "4.6.2",
"lodash.template": "4.5.0", "lodash.template": "4.5.0",
"magic-string": "0.30.12", "magic-string": "0.30.11",
"map-stream": "0.0.7", "map-stream": "0.0.7",
"mocha": "10.7.3", "mocha": "10.5.0",
"object-hash": "3.0.0", "object-hash": "3.0.0",
"open": "10.1.0", "open": "10.1.0",
"pinst": "3.0.0", "pinst": "3.0.0",
"prettier": "3.3.3", "prettier": "3.3.3",
"rollup": "2.79.2", "rollup": "2.79.1",
"rollup-plugin-string": "3.0.0", "rollup-plugin-string": "3.0.0",
"rollup-plugin-terser": "7.0.2", "rollup-plugin-terser": "7.0.2",
"rollup-plugin-visualizer": "5.12.0", "rollup-plugin-visualizer": "5.12.0",
"serve-handler": "6.1.6", "serve-handler": "6.1.5",
"sinon": "19.0.2", "sinon": "18.0.0",
"systemjs": "6.15.1", "systemjs": "6.15.1",
"tar": "7.4.3", "tar": "7.4.3",
"terser-webpack-plugin": "5.3.10", "terser-webpack-plugin": "5.3.10",
"transform-async-modules-webpack-plugin": "1.1.1", "transform-async-modules-webpack-plugin": "1.1.1",
"ts-lit-plugin": "2.0.2", "ts-lit-plugin": "2.0.2",
"typescript": "5.6.3", "typescript": "5.5.4",
"webpack": "5.95.0", "webpack": "5.94.0",
"webpack-cli": "5.1.4", "webpack-cli": "5.1.4",
"webpack-dev-server": "5.1.0", "webpack-dev-server": "5.0.4",
"webpack-manifest-plugin": "5.0.0", "webpack-manifest-plugin": "5.0.0",
"webpack-stats-plugin": "1.1.3", "webpack-stats-plugin": "1.1.3",
"webpackbar": "6.0.1", "webpackbar": "6.0.1",
@@ -251,12 +249,14 @@
}, },
"_comment": "Polymer 3.2 contained a bug, fixed in https://github.com/Polymer/polymer/pull/5569, add as patch", "_comment": "Polymer 3.2 contained a bug, fixed in https://github.com/Polymer/polymer/pull/5569, add as patch",
"resolutions": { "resolutions": {
"@polymer/polymer": "patch:@polymer/polymer@3.5.2#./.yarn/patches/@polymer/polymer/pr-5569.patch", "@polymer/polymer": "patch:@polymer/polymer@3.5.1#./.yarn/patches/@polymer/polymer/pr-5569.patch",
"@material/mwc-button@^0.25.3": "^0.27.0", "@material/mwc-button@^0.25.3": "^0.27.0",
"lit": "2.8.0", "lit": "2.8.0",
"clean-css": "5.3.3", "clean-css": "5.3.3",
"@lit/reactive-element": "1.6.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.1" "packageManager": "yarn@4.4.1"
} }

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.9 KiB

View File

@@ -1,10 +1,10 @@
[build-system] [build-system]
requires = ["setuptools~=75.1"] requires = ["setuptools~=68.0", "wheel~=0.40.0"]
build-backend = "setuptools.build_meta" build-backend = "setuptools.build_meta"
[project] [project]
name = "home-assistant-frontend" name = "home-assistant-frontend"
version = "20241010.0" version = "20240904.0"
license = {text = "Apache-2.0"} license = {text = "Apache-2.0"}
description = "The Home Assistant frontend" description = "The Home Assistant frontend"
readme = "README.md" readme = "README.md"

View File

@@ -18,9 +18,5 @@ if [[ -n "$DEVCONTAINER" ]]; then
fi fi
fi fi
if ! command -v yarn &> /dev/null; then
echo "Error: yarn not found. Please install it following the official instructions: https://yarnpkg.com/getting-started/install" >&2
exit 1
fi
# Install node modules # Install node modules
yarn install yarn install

View File

@@ -1,36 +1,36 @@
import { theme2hex } from "./convert-color"; import { theme2hex } from "./convert-color";
export const COLORS = [ export const COLORS = [
"#4269d0", "#44739e",
"#f4bd4a", "#984ea3",
"#ff725c", "#00d2d5",
"#6cc5b0", "#ff7f00",
"#a463f2", "#af8d00",
"#ff8ab7", "#7f80cd",
"#9c6b4e", "#b3e900",
"#97bbf5", "#c42e60",
"#01ab63", "#a65628",
"#9498a0", "#f781bf",
"#094bad", "#8dd3c7",
"#c99000", "#bebada",
"#d84f3e", "#fb8072",
"#49a28f", "#80b1d3",
"#048732", "#fdb462",
"#d96895", "#fccde5",
"#8043ce", "#bc80bd",
"#7599d1", "#ffed6f",
"#7a4c31", "#c4eaff",
"#74787f", "#cf8c00",
"#6989f4", "#1b9e77",
"#ffd444", "#d95f02",
"#ff957c", "#e7298a",
"#8fe9d3", "#e6ab02",
"#62cc71", "#a6761d",
"#ffadda", "#0097ff",
"#c884ff", "#00d067",
"#badeff", "#f43600",
"#bf8b6d", "#4ba93b",
"#b6bac2", "#5779bb",
"#927acc", "#927acc",
"#97ee3f", "#97ee3f",
"#bf3947", "#bf3947",

View File

@@ -234,12 +234,7 @@ export const SENSOR_ENTITIES = [
"weather", "weather",
]; ];
export const ASSIST_ENTITIES = [ export const ASSIST_ENTITIES = ["conversation", "stt", "tts"];
"assist_satellite",
"conversation",
"stt",
"tts",
];
/** Domains that render an input element instead of a text value when displayed in a row. /** 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 * Those rows should then not show a cursor pointer when hovered (which would normally

View File

@@ -15,6 +15,7 @@ export type LocalizeKeys =
| `ui.card.weather.cardinal_direction.${string}` | `ui.card.weather.cardinal_direction.${string}`
| `ui.card.lawn_mower.actions.${string}` | `ui.card.lawn_mower.actions.${string}`
| `ui.components.calendar.event.rrule.${string}` | `ui.components.calendar.event.rrule.${string}`
| `ui.components.logbook.${string}`
| `ui.components.selectors.file.${string}` | `ui.components.selectors.file.${string}`
| `ui.dialogs.entity_registry.editor.${string}` | `ui.dialogs.entity_registry.editor.${string}`
| `ui.dialogs.more_info_control.lawn_mower.${string}` | `ui.dialogs.more_info_control.lawn_mower.${string}`

View File

@@ -20,15 +20,6 @@ function findNestedItem(
}, obj); }, 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>( export function nestedArrayMove<A>(
obj: A, obj: A,
oldIndex: number, oldIndex: number,
@@ -36,18 +27,14 @@ export function nestedArrayMove<A>(
oldPath?: ItemPath, oldPath?: ItemPath,
newPath?: ItemPath newPath?: ItemPath
): A { ): A {
let newObj = (Array.isArray(obj) ? [...obj] : { ...obj }) as A; const newObj = (Array.isArray(obj) ? [...obj] : { ...obj }) as A;
if (oldPath) {
newObj = updateNestedItem(newObj, [...oldPath]);
}
if (newPath) {
newObj = updateNestedItem(newObj, [...newPath]);
}
const from = oldPath ? findNestedItem(newObj, oldPath) : newObj; const from = oldPath ? findNestedItem(newObj, oldPath) : newObj;
const to = newPath ? findNestedItem(newObj, newPath, true) : 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]; const item = from.splice(oldIndex, 1)[0];
to.splice(newIndex, 0, item); to.splice(newIndex, 0, item);

View File

@@ -204,29 +204,6 @@ export class HaDataTable extends LitElement {
this._checkedRowsChanged(); 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() { public connectedCallback() {
super.connectedCallback(); super.connectedCallback();
if (this._filteredData.length) { if (this._filteredData.length) {
@@ -1034,7 +1011,6 @@ export class HaDataTable extends LitElement {
/* @noflip */ /* @noflip */
padding-inline-end: initial; padding-inline-end: initial;
width: 60px; width: 60px;
min-width: 60px;
} }
.mdc-data-table__table { .mdc-data-table__table {
@@ -1200,7 +1176,6 @@ export class HaDataTable extends LitElement {
display: flex; display: flex;
align-items: center; align-items: center;
cursor: pointer; cursor: pointer;
background-color: var(--primary-background-color);
} }
.group-header ha-icon-button { .group-header ha-icon-button {

View File

@@ -26,7 +26,7 @@ class HaDeviceTriggerPicker extends HaDeviceAutomationPicker<DeviceTrigger> {
fetchDeviceTriggers, fetchDeviceTriggers,
(deviceId?: string) => ({ (deviceId?: string) => ({
device_id: deviceId || "", device_id: deviceId || "",
trigger: "device", platform: "device",
domain: "", domain: "",
entity_id: "", entity_id: "",
}) })

View File

@@ -1,9 +1,10 @@
import type { HassEntity } from "home-assistant-js-websocket";
import { css, html, LitElement, nothing } from "lit"; import { css, html, LitElement, nothing } from "lit";
import { customElement, property } from "lit/decorators"; import { customElement, property } from "lit/decorators";
import memoizeOne from "memoize-one"; import memoizeOne from "memoize-one";
import { fireEvent } from "../../common/dom/fire_event"; import { fireEvent } from "../../common/dom/fire_event";
import { isValidEntityId } from "../../common/entity/valid_entity_id"; 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 "./ha-entity-picker";
import type { HaEntityPickerEntityFilterFunc } from "./ha-entity-picker"; import type { HaEntityPickerEntityFilterFunc } from "./ha-entity-picker";
@@ -97,7 +98,10 @@ class HaEntitiesPickerLight extends LitElement {
.excludeEntities=${this.excludeEntities} .excludeEntities=${this.excludeEntities}
.includeDeviceClasses=${this.includeDeviceClasses} .includeDeviceClasses=${this.includeDeviceClasses}
.includeUnitOfMeasurement=${this.includeUnitOfMeasurement} .includeUnitOfMeasurement=${this.includeUnitOfMeasurement}
.entityFilter=${this.entityFilter} .entityFilter=${this._getEntityFilter(
this.value,
this.entityFilter
)}
.value=${entityId} .value=${entityId}
.label=${this.pickedEntityLabel} .label=${this.pickedEntityLabel}
.disabled=${this.disabled} .disabled=${this.disabled}
@@ -114,13 +118,10 @@ class HaEntitiesPickerLight extends LitElement {
.includeDomains=${this.includeDomains} .includeDomains=${this.includeDomains}
.excludeDomains=${this.excludeDomains} .excludeDomains=${this.excludeDomains}
.includeEntities=${this.includeEntities} .includeEntities=${this.includeEntities}
.excludeEntities=${this._excludeEntities( .excludeEntities=${this.excludeEntities}
this.value,
this.excludeEntities
)}
.includeDeviceClasses=${this.includeDeviceClasses} .includeDeviceClasses=${this.includeDeviceClasses}
.includeUnitOfMeasurement=${this.includeUnitOfMeasurement} .includeUnitOfMeasurement=${this.includeUnitOfMeasurement}
.entityFilter=${this.entityFilter} .entityFilter=${this._getEntityFilter(this.value, this.entityFilter)}
.label=${this.pickEntityLabel} .label=${this.pickEntityLabel}
.helper=${this.helper} .helper=${this.helper}
.disabled=${this.disabled} .disabled=${this.disabled}
@@ -132,16 +133,14 @@ class HaEntitiesPickerLight extends LitElement {
`; `;
} }
private _excludeEntities = memoizeOne( private _getEntityFilter = memoizeOne(
( (
value: string[] | undefined, value: string[] | undefined,
excludeEntities: string[] | undefined entityFilter: HaEntityPickerEntityFilterFunc | undefined
): string[] | undefined => { ): HaEntityPickerEntityFilterFunc =>
if (value === undefined) { (stateObj: HassEntity) =>
return excludeEntities; (!value || !value.includes(stateObj.entity_id)) &&
} (!entityFilter || entityFilter(stateObj))
return [...(excludeEntities || []), ...value];
}
); );
private get _currentEntities() { private get _currentEntities() {

View File

@@ -87,7 +87,7 @@ export class HaEntityPicker extends LitElement {
public includeUnitOfMeasurement?: string[]; public includeUnitOfMeasurement?: string[];
/** /**
* List of allowed entities to show. * List of allowed entities to show. Will ignore all other filters.
* @type {Array} * @type {Array}
* @attr include-entities * @attr include-entities
*/ */
@@ -220,13 +220,30 @@ export class HaEntityPicker extends LitElement {
if (includeEntities) { if (includeEntities) {
entityIds = entityIds.filter((entityId) => 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) { if (excludeEntities) {
entityIds = entityIds.filter( entityIds = entityIds.filter(
(entityId) => !excludeEntities.includes(entityId) (entityId) => !excludeEntities!.includes(entityId)
); );
} }

View File

@@ -173,7 +173,6 @@ class HaEntityStatePicker extends LitElement {
no-style no-style
@item-moved=${this._moveItem} @item-moved=${this._moveItem}
.disabled=${this.disabled} .disabled=${this.disabled}
filter="button.trailing.action"
> >
<ha-chip-set> <ha-chip-set>
${repeat( ${repeat(

View File

@@ -1,6 +1,6 @@
import { mdiTextureBox } from "@mdi/js"; import { mdiTextureBox } from "@mdi/js";
import { ComboBoxLitRenderer } from "@vaadin/combo-box/lit"; 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 { LitElement, PropertyValues, TemplateResult, html, nothing } from "lit";
import { customElement, property, query, state } from "lit/decorators"; import { customElement, property, query, state } from "lit/decorators";
import { styleMap } from "lit/directives/style-map"; import { styleMap } from "lit/directives/style-map";
@@ -20,7 +20,12 @@ import {
getDeviceEntityDisplayLookup, getDeviceEntityDisplayLookup,
} from "../data/device_registry"; } from "../data/device_registry";
import { EntityRegistryDisplayEntry } from "../data/entity_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 { HomeAssistant, ValueChangedEvent } from "../types";
import type { HaDevicePickerDeviceFilterFunc } from "./device/ha-device-picker"; import type { HaDevicePickerDeviceFilterFunc } from "./device/ha-device-picker";
import "./ha-combo-box"; import "./ha-combo-box";
@@ -45,7 +50,7 @@ interface FloorAreaEntry {
} }
@customElement("ha-area-floor-picker") @customElement("ha-area-floor-picker")
export class HaAreaFloorPicker extends LitElement { export class HaAreaFloorPicker extends SubscribeMixin(LitElement) {
@property({ attribute: false }) public hass!: HomeAssistant; @property({ attribute: false }) public hass!: HomeAssistant;
@property() public label?: string; @property() public label?: string;
@@ -106,12 +111,22 @@ export class HaAreaFloorPicker extends LitElement {
@property({ type: Boolean }) public required = false; @property({ type: Boolean }) public required = false;
@state() private _floors?: FloorRegistryEntry[];
@state() private _opened?: boolean; @state() private _opened?: boolean;
@query("ha-combo-box", true) public comboBox!: HaComboBox; @query("ha-combo-box", true) public comboBox!: HaComboBox;
private _init = false; private _init = false;
protected hassSubscribe(): (UnsubscribeFunc | Promise<UnsubscribeFunc>)[] {
return [
subscribeFloorRegistry(this.hass.connection, (floors) => {
this._floors = floors;
}),
];
}
public async open() { public async open() {
await this.updateComplete; await this.updateComplete;
await this.comboBox?.open(); await this.comboBox?.open();
@@ -416,12 +431,12 @@ export class HaAreaFloorPicker extends LitElement {
protected updated(changedProps: PropertyValues) { protected updated(changedProps: PropertyValues) {
if ( if (
(!this._init && this.hass) || (!this._init && this.hass && this._floors) ||
(this._init && changedProps.has("_opened") && this._opened) (this._init && changedProps.has("_opened") && this._opened)
) { ) {
this._init = true; this._init = true;
const areas = this._getAreas( const areas = this._getAreas(
Object.values(this.hass.floors), this._floors!,
Object.values(this.hass.areas), Object.values(this.hass.areas),
Object.values(this.hass.devices), Object.values(this.hass.devices),
Object.values(this.hass.entities), Object.values(this.hass.entities),

View File

@@ -6,8 +6,8 @@ import type { HaIconButton } from "./ha-icon-button";
import "./ha-menu"; import "./ha-menu";
import type { HaMenu } from "./ha-menu"; import type { HaMenu } from "./ha-menu";
@customElement("ha-md-button-menu") @customElement("ha-button-menu-new")
export class HaMdButtonMenu extends LitElement { export class HaButtonMenuNew extends LitElement {
protected readonly [FOCUS_TARGET]; protected readonly [FOCUS_TARGET];
@property({ type: Boolean }) public disabled = false; @property({ type: Boolean }) public disabled = false;
@@ -84,6 +84,6 @@ export class HaMdButtonMenu extends LitElement {
declare global { declare global {
interface HTMLElementTagNameMap { interface HTMLElementTagNameMap {
"ha-md-button-menu": HaMdButtonMenu; "ha-button-menu-new": HaButtonMenuNew;
} }
} }

View File

@@ -124,12 +124,9 @@ export class HaCodeEditor extends ReactiveElement {
const transactions: TransactionSpec[] = []; const transactions: TransactionSpec[] = [];
if (changedProps.has("mode")) { if (changedProps.has("mode")) {
transactions.push({ transactions.push({
effects: [ effects: this._loadedCodeMirror!.langCompartment!.reconfigure(
this._loadedCodeMirror!.langCompartment!.reconfigure(this._mode), this._mode
this._loadedCodeMirror!.foldingCompartment.reconfigure(
this._getFoldingExtensions()
), ),
],
}); });
} }
if (changedProps.has("readOnly")) { if (changedProps.has("readOnly")) {
@@ -180,14 +177,6 @@ export class HaCodeEditor extends ReactiveElement {
this._loadedCodeMirror.crosshairCursor(), this._loadedCodeMirror.crosshairCursor(),
this._loadedCodeMirror.highlightSelectionMatches(), this._loadedCodeMirror.highlightSelectionMatches(),
this._loadedCodeMirror.highlightActiveLine(), 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.keymap.of([
...this._loadedCodeMirror.defaultKeymap, ...this._loadedCodeMirror.defaultKeymap,
...this._loadedCodeMirror.searchKeymap, ...this._loadedCodeMirror.searchKeymap,
@@ -205,9 +194,6 @@ export class HaCodeEditor extends ReactiveElement {
this.linewrap ? this._loadedCodeMirror.EditorView.lineWrapping : [] this.linewrap ? this._loadedCodeMirror.EditorView.lineWrapping : []
), ),
this._loadedCodeMirror.EditorView.updateListener.of(this._onUpdate), this._loadedCodeMirror.EditorView.updateListener.of(this._onUpdate),
this._loadedCodeMirror.foldingCompartment.of(
this._getFoldingExtensions()
),
]; ];
if (!this.readOnly) { if (!this.readOnly) {
@@ -325,17 +311,6 @@ export class HaCodeEditor extends ReactiveElement {
fireEvent(this, "value-changed", { value: this._value }); 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 { static get styles(): CSSResultGroup {
return css` return css`
:host(.error-state) .cm-gutters { :host(.error-state) .cm-gutters {

View File

@@ -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 { 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 { styleMap } from "lit/directives/style-map";
import { computeCssColor, THEME_COLORS } from "../common/color/compute-color"; import { computeCssColor, THEME_COLORS } from "../common/color/compute-color";
import { fireEvent } from "../common/dom/fire_event"; import { fireEvent } from "../common/dom/fire_event";
import { stopPropagation } from "../common/dom/stop_propagation"; 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 "./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") @customElement("ha-color-picker")
export class HaColorPicker extends LitElement { export class HaColorPicker extends LitElement {
@@ -22,97 +20,43 @@ export class HaColorPicker extends LitElement {
@property() public value?: string; @property() public value?: string;
@property({ type: String, attribute: "default_color" }) @property({ type: Boolean }) public defaultColor = false;
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 disabled = false; @property({ type: Boolean }) public disabled = false;
@query("ha-select") private _select?: HaSelect; _valueSelected(ev) {
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;
const value = ev.target.value; const value = ev.target.value;
this.value = value === this.defaultColor ? undefined : value; if (value) {
fireEvent(this, "value-changed", { fireEvent(this, "value-changed", {
value: this.value, value: value !== "default" ? value : undefined,
}); });
} }
}
render() { render() {
const value = this.value || this.defaultColor || "";
const isCustom = !(
THEME_COLORS.has(value) ||
value === "none" ||
value === "state"
);
return html` return html`
<ha-select <ha-select
.icon=${Boolean(value)} .icon=${Boolean(this.value)}
.label=${this.label} .label=${this.label}
.value=${value} .value=${this.value || "default"}
.helper=${this.helper} .helper=${this.helper}
.disabled=${this.disabled} .disabled=${this.disabled}
@closed=${stopPropagation} @closed=${stopPropagation}
@selected=${this._valueSelected} @selected=${this._valueSelected}
fixedMenuPosition fixedMenuPosition
naturalMenuWidth naturalMenuWidth
.clearable=${!this.defaultColor}
> >
${value ${this.value
? html` ? html`
<span slot="icon"> <span slot="icon">
${value === "none" ${this.renderColorCircle(this.value || "grey")}
? html`
<ha-svg-icon path=${mdiInvertColorsOff}></ha-svg-icon>
`
: value === "state"
? html`<ha-svg-icon path=${mdiPalette}></ha-svg-icon>`
: this.renderColorCircle(value || "grey")}
</span> </span>
` `
: nothing} : nothing}
${this.includeNone ${this.defaultColor
? html` ? html` <ha-list-item value="default">
<ha-list-item value="none" graphic="icon"> ${this.hass.localize(`ui.components.color-picker.default_color`)}
${this.hass.localize("ui.components.color-picker.none")} </ha-list-item>`
${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>`
: nothing} : nothing}
${Array.from(THEME_COLORS).map( ${Array.from(THEME_COLORS).map(
(color) => html` (color) => html`
@@ -120,21 +64,10 @@ export class HaColorPicker extends LitElement {
${this.hass.localize( ${this.hass.localize(
`ui.components.color-picker.colors.${color}` as LocalizeKeys `ui.components.color-picker.colors.${color}` as LocalizeKeys
) || color} ) || color}
${this.defaultColor === color
? ` (${this.hass.localize("ui.components.color-picker.default")})`
: nothing}
<span slot="graphic">${this.renderColorCircle(color)}</span> <span slot="graphic">${this.renderColorCircle(color)}</span>
</ha-list-item> </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> </ha-select>
`; `;
} }
@@ -154,11 +87,10 @@ export class HaColorPicker extends LitElement {
return css` return css`
.circle-color { .circle-color {
display: block; display: block;
background-color: var(--circle-color, var(--divider-color)); background-color: var(--circle-color);
border-radius: 10px; border-radius: 10px;
width: 20px; width: 20px;
height: 20px; height: 20px;
box-sizing: border-box;
} }
ha-select { ha-select {
width: 100%; width: 100%;

View File

@@ -45,7 +45,7 @@ export class HaControlButton extends LitElement {
position: relative; position: relative;
cursor: pointer; cursor: pointer;
display: flex; display: flex;
flex-direction: row; flex-direction: column;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
text-align: center; text-align: center;

View File

@@ -10,13 +10,8 @@ export class HaDialogHeader extends LitElement {
<section class="header-navigation-icon"> <section class="header-navigation-icon">
<slot name="navigationIcon"></slot> <slot name="navigationIcon"></slot>
</section> </section>
<section class="header-content"> <section class="header-title">
<div class="header-title">
<slot name="title"></slot> <slot name="title"></slot>
</div>
<div class="header-subtitle">
<slot name="subtitle"></slot>
</div>
</section> </section>
<section class="header-action-items"> <section class="header-action-items">
<slot name="actionItems"></slot> <slot name="actionItems"></slot>
@@ -44,24 +39,17 @@ export class HaDialogHeader extends LitElement {
padding: 4px; padding: 4px;
box-sizing: border-box; box-sizing: border-box;
} }
.header-content { .header-title {
flex: 1; flex: 1;
font-size: 22px;
line-height: 28px;
font-weight: 400;
padding: 10px 4px; padding: 10px 4px;
min-width: 0; min-width: 0;
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
white-space: nowrap; 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) { @media all and (min-width: 450px) and (min-height: 500px) {
.header-bar { .header-bar {
padding: 12px; padding: 12px;

View File

@@ -1,5 +1,6 @@
import "@material/mwc-menu/mwc-menu-surface"; import "@material/mwc-menu/mwc-menu-surface";
import { mdiFilterVariantRemove, mdiTextureBox } from "@mdi/js"; import { mdiFilterVariantRemove, mdiTextureBox } from "@mdi/js";
import { UnsubscribeFunc } from "home-assistant-js-websocket";
import { import {
CSSResultGroup, CSSResultGroup,
LitElement, LitElement,
@@ -14,8 +15,13 @@ import { repeat } from "lit/directives/repeat";
import memoizeOne from "memoize-one"; import memoizeOne from "memoize-one";
import { fireEvent } from "../common/dom/fire_event"; import { fireEvent } from "../common/dom/fire_event";
import { computeRTL } from "../common/util/compute_rtl"; 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 { RelatedResult, findRelated } from "../data/search";
import { SubscribeMixin } from "../mixins/subscribe-mixin";
import { haStyleScrollbar } from "../resources/styles"; import { haStyleScrollbar } from "../resources/styles";
import type { HomeAssistant } from "../types"; import type { HomeAssistant } from "../types";
import "./ha-check-list-item"; import "./ha-check-list-item";
@@ -25,7 +31,7 @@ import "./ha-svg-icon";
import "./ha-tree-indicator"; import "./ha-tree-indicator";
@customElement("ha-filter-floor-areas") @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 hass!: HomeAssistant;
@property({ attribute: false }) public value?: { @property({ attribute: false }) public value?: {
@@ -41,6 +47,8 @@ export class HaFilterFloorAreas extends LitElement {
@state() private _shouldRender = false; @state() private _shouldRender = false;
@state() private _floors?: FloorRegistryEntry[];
public willUpdate(properties: PropertyValues) { public willUpdate(properties: PropertyValues) {
super.willUpdate(properties); super.willUpdate(properties);
@@ -52,7 +60,7 @@ export class HaFilterFloorAreas extends LitElement {
} }
protected render() { protected render() {
const areas = this._areas(this.hass.areas, this.hass.floors); const areas = this._areas(this.hass.areas, this._floors);
return html` return html`
<ha-expansion-panel <ha-expansion-panel
@@ -181,6 +189,14 @@ export class HaFilterFloorAreas extends LitElement {
this._findRelated(); this._findRelated();
} }
protected hassSubscribe(): (UnsubscribeFunc | Promise<UnsubscribeFunc>)[] {
return [
subscribeFloorRegistry(this.hass.connection, (floors) => {
this._floors = floors;
}),
];
}
protected updated(changed) { protected updated(changed) {
if (changed.has("expanded") && this.expanded) { if (changed.has("expanded") && this.expanded) {
setTimeout(() => { setTimeout(() => {
@@ -204,9 +220,9 @@ export class HaFilterFloorAreas extends LitElement {
} }
private _areas = memoizeOne( private _areas = memoizeOne(
(areaReg: HomeAssistant["areas"], floorReg: HomeAssistant["floors"]) => { (areaReg: HomeAssistant["areas"], floors?: FloorRegistryEntry[]) => {
const areas = Object.values(areaReg); const areas = Object.values(areaReg);
const floors = Object.values(floorReg);
const floorAreaLookup = getFloorAreaLookup(areas); const floorAreaLookup = getFloorAreaLookup(areas);
const unassisgnedAreas = areas.filter( const unassisgnedAreas = areas.filter(

View File

@@ -1,5 +1,5 @@
import { ComboBoxLitRenderer } from "@vaadin/combo-box/lit"; 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 { LitElement, PropertyValues, TemplateResult, html } from "lit";
import { customElement, property, query, state } from "lit/decorators"; import { customElement, property, query, state } from "lit/decorators";
import { classMap } from "lit/directives/class-map"; import { classMap } from "lit/directives/class-map";
@@ -24,8 +24,10 @@ import {
FloorRegistryEntry, FloorRegistryEntry,
createFloorRegistryEntry, createFloorRegistryEntry,
getFloorAreaLookup, getFloorAreaLookup,
subscribeFloorRegistry,
} from "../data/floor_registry"; } from "../data/floor_registry";
import { showAlertDialog } from "../dialogs/generic/show-dialog-box"; 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 { showFloorRegistryDetailDialog } from "../panels/config/areas/show-dialog-floor-registry-detail";
import { HomeAssistant, ValueChangedEvent } from "../types"; import { HomeAssistant, ValueChangedEvent } from "../types";
import type { HaDevicePickerDeviceFilterFunc } from "./device/ha-device-picker"; import type { HaDevicePickerDeviceFilterFunc } from "./device/ha-device-picker";
@@ -51,7 +53,7 @@ const rowRenderer: ComboBoxLitRenderer<FloorRegistryEntry> = (item) =>
</ha-list-item>`; </ha-list-item>`;
@customElement("ha-floor-picker") @customElement("ha-floor-picker")
export class HaFloorPicker extends LitElement { export class HaFloorPicker extends SubscribeMixin(LitElement) {
@property({ attribute: false }) public hass!: HomeAssistant; @property({ attribute: false }) public hass!: HomeAssistant;
@property() public label?: string; @property() public label?: string;
@@ -109,6 +111,8 @@ export class HaFloorPicker extends LitElement {
@state() private _opened?: boolean; @state() private _opened?: boolean;
@state() private _floors?: FloorRegistryEntry[];
@query("ha-combo-box", true) public comboBox!: HaComboBox; @query("ha-combo-box", true) public comboBox!: HaComboBox;
private _suggestion?: string; private _suggestion?: string;
@@ -125,6 +129,14 @@ export class HaFloorPicker extends LitElement {
await this.comboBox?.focus(); await this.comboBox?.focus();
} }
protected hassSubscribe(): (UnsubscribeFunc | Promise<UnsubscribeFunc>)[] {
return [
subscribeFloorRegistry(this.hass.connection, (floors) => {
this._floors = floors;
}),
];
}
private _getFloors = memoizeOne( private _getFloors = memoizeOne(
( (
floors: FloorRegistryEntry[], floors: FloorRegistryEntry[],
@@ -308,12 +320,12 @@ export class HaFloorPicker extends LitElement {
protected updated(changedProps: PropertyValues) { protected updated(changedProps: PropertyValues) {
if ( if (
(!this._init && this.hass) || (!this._init && this.hass && this._floors) ||
(this._init && changedProps.has("_opened") && this._opened) (this._init && changedProps.has("_opened") && this._opened)
) { ) {
this._init = true; this._init = true;
const floors = this._getFloors( const floors = this._getFloors(
Object.values(this.hass.floors), this._floors!,
Object.values(this.hass.areas), Object.values(this.hass.areas),
Object.values(this.hass.devices), Object.values(this.hass.devices),
Object.values(this.hass.entities), Object.values(this.hass.entities),
@@ -348,7 +360,8 @@ export class HaFloorPicker extends LitElement {
? this.hass.localize("ui.components.floor-picker.floor") ? this.hass.localize("ui.components.floor-picker.floor")
: this.label} : this.label}
.placeholder=${this.placeholder .placeholder=${this.placeholder
? this.hass.floors[this.placeholder]?.name ? this._floors?.find((floor) => floor.floor_id === this.placeholder)
?.name
: undefined} : undefined}
.renderer=${rowRenderer} .renderer=${rowRenderer}
@filter-changed=${this._filterChanged} @filter-changed=${this._filterChanged}
@@ -447,7 +460,7 @@ export class HaFloorPicker extends LitElement {
floor_id: floor.floor_id, floor_id: floor.floor_id,
}); });
}); });
const floors = [...Object.values(this.hass.floors), floor]; const floors = [...this._floors!, floor];
this.comboBox.filteredItems = this._getFloors( this.comboBox.filteredItems = this._getFloors(
floors, floors,
Object.values(this.hass.areas)!, Object.values(this.hass.areas)!,

View File

@@ -95,10 +95,10 @@ export const computeInitialHaFormData = (
} else if ( } else if (
"action" in selector || "action" in selector ||
"trigger" 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] = {}; data[field.name] = {};
} else { } else {
throw new Error( throw new Error(

View File

@@ -30,10 +30,6 @@ export class HaFormExpendable extends LitElement implements HaFormElement {
options?: { path?: string[] } options?: { path?: string[] }
) => string; ) => string;
@property({ attribute: false }) public localizeValue?: (
key: string
) => string;
private _renderDescription() { private _renderDescription() {
const description = this.computeHelper?.(this.schema); const description = this.computeHelper?.(this.schema);
return description ? html`<p>${description}</p>` : nothing; return description ? html`<p>${description}</p>` : nothing;
@@ -90,7 +86,6 @@ export class HaFormExpendable extends LitElement implements HaFormElement {
.disabled=${this.disabled} .disabled=${this.disabled}
.computeLabel=${this._computeLabel} .computeLabel=${this._computeLabel}
.computeHelper=${this._computeHelper} .computeHelper=${this._computeHelper}
.localizeValue=${this.localizeValue}
></ha-form> ></ha-form>
</div> </div>
</ha-expansion-panel> </ha-expansion-panel>

View File

@@ -35,10 +35,6 @@ export class HaFormGrid extends LitElement implements HaFormElement {
schema: HaFormSchema schema: HaFormSchema
) => string; ) => string;
@property({ attribute: false }) public localizeValue?: (
key: string
) => string;
public async focus() { public async focus() {
await this.updateComplete; await this.updateComplete;
this.renderRoot.querySelector("ha-form")?.focus(); this.renderRoot.querySelector("ha-form")?.focus();
@@ -69,7 +65,6 @@ export class HaFormGrid extends LitElement implements HaFormElement {
.disabled=${this.disabled} .disabled=${this.disabled}
.computeLabel=${this.computeLabel} .computeLabel=${this.computeLabel}
.computeHelper=${this.computeHelper} .computeHelper=${this.computeHelper}
.localizeValue=${this.localizeValue}
></ha-form> ></ha-form>
` `
)} )}

View File

@@ -73,10 +73,6 @@ export class HaForm extends LitElement implements HaFormElement {
schema: any schema: any
) => string | undefined; ) => string | undefined;
@property({ attribute: false }) public localizeValue?: (
key: string
) => string;
protected getFormProperties(): Record<string, any> { protected getFormProperties(): Record<string, any> {
return {}; return {};
} }
@@ -149,7 +145,6 @@ export class HaForm extends LitElement implements HaFormElement {
.disabled=${item.disabled || this.disabled || false} .disabled=${item.disabled || this.disabled || false}
.placeholder=${item.required ? "" : item.default} .placeholder=${item.required ? "" : item.default}
.helper=${this._computeHelper(item)} .helper=${this._computeHelper(item)}
.localizeValue=${this.localizeValue}
.required=${item.required || false} .required=${item.required || false}
.context=${this._generateContext(item)} .context=${this._generateContext(item)}
></ha-selector>` ></ha-selector>`
@@ -163,7 +158,6 @@ export class HaForm extends LitElement implements HaFormElement {
localize: this.hass?.localize, localize: this.hass?.localize,
computeLabel: this.computeLabel, computeLabel: this.computeLabel,
computeHelper: this.computeHelper, computeHelper: this.computeHelper,
localizeValue: this.localizeValue,
context: this._generateContext(item), context: this._generateContext(item),
...this.getFormProperties(), ...this.getFormProperties(),
})} })}

View File

@@ -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;
}
}

View File

@@ -2,8 +2,8 @@ import { MdListItem } from "@material/web/list/list-item";
import { css } from "lit"; import { css } from "lit";
import { customElement } from "lit/decorators"; import { customElement } from "lit/decorators";
@customElement("ha-md-list-item") @customElement("ha-list-item-new")
export class HaMdListItem extends MdListItem { export class HaListItemNew extends MdListItem {
static override styles = [ static override styles = [
...super.styles, ...super.styles,
css` css`
@@ -21,6 +21,6 @@ export class HaMdListItem extends MdListItem {
declare global { declare global {
interface HTMLElementTagNameMap { interface HTMLElementTagNameMap {
"ha-md-list-item": HaMdListItem; "ha-list-item-new": HaListItemNew;
} }
} }

View File

@@ -2,8 +2,8 @@ import { MdList } from "@material/web/list/list";
import { css } from "lit"; import { css } from "lit";
import { customElement } from "lit/decorators"; import { customElement } from "lit/decorators";
@customElement("ha-md-list") @customElement("ha-list-new")
export class HaMdList extends MdList { export class HaListNew extends MdList {
static override styles = [ static override styles = [
...super.styles, ...super.styles,
css` css`
@@ -16,6 +16,6 @@ export class HaMdList extends MdList {
declare global { declare global {
interface HTMLElementTagNameMap { interface HTMLElementTagNameMap {
"ha-md-list": HaMdList; "ha-list-new": HaListNew;
} }
} }

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -2,8 +2,8 @@ import { MdMenuItem } from "@material/web/menu/menu-item";
import { css } from "lit"; import { css } from "lit";
import { customElement, property } from "lit/decorators"; import { customElement, property } from "lit/decorators";
@customElement("ha-md-menu-item") @customElement("ha-menu-item")
export class HaMdMenuItem extends MdMenuItem { export class HaMenuItem extends MdMenuItem {
@property({ attribute: false }) clickAction?: (item?: HTMLElement) => void; @property({ attribute: false }) clickAction?: (item?: HTMLElement) => void;
static override styles = [ static override styles = [
@@ -41,6 +41,6 @@ export class HaMdMenuItem extends MdMenuItem {
declare global { declare global {
interface HTMLElementTagNameMap { interface HTMLElementTagNameMap {
"ha-md-menu-item": HaMdMenuItem; "ha-menu-item": HaMenuItem;
} }
} }

View File

@@ -6,7 +6,7 @@ import {
} from "@material/web/menu/internal/controllers/shared"; } from "@material/web/menu/internal/controllers/shared";
import { css } from "lit"; import { css } from "lit";
import { customElement } from "lit/decorators"; import { customElement } from "lit/decorators";
import type { HaMdMenuItem } from "./ha-md-menu-item"; import type { HaMenuItem } from "./ha-menu-item";
@customElement("ha-menu") @customElement("ha-menu")
export class HaMenu extends MdMenu { export class HaMenu extends MdMenu {
@@ -22,7 +22,7 @@ export class HaMenu extends MdMenu {
) { ) {
return; return;
} }
(ev.detail.initiator as HaMdMenuItem).clickAction?.(ev.detail.initiator); (ev.detail.initiator as HaMenuItem).clickAction?.(ev.detail.initiator);
} }
static override styles = [ static override styles = [

View File

@@ -24,11 +24,9 @@ export class HaOutlinedField extends MdOutlinedField {
} }
.with-start .start { .with-start .start {
margin-inline-end: var(--ha-outlined-field-start-margin, 4px); margin-inline-end: var(--ha-outlined-field-start-margin, 4px);
margin-inline-start: initial;
} }
.with-end .end { .with-end .end {
margin-inline-start: var(--ha-outlined-field-end-margin, 4px); margin-inline-start: var(--ha-outlined-field-end-margin, 4px);
margin-inline-end: initial;
} }
`, `,
]; ];

View File

@@ -1,192 +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}
@change=${this._reDispatchEvent}
></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;
}
@eventOptions({ passive: true })
private _reDispatchEvent(oldEvent: Event) {
const newEvent = new Event(oldEvent.type, oldEvent);
this.dispatchEvent(newEvent);
}
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;
}
}

View File

@@ -1,7 +1,6 @@
import { css, CSSResultGroup, html, LitElement, nothing } from "lit"; import { css, CSSResultGroup, html, LitElement, nothing } from "lit";
import { customElement, property } from "lit/decorators"; import { customElement, property } from "lit/decorators";
import memoizeOne from "memoize-one"; import { Action } from "../../data/script";
import { Action, migrateAutomationAction } from "../../data/script";
import { ActionSelector } from "../../data/selector"; import { ActionSelector } from "../../data/selector";
import "../../panels/config/automation/action/ha-automation-action"; import "../../panels/config/automation/action/ha-automation-action";
import { HomeAssistant } from "../../types"; import { HomeAssistant } from "../../types";
@@ -18,19 +17,12 @@ export class HaActionSelector extends LitElement {
@property({ type: Boolean, reflect: true }) public disabled = false; @property({ type: Boolean, reflect: true }) public disabled = false;
private _actions = memoizeOne((action: Action | undefined) => {
if (!action) {
return [];
}
return migrateAutomationAction(action);
});
protected render() { protected render() {
return html` return html`
${this.label ? html`<label>${this.label}</label>` : nothing} ${this.label ? html`<label>${this.label}</label>` : nothing}
<ha-automation-action <ha-automation-action
.disabled=${this.disabled} .disabled=${this.disabled}
.actions=${this._actions(this.value)} .actions=${this.value || []}
.hass=${this.hass} .hass=${this.hass}
.path=${this.selector.action?.path} .path=${this.selector.action?.path}
></ha-automation-action> ></ha-automation-action>

View File

@@ -31,7 +31,7 @@ export class HaColorRGBSelector extends LitElement {
.label=${this.label || ""} .label=${this.label || ""}
.required=${this.required} .required=${this.required}
.helper=${this.helper} .helper=${this.helper}
.disabled=${this.disabled} .disalbled=${this.disabled}
@change=${this._valueChanged} @change=${this._valueChanged}
></ha-textfield> ></ha-textfield>
`; `;

View File

@@ -7,7 +7,12 @@ import "../ha-code-editor";
import "../ha-input-helper-text"; import "../ha-input-helper-text";
import "../ha-alert"; import "../ha-alert";
const WARNING_STRINGS = ["template:", "sensor:", "state:", "trigger: template"]; const WARNING_STRINGS = [
"template:",
"sensor:",
"state:",
"platform: template",
];
@customElement("ha-selector-template") @customElement("ha-selector-template")
export class HaTemplateSelector extends LitElement { export class HaTemplateSelector extends LitElement {

View File

@@ -1,7 +1,6 @@
import { css, CSSResultGroup, html, LitElement, nothing } from "lit"; import { css, CSSResultGroup, html, LitElement, nothing } from "lit";
import { customElement, property } from "lit/decorators"; import { customElement, property } from "lit/decorators";
import memoizeOne from "memoize-one"; import { Trigger } from "../../data/automation";
import { migrateAutomationTrigger, Trigger } from "../../data/automation";
import { TriggerSelector } from "../../data/selector"; import { TriggerSelector } from "../../data/selector";
import "../../panels/config/automation/trigger/ha-automation-trigger"; import "../../panels/config/automation/trigger/ha-automation-trigger";
import { HomeAssistant } from "../../types"; import { HomeAssistant } from "../../types";
@@ -18,19 +17,12 @@ export class HaTriggerSelector extends LitElement {
@property({ type: Boolean, reflect: true }) public disabled = false; @property({ type: Boolean, reflect: true }) public disabled = false;
private _triggers = memoizeOne((trigger: Trigger | undefined) => {
if (!trigger) {
return [];
}
return migrateAutomationTrigger(trigger);
});
protected render() { protected render() {
return html` return html`
${this.label ? html`<label>${this.label}</label>` : nothing} ${this.label ? html`<label>${this.label}</label>` : nothing}
<ha-automation-trigger <ha-automation-trigger
.disabled=${this.disabled} .disabled=${this.disabled}
.triggers=${this._triggers(this.value)} .triggers=${this.value || []}
.hass=${this.hass} .hass=${this.hass}
.path=${this.selector.trigger?.path} .path=${this.selector.trigger?.path}
></ha-automation-trigger> ></ha-automation-trigger>

View File

@@ -24,8 +24,6 @@ export class HaSelectorUiColor extends LitElement {
.hass=${this.hass} .hass=${this.hass}
.value=${this.value} .value=${this.value}
.helper=${this.helper} .helper=${this.helper}
.includeNone=${this.selector.ui_color?.include_none}
.includeState=${this.selector.ui_color?.include_state}
.defaultColor=${this.selector.ui_color?.default_color} .defaultColor=${this.selector.ui_color?.default_color}
@value-changed=${this._valueChanged} @value-changed=${this._valueChanged}
></ha-color-picker> ></ha-color-picker>

View File

@@ -240,24 +240,12 @@ export class HaServiceControl extends LitElement {
...value, ...value,
selector: value.selector as Selector | undefined, 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 { return {
...serviceDomains[domain][serviceName], ...serviceDomains[domain][serviceName],
fields, fields,
hasSelector, hasSelector: fields.length
? fields.filter((field) => field.selector).map((field) => field.key)
: [],
}; };
} }
); );
@@ -499,23 +487,8 @@ export class HaServiceControl extends LitElement {
.defaultValue=${this._value?.data} .defaultValue=${this._value?.data}
@value-changed=${this._dataChanged} @value-changed=${this._dataChanged}
></ha-yaml-editor>` ></ha-yaml-editor>`
: serviceData?.fields.map((dataField) => { : serviceData?.fields.map((dataField) =>
if (!dataField.fields) { dataField.fields
return this._renderField(
dataField,
hasOptional,
domain,
serviceName,
targetEntities
);
}
const fields = Object.entries(dataField.fields).map(
([key, field]) => ({ key, ...field })
);
return fields.length &&
this._hasFilteredFields(fields, targetEntities)
? html`<ha-expansion-panel ? html`<ha-expansion-panel
leftChevron leftChevron
.expanded=${!dataField.collapsed} .expanded=${!dataField.collapsed}
@@ -546,8 +519,14 @@ export class HaServiceControl extends LitElement {
) )
)} )}
</ha-expansion-panel>` </ha-expansion-panel>`
: nothing; : this._renderField(
})} `; dataField,
hasOptional,
domain,
serviceName,
targetEntities
)
)} `;
} }
private _getSectionDescription( private _getSectionDescription(
@@ -560,16 +539,6 @@ export class HaServiceControl extends LitElement {
); );
} }
private _hasFilteredFields(
dataFields: ExtHassService["fields"],
targetEntities: string[]
) {
return dataFields.some(
(dataField) =>
!dataField.filter || this._filterField(dataField.filter, targetEntities)
);
}
private _renderField = ( private _renderField = (
dataField: ExtHassService["fields"][number], dataField: ExtHassService["fields"][number],
hasOptional: boolean, hasOptional: boolean,
@@ -824,8 +793,7 @@ export class HaServiceControl extends LitElement {
const value = ev.detail.value; const value = ev.detail.value;
if ( if (
this._value?.data?.[key] === value || this._value?.data?.[key] === value ||
((!this._value?.data || !(key in this._value.data)) && (!this._value?.data?.[key] && (value === "" || value === undefined))
(value === "" || value === undefined))
) { ) {
return; return;
} }

View File

@@ -43,13 +43,6 @@ export class HaSortable extends LitElement {
@property({ type: String, attribute: "handle-selector" }) @property({ type: String, attribute: "handle-selector" })
public handleSelector?: string; 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 }) @property({ type: String })
public group?: string | SortableInstance.GroupOptions; public group?: string | SortableInstance.GroupOptions;
@@ -152,9 +145,6 @@ export class HaSortable extends LitElement {
if (this.group) { if (this.group) {
options.group = this.group; options.group = this.group;
} }
if (this.filter) {
options.filter = this.filter;
}
this._sortable = new Sortable(container, options); this._sortable = new Sortable(container, options);
} }

View File

@@ -35,6 +35,10 @@ import {
computeDeviceName, computeDeviceName,
} from "../data/device_registry"; } from "../data/device_registry";
import { EntityRegistryDisplayEntry } from "../data/entity_registry"; import { EntityRegistryDisplayEntry } from "../data/entity_registry";
import {
FloorRegistryEntry,
subscribeFloorRegistry,
} from "../data/floor_registry";
import { import {
LabelRegistryEntry, LabelRegistryEntry,
subscribeLabelRegistry, subscribeLabelRegistry,
@@ -99,12 +103,17 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
@query(".add-container", true) private _addContainer?: HTMLDivElement; @query(".add-container", true) private _addContainer?: HTMLDivElement;
@state() private _floors?: FloorRegistryEntry[];
@state() private _labels?: LabelRegistryEntry[]; @state() private _labels?: LabelRegistryEntry[];
private _opened = false; private _opened = false;
protected hassSubscribe(): (UnsubscribeFunc | Promise<UnsubscribeFunc>)[] { protected hassSubscribe(): (UnsubscribeFunc | Promise<UnsubscribeFunc>)[] {
return [ return [
subscribeFloorRegistry(this.hass.connection, (floors) => {
this._floors = floors;
}),
subscribeLabelRegistry(this.hass.connection, (labels) => { subscribeLabelRegistry(this.hass.connection, (labels) => {
this._labels = labels; this._labels = labels;
}), }),
@@ -123,7 +132,9 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
<div class="mdc-chip-set items"> <div class="mdc-chip-set items">
${this.value?.floor_id ${this.value?.floor_id
? ensureArray(this.value.floor_id).map((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( return this._renderChip(
"floor_id", "floor_id",
floor_id, floor_id,

View File

@@ -6,7 +6,7 @@ import { mainWindow } from "../common/dom/get_main_window";
@customElement("ha-textfield") @customElement("ha-textfield")
export class HaTextField extends TextFieldBase { export class HaTextField extends TextFieldBase {
@property({ type: Boolean }) public invalid?: boolean; @property({ type: Boolean }) public invalid = false;
@property({ attribute: "error-message" }) public errorMessage?: string; @property({ attribute: "error-message" }) public errorMessage?: string;
@@ -28,25 +28,15 @@ export class HaTextField extends TextFieldBase {
override updated(changedProperties: PropertyValues) { override updated(changedProperties: PropertyValues) {
super.updated(changedProperties); super.updated(changedProperties);
if ( if (
changedProperties.has("invalid") || (changedProperties.has("invalid") &&
(this.invalid || changedProperties.get("invalid") !== undefined)) ||
changedProperties.has("errorMessage") changedProperties.has("errorMessage")
) { ) {
this.setCustomValidity( this.setCustomValidity(
this.invalid this.invalid ? this.errorMessage || "Invalid" : ""
? this.errorMessage || this.validationMessage || "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 (changedProperties.has("autocomplete")) {
if (this.autocomplete) { if (this.autocomplete) {
this.formElement.setAttribute("autocomplete", this.autocomplete); this.formElement.setAttribute("autocomplete", this.autocomplete);
@@ -119,7 +109,7 @@ export class HaTextField extends TextFieldBase {
color: var(--secondary-text-color); 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); color: var(--secondary-text-color);
} }

View File

@@ -1,49 +1,8 @@
import { Snackbar } from "@material/mwc-snackbar/mwc-snackbar";
import { styles } from "@material/mwc-snackbar/mwc-snackbar.css";
import { css } from "lit";
import { customElement } from "lit/decorators"; import { customElement } from "lit/decorators";
import { Snackbar } from "@material/mwc-snackbar/mwc-snackbar";
@customElement("ha-toast") @customElement("ha-toast")
export class HaToast extends Snackbar { export class HaToast extends Snackbar {}
static override styles = [
styles,
css`
.mdc-snackbar--leading {
justify-content: center;
}
.mdc-snackbar {
margin: 8px;
right: calc(8px + env(safe-area-inset-right));
bottom: calc(8px + env(safe-area-inset-bottom));
left: calc(8px + env(safe-area-inset-left));
}
.mdc-snackbar__surface {
min-width: 350px;
max-width: 650px;
}
// Revert the default styles set by mwc-snackbar
@media (max-width: 480px), (max-width: 344px) {
.mdc-snackbar__surface {
min-width: inherit;
}
}
@media all and (max-width: 450px), all and (max-height: 500px) {
.mdc-snackbar {
right: env(safe-area-inset-right);
bottom: env(safe-area-inset-bottom);
left: env(safe-area-inset-left);
}
.mdc-snackbar__surface {
min-width: 100%;
}
}
`,
];
}
declare global { declare global {
interface HTMLElementTagNameMap { interface HTMLElementTagNameMap {

View File

@@ -1,4 +1,3 @@
/* eslint-disable no-console */
import { import {
css, css,
CSSResultGroup, CSSResultGroup,
@@ -7,14 +6,11 @@ import {
PropertyValues, PropertyValues,
TemplateResult, TemplateResult,
} from "lit"; } from "lit";
import { customElement, property, query, state } from "lit/decorators"; import { customElement, property, state, query } from "lit/decorators";
import { ifDefined } from "lit/directives/if-defined"; import { isComponentLoaded } from "../common/config/is_component_loaded";
import { fireEvent } from "../common/dom/fire_event"; import { fireEvent } from "../common/dom/fire_event";
import { import { handleWebRtcOffer, WebRtcAnswer } from "../data/camera";
fetchWebRtcClientConfiguration, import { fetchWebRtcSettings } from "../data/rtsp_to_webrtc";
handleWebRtcOffer,
WebRtcAnswer,
} from "../data/camera";
import type { HomeAssistant } from "../types"; import type { HomeAssistant } from "../types";
import "./ha-alert"; import "./ha-alert";
@@ -41,11 +37,12 @@ class HaWebRtcPlayer extends LitElement {
@property({ type: Boolean, attribute: "playsinline" }) @property({ type: Boolean, attribute: "playsinline" })
public playsInline = false; public playsInline = false;
@property({ attribute: "poster-url" }) public posterUrl?: string; @property() public posterUrl!: string;
@state() private _error?: 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; private _peerConnection?: RTCPeerConnection;
@@ -62,7 +59,7 @@ class HaWebRtcPlayer extends LitElement {
.muted=${this.muted} .muted=${this.muted}
?playsinline=${this.playsInline} ?playsinline=${this.playsInline}
?controls=${this.controls} ?controls=${this.controls}
poster=${ifDefined(this.posterUrl)} .poster=${this.posterUrl}
@loadeddata=${this._loadedData} @loadeddata=${this._loadedData}
></video> ></video>
`; `;
@@ -84,30 +81,20 @@ class HaWebRtcPlayer extends LitElement {
if (!changedProperties.has("entityid")) { if (!changedProperties.has("entityid")) {
return; return;
} }
if (!this._videoEl) {
return;
}
this._startWebRtc(); this._startWebRtc();
} }
private async _startWebRtc(): Promise<void> { private async _startWebRtc(): Promise<void> {
console.time("WebRTC");
this._error = undefined; this._error = undefined;
console.timeLog("WebRTC", "start clientConfig"); const configuration = await this._fetchPeerConfiguration();
const peerConnection = new RTCPeerConnection(configuration);
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 // Some cameras (such as nest) require a data channel to establish a stream
// however, not used by any integrations. // however, not used by any integrations.
peerConnection.createDataChannel(clientConfig.dataChannel); peerConnection.createDataChannel("dataSendChannel");
}
peerConnection.addTransceiver("audio", { direction: "recvonly" }); peerConnection.addTransceiver("audio", { direction: "recvonly" });
peerConnection.addTransceiver("video", { direction: "recvonly" }); peerConnection.addTransceiver("video", { direction: "recvonly" });
@@ -115,48 +102,30 @@ class HaWebRtcPlayer extends LitElement {
offerToReceiveAudio: true, offerToReceiveAudio: true,
offerToReceiveVideo: true, offerToReceiveVideo: true,
}; };
console.timeLog("WebRTC", "start createOffer", offerOptions);
const offer: RTCSessionDescriptionInit = const offer: RTCSessionDescriptionInit =
await peerConnection.createOffer(offerOptions); await peerConnection.createOffer(offerOptions);
console.timeLog("WebRTC", "end createOffer", offer);
console.timeLog("WebRTC", "start setLocalDescription");
await peerConnection.setLocalDescription(offer); await peerConnection.setLocalDescription(offer);
console.timeLog("WebRTC", "end setLocalDescription");
console.timeLog("WebRTC", "start iceResolver");
let candidates = ""; // Build an Offer SDP string with ice candidates let candidates = ""; // Build an Offer SDP string with ice candidates
const iceResolver = new Promise<void>((resolve) => { const iceResolver = new Promise<void>((resolve) => {
peerConnection.addEventListener("icecandidate", (event) => { peerConnection.addEventListener("icecandidate", async (event) => {
if (!event.candidate?.candidate) { if (!event.candidate) {
resolve(); // Gathering complete resolve(); // Gathering complete
return; return;
} }
console.timeLog("WebRTC", "iceResolver candidate", event.candidate);
candidates += `a=${event.candidate.candidate}\r\n`; candidates += `a=${event.candidate.candidate}\r\n`;
}); });
}); });
await iceResolver; await iceResolver;
console.timeLog("WebRTC", "end iceResolver", candidates);
const offer_sdp = offer.sdp! + candidates; const offer_sdp = offer.sdp! + candidates;
let webRtcAnswer: WebRtcAnswer; let webRtcAnswer: WebRtcAnswer;
try { try {
console.timeLog("WebRTC", "start WebRTCOffer", offer_sdp);
webRtcAnswer = await handleWebRtcOffer( webRtcAnswer = await handleWebRtcOffer(
this.hass, this.hass,
this.entityid, this.entityid,
offer_sdp offer_sdp
); );
console.timeLog("WebRTC", "end webRtcOffer", webRtcAnswer);
} catch (err: any) { } catch (err: any) {
this._error = "Failed to start WebRTC stream: " + err.message; this._error = "Failed to start WebRTC stream: " + err.message;
peerConnection.close(); peerConnection.close();
@@ -166,7 +135,6 @@ class HaWebRtcPlayer extends LitElement {
// Setup callbacks to render remote stream once media tracks are discovered. // Setup callbacks to render remote stream once media tracks are discovered.
const remoteStream = new MediaStream(); const remoteStream = new MediaStream();
peerConnection.addEventListener("track", (event) => { peerConnection.addEventListener("track", (event) => {
console.timeLog("WebRTC", "track", event);
remoteStream.addTrack(event.track); remoteStream.addTrack(event.track);
this._videoEl.srcObject = remoteStream; this._videoEl.srcObject = remoteStream;
}); });
@@ -178,9 +146,7 @@ class HaWebRtcPlayer extends LitElement {
sdp: webRtcAnswer.answer, sdp: webRtcAnswer.answer,
}); });
try { try {
console.timeLog("WebRTC", "start setRemoteDescription", remoteDesc);
await peerConnection.setRemoteDescription(remoteDesc); await peerConnection.setRemoteDescription(remoteDesc);
console.timeLog("WebRTC", "end setRemoteDescription");
} catch (err: any) { } catch (err: any) {
this._error = "Failed to connect WebRTC stream: " + err.message; this._error = "Failed to connect WebRTC stream: " + err.message;
peerConnection.close(); peerConnection.close();
@@ -189,6 +155,23 @@ class HaWebRtcPlayer extends LitElement {
this._peerConnection = peerConnection; 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() { private _cleanUp() {
if (this._remoteStream) { if (this._remoteStream) {
this._remoteStream.getTracks().forEach((track) => { this._remoteStream.getTracks().forEach((track) => {
@@ -207,8 +190,6 @@ class HaWebRtcPlayer extends LitElement {
} }
private _loadedData() { private _loadedData() {
console.timeLog("WebRTC", "loadedData");
console.timeEnd("WebRTC");
// @ts-ignore // @ts-ignore
fireEvent(this, "load"); fireEvent(this, "load");
} }

View File

@@ -7,18 +7,16 @@ import {
nothing, nothing,
PropertyValues, PropertyValues,
} from "lit"; } 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 { fireEvent } from "../common/dom/fire_event";
import type { HomeAssistant } from "../types"; import type { HomeAssistant } from "../types";
import { haStyle } from "../resources/styles"; import { haStyle } from "../resources/styles";
import "./ha-code-editor"; import "./ha-code-editor";
import { showToast } from "../util/toast"; import { showToast } from "../util/toast";
import { copyToClipboard } from "../common/util/copy-clipboard"; import { copyToClipboard } from "../common/util/copy-clipboard";
import type { HaCodeEditor } from "./ha-code-editor";
import "./ha-button";
const isEmpty = (obj: Record<string, unknown>): boolean => { const isEmpty = (obj: Record<string, unknown>): boolean => {
if (typeof obj !== "object" || obj === null) { if (typeof obj !== "object") {
return false; return false;
} }
for (const key in obj) { for (const key in obj) {
@@ -55,11 +53,10 @@ export class HaYamlEditor extends LitElement {
@state() private _yaml = ""; @state() private _yaml = "";
@query("ha-code-editor") _codeEditor?: HaCodeEditor;
public setValue(value): void { public setValue(value): void {
try { try {
this._yaml = !isEmpty(value) this._yaml =
value && !isEmpty(value)
? dump(value, { ? dump(value, {
schema: this.yamlSchema, schema: this.yamlSchema,
quotingType: '"', quotingType: '"',
@@ -74,7 +71,7 @@ export class HaYamlEditor extends LitElement {
} }
protected firstUpdated(): void { protected firstUpdated(): void {
if (this.defaultValue !== undefined) { if (this.defaultValue) {
this.setValue(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() { protected render() {
if (this._yaml === undefined) { if (this._yaml === undefined) {
return nothing; return nothing;
@@ -99,7 +90,7 @@ export class HaYamlEditor extends LitElement {
return html` return html`
${this.label ${this.label
? html`<p>${this.label}${this.required ? " *" : ""}</p>` ? html`<p>${this.label}${this.required ? " *" : ""}</p>`
: nothing} : ""}
<ha-code-editor <ha-code-editor
.hass=${this.hass} .hass=${this.hass}
.value=${this._yaml} .value=${this._yaml}
@@ -112,20 +103,16 @@ export class HaYamlEditor extends LitElement {
dir="ltr" dir="ltr"
></ha-code-editor> ></ha-code-editor>
${this.copyClipboard || this.hasExtraActions ${this.copyClipboard || this.hasExtraActions
? html` ? html`<div class="card-actions">
<div class="card-actions">
${this.copyClipboard ${this.copyClipboard
? html` ? html` <mwc-button @click=${this._copyYaml}>
<ha-button @click=${this._copyYaml}>
${this.hass.localize( ${this.hass.localize(
"ui.components.yaml-editor.copy_to_clipboard" "ui.components.yaml-editor.copy_to_clipboard"
)} )}
</ha-button> </mwc-button>`
`
: nothing} : nothing}
<slot name="extra-actions"></slot> <slot name="extra-actions"></slot>
</div> </div>`
`
: nothing} : nothing}
`; `;
} }

View File

@@ -22,7 +22,7 @@ import { LitElement, PropertyValues, css, html, nothing } from "lit";
import { customElement, property } from "lit/decorators"; import { customElement, property } from "lit/decorators";
import { ensureArray } from "../../common/array/ensure-array"; import { ensureArray } from "../../common/array/ensure-array";
import { fireEvent } from "../../common/dom/fire_event"; import { fireEvent } from "../../common/dom/fire_event";
import { Condition, Trigger, flattenTriggers } from "../../data/automation"; import { Condition, Trigger } from "../../data/automation";
import { import {
Action, Action,
ChooseAction, ChooseAction,
@@ -94,7 +94,7 @@ export class HatScriptGraph extends LitElement {
@focus=${this.selectNode(config, path)} @focus=${this.selectNode(config, path)}
?active=${this.selected === path} ?active=${this.selected === path}
.iconPath=${mdiAsterisk} .iconPath=${mdiAsterisk}
.notEnabled=${"enabled" in config && config.enabled === false} .notEnabled=${config.enabled === false}
.error=${this.trace.trace[path]?.some((tr) => tr.error)} .error=${this.trace.trace[path]?.some((tr) => tr.error)}
tabindex=${track ? "0" : "-1"} tabindex=${track ? "0" : "-1"}
></hat-graph-node> ></hat-graph-node>
@@ -569,16 +569,11 @@ export class HatScriptGraph extends LitElement {
} }
protected render() { 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 paths = Object.keys(this.trackedNodes);
const trigger_nodes = const trigger_nodes =
triggerKey in this.trace.config "trigger" in this.trace.config
? flattenTriggers(ensureArray(this.trace.config[triggerKey])).map( ? ensureArray(this.trace.config.trigger).map((trigger, i) =>
(trigger, i) => this.render_trigger(trigger, i) this.render_trigger(trigger, i)
) )
: undefined; : undefined;
try { try {
@@ -589,14 +584,14 @@ export class HatScriptGraph extends LitElement {
${trigger_nodes} ${trigger_nodes}
</hat-graph-branch>` </hat-graph-branch>`
: ""} : ""}
${conditionKey in this.trace.config ${"condition" in this.trace.config
? html`${ensureArray(this.trace.config[conditionKey])?.map( ? html`${ensureArray(this.trace.config.condition)?.map(
(condition, i) => this.render_condition(condition, i) (condition, i) => this.render_condition(condition, i)
)}` )}`
: ""} : ""}
${actionKey in this.trace.config ${"action" in this.trace.config
? html`${ensureArray(this.trace.config[actionKey]).map( ? html`${ensureArray(this.trace.config.action).map((action, i) =>
(action, i) => this.render_action_node(action, `action/${i}`) this.render_action_node(action, `action/${i}`)
)}` )}`
: ""} : ""}
${"sequence" in this.trace.config ${"sequence" in this.trace.config

View File

@@ -22,8 +22,13 @@ import { formatDateTimeWithSeconds } from "../../common/datetime/format_date_tim
import { relativeTime } from "../../common/datetime/relative_time"; import { relativeTime } from "../../common/datetime/relative_time";
import { fireEvent } from "../../common/dom/fire_event"; import { fireEvent } from "../../common/dom/fire_event";
import { toggleAttribute } from "../../common/dom/toggle_attribute"; 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 { EntityRegistryEntry } from "../../data/entity_registry";
import { FloorRegistryEntry } from "../../data/floor_registry";
import { LabelRegistryEntry } from "../../data/label_registry"; import { LabelRegistryEntry } from "../../data/label_registry";
import { LogbookEntry } from "../../data/logbook"; import { LogbookEntry } from "../../data/logbook";
import { import {
@@ -201,6 +206,7 @@ class ActionRenderer {
private hass: HomeAssistant, private hass: HomeAssistant,
private entityReg: EntityRegistryEntry[], private entityReg: EntityRegistryEntry[],
private labelReg: LabelRegistryEntry[], private labelReg: LabelRegistryEntry[],
private floorReg: FloorRegistryEntry[],
private entries: TemplateResult[], private entries: TemplateResult[],
private trace: AutomationTraceExtended, private trace: AutomationTraceExtended,
private logbookRenderer: LogbookRenderer, private logbookRenderer: LogbookRenderer,
@@ -319,6 +325,7 @@ class ActionRenderer {
this.hass, this.hass,
this.entityReg, this.entityReg,
this.labelReg, this.labelReg,
this.floorReg,
data, data,
actionType actionType
), ),
@@ -486,7 +493,13 @@ class ActionRenderer {
const name = const name =
repeatConfig.alias || 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); this._renderEntry(repeatPath, name, undefined, disabled);
@@ -584,6 +597,7 @@ class ActionRenderer {
this.hass, this.hass,
this.entityReg, this.entityReg,
this.labelReg, this.labelReg,
this.floorReg,
sequenceConfig, sequenceConfig,
"sequence" "sequence"
), ),
@@ -680,6 +694,10 @@ export class HaAutomationTracer extends LitElement {
@consume({ context: labelsContext, subscribe: true }) @consume({ context: labelsContext, subscribe: true })
_labelReg!: LabelRegistryEntry[]; _labelReg!: LabelRegistryEntry[];
@state()
@consume({ context: floorsContext, subscribe: true })
_floorReg!: FloorRegistryEntry[];
protected render() { protected render() {
if (!this.trace) { if (!this.trace) {
return nothing; return nothing;
@@ -697,6 +715,7 @@ export class HaAutomationTracer extends LitElement {
this.hass, this.hass,
this._entityReg, this._entityReg,
this._labelReg, this._labelReg,
this._floorReg,
entries, entries,
this.trace, this.trace,
logbookRenderer, logbookRenderer,

View File

@@ -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);

View File

@@ -3,12 +3,10 @@ import {
HassEntityBase, HassEntityBase,
} from "home-assistant-js-websocket"; } from "home-assistant-js-websocket";
import { navigate } from "../common/navigate"; import { navigate } from "../common/navigate";
import { ensureArray } from "../common/array/ensure-array";
import { Context, HomeAssistant } from "../types"; import { Context, HomeAssistant } from "../types";
import { BlueprintInput } from "./blueprint"; import { BlueprintInput } from "./blueprint";
import { DeviceCondition, DeviceTrigger } from "./device_automation"; import { DeviceCondition, DeviceTrigger } from "./device_automation";
import { Action, MODES, migrateAutomationAction } from "./script"; 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_MODE: (typeof MODES)[number] = "single";
export const AUTOMATION_DEFAULT_MAX = 10; export const AUTOMATION_DEFAULT_MAX = 10;
@@ -28,14 +26,8 @@ export interface ManualAutomationConfig {
id?: string; id?: string;
alias?: string; alias?: string;
description?: string; description?: string;
triggers: Trigger | Trigger[]; trigger: Trigger | Trigger[];
/** @deprecated Use `triggers` instead */
trigger?: Trigger | Trigger[];
conditions?: Condition | Condition[];
/** @deprecated Use `conditions` instead */
condition?: Condition | Condition[]; condition?: Condition | Condition[];
actions: Action | Action[];
/** @deprecated Use `actions` instead */
action?: Action | Action[]; action?: Action | Action[];
mode?: (typeof MODES)[number]; mode?: (typeof MODES)[number];
max?: number; max?: number;
@@ -70,22 +62,16 @@ export interface ContextConstraint {
user_id?: string | string[]; user_id?: string | string[];
} }
export interface TriggerList {
triggers: Trigger | Trigger[] | undefined;
}
export interface BaseTrigger { export interface BaseTrigger {
alias?: string; alias?: string;
/** @deprecated Use `trigger` instead */ platform: string;
platform?: string;
trigger: string;
id?: string; id?: string;
variables?: Record<string, unknown>; variables?: Record<string, unknown>;
enabled?: boolean; enabled?: boolean;
} }
export interface StateTrigger extends BaseTrigger { export interface StateTrigger extends BaseTrigger {
trigger: "state"; platform: "state";
entity_id: string | string[]; entity_id: string | string[];
attribute?: string; attribute?: string;
from?: string | string[]; from?: string | string[];
@@ -94,25 +80,25 @@ export interface StateTrigger extends BaseTrigger {
} }
export interface MqttTrigger extends BaseTrigger { export interface MqttTrigger extends BaseTrigger {
trigger: "mqtt"; platform: "mqtt";
topic: string; topic: string;
payload?: string; payload?: string;
} }
export interface GeoLocationTrigger extends BaseTrigger { export interface GeoLocationTrigger extends BaseTrigger {
trigger: "geo_location"; platform: "geo_location";
source: string; source: string;
zone: string; zone: string;
event: "enter" | "leave"; event: "enter" | "leave";
} }
export interface HassTrigger extends BaseTrigger { export interface HassTrigger extends BaseTrigger {
trigger: "homeassistant"; platform: "homeassistant";
event: "start" | "shutdown"; event: "start" | "shutdown";
} }
export interface NumericStateTrigger extends BaseTrigger { export interface NumericStateTrigger extends BaseTrigger {
trigger: "numeric_state"; platform: "numeric_state";
entity_id: string | string[]; entity_id: string | string[];
attribute?: string; attribute?: string;
above?: number; above?: number;
@@ -122,69 +108,69 @@ export interface NumericStateTrigger extends BaseTrigger {
} }
export interface ConversationTrigger extends BaseTrigger { export interface ConversationTrigger extends BaseTrigger {
trigger: "conversation"; platform: "conversation";
command: string | string[]; command: string | string[];
} }
export interface SunTrigger extends BaseTrigger { export interface SunTrigger extends BaseTrigger {
trigger: "sun"; platform: "sun";
offset: number; offset: number;
event: "sunrise" | "sunset"; event: "sunrise" | "sunset";
} }
export interface TimePatternTrigger extends BaseTrigger { export interface TimePatternTrigger extends BaseTrigger {
trigger: "time_pattern"; platform: "time_pattern";
hours?: number | string; hours?: number | string;
minutes?: number | string; minutes?: number | string;
seconds?: number | string; seconds?: number | string;
} }
export interface WebhookTrigger extends BaseTrigger { export interface WebhookTrigger extends BaseTrigger {
trigger: "webhook"; platform: "webhook";
webhook_id: string; webhook_id: string;
allowed_methods?: string[]; allowed_methods?: string[];
local_only?: boolean; local_only?: boolean;
} }
export interface PersistentNotificationTrigger extends BaseTrigger { export interface PersistentNotificationTrigger extends BaseTrigger {
trigger: "persistent_notification"; platform: "persistent_notification";
notification_id?: string; notification_id?: string;
update_type?: string[]; update_type?: string[];
} }
export interface ZoneTrigger extends BaseTrigger { export interface ZoneTrigger extends BaseTrigger {
trigger: "zone"; platform: "zone";
entity_id: string; entity_id: string;
zone: string; zone: string;
event: "enter" | "leave"; event: "enter" | "leave";
} }
export interface TagTrigger extends BaseTrigger { export interface TagTrigger extends BaseTrigger {
trigger: "tag"; platform: "tag";
tag_id: string; tag_id: string;
device_id?: string; device_id?: string;
} }
export interface TimeTrigger extends BaseTrigger { export interface TimeTrigger extends BaseTrigger {
trigger: "time"; platform: "time";
at: string | { entity_id: string; offset?: string }; at: string;
} }
export interface TemplateTrigger extends BaseTrigger { export interface TemplateTrigger extends BaseTrigger {
trigger: "template"; platform: "template";
value_template: string; value_template: string;
for?: string | number | ForDict; for?: string | number | ForDict;
} }
export interface EventTrigger extends BaseTrigger { export interface EventTrigger extends BaseTrigger {
trigger: "event"; platform: "event";
event_type: string; event_type: string;
event_data?: any; event_data?: any;
context?: ContextConstraint; context?: ContextConstraint;
} }
export interface CalendarTrigger extends BaseTrigger { export interface CalendarTrigger extends BaseTrigger {
trigger: "calendar"; platform: "calendar";
event: "start" | "end"; event: "start" | "end";
entity_id: string; entity_id: string;
offset: string; offset: string;
@@ -207,8 +193,7 @@ export type Trigger =
| TemplateTrigger | TemplateTrigger
| EventTrigger | EventTrigger
| DeviceTrigger | DeviceTrigger
| CalendarTrigger | CalendarTrigger;
| TriggerList;
interface BaseCondition { interface BaseCondition {
condition: string; condition: string;
@@ -372,104 +357,25 @@ export const normalizeAutomationConfig = <
>( >(
config: T config: T
): T => { ): T => {
config = migrateAutomationConfig(config);
// Normalize data: ensure triggers, actions and conditions are lists // Normalize data: ensure triggers, actions and conditions are lists
// Happens when people copy paste their automations into the config // 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]; const value = config[key];
if (value && !Array.isArray(value)) { if (value && !Array.isArray(value)) {
config[key] = [value]; config[key] = [value];
} }
} }
return config; if (config.action) {
}; config.action = migrateAutomationAction(config.action);
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);
} }
return config; return config;
}; };
export const migrateAutomationTrigger = ( export const showAutomationEditor = (data?: Partial<AutomationConfig>) => {
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
) => {
initialAutomationEditorData = data; initialAutomationEditorData = data;
const params = expanded ? `?${createSearchParam({ expanded: "1" })}` : ""; navigate("/config/automation/edit/new");
navigate(`/config/automation/edit/new${params}`);
}; };
export const duplicateAutomation = (config: AutomationConfig) => { export const duplicateAutomation = (config: AutomationConfig) => {

View File

@@ -8,7 +8,6 @@ import {
import secondsToDuration from "../common/datetime/seconds_to_duration"; import secondsToDuration from "../common/datetime/seconds_to_duration";
import { computeAttributeNameDisplay } from "../common/entity/compute_attribute_display"; import { computeAttributeNameDisplay } from "../common/entity/compute_attribute_display";
import { computeStateName } from "../common/entity/compute_state_name"; import { computeStateName } from "../common/entity/compute_state_name";
import { isValidEntityId } from "../common/entity/valid_entity_id";
import type { HomeAssistant } from "../types"; import type { HomeAssistant } from "../types";
import { Condition, ForDict, Trigger } from "./automation"; import { Condition, ForDict, Trigger } from "./automation";
import { import {
@@ -23,7 +22,6 @@ import {
formatListWithAnds, formatListWithAnds,
formatListWithOrs, formatListWithOrs,
} from "../common/string/format-list"; } from "../common/string/format-list";
import { isTriggerList } from "./trigger";
const triggerTranslationBaseKey = const triggerTranslationBaseKey =
"ui.panel.config.automation.editor.triggers.type"; "ui.panel.config.automation.editor.triggers.type";
@@ -70,18 +68,9 @@ export const describeTrigger = (
hass: HomeAssistant, hass: HomeAssistant,
entityRegistry: EntityRegistryEntry[], entityRegistry: EntityRegistryEntry[],
ignoreAlias = false ignoreAlias = false
): string => { ) => {
try { try {
const description = tryDescribeTrigger( return tryDescribeTrigger(trigger, hass, entityRegistry, ignoreAlias);
trigger,
hass,
entityRegistry,
ignoreAlias
);
if (typeof description !== "string") {
throw new Error(String(description));
}
return description;
} catch (error: any) { } catch (error: any) {
// eslint-disable-next-line no-console // eslint-disable-next-line no-console
console.error(error); console.error(error);
@@ -100,26 +89,12 @@ const tryDescribeTrigger = (
entityRegistry: EntityRegistryEntry[], entityRegistry: EntityRegistryEntry[],
ignoreAlias = false 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) { if (trigger.alias && !ignoreAlias) {
return trigger.alias; return trigger.alias;
} }
// Event Trigger // Event Trigger
if (trigger.trigger === "event" && trigger.event_type) { if (trigger.platform === "event" && trigger.event_type) {
const eventTypes: string[] = []; const eventTypes: string[] = [];
if (Array.isArray(trigger.event_type)) { if (Array.isArray(trigger.event_type)) {
@@ -138,7 +113,7 @@ const tryDescribeTrigger = (
} }
// Home Assistant Trigger // Home Assistant Trigger
if (trigger.trigger === "homeassistant" && trigger.event) { if (trigger.platform === "homeassistant" && trigger.event) {
return hass.localize( return hass.localize(
trigger.event === "start" trigger.event === "start"
? `${triggerTranslationBaseKey}.homeassistant.description.started` ? `${triggerTranslationBaseKey}.homeassistant.description.started`
@@ -147,7 +122,7 @@ const tryDescribeTrigger = (
} }
// Numeric State Trigger // Numeric State Trigger
if (trigger.trigger === "numeric_state" && trigger.entity_id) { if (trigger.platform === "numeric_state" && trigger.entity_id) {
const entities: string[] = []; const entities: string[] = [];
const states = hass.states; const states = hass.states;
@@ -222,7 +197,7 @@ const tryDescribeTrigger = (
} }
// State Trigger // State Trigger
if (trigger.trigger === "state") { if (trigger.platform === "state") {
const entities: string[] = []; const entities: string[] = [];
const states = hass.states; const states = hass.states;
@@ -345,7 +320,7 @@ const tryDescribeTrigger = (
} }
// Sun Trigger // Sun Trigger
if (trigger.trigger === "sun" && trigger.event) { if (trigger.platform === "sun" && trigger.event) {
let duration = ""; let duration = "";
if (trigger.offset) { if (trigger.offset) {
if (typeof trigger.offset === "number") { if (typeof trigger.offset === "number") {
@@ -366,28 +341,19 @@ const tryDescribeTrigger = (
} }
// Tag Trigger // Tag Trigger
if (trigger.trigger === "tag") { if (trigger.platform === "tag") {
return hass.localize(`${triggerTranslationBaseKey}.tag.description.full`); return hass.localize(`${triggerTranslationBaseKey}.tag.description.full`);
} }
// Time Trigger // Time Trigger
if (trigger.trigger === "time" && trigger.at) { if (trigger.platform === "time" && trigger.at) {
const result = ensureArray(trigger.at).map((at) => { const result = ensureArray(trigger.at).map((at) =>
if (typeof at === "string") { typeof at !== "string"
if (isValidEntityId(at)) { ? at
return `entity ${hass.states[at] ? computeStateName(hass.states[at]) : at}`; : at.includes(".")
} ? `entity ${hass.states[at] ? computeStateName(hass.states[at]) : at}`
return localizeTimeString(at, hass.locale, hass.config); : localizeTimeString(at, hass.locale, hass.config)
} );
const entityStr = `entity ${hass.states[at.entity_id] ? computeStateName(hass.states[at.entity_id]) : at.entity_id}`;
const offsetStr = at.offset
? " " +
hass.localize(`${triggerTranslationBaseKey}.time.offset_by`, {
offset: describeDuration(hass.locale, at.offset),
})
: "";
return `${entityStr}${offsetStr}`;
});
return hass.localize(`${triggerTranslationBaseKey}.time.description.full`, { return hass.localize(`${triggerTranslationBaseKey}.time.description.full`, {
time: formatListWithOrs(hass.locale, result), time: formatListWithOrs(hass.locale, result),
@@ -395,7 +361,7 @@ const tryDescribeTrigger = (
} }
// Time Pattern Trigger // Time Pattern Trigger
if (trigger.trigger === "time_pattern") { if (trigger.platform === "time_pattern") {
if (!trigger.seconds && !trigger.minutes && !trigger.hours) { if (!trigger.seconds && !trigger.minutes && !trigger.hours) {
return hass.localize( return hass.localize(
`${triggerTranslationBaseKey}.time_pattern.description.initial` `${triggerTranslationBaseKey}.time_pattern.description.initial`
@@ -572,7 +538,7 @@ const tryDescribeTrigger = (
} }
// Zone Trigger // Zone Trigger
if (trigger.trigger === "zone" && trigger.entity_id && trigger.zone) { if (trigger.platform === "zone" && trigger.entity_id && trigger.zone) {
const entities: string[] = []; const entities: string[] = [];
const zones: string[] = []; const zones: string[] = [];
@@ -615,7 +581,7 @@ const tryDescribeTrigger = (
} }
// Geo Location Trigger // 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 sources: string[] = [];
const zones: string[] = []; const zones: string[] = [];
const states = hass.states; const states = hass.states;
@@ -654,12 +620,12 @@ const tryDescribeTrigger = (
} }
// MQTT Trigger // MQTT Trigger
if (trigger.trigger === "mqtt") { if (trigger.platform === "mqtt") {
return hass.localize(`${triggerTranslationBaseKey}.mqtt.description.full`); return hass.localize(`${triggerTranslationBaseKey}.mqtt.description.full`);
} }
// Template Trigger // Template Trigger
if (trigger.trigger === "template") { if (trigger.platform === "template") {
let duration = ""; let duration = "";
if (trigger.for) { if (trigger.for) {
duration = describeDuration(hass.locale, trigger.for) ?? ""; duration = describeDuration(hass.locale, trigger.for) ?? "";
@@ -672,14 +638,14 @@ const tryDescribeTrigger = (
} }
// Webhook Trigger // Webhook Trigger
if (trigger.trigger === "webhook") { if (trigger.platform === "webhook") {
return hass.localize( return hass.localize(
`${triggerTranslationBaseKey}.webhook.description.full` `${triggerTranslationBaseKey}.webhook.description.full`
); );
} }
// Conversation Trigger // Conversation Trigger
if (trigger.trigger === "conversation") { if (trigger.platform === "conversation") {
if (!trigger.command) { if (!trigger.command) {
return hass.localize( return hass.localize(
`${triggerTranslationBaseKey}.conversation.description.empty` `${triggerTranslationBaseKey}.conversation.description.empty`
@@ -698,14 +664,14 @@ const tryDescribeTrigger = (
} }
// Persistent Notification Trigger // Persistent Notification Trigger
if (trigger.trigger === "persistent_notification") { if (trigger.platform === "persistent_notification") {
return hass.localize( return hass.localize(
`${triggerTranslationBaseKey}.persistent_notification.description.full` `${triggerTranslationBaseKey}.persistent_notification.description.full`
); );
} }
// Device Trigger // Device Trigger
if (trigger.trigger === "device" && trigger.device_id) { if (trigger.platform === "device" && trigger.device_id) {
const config = trigger as DeviceTrigger; const config = trigger as DeviceTrigger;
const localized = localizeDeviceAutomationTrigger( const localized = localizeDeviceAutomationTrigger(
hass, hass,
@@ -723,7 +689,7 @@ const tryDescribeTrigger = (
return ( return (
hass.localize( 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`) hass.localize(`ui.panel.config.automation.editor.triggers.unknown_trigger`)
); );
@@ -734,18 +700,9 @@ export const describeCondition = (
hass: HomeAssistant, hass: HomeAssistant,
entityRegistry: EntityRegistryEntry[], entityRegistry: EntityRegistryEntry[],
ignoreAlias = false ignoreAlias = false
): string => { ) => {
try { try {
const description = tryDescribeCondition( return tryDescribeCondition(condition, hass, entityRegistry, ignoreAlias);
condition,
hass,
entityRegistry,
ignoreAlias
);
if (typeof description !== "string") {
throw new Error(String(description));
}
return description;
} catch (error: any) { } catch (error: any) {
// eslint-disable-next-line no-console // eslint-disable-next-line no-console
console.error(error); console.error(error);
@@ -932,14 +889,8 @@ const tryDescribeCondition = (
// Numeric State Condition // Numeric State Condition
if (condition.condition === "numeric_state" && condition.entity_id) { if (condition.condition === "numeric_state" && condition.entity_id) {
const entity_ids = ensureArray(condition.entity_id); const stateObj = hass.states[condition.entity_id];
const stateObj = hass.states[entity_ids[0]]; const entity = stateObj ? computeStateName(stateObj) : condition.entity_id;
const entity = formatListWithAnds(
hass.locale,
entity_ids.map((id) =>
hass.states[id] ? computeStateName(hass.states[id]) : id || ""
)
);
const attribute = condition.attribute const attribute = condition.attribute
? computeAttributeNameDisplay( ? computeAttributeNameDisplay(
@@ -954,9 +905,8 @@ const tryDescribeCondition = (
return hass.localize( return hass.localize(
`${conditionsTranslationBaseKey}.numeric_state.description.above-below`, `${conditionsTranslationBaseKey}.numeric_state.description.above-below`,
{ {
attribute, attribute: attribute,
entity, entity: entity,
numberOfEntities: entity_ids.length,
above: condition.above, above: condition.above,
below: condition.below, below: condition.below,
} }
@@ -966,9 +916,8 @@ const tryDescribeCondition = (
return hass.localize( return hass.localize(
`${conditionsTranslationBaseKey}.numeric_state.description.above`, `${conditionsTranslationBaseKey}.numeric_state.description.above`,
{ {
attribute, attribute: attribute,
entity, entity: entity,
numberOfEntities: entity_ids.length,
above: condition.above, above: condition.above,
} }
); );
@@ -977,9 +926,8 @@ const tryDescribeCondition = (
return hass.localize( return hass.localize(
`${conditionsTranslationBaseKey}.numeric_state.description.below`, `${conditionsTranslationBaseKey}.numeric_state.description.below`,
{ {
attribute, attribute: attribute,
entity, entity: entity,
numberOfEntities: entity_ids.length,
below: condition.below, below: condition.below,
} }
); );

View File

@@ -133,17 +133,3 @@ export const isCameraMediaSource = (mediaContentId: string) =>
export const getEntityIdFromCameraMediaSource = (mediaContentId: string) => export const getEntityIdFromCameraMediaSource = (mediaContentId: string) =>
mediaContentId.substring(CAMERA_MEDIA_SOURCE_PREFIX.length); 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,
});

View File

@@ -10,7 +10,7 @@ interface InvalidConfig {
error: string; error: string;
} }
type ValidKeys = "triggers" | "actions" | "conditions"; type ValidKeys = "trigger" | "action" | "condition";
export const validateConfig = < export const validateConfig = <
T extends Partial<{ [key in ValidKeys]: unknown }>, T extends Partial<{ [key in ValidKeys]: unknown }>,

View File

@@ -1,6 +1,6 @@
import type { UnsubscribeFunc } from "home-assistant-js-websocket"; import type { UnsubscribeFunc } from "home-assistant-js-websocket";
import type { HomeAssistant } from "../types"; import type { HomeAssistant } from "../types";
import type { IntegrationType } from "./integration"; import type { IntegrationManifest, IntegrationType } from "./integration";
export interface ConfigEntry { export interface ConfigEntry {
entry_id: string; entry_id: string;
@@ -149,19 +149,20 @@ export const enableConfigEntry = (hass: HomeAssistant, configEntryId: string) =>
export const sortConfigEntries = ( export const sortConfigEntries = (
configEntries: ConfigEntry[], configEntries: ConfigEntry[],
primaryConfigEntry: string | null manifestLookup: { [domain: string]: IntegrationManifest }
): ConfigEntry[] => { ): ConfigEntry[] => {
if (!primaryConfigEntry) { const sortedConfigEntries = [...configEntries];
return configEntries;
} const getScore = (entry: ConfigEntry) => {
const primaryEntry = configEntries.find( const manifest = manifestLookup[entry.domain] as
(e) => e.entry_id === primaryConfigEntry | IntegrationManifest
); | undefined;
if (!primaryEntry) { const isHelper = manifest?.integration_type === "helper";
return configEntries; return isHelper ? -1 : 1;
} };
const otherEntries = configEntries.filter(
(e) => e.entry_id !== primaryConfigEntry const configEntriesCompare = (a: ConfigEntry, b: ConfigEntry) =>
); getScore(b) - getScore(a);
return [primaryEntry, ...otherEntries];
return sortedConfigEntries.sort(configEntriesCompare);
}; };

View File

@@ -2,6 +2,7 @@ import { createContext } from "@lit-labs/context";
import { HassConfig } from "home-assistant-js-websocket"; import { HassConfig } from "home-assistant-js-websocket";
import { HomeAssistant } from "../types"; import { HomeAssistant } from "../types";
import { EntityRegistryEntry } from "./entity_registry"; import { EntityRegistryEntry } from "./entity_registry";
import { FloorRegistryEntry } from "./floor_registry";
import { LabelRegistryEntry } from "./label_registry"; import { LabelRegistryEntry } from "./label_registry";
export const connectionContext = export const connectionContext =
@@ -27,4 +28,6 @@ export const panelsContext = createContext<HomeAssistant["panels"]>("panels");
export const fullEntitiesContext = export const fullEntitiesContext =
createContext<EntityRegistryEntry[]>("extendedEntities"); createContext<EntityRegistryEntry[]>("extendedEntities");
export const floorsContext = createContext<FloorRegistryEntry[]>("floors");
export const labelsContext = createContext<LabelRegistryEntry[]>("labels"); export const labelsContext = createContext<LabelRegistryEntry[]>("labels");

View File

@@ -1,20 +1,10 @@
export interface DataTableFilters { export interface DataTableFilters {
[key: string]: { [key: string]: {
value: DataTableFiltersValue; value: string[] | { key: string[] } | undefined;
items: Set<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) => { export const serializeFilters = (value: DataTableFilters) => {
const serializedValue = {}; const serializedValue = {};
Object.entries(value).forEach(([key, val]) => { Object.entries(value).forEach(([key, val]) => {

View File

@@ -1,7 +1,7 @@
import { computeStateName } from "../common/entity/compute_state_name"; import { computeStateName } from "../common/entity/compute_state_name";
import type { HaFormSchema } from "../components/ha-form/types"; import type { HaFormSchema } from "../components/ha-form/types";
import { HomeAssistant } from "../types"; import { HomeAssistant } from "../types";
import { BaseTrigger, migrateAutomationTrigger } from "./automation"; import { BaseTrigger } from "./automation";
import { import {
computeEntityRegistryName, computeEntityRegistryName,
entityRegistryByEntityId, entityRegistryByEntityId,
@@ -31,7 +31,7 @@ export interface DeviceCondition extends DeviceAutomation {
export type DeviceTrigger = DeviceAutomation & export type DeviceTrigger = DeviceAutomation &
BaseTrigger & { BaseTrigger & {
trigger: "device"; platform: "device";
}; };
export interface DeviceCapabilities { export interface DeviceCapabilities {
@@ -51,12 +51,10 @@ export const fetchDeviceConditions = (hass: HomeAssistant, deviceId: string) =>
}); });
export const fetchDeviceTriggers = (hass: HomeAssistant, deviceId: string) => export const fetchDeviceTriggers = (hass: HomeAssistant, deviceId: string) =>
hass hass.callWS<DeviceTrigger[]>({
.callWS<DeviceTrigger[]>({
type: "device_automation/trigger/list", type: "device_automation/trigger/list",
device_id: deviceId, device_id: deviceId,
}) });
.then((triggers) => migrateAutomationTrigger(triggers) as DeviceTrigger[]);
export const fetchDeviceActionCapabilities = ( export const fetchDeviceActionCapabilities = (
hass: HomeAssistant, hass: HomeAssistant,
@@ -93,7 +91,7 @@ const deviceAutomationIdentifiers = [
"subtype", "subtype",
"event", "event",
"condition", "condition",
"trigger", "platform",
]; ];
export const deviceAutomationsEqual = ( export const deviceAutomationsEqual = (

View File

@@ -33,7 +33,6 @@ export interface DeviceRegistryEntry extends RegistryEntry {
entry_type: "service" | null; entry_type: "service" | null;
disabled_by: "user" | "integration" | "config_entry" | null; disabled_by: "user" | "integration" | "config_entry" | null;
configuration_url: string | null; configuration_url: string | null;
primary_config_entry: string | null;
} }
export interface DeviceEntityDisplayLookup { export interface DeviceEntityDisplayLookup {

View File

@@ -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 { stringCompare } from "../common/string/compare";
import { debounce } from "../common/util/debounce";
import { HomeAssistant } from "../types"; import { HomeAssistant } from "../types";
import { AreaRegistryEntry } from "./area_registry"; import { AreaRegistryEntry } from "./area_registry";
import { RegistryEntry } from "./registry"; import { RegistryEntry } from "./registry";
@@ -24,6 +27,48 @@ export interface FloorRegistryEntryMutableParams {
aliases?: string[]; 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 = ( export const createFloorRegistryEntry = (
hass: HomeAssistant, hass: HomeAssistant,
values: FloorRegistryEntryMutableParams values: FloorRegistryEntryMutableParams

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