Compare commits

..

1 Commits

Author SHA1 Message Date
Bram Kragten
d215064c6e Allow the number selector to pick an entity 2025-11-26 10:40:22 +01:00
249 changed files with 4703 additions and 10836 deletions

View File

@@ -36,14 +36,14 @@ jobs:
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@fdbfb4d2750291e159f0156def62b853c2798ca2 # v4.31.5
uses: github/codeql-action/init@e12f0178983d466f2f6028f5cc7a6d786fd97f4b # v4.31.4
with:
languages: ${{ matrix.language }}
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
# If this step fails, then you should remove it and run the build manually (see below)
- name: Autobuild
uses: github/codeql-action/autobuild@fdbfb4d2750291e159f0156def62b853c2798ca2 # v4.31.5
uses: github/codeql-action/autobuild@e12f0178983d466f2f6028f5cc7a6d786fd97f4b # v4.31.4
# Command-line programs to run using the OS shell.
# 📚 https://git.io/JvXDl
@@ -57,4 +57,4 @@ jobs:
# make release
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@fdbfb4d2750291e159f0156def62b853c2798ca2 # v4.31.5
uses: github/codeql-action/analyze@e12f0178983d466f2f6028f5cc7a6d786fd97f4b # v4.31.4

View File

@@ -23,7 +23,7 @@ jobs:
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
- name: Set up Python ${{ env.PYTHON_VERSION }}
uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6
uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6
with:
python-version: ${{ env.PYTHON_VERSION }}

View File

@@ -17,7 +17,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Send bundle stats and build information to RelativeCI
uses: relative-ci/agent-action@c45aaa919ef85620af54242a241ac17a8fa35983 # v3.2.1
uses: relative-ci/agent-action@feb19ddc698445db27401f1490f6ac182da0816f # v3.2.0
with:
key: ${{ secrets[format('RELATIVE_CI_KEY_{0}_{1}', matrix.bundle, matrix.build)] }}
token: ${{ github.token }}

View File

@@ -26,7 +26,7 @@ jobs:
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
- name: Set up Python ${{ env.PYTHON_VERSION }}
uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0
uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0
with:
python-version: ${{ env.PYTHON_VERSION }}
@@ -55,7 +55,7 @@ jobs:
script/release
- name: Upload release assets
uses: softprops/action-gh-release@a06a81a03ee405af7f2048a818ed3f03bbf83c7b # v2.5.0
uses: softprops/action-gh-release@5be0e66d93ac7ed76da52eca8bb058f665c3a5fe # v2.4.2
with:
files: |
dist/*.whl
@@ -75,7 +75,7 @@ jobs:
# home-assistant/wheels doesn't support SHA pinning
- name: Build wheels
uses: home-assistant/wheels@2025.11.0
uses: home-assistant/wheels@2025.10.0
with:
abi: cp313
tag: musllinux_1_2
@@ -108,7 +108,7 @@ jobs:
- name: Tar folder
run: tar -czf landing-page/home_assistant_frontend_landingpage-${{ github.event.release.tag_name }}.tar.gz -C landing-page/dist .
- name: Upload release asset
uses: softprops/action-gh-release@a06a81a03ee405af7f2048a818ed3f03bbf83c7b # v2.5.0
uses: softprops/action-gh-release@5be0e66d93ac7ed76da52eca8bb058f665c3a5fe # v2.4.2
with:
files: landing-page/home_assistant_frontend_landingpage-${{ github.event.release.tag_name }}.tar.gz
@@ -137,6 +137,6 @@ jobs:
- name: Tar folder
run: tar -czf hassio/home_assistant_frontend_supervisor-${{ github.event.release.tag_name }}.tar.gz -C hassio/build .
- name: Upload release asset
uses: softprops/action-gh-release@a06a81a03ee405af7f2048a818ed3f03bbf83c7b # v2.5.0
uses: softprops/action-gh-release@5be0e66d93ac7ed76da52eca8bb058f665c3a5fe # v2.4.2
with:
files: hassio/home_assistant_frontend_supervisor-${{ github.event.release.tag_name }}.tar.gz

File diff suppressed because one or more lines are too long

View File

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

View File

@@ -156,9 +156,7 @@ const createTestTranslation = () =>
*/
const createMasterTranslation = () =>
gulp
.src([EN_SRC, ...(mergeBackend ? [`${inBackendDir}/en.json`] : [])], {
allowEmpty: true,
})
.src([EN_SRC, ...(mergeBackend ? [`${inBackendDir}/en.json`] : [])])
.pipe(new CustomJSON(lokaliseTransform))
.pipe(new MergeJSON("en"))
.pipe(gulp.dest(workDir));

View File

@@ -305,8 +305,9 @@ export class HcMain extends HassElement {
await llColl.refresh();
this._unsubLovelace = llColl.subscribe(async (rawConfig) => {
if (isStrategyDashboard(rawConfig)) {
const { generateLovelaceDashboardStrategy } =
await import("../../../../src/panels/lovelace/strategies/get-strategy");
const { generateLovelaceDashboardStrategy } = await import(
"../../../../src/panels/lovelace/strategies/get-strategy"
);
const config = await generateLovelaceDashboardStrategy(
rawConfig,
this.hass!
@@ -346,8 +347,9 @@ export class HcMain extends HassElement {
}
private async _generateDefaultLovelaceConfig() {
const { generateLovelaceDashboardStrategy } =
await import("../../../../src/panels/lovelace/strategies/get-strategy");
const { generateLovelaceDashboardStrategy } = await import(
"../../../../src/panels/lovelace/strategies/get-strategy"
);
this._handleNewLovelaceConfig(
await generateLovelaceDashboardStrategy(DEFAULT_CONFIG, this.hass!)
);

View File

@@ -44,24 +44,18 @@ export const mockEnergy = (hass: MockHomeAssistant) => {
number_energy_price: null,
},
],
power: [
{ stat_rate: "sensor.power_grid" },
{ stat_rate: "sensor.power_grid_return" },
],
cost_adjustment_day: 0,
},
{
type: "solar",
stat_energy_from: "sensor.solar_production",
stat_rate: "sensor.power_solar",
config_entry_solar_forecast: ["solar_forecast"],
},
{
/* {
type: "battery",
stat_energy_from: "sensor.battery_output",
stat_energy_to: "sensor.battery_input",
stat_rate: "sensor.power_battery",
},
}, */
{
type: "gas",
stat_energy_from: "sensor.energy_gas",
@@ -69,48 +63,28 @@ export const mockEnergy = (hass: MockHomeAssistant) => {
entity_energy_price: null,
number_energy_price: null,
},
{
type: "water",
stat_energy_from: "sensor.energy_water",
stat_cost: "sensor.energy_water_cost",
entity_energy_price: null,
number_energy_price: null,
},
],
device_consumption: [
{
stat_consumption: "sensor.energy_car",
stat_rate: "sensor.power_car",
},
{
stat_consumption: "sensor.energy_ac",
stat_rate: "sensor.power_ac",
},
{
stat_consumption: "sensor.energy_washing_machine",
stat_rate: "sensor.power_washing_machine",
},
{
stat_consumption: "sensor.energy_dryer",
stat_rate: "sensor.power_dryer",
},
{
stat_consumption: "sensor.energy_heat_pump",
stat_rate: "sensor.power_heat_pump",
},
{
stat_consumption: "sensor.energy_boiler",
stat_rate: "sensor.power_boiler",
},
],
device_consumption_water: [
{
stat_consumption: "sensor.water_kitchen",
},
{
stat_consumption: "sensor.water_garden",
},
],
device_consumption_water: [],
})
);
hass.mockWS(

View File

@@ -154,38 +154,6 @@ export const energyEntities = () =>
unit_of_measurement: "EUR",
},
},
"sensor.power_grid": {
entity_id: "sensor.power_grid",
state: "500",
attributes: {
state_class: "measurement",
unit_of_measurement: "W",
},
},
"sensor.power_grid_return": {
entity_id: "sensor.power_grid_return",
state: "-100",
attributes: {
state_class: "measurement",
unit_of_measurement: "W",
},
},
"sensor.power_solar": {
entity_id: "sensor.power_solar",
state: "200",
attributes: {
state_class: "measurement",
unit_of_measurement: "W",
},
},
"sensor.power_battery": {
entity_id: "sensor.power_battery",
state: "100",
attributes: {
state_class: "measurement",
unit_of_measurement: "W",
},
},
"sensor.energy_gas_cost": {
entity_id: "sensor.energy_gas_cost",
state: "2",
@@ -203,15 +171,6 @@ export const energyEntities = () =>
unit_of_measurement: "m³",
},
},
"sensor.energy_water": {
entity_id: "sensor.energy_water",
state: "4000",
attributes: {
last_reset: "1970-01-01T00:00:00:00+00",
friendly_name: "Water",
unit_of_measurement: "L",
},
},
"sensor.energy_car": {
entity_id: "sensor.energy_car",
state: "4",
@@ -266,58 +225,4 @@ export const energyEntities = () =>
unit_of_measurement: "kWh",
},
},
"sensor.power_car": {
entity_id: "sensor.power_car",
state: "40",
attributes: {
state_class: "measurement",
friendly_name: "Electric car",
unit_of_measurement: "W",
},
},
"sensor.power_ac": {
entity_id: "sensor.power_ac",
state: "30",
attributes: {
state_class: "measurement",
friendly_name: "Air conditioning",
unit_of_measurement: "W",
},
},
"sensor.power_washing_machine": {
entity_id: "sensor.power_washing_machine",
state: "60",
attributes: {
state_class: "measurement",
friendly_name: "Washing machine",
unit_of_measurement: "W",
},
},
"sensor.power_dryer": {
entity_id: "sensor.power_dryer",
state: "55",
attributes: {
state_class: "measurement",
friendly_name: "Dryer",
unit_of_measurement: "W",
},
},
"sensor.power_heat_pump": {
entity_id: "sensor.power_heat_pump",
state: "60",
attributes: {
state_class: "measurement",
friendly_name: "Heat pump",
unit_of_measurement: "W",
},
},
"sensor.power_boiler": {
entity_id: "sensor.power_boiler",
state: "70",
attributes: {
state_class: "measurement",
friendly_name: "Boiler",
unit_of_measurement: "W",
},
},
});

View File

@@ -17,15 +17,17 @@ const generateMeanStatistics = (
end: Date,
// eslint-disable-next-line default-param-last
period: "5minute" | "hour" | "day" | "month" = "hour",
initValue: number,
maxDiff: number
): StatisticValue[] => {
const statistics: StatisticValue[] = [];
let currentDate = new Date(start);
currentDate.setMinutes(0, 0, 0);
let lastVal = initValue;
const now = new Date();
while (end > currentDate && currentDate < now) {
const delta = Math.random() * maxDiff;
const mean = delta;
const mean = lastVal + delta;
statistics.push({
start: currentDate.getTime(),
end: currentDate.getTime(),
@@ -36,6 +38,7 @@ const generateMeanStatistics = (
state: mean,
sum: null,
});
lastVal = mean;
currentDate =
period === "day"
? addDays(currentDate, 1)
@@ -333,6 +336,7 @@ export const mockRecorder = (mockHass: MockHomeAssistant) => {
start,
end,
period,
state,
state * (state > 80 ? 0.05 : 0.1)
);
}

View File

@@ -18,6 +18,7 @@ import { HaEventTrigger } from "../../../../src/panels/config/automation/trigger
import { HaGeolocationTrigger } from "../../../../src/panels/config/automation/trigger/types/ha-automation-trigger-geo_location";
import { HaHassTrigger } from "../../../../src/panels/config/automation/trigger/types/ha-automation-trigger-homeassistant";
import { HaTriggerList } from "../../../../src/panels/config/automation/trigger/types/ha-automation-trigger-list";
import { HaMQTTTrigger } from "../../../../src/panels/config/automation/trigger/types/ha-automation-trigger-mqtt";
import { HaNumericStateTrigger } from "../../../../src/panels/config/automation/trigger/types/ha-automation-trigger-numeric_state";
import { HaPersistentNotificationTrigger } from "../../../../src/panels/config/automation/trigger/types/ha-automation-trigger-persistent_notification";
import { HaStateTrigger } from "../../../../src/panels/config/automation/trigger/types/ha-automation-trigger-state";
@@ -37,6 +38,11 @@ const SCHEMAS: { name: string; triggers: Trigger[] }[] = [
triggers: [{ ...HaStateTrigger.defaultConfig }],
},
{
name: "MQTT",
triggers: [{ ...HaMQTTTrigger.defaultConfig }],
},
{
name: "GeoLocation",
triggers: [{ ...HaGeolocationTrigger.defaultConfig }],

View File

@@ -381,6 +381,10 @@ export class DemoHaWaDialog extends LitElement {
<td><code>--dialog-z-index</code></td>
<td>Z-index for the dialog.</td>
</tr>
<tr>
<td><code>--dialog-surface-position</code></td>
<td>CSS position of the dialog surface.</td>
</tr>
<tr>
<td><code>--dialog-surface-margin-top</code></td>
<td>Top margin for the dialog surface.</td>

View File

@@ -88,8 +88,8 @@ class HassioRegistriesDialog extends LitElement {
<ha-button
?disabled=${Boolean(
!this._input.registry ||
!this._input.username ||
!this._input.password
!this._input.username ||
!this._input.password
)}
@click=${this._addNewRegistry}
appearance="filled"

View File

@@ -28,13 +28,13 @@
"dependencies": {
"@babel/runtime": "7.28.4",
"@braintree/sanitize-url": "7.1.1",
"@codemirror/autocomplete": "6.20.0",
"@codemirror/autocomplete": "6.19.1",
"@codemirror/commands": "6.10.0",
"@codemirror/language": "6.11.3",
"@codemirror/legacy-modes": "6.5.2",
"@codemirror/search": "6.5.11",
"@codemirror/state": "6.5.2",
"@codemirror/view": "6.38.8",
"@codemirror/view": "6.38.6",
"@date-fns/tz": "1.4.1",
"@egjs/hammerjs": "2.0.17",
"@formatjs/intl-datetimeformat": "6.18.2",
@@ -52,7 +52,7 @@
"@fullcalendar/list": "6.1.19",
"@fullcalendar/luxon3": "6.1.19",
"@fullcalendar/timegrid": "6.1.19",
"@home-assistant/webawesome": "3.0.0-ha.1",
"@home-assistant/webawesome": "3.0.0",
"@lezer/highlight": "1.2.3",
"@lit-labs/motion": "1.0.9",
"@lit-labs/observers": "2.0.6",
@@ -89,17 +89,17 @@
"@thomasloven/round-slider": "0.6.0",
"@tsparticles/engine": "3.9.1",
"@tsparticles/preset-links": "3.2.0",
"@vaadin/combo-box": "24.9.6",
"@vaadin/vaadin-themable-mixin": "24.9.6",
"@vaadin/combo-box": "24.9.5",
"@vaadin/vaadin-themable-mixin": "24.9.5",
"@vibrant/color": "4.0.0",
"@vue/web-component-wrapper": "1.3.0",
"@webcomponents/scoped-custom-element-registry": "0.0.10",
"@webcomponents/webcomponentsjs": "2.8.0",
"app-datepicker": "5.1.1",
"barcode-detector": "3.0.8",
"color-name": "2.1.0",
"barcode-detector": "3.0.6",
"color-name": "2.0.2",
"comlink": "4.4.2",
"core-js": "3.47.0",
"core-js": "3.46.0",
"cropperjs": "1.6.2",
"culori": "4.0.2",
"date-fns": "4.1.0",
@@ -111,7 +111,7 @@
"fuse.js": "7.1.0",
"google-timezones-json": "1.2.0",
"gulp-zopfli-green": "6.0.2",
"hls.js": "1.6.15",
"hls.js": "1.6.14",
"home-assistant-js-websocket": "9.6.0",
"idb-keyval": "6.2.2",
"intl-messageformat": "10.7.18",
@@ -122,7 +122,7 @@
"lit": "3.3.1",
"lit-html": "3.3.1",
"luxon": "3.7.2",
"marked": "17.0.1",
"marked": "17.0.0",
"memoize-one": "6.0.0",
"node-vibrant": "4.0.3",
"object-hash": "3.0.0",
@@ -139,12 +139,12 @@
"vue": "2.7.16",
"vue2-daterange-picker": "0.6.8",
"weekstart": "2.0.0",
"workbox-cacheable-response": "7.4.0",
"workbox-core": "7.4.0",
"workbox-expiration": "7.4.0",
"workbox-precaching": "7.4.0",
"workbox-routing": "7.4.0",
"workbox-strategies": "7.4.0",
"workbox-cacheable-response": "7.3.0",
"workbox-core": "7.3.0",
"workbox-expiration": "7.3.0",
"workbox-precaching": "7.3.0",
"workbox-routing": "7.3.0",
"workbox-strategies": "7.3.0",
"xss": "1.0.15"
},
"devDependencies": {
@@ -152,13 +152,13 @@
"@babel/helper-define-polyfill-provider": "0.6.5",
"@babel/plugin-transform-runtime": "7.28.5",
"@babel/preset-env": "7.28.5",
"@bundle-stats/plugin-webpack-filter": "4.21.7",
"@lokalise/node-api": "15.4.0",
"@bundle-stats/plugin-webpack-filter": "4.21.6",
"@lokalise/node-api": "15.3.1",
"@octokit/auth-oauth-device": "8.0.3",
"@octokit/plugin-retry": "8.0.3",
"@octokit/rest": "22.0.1",
"@rsdoctor/rspack-plugin": "1.3.11",
"@rspack/core": "1.6.5",
"@rspack/core": "1.6.4",
"@rspack/dev-server": "1.1.4",
"@types/babel__plugin-transform-runtime": "7.9.5",
"@types/chromecast-caf-receiver": "6.0.22",
@@ -178,7 +178,7 @@
"@types/tar": "6.1.13",
"@types/ua-parser-js": "0.7.39",
"@types/webspeechapi": "0.0.29",
"@vitest/coverage-v8": "4.0.14",
"@vitest/coverage-v8": "4.0.8",
"babel-loader": "10.0.0",
"babel-plugin-template-html-minifier": "4.1.0",
"browserslist-useragent-regexp": "4.1.3",
@@ -194,32 +194,32 @@
"eslint-plugin-wc": "3.0.2",
"fancy-log": "2.0.0",
"fs-extra": "11.3.2",
"glob": "13.0.0",
"glob": "12.0.0",
"gulp": "5.0.1",
"gulp-brotli": "3.0.0",
"gulp-json-transform": "0.5.0",
"gulp-rename": "2.1.0",
"html-minifier-terser": "7.2.0",
"husky": "9.1.7",
"jsdom": "27.2.0",
"jsdom": "27.1.0",
"jszip": "3.10.1",
"lint-staged": "16.2.7",
"lint-staged": "16.2.6",
"lit-analyzer": "2.0.3",
"lodash.merge": "4.6.2",
"lodash.template": "4.5.0",
"map-stream": "0.0.7",
"pinst": "3.0.0",
"prettier": "3.7.3",
"rspack-manifest-plugin": "5.2.0",
"prettier": "3.6.2",
"rspack-manifest-plugin": "5.1.0",
"serve": "14.2.5",
"sinon": "21.0.0",
"tar": "7.5.2",
"terser-webpack-plugin": "5.3.14",
"ts-lit-plugin": "2.0.2",
"typescript": "5.9.3",
"typescript-eslint": "8.48.0",
"typescript-eslint": "8.46.3",
"vite-tsconfig-paths": "5.1.4",
"vitest": "4.0.14",
"vitest": "4.0.8",
"webpack-stats-plugin": "1.1.3",
"webpackbar": "7.0.0",
"workbox-build": "patch:workbox-build@npm%3A7.1.1#~/.yarn/patches/workbox-build-npm-7.1.1-a854f3faae.patch"
@@ -236,7 +236,7 @@
"@material/mwc-list@^0.27.0": "patch:@material/mwc-list@npm%3A0.27.0#~/.yarn/patches/@material-mwc-list-npm-0.27.0-5344fc9de4.patch",
"glob@^10.2.2": "^10.5.0"
},
"packageManager": "yarn@4.12.0",
"packageManager": "yarn@4.11.0",
"volta": {
"node": "22.21.1"
}

View File

@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "home-assistant-frontend"
version = "20251203.0"
version = "20251029.0"
license = "Apache-2.0"
license-files = ["LICENSE*"]
description = "The Home Assistant frontend"

View File

@@ -1,6 +1,5 @@
import type { AuthData } from "home-assistant-js-websocket";
import { extractSearchParam } from "../url/search-params";
import { hassUrl } from "../../data/auth";
declare global {
interface Window {
@@ -31,11 +30,7 @@ export function askWrite() {
export function saveTokens(tokens: AuthData | null) {
tokenCache.tokens = tokens;
if (
!tokenCache.writeEnabled &&
(extractSearchParam("storeToken") === "true" ||
hassUrl !== `${location.protocol}//${location.host}`)
) {
if (!tokenCache.writeEnabled && extractSearchParam("storeToken") === "true") {
tokenCache.writeEnabled = true;
}

View File

@@ -1,16 +1,64 @@
import memoizeOne from "memoize-one";
import { theme2hex } from "./convert-color";
// Total number of colors defined in CSS variables (--color-1 through --color-54)
export const COLORS_COUNT = 54;
export const COLORS = [
"#4269d0",
"#f4bd4a",
"#ff725c",
"#6cc5b0",
"#a463f2",
"#ff8ab7",
"#9c6b4e",
"#97bbf5",
"#01ab63",
"#094bad",
"#c99000",
"#d84f3e",
"#49a28f",
"#048732",
"#d96895",
"#8043ce",
"#7599d1",
"#7a4c31",
"#6989f4",
"#ffd444",
"#ff957c",
"#8fe9d3",
"#62cc71",
"#ffadda",
"#c884ff",
"#badeff",
"#bf8b6d",
"#927acc",
"#97ee3f",
"#bf3947",
"#9f5b00",
"#f48758",
"#8caed6",
"#f2b94f",
"#eff26e",
"#e43872",
"#d9b100",
"#9d7a00",
"#698cff",
"#00d27e",
"#d06800",
"#009f82",
"#c49200",
"#cbe8ff",
"#fecddf",
"#c27eb6",
"#8cd2ce",
"#c4b8d9",
"#f883b0",
"#a49100",
"#f48800",
"#27d0df",
"#a04a9b",
];
export function getColorByIndex(
index: number,
style: CSSStyleDeclaration
): string {
// Wrap around using modulo to support unlimited indices
const colorIndex = (index % COLORS_COUNT) + 1;
return style.getPropertyValue(`--color-${colorIndex}`);
export function getColorByIndex(index: number) {
return COLORS[index % COLORS.length];
}
export function getGraphColorByIndex(
@@ -20,19 +68,15 @@ export function getGraphColorByIndex(
// The CSS vars for the colors use range 1..n, so we need to adjust the index from the internal 0..n color index range.
const themeColor =
style.getPropertyValue(`--graph-color-${index + 1}`) ||
getColorByIndex(index, style);
getColorByIndex(index);
return theme2hex(themeColor);
}
export const getAllGraphColors = memoizeOne(
(style: CSSStyleDeclaration) =>
Array.from({ length: COLORS_COUNT }, (_, index) =>
getGraphColorByIndex(index, style)
),
COLORS.map((_color, index) => getGraphColorByIndex(index, style)),
(newArgs: [CSSStyleDeclaration], lastArgs: [CSSStyleDeclaration]) =>
// this is not ideal, but we need to memoize the colors
newArgs[0].getPropertyValue("--graph-color-1") ===
lastArgs[0].getPropertyValue("--graph-color-1") &&
newArgs[0].getPropertyValue("--color-1") ===
lastArgs[0].getPropertyValue("--color-1")
lastArgs[0].getPropertyValue("--graph-color-1")
);

View File

@@ -45,8 +45,9 @@ export const computeFormatFunctions = async (
formatEntityAttributeName: FormatEntityAttributeNameFunc;
formatEntityName: FormatEntityNameFunc;
}> => {
const { computeStateDisplay } =
await import("../entity/compute_state_display");
const { computeStateDisplay } = await import(
"../entity/compute_state_display"
);
const { computeAttributeValueDisplay, computeAttributeNameDisplay } =
await import("../entity/compute_attribute_display");

View File

@@ -593,7 +593,6 @@ export class HaChartBase extends LitElement {
}
const options = {
animation: !this._reducedMotion,
animationDuration: 500,
darkMode: this._themes.darkMode ?? false,
aria: { show: true },
dataZoom: this._getDataZoomConfig(),

View File

@@ -167,7 +167,6 @@ export class HaSankeyChart extends LitElement {
curveness: 0.5,
},
layoutIterations: 0,
animationDuration: 500,
label: {
formatter: (params) =>
data.nodes.find((node) => node.id === (params.data as Node).id)
@@ -280,7 +279,6 @@ export class HaSankeyChart extends LitElement {
:host {
display: block;
flex: 1;
max-width: 100%;
background: var(--ha-card-background, var(--card-background-color));
}
ha-chart-base {

View File

@@ -1,6 +1,6 @@
import type { PropertyValues } from "lit";
import { html, LitElement } from "lit";
import { customElement, property, state } from "lit/decorators";
import { property, state } from "lit/decorators";
import type { VisualMapComponentOption } from "echarts/components";
import type { LineSeriesOption } from "echarts/charts";
import type { YAXisOption } from "echarts/types/dist/shared";
@@ -27,7 +27,6 @@ const safeParseFloat = (value) => {
return isFinite(parsed) ? parsed : null;
};
@customElement("state-history-chart-line")
export class StateHistoryChartLine extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@@ -796,6 +795,7 @@ export class StateHistoryChartLine extends LitElement {
return Math.abs(value) < 1 ? value : roundingFn(value);
}
}
customElements.define("state-history-chart-line", StateHistoryChartLine);
declare global {
interface HTMLElementTagNameMap {

View File

@@ -373,7 +373,6 @@ export class StateHistoryChartTimeline extends LitElement {
itemName: 3,
},
renderItem: this._renderItem,
progressive: 0,
});
});

View File

@@ -838,10 +838,10 @@ export class HaDataTable extends LitElement {
} else if (this.sortDirection === "asc") {
this.sortDirection = "desc";
} else {
this.sortDirection = "asc";
this.sortDirection = null;
}
this.sortColumn = columnId;
this.sortColumn = this.sortDirection === null ? undefined : columnId;
fireEvent(this, "sorting-changed", {
column: columnId,

View File

@@ -2,7 +2,7 @@ import { mdiAlert } from "@mdi/js";
import type { HassEntity } from "home-assistant-js-websocket";
import type { CSSResultGroup, PropertyValues } from "lit";
import { LitElement, css, html, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { property, state } from "lit/decorators";
import { ifDefined } from "lit/directives/if-defined";
import { styleMap } from "lit/directives/style-map";
import { computeDomain } from "../../common/entity/compute_domain";
@@ -17,7 +17,6 @@ import { CLIMATE_HVAC_ACTION_TO_MODE } from "../../data/climate";
import type { HomeAssistant } from "../../types";
import "../ha-state-icon";
@customElement("state-badge")
export class StateBadge extends LitElement {
public hass?: HomeAssistant;
@@ -266,3 +265,5 @@ declare global {
"state-badge": StateBadge;
}
}
customElements.define("state-badge", StateBadge);

View File

@@ -4,6 +4,7 @@ import { LitElement, html } from "lit";
import { customElement, property } from "lit/decorators";
import { fireEvent } from "../common/dom/fire_event";
import { getAreaContext } from "../common/entity/context/get_area_context";
import { areaCompare } from "../data/area_registry";
import type { HomeAssistant } from "../types";
import "./ha-expansion-panel";
import "./ha-items-display-editor";
@@ -36,7 +37,11 @@ export class HaAreasDisplayEditor extends LitElement {
public showNavigationButton = false;
protected render(): TemplateResult {
const areas = Object.values(this.hass.areas);
const compare = areaCompare(this.hass.areas);
const areas = Object.values(this.hass.areas).sort((areaA, areaB) =>
compare(areaA.area_id, areaB.area_id)
);
const items: DisplayItem[] = areas.map((area) => {
const { floor } = getAreaContext(area, this.hass.floors);

View File

@@ -7,6 +7,7 @@ import memoizeOne from "memoize-one";
import { fireEvent } from "../common/dom/fire_event";
import { computeFloorName } from "../common/entity/compute_floor_name";
import { getAreaContext } from "../common/entity/context/get_area_context";
import { areaCompare } from "../data/area_registry";
import type { FloorRegistryEntry } from "../data/floor_registry";
import { getFloors } from "../panels/lovelace/strategies/areas/helpers/areas-strategy-helper";
import type { HomeAssistant } from "../types";
@@ -130,8 +131,11 @@ export class HaAreasFloorsDisplayEditor extends LitElement {
// update items if floors change
_hassFloors: HomeAssistant["floors"]
): Record<string, DisplayItem[]> => {
const areas = Object.values(hassAreas);
const compare = areaCompare(hassAreas);
const areas = Object.values(hassAreas).sort((areaA, areaB) =>
compare(areaA.area_id, areaB.area_id)
);
const groupedItems: Record<string, DisplayItem[]> = areas.reduce(
(acc, area) => {
const { floor } = getAreaContext(area, this.hass.floors);

View File

@@ -659,7 +659,6 @@ export class HaAssistChat extends LitElement {
--markdown-table-border-color: var(--divider-color);
--markdown-code-background-color: var(--primary-background-color);
--markdown-code-text-color: var(--primary-text-color);
--markdown-list-indent: 1rem;
&:not(:has(ha-markdown-element)) {
min-height: 1lh;
min-width: 1lh;

View File

@@ -21,8 +21,7 @@ export class HaBottomSheet extends LitElement {
private _isDragging = false;
private _handleAfterHide(afterHideEvent: Event) {
afterHideEvent.stopPropagation();
private _handleAfterHide() {
this.open = false;
const ev = new Event("closed", {
bubbles: true,

View File

@@ -202,7 +202,6 @@ export class HaControlSelect extends LitElement {
color: var(--primary-text-color);
user-select: none;
-webkit-tap-highlight-color: transparent;
border-radius: var(--control-select-border-radius);
}
:host([vertical]) {
width: var(--control-select-thickness);
@@ -212,6 +211,7 @@ export class HaControlSelect extends LitElement {
position: relative;
height: 100%;
width: 100%;
border-radius: var(--control-select-border-radius);
transform: translateZ(0);
display: flex;
flex-direction: row;

View File

@@ -167,33 +167,30 @@ export class HaFilterLabels extends SubscribeMixin(LitElement) {
}
private async _labelSelected(ev: CustomEvent<SelectedDetail<Set<number>>>) {
if (!ev.detail.index.size) {
fireEvent(this, "data-table-filter-changed", {
value: [],
items: undefined,
});
this.value = [];
return;
}
const value: string[] = [];
const filteredLabels = this._filteredLabels(
this._labels,
this._filter,
this.value
);
const filteredLabelIds = new Set(filteredLabels.map((l) => l.label_id));
// Keep previously selected labels that are not in the current filtered view
const preservedLabels = (this.value || []).filter(
(id) => !filteredLabelIds.has(id)
);
// Build the new selection from the filtered labels based on selected indices
const newlySelectedLabels: string[] = [];
for (const index of ev.detail.index) {
const labelId = filteredLabels[index]?.label_id;
if (labelId) {
newlySelectedLabels.push(labelId);
}
const labelId = filteredLabels[index].label_id;
value.push(labelId);
}
const value = [...preservedLabels, ...newlySelectedLabels];
this.value = value.length ? value : [];
this.value = value;
fireEvent(this, "data-table-filter-changed", {
value: value.length ? value : undefined,
value,
items: undefined,
});
}

View File

@@ -6,7 +6,7 @@ import {
mdiHomeFloor3,
mdiHomeFloorNegative1,
} from "@mdi/js";
import { LitElement, html, nothing } from "lit";
import { LitElement, html } from "lit";
import { customElement, property } from "lit/decorators";
import type { FloorRegistryEntry } from "../data/floor_registry";
import "./ha-icon";
@@ -48,7 +48,7 @@ export const floorDefaultIcon = (floor: Pick<FloorRegistryEntry, "level">) => {
@customElement("ha-floor-icon")
export class HaFloorIcon extends LitElement {
@property({ attribute: false }) public floor?: Pick<
@property({ attribute: false }) public floor!: Pick<
FloorRegistryEntry,
"icon" | "level"
>;
@@ -56,9 +56,6 @@ export class HaFloorIcon extends LitElement {
@property() public icon?: string;
protected render() {
if (!this.floor) {
return nothing;
}
if (this.floor.icon) {
return html`<ha-icon .icon=${this.floor.icon}></ha-icon>`;
}

View File

@@ -248,7 +248,7 @@ export class HaGenericPicker extends LitElement {
});
};
private _hidePicker(ev: Event) {
private _hidePicker(ev) {
ev.stopPropagation();
if (this._newValue) {
fireEvent(this, "value-changed", { value: this._newValue });

View File

@@ -154,10 +154,7 @@ export class HaLabelPicker extends SubscribeMixin(LitElement) {
}
return this._getLabelsMemoized(
this.hass.states,
this.hass.areas,
this.hass.devices,
this.hass.entities,
this.hass,
this._labels,
this.includeDomains,
this.excludeDomains,

View File

@@ -81,7 +81,6 @@ class HaLabel extends LitElement {
.container {
display: flex;
align-items: center;
position: relative;
height: 100%;
padding: 0 16px;

View File

@@ -73,8 +73,6 @@ export class HaLanguagePicker extends LitElement {
@property({ type: Boolean }) public required = false;
@property() public helper?: string;
@property({ attribute: "native-name", type: Boolean })
public nativeName = false;
@@ -137,7 +135,6 @@ export class HaLanguagePicker extends LitElement {
.value=${value}
.valueRenderer=${this._valueRenderer}
.disabled=${this.disabled}
.helper=${this.helper}
.getItems=${this._getItems}
@value-changed=${this._changed}
hide-clear-icon

View File

@@ -71,7 +71,7 @@ class HaMarkdownElement extends ReactiveElement {
if (!this.innerHTML && this.cache) {
const key = this._computeCacheKey();
if (markdownCache.has(key)) {
render(h(unsafeHTML(markdownCache.get(key))), this.renderRoot);
render(markdownCache.get(key)!, this.renderRoot);
this._resize();
}
}
@@ -99,7 +99,10 @@ class HaMarkdownElement extends ReactiveElement {
}
);
render(h(unsafeHTML(elements.join(""))), this.renderRoot);
render(
elements.map((e) => h(unsafeHTML(e))),
this.renderRoot
);
this._resize();

View File

@@ -25,11 +25,11 @@ export class HaMarkdown extends LitElement {
@property({ type: Boolean }) public cache = false;
@query("ha-markdown-element") private _markdownElement?: ReactiveElement;
@query("ha-markdown-element") private _markdownElement!: ReactiveElement;
protected async getUpdateComplete() {
const result = await super.getUpdateComplete();
await this._markdownElement?.updateComplete;
await this._markdownElement.updateComplete;
return result;
}
@@ -71,11 +71,13 @@ export class HaMarkdown extends LitElement {
color: var(--markdown-link-color, var(--primary-color));
}
img {
background-color: var(--markdown-image-background-color);
background-color: rgba(10, 10, 10, 0.15);
border-radius: var(--markdown-image-border-radius);
max-width: 100%;
min-height: 2lh;
height: auto;
width: auto;
text-indent: 4px;
transition: height 0.2s ease-in-out;
}
p:first-child > img:first-child {
@@ -84,9 +86,10 @@ export class HaMarkdown extends LitElement {
p:first-child > img:last-child {
vertical-align: top;
}
:host > ul,
:host > ol {
padding-inline-start: var(--markdown-list-indent, revert);
ol,
ul {
list-style-position: inside;
padding-inline-start: 0;
}
li {
&:has(input[type="checkbox"]) {
@@ -137,19 +140,16 @@ export class HaMarkdown extends LitElement {
margin: var(--ha-space-4) 0;
}
table {
border-collapse: var(--markdown-table-border-collapse, collapse);
}
div:has(> table) {
overflow: auto;
border-collapse: collapse;
display: block;
overflow-x: auto;
}
th {
text-align: start;
}
td,
th {
border-width: var(--markdown-table-border-width, 1px);
border-style: var(--markdown-table-border-style, solid);
border-color: var(--markdown-table-border-color, var(--divider-color));
border: 1px solid var(--markdown-table-border-color, transparent);
padding: 0.25em 0.5em;
}
blockquote {

View File

@@ -192,7 +192,7 @@ export class HaPickerComboBox extends LitElement {
@focus=${this._focusList}
@visibilityChanged=${this._visibilityChanged}
>
</lit-virtualizer>`;
</lit-virtualizer> `;
}
private _renderSectionButtons() {

View File

@@ -103,8 +103,8 @@ export class HaPickerField extends LitElement {
--md-list-item-two-line-container-height: 56px;
--md-list-item-top-space: 0px;
--md-list-item-bottom-space: 0px;
--md-list-item-leading-space: var(--ha-space-4);
--md-list-item-trailing-space: var(--ha-space-2);
--md-list-item-leading-space: 8px;
--md-list-item-trailing-space: 8px;
--ha-md-list-item-gap: var(--ha-space-2);
/* Remove the default focus ring */
--md-focus-ring-width: 0px;

View File

@@ -1,28 +0,0 @@
import { css, html, LitElement } from "lit";
import { customElement } from "lit/decorators";
@customElement("ha-section-title")
class HaSectionTitle extends LitElement {
protected render() {
return html`<slot></slot>`;
}
static styles = css`
:host {
background-color: var(--ha-color-fill-neutral-quiet-resting);
padding: var(--ha-space-1) var(--ha-space-2);
font-weight: var(--ha-font-weight-bold);
color: var(--secondary-text-color);
min-height: var(--ha-space-6);
display: flex;
align-items: center;
box-sizing: border-box;
}
`;
}
declare global {
interface HTMLElementTagNameMap {
"ha-section-title": HaSectionTitle;
}
}

View File

@@ -1,10 +1,13 @@
import type { PropertyValues } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property } from "lit/decorators";
import { customElement, property, state } from "lit/decorators";
import { classMap } from "lit/directives/class-map";
import memoizeOne from "memoize-one";
import { fireEvent } from "../../common/dom/fire_event";
import type { NumberSelector } from "../../data/selector";
import type { HomeAssistant } from "../../types";
import "../entity/ha-entity-picker";
import "../ha-button-toggle-group";
import "../ha-input-helper-text";
import "../ha-slider";
import "../ha-textfield";
@@ -15,7 +18,7 @@ export class HaNumberSelector extends LitElement {
@property({ attribute: false }) public selector!: NumberSelector;
@property({ type: Number }) public value?: number;
@property({ type: Number }) public value?: number | string;
@property({ type: Number }) public placeholder?: number;
@@ -30,13 +33,30 @@ export class HaNumberSelector extends LitElement {
@property({ type: Boolean }) public disabled = false;
@state() private _mode: "number" | "entity" = "number";
private _valueStr = "";
protected willUpdate(changedProps: PropertyValues) {
if (!this.hasUpdated) {
if (
this.selector.number?.entity?.domains.length &&
typeof this.value === "string" &&
this.selector.number?.entity?.domains.some((domain) =>
(this.value as string).startsWith(`${domain}.`)
)
) {
this._mode = "entity";
}
}
if (changedProps.has("value")) {
if (this._valueStr === "" || this.value !== Number(this._valueStr)) {
this._valueStr =
this.value == null || isNaN(this.value) ? "" : this.value.toString();
this.value == null ||
typeof this.value === "string" ||
isNaN(this.value)
? ""
: this.value.toString();
}
}
}
@@ -47,6 +67,8 @@ export class HaNumberSelector extends LitElement {
this.selector.number?.min === undefined ||
this.selector.number?.max === undefined;
const multiMode = Boolean(this.selector.number?.entity?.domains.length);
let sliderStep;
if (!isBox) {
@@ -72,51 +94,73 @@ export class HaNumberSelector extends LitElement {
}
return html`
${this.label && !isBox
${this.label && !isBox && !multiMode
? html`${this.label}${this.required ? "*" : ""}`
: nothing}
${multiMode
? html`<div class="multi-header">
<span>${this.label}${this.required ? "*" : ""}</span>
<ha-button-toggle-group
size="small"
.buttons=${this._toggleButtons(this.hass.localize)}
.active=${this._mode}
@value-changed=${this._modeChanged}
></ha-button-toggle-group>
</div>`
: nothing}
<div class="input">
${!isBox
? html`
<ha-slider
labeled
.min=${this.selector.number!.min}
.max=${this.selector.number!.max}
.value=${this.value}
.step=${sliderStep}
${multiMode && this._mode === "entity"
? html`<ha-entity-picker
.hass=${this.hass}
.includeDomains=${this.selector.number!.entity!.domains}
.value=${this.value}
.placeholder=${this.placeholder}
.helper=${this.helper}
.disabled=${this.disabled}
.required=${this.required}
></ha-entity-picker>`
: html`${!isBox
? html`
<ha-slider
labeled
.min=${this.selector.number!.min}
.max=${this.selector.number!.max}
.value=${this.value}
.step=${sliderStep}
.disabled=${this.disabled}
.required=${this.required}
@change=${this._handleSliderChange}
.withMarkers=${this.selector.number?.slider_ticks ||
false}
>
</ha-slider>
`
: nothing}
<ha-textfield
.inputMode=${this.selector.number?.step === "any" ||
(this.selector.number?.step ?? 1) % 1 !== 0
? "decimal"
: "numeric"}
.label=${!isBox ? undefined : this.label}
.placeholder=${this.placeholder}
class=${classMap({ single: isBox })}
.min=${this.selector.number?.min}
.max=${this.selector.number?.max}
.value=${this._valueStr ?? ""}
.step=${this.selector.number?.step ?? 1}
helperPersistent
.helper=${isBox ? this.helper : undefined}
.disabled=${this.disabled}
.required=${this.required}
@change=${this._handleSliderChange}
.withMarkers=${this.selector.number?.slider_ticks || false}
.suffix=${unit}
type="number"
autoValidate
?no-spinner=${!isBox}
@input=${this._handleInputChange}
>
</ha-slider>
`
: nothing}
<ha-textfield
.inputMode=${this.selector.number?.step === "any" ||
(this.selector.number?.step ?? 1) % 1 !== 0
? "decimal"
: "numeric"}
.label=${!isBox ? undefined : this.label}
.placeholder=${this.placeholder}
class=${classMap({ single: isBox })}
.min=${this.selector.number?.min}
.max=${this.selector.number?.max}
.value=${this._valueStr ?? ""}
.step=${this.selector.number?.step ?? 1}
helperPersistent
.helper=${isBox ? this.helper : undefined}
.disabled=${this.disabled}
.required=${this.required}
.suffix=${unit}
type="number"
autoValidate
?no-spinner=${!isBox}
@input=${this._handleInputChange}
>
</ha-textfield>
</ha-textfield>`}
</div>
${!isBox && this.helper
${!isBox && !(multiMode && this._mode === "entity") && this.helper
? html`<ha-input-helper-text .disabled=${this.disabled}
>${this.helper}</ha-input-helper-text
>`
@@ -124,6 +168,22 @@ export class HaNumberSelector extends LitElement {
`;
}
private _toggleButtons = memoizeOne((localize: HomeAssistant["localize"]) => [
{
label: localize("ui.components.selectors.number.value"),
value: "number",
},
{
label: localize("ui.components.selectors.number.entity_value"),
value: "entity",
},
]);
private _modeChanged(ev) {
ev.stopPropagation();
this._mode = ev.detail?.value || ev.target.value;
}
private _handleInputChange(ev) {
ev.stopPropagation();
this._valueStr = ev.target.value;
@@ -155,17 +215,32 @@ export class HaNumberSelector extends LitElement {
}
ha-slider {
flex: 1;
margin-right: 16px;
margin-inline-end: 16px;
margin-right: var(--ha-space-4);
margin-inline-end: var(--ha-space-4);
margin-inline-start: 0;
}
ha-textfield {
--ha-textfield-input-width: 40px;
}
.multi-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: var(--ha-space-2);
}
.single {
--ha-textfield-input-width: unset;
flex: 1;
}
ha-entity-picker {
display: block;
width: 100%;
}
ha-button-toggle-group {
display: block;
justify-self: end;
}
`;
}

View File

@@ -450,7 +450,7 @@ export class HaServiceControl extends LitElement {
const hasOptional = Boolean(
!shouldRenderServiceDataYaml &&
serviceData?.flatFields.some((field) => showOptionalToggle(field))
serviceData?.flatFields.some((field) => showOptionalToggle(field))
);
const targetEntities = this._getTargetedEntities(
@@ -465,16 +465,10 @@ export class HaServiceControl extends LitElement {
? computeObjectId(this._value.action)
: undefined;
const descriptionPlaceholders =
domain && serviceName
? this.hass.services[domain]?.[serviceName]?.description_placeholders
: undefined;
const description =
(serviceName &&
this.hass.localize(
`component.${domain}.services.${serviceName}.description`,
descriptionPlaceholders
`component.${domain}.services.${serviceName}.description`
)) ||
serviceData?.description;
@@ -543,8 +537,7 @@ export class HaServiceControl extends LitElement {
.disabled=${this.disabled}
.value=${this._value?.data?.entity_id}
.label=${this.hass.localize(
`component.${domain}.services.${serviceName}.fields.entity_id.description`,
descriptionPlaceholders
`component.${domain}.services.${serviceName}.fields.entity_id.description`
) || entityId.description}
@value-changed=${this._entityPicked}
allow-custom-entity
@@ -582,8 +575,7 @@ export class HaServiceControl extends LitElement {
left-chevron
.expanded=${!dataField.collapsed}
.header=${this.hass.localize(
`component.${domain}.services.${serviceName}.sections.${dataField.key}.name`,
descriptionPlaceholders
`component.${domain}.services.${serviceName}.sections.${dataField.key}.name`
) ||
dataField.name ||
dataField.key}
@@ -619,10 +611,7 @@ export class HaServiceControl extends LitElement {
serviceName: string | undefined
) {
return this.hass!.localize(
`component.${domain}.services.${serviceName}.sections.${dataField.key}.description`,
domain && serviceName
? this.hass.services[domain][serviceName].description_placeholders
: undefined
`component.${domain}.services.${serviceName}.sections.${dataField.key}.description`
);
}
@@ -669,10 +658,6 @@ export class HaServiceControl extends LitElement {
}
const showOptional = showOptionalToggle(dataField);
const descriptionPlaceholders =
domain && serviceName
? this.hass.services[domain][serviceName].description_placeholders
: undefined;
return dataField.selector &&
(!dataField.advanced ||
@@ -694,8 +679,7 @@ export class HaServiceControl extends LitElement {
></ha-checkbox>`}
<span slot="heading"
>${this.hass.localize(
`component.${domain}.services.${serviceName}.fields.${dataField.key}.name`,
descriptionPlaceholders
`component.${domain}.services.${serviceName}.fields.${dataField.key}.name`
) ||
dataField.name ||
dataField.key}</span
@@ -705,8 +689,7 @@ export class HaServiceControl extends LitElement {
breaks
allow-svg
.content=${this.hass.localize(
`component.${domain}.services.${serviceName}.fields.${dataField.key}.description`,
descriptionPlaceholders
`component.${domain}.services.${serviceName}.fields.${dataField.key}.description`
) || dataField?.description}
></ha-markdown>
</span>

View File

@@ -92,14 +92,8 @@ class HaServicePicker extends LitElement {
`;
}
const descriptionPlaceholders =
this.hass.services[domain][service].description_placeholders;
const serviceName =
localize(
`component.${domain}.services.${service}.name`,
descriptionPlaceholders
) ||
localize(`component.${domain}.services.${service}.name`) ||
services[domain][service].name ||
service;
@@ -169,21 +163,16 @@ class HaServicePicker extends LitElement {
const serviceId = `${domain}.${service}`;
const domainName = domainToName(localize, domain);
const descriptionPlaceholders =
this.hass.services[domain][service].description_placeholders;
const name =
this.hass.localize(
`component.${domain}.services.${service}.name`,
descriptionPlaceholders
`component.${domain}.services.${service}.name`
) ||
services[domain][service].name ||
service;
const description =
this.hass.localize(
`component.${domain}.services.${service}.description`,
descriptionPlaceholders
`component.${domain}.services.${service}.description`
) ||
services[domain][service].description ||
"";

View File

@@ -197,8 +197,6 @@ class HaSidebar extends SubscribeMixin(LitElement) {
private _mouseLeaveTimeout?: number;
private _touchendTimeout?: number;
private _tooltipHideTimeout?: number;
private _recentKeydownActiveUntil = 0;
@@ -239,18 +237,6 @@ class HaSidebar extends SubscribeMixin(LitElement) {
];
}
public disconnectedCallback() {
super.disconnectedCallback();
// clear timeouts
clearTimeout(this._mouseLeaveTimeout);
clearTimeout(this._tooltipHideTimeout);
clearTimeout(this._touchendTimeout);
// set undefined values
this._mouseLeaveTimeout = undefined;
this._tooltipHideTimeout = undefined;
this._touchendTimeout = undefined;
}
protected render() {
if (!this.hass) {
return nothing;
@@ -420,7 +406,6 @@ class HaSidebar extends SubscribeMixin(LitElement) {
class="ha-scrollbar"
@focusin=${this._listboxFocusIn}
@focusout=${this._listboxFocusOut}
@touchend=${this._listboxTouchend}
@scroll=${this._listboxScroll}
@keydown=${this._listboxKeydown}
>
@@ -635,14 +620,6 @@ class HaSidebar extends SubscribeMixin(LitElement) {
this._hideTooltip();
}
private _listboxTouchend() {
clearTimeout(this._touchendTimeout);
this._touchendTimeout = window.setTimeout(() => {
// Allow 1 second for users to read the tooltip on touch devices
this._hideTooltip();
}, 1000);
}
@eventOptions({
passive: true,
})

File diff suppressed because one or more lines are too long

View File

@@ -30,7 +30,6 @@ import {
areaMeetsFilter,
deviceMeetsFilter,
entityRegMeetsFilter,
getTargetComboBoxItemType,
type TargetType,
type TargetTypeFloorless,
} from "../data/target";
@@ -48,6 +47,7 @@ import "./ha-tree-indicator";
import "./target-picker/ha-target-picker-item-group";
import "./target-picker/ha-target-picker-value-chip";
const EMPTY_SEARCH = "___EMPTY_SEARCH___";
const SEPARATOR = "________";
const CREATE_ID = "___create-new-entity___";
@@ -634,6 +634,35 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
return undefined;
}
private _getRowType = (
item:
| PickerComboBoxItem
| (FloorComboBoxItem & { last?: boolean | undefined })
| EntityComboBoxItem
| DevicePickerItem
) => {
if (
(item as FloorComboBoxItem).type === "area" ||
(item as FloorComboBoxItem).type === "floor"
) {
return (item as FloorComboBoxItem).type;
}
if ("domain" in item) {
return "device";
}
if ("stateObj" in item) {
return "entity";
}
if (item.id === EMPTY_SEARCH) {
return "empty";
}
return "label";
};
private _sectionTitleFunction = ({
firstIndex,
lastIndex,
@@ -657,7 +686,7 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
return undefined;
}
const type = getTargetComboBoxItemType(firstItem as PickerComboBoxItem);
const type = this._getRowType(firstItem as PickerComboBoxItem);
const translationType:
| "areas"
| "entities"
@@ -829,10 +858,7 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
if (!filterType || filterType === "label") {
let labels = this._getLabelsMemoized(
this.hass.states,
this.hass.areas,
this.hass.devices,
this.hass.entities,
this.hass,
this._labelRegistry,
includeDomains,
undefined,
@@ -948,7 +974,7 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
return nothing;
}
const type = getTargetComboBoxItemType(item);
const type = this._getRowType(item);
let hasFloor = false;
let rtl = false;
let showEntityId = false;

View File

@@ -6,6 +6,7 @@ import {
mdiDevices,
mdiFormatListBulleted,
mdiGestureDoubleTap,
mdiHomeAssistant,
mdiMapMarker,
mdiMapMarkerRadius,
mdiMessageAlert,
@@ -22,7 +23,6 @@ import { customElement, property } from "lit/decorators";
import { until } from "lit/directives/until";
import { computeDomain } from "../common/entity/compute_domain";
import { FALLBACK_DOMAIN_ICONS, triggerIcon } from "../data/icons";
import { mdiHomeAssistant } from "../resources/home-assistant-logo-svg";
import type { HomeAssistant } from "../types";
import "./ha-icon";
import "./ha-svg-icon";

View File

@@ -1,5 +1,3 @@
import "@home-assistant/webawesome/dist/components/dialog/dialog";
import { mdiClose } from "@mdi/js";
import { css, html, LitElement } from "lit";
import {
customElement,
@@ -9,6 +7,8 @@ import {
state,
} from "lit/decorators";
import { ifDefined } from "lit/directives/if-defined";
import { mdiClose } from "@mdi/js";
import "@home-assistant/webawesome/dist/components/dialog/dialog";
import { fireEvent } from "../common/dom/fire_event";
import { haStyleScrollbar } from "../resources/styles";
import type { HomeAssistant } from "../types";
@@ -49,10 +49,10 @@ export type DialogWidth = "small" | "medium" | "large" | "full";
* @cssprop --ha-dialog-surface-background - Dialog background color.
* @cssprop --ha-dialog-border-radius - Border radius of the dialog surface.
* @cssprop --dialog-z-index - Z-index for the dialog.
* @cssprop --dialog-surface-position - CSS position of the dialog surface.
* @cssprop --dialog-surface-margin-top - Top margin for the dialog surface.
*
* @attr {boolean} open - Controls the dialog open state.
* @attr {("alert"|"standard")} type - Dialog type. Defaults to "standard".
* @attr {("small"|"medium"|"large"|"full")} width - Preferred dialog width preset. Defaults to "medium".
* @attr {boolean} prevent-scrim-close - Prevents closing the dialog by clicking the scrim/overlay. Defaults to false.
* @attr {string} header-title - Header title text. If not set, the headerTitle slot is used.
@@ -84,9 +84,6 @@ export class HaWaDialog extends LitElement {
@property({ type: Boolean, reflect: true })
public open = false;
@property({ reflect: true })
public type: "alert" | "standard" = "standard";
@property({ type: String, reflect: true, attribute: "width" })
public width: DialogWidth = "medium";
@@ -175,9 +172,7 @@ export class HaWaDialog extends LitElement {
await this.updateComplete;
requestAnimationFrame(() => {
(this.querySelector("[autofocus]") as HTMLElement | null)?.focus();
});
(this.querySelector("[autofocus]") as HTMLElement | null)?.focus();
};
private _handleAfterShow = () => {
@@ -203,7 +198,18 @@ export class HaWaDialog extends LitElement {
haStyleScrollbar,
css`
wa-dialog {
--full-width: var(--ha-dialog-width-full, min(95vw, var(--safe-width)));
--full-width: var(
--ha-dialog-width-full,
min(
95vw,
calc(
100vw - var(--safe-area-inset-left, var(--ha-space-0)) - var(
--safe-area-inset-right,
var(--ha-space-0)
)
)
)
);
--width: min(var(--ha-dialog-width-md, 580px), var(--full-width));
--spacing: var(--dialog-content-padding, var(--ha-space-6));
--show-duration: var(--ha-dialog-show-duration, 200ms);
@@ -220,7 +226,8 @@ export class HaWaDialog extends LitElement {
--ha-dialog-border-radius,
var(--ha-border-radius-3xl)
);
max-width: var(--ha-dialog-max-width, var(--safe-width));
max-width: var(--ha-dialog-max-width, 100vw);
max-width: var(--ha-dialog-max-width, 100svw);
}
:host([width="small"]) wa-dialog {
@@ -228,7 +235,7 @@ export class HaWaDialog extends LitElement {
}
:host([width="large"]) wa-dialog {
--width: min(var(--ha-dialog-width-lg, 1024px), var(--full-width));
--width: min(var(--ha-dialog-width-lg, 720px), var(--full-width));
}
:host([width="full"]) wa-dialog {
@@ -240,56 +247,34 @@ export class HaWaDialog extends LitElement {
max-width: var(--width, var(--full-width));
max-height: var(
--ha-dialog-max-height,
calc(var(--safe-height) - var(--ha-space-20))
calc(100% - var(--ha-space-20))
);
min-height: var(--ha-dialog-min-height);
position: var(--dialog-surface-position, relative);
margin-top: var(--dialog-surface-margin-top, auto);
/* Used to offset the dialog from the safe areas when space is limited */
transform: translate(
calc(
var(--safe-area-offset-left, var(--ha-space-0)) - var(
--safe-area-offset-right,
var(--ha-space-0)
)
),
calc(
var(--safe-area-offset-top, var(--ha-space-0)) - var(
--safe-area-offset-bottom,
var(--ha-space-0)
)
)
);
display: flex;
flex-direction: column;
overflow: hidden;
}
@media all and (max-width: 450px), all and (max-height: 500px) {
:host([type="standard"]) {
:host {
--ha-dialog-border-radius: var(--ha-space-0);
}
wa-dialog {
/* Make the container fill the whole screen width and not the safe width */
--full-width: var(--ha-dialog-width-full, 100vw);
--width: var(--full-width);
}
wa-dialog {
--full-width: var(--ha-dialog-width-full, 100vw);
}
wa-dialog::part(dialog) {
/* Make the dialog fill the whole screen height and not the safe height */
min-height: var(--ha-dialog-min-height, 100vh);
min-height: var(--ha-dialog-min-height, 100dvh);
max-height: var(--ha-dialog-max-height, 100vh);
max-height: var(--ha-dialog-max-height, 100dvh);
margin-top: 0;
margin-bottom: 0;
/* Use safe area as padding instead of the container size */
padding-top: var(--safe-area-inset-top);
padding-bottom: var(--safe-area-inset-bottom);
padding-left: var(--safe-area-inset-left);
padding-right: var(--safe-area-inset-right);
/* Reset the transform to center the dialog */
transform: none;
}
wa-dialog::part(dialog) {
min-height: var(--ha-dialog-min-height, 100vh);
min-height: var(--ha-dialog-min-height, 100svh);
max-height: var(--ha-dialog-max-height, 100vh);
max-height: var(--ha-dialog-max-height, 100svh);
padding-top: var(--safe-area-inset-top, var(--ha-space-0));
padding-bottom: var(--safe-area-inset-bottom, var(--ha-space-0));
padding-left: var(--safe-area-inset-left, var(--ha-space-0));
padding-right: var(--safe-area-inset-right, var(--ha-space-0));
}
}

View File

@@ -1,11 +1,10 @@
import { LitElement, html, css } from "lit";
import { customElement, property } from "lit/decorators";
import { property } from "lit/decorators";
import { styleMap } from "lit/directives/style-map";
import type { HomeAssistant } from "../../types";
import { fireEvent } from "../../common/dom/fire_event";
import "../ha-state-icon";
@customElement("ha-entity-marker")
class HaEntityMarker extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@@ -90,6 +89,8 @@ class HaEntityMarker extends LitElement {
`;
}
customElements.define("ha-entity-marker", HaEntityMarker);
declare global {
interface HTMLElementTagNameMap {
"ha-entity-marker": HaEntityMarker;

View File

@@ -21,52 +21,11 @@ export interface FloorComboBoxItem extends PickerComboBoxItem {
area?: AreaRegistryEntry;
}
export interface FloorNestedComboBoxItem extends PickerComboBoxItem {
floor?: FloorRegistryEntry;
areas: FloorComboBoxItem[];
}
export interface UnassignedAreasFloorComboBoxItem extends PickerComboBoxItem {
areas: FloorComboBoxItem[];
}
export interface AreaFloorValue {
id: string;
type: "floor" | "area";
}
export const getAreasNestedInFloors = (
states: HomeAssistant["states"],
haFloors: HomeAssistant["floors"],
haAreas: HomeAssistant["areas"],
haDevices: HomeAssistant["devices"],
haEntities: HomeAssistant["entities"],
formatId: (value: AreaFloorValue) => string,
includeDomains?: string[],
excludeDomains?: string[],
includeDeviceClasses?: string[],
deviceFilter?: HaDevicePickerDeviceFilterFunc,
entityFilter?: HaEntityPickerEntityFilterFunc,
excludeAreas?: string[],
excludeFloors?: string[]
) =>
getAreasAndFloorsItems(
states,
haFloors,
haAreas,
haDevices,
haEntities,
formatId,
includeDomains,
excludeDomains,
includeDeviceClasses,
deviceFilter,
entityFilter,
excludeAreas,
excludeFloors,
true
) as (FloorNestedComboBoxItem | UnassignedAreasFloorComboBoxItem)[];
export const getAreasAndFloors = (
states: HomeAssistant["states"],
haFloors: HomeAssistant["floors"],
@@ -81,43 +40,7 @@ export const getAreasAndFloors = (
entityFilter?: HaEntityPickerEntityFilterFunc,
excludeAreas?: string[],
excludeFloors?: string[]
) =>
getAreasAndFloorsItems(
states,
haFloors,
haAreas,
haDevices,
haEntities,
formatId,
includeDomains,
excludeDomains,
includeDeviceClasses,
deviceFilter,
entityFilter,
excludeAreas,
excludeFloors
) as FloorComboBoxItem[];
const getAreasAndFloorsItems = (
states: HomeAssistant["states"],
haFloors: HomeAssistant["floors"],
haAreas: HomeAssistant["areas"],
haDevices: HomeAssistant["devices"],
haEntities: HomeAssistant["entities"],
formatId: (value: AreaFloorValue) => string,
includeDomains?: string[],
excludeDomains?: string[],
includeDeviceClasses?: string[],
deviceFilter?: HaDevicePickerDeviceFilterFunc,
entityFilter?: HaEntityPickerEntityFilterFunc,
excludeAreas?: string[],
excludeFloors?: string[],
nested = false
): (
| FloorComboBoxItem
| FloorNestedComboBoxItem
| UnassignedAreasFloorComboBoxItem
)[] => {
): FloorComboBoxItem[] => {
const floors = Object.values(haFloors);
const areas = Object.values(haAreas);
const devices = Object.values(haDevices);
@@ -223,7 +146,6 @@ const getAreasAndFloorsItems = (
}
let outputAreas = areas;
let outputFloors = floors;
let areaIds: string[] | undefined;
@@ -255,35 +177,11 @@ const getAreasAndFloorsItems = (
outputAreas = outputAreas.filter(
(area) => !area.floor_id || !excludeFloors!.includes(area.floor_id)
);
outputFloors = outputFloors.filter(
(floor) => !excludeFloors!.includes(floor.floor_id)
);
}
if (
entityFilter ||
deviceFilter ||
includeDomains ||
excludeDomains ||
includeDeviceClasses
) {
// Ensure we only include floors that have areas with the filtered entities/devices
const validFloorIds = new Set(
outputAreas.map((area) => area.floor_id).filter((id) => id)
);
outputFloors = outputFloors.filter((floor) =>
validFloorIds.has(floor.floor_id)
);
}
const hierarchy = getAreasFloorHierarchy(floors, outputAreas);
const hierarchy = getAreasFloorHierarchy(outputFloors, outputAreas);
const items: (
| FloorComboBoxItem
| FloorNestedComboBoxItem
| UnassignedAreasFloorComboBoxItem
)[] = [];
const items: FloorComboBoxItem[] = [];
hierarchy.floors.forEach((f) => {
const floor = haFloors[f.id];
@@ -298,7 +196,7 @@ const getAreasAndFloorsItems = (
})
.flat();
const floorItem: FloorComboBoxItem | FloorNestedComboBoxItem = {
items.push({
id: formatId({ id: floor.floor_id, type: "floor" }),
type: "floor",
primary: floorName,
@@ -310,53 +208,41 @@ const getAreasAndFloorsItems = (
...floor.aliases,
...areaSearchLabels,
],
};
});
items.push(floorItem);
items.push(
...floorAreas.map((area) => {
const areaName = computeAreaName(area);
return {
id: formatId({ id: area.area_id, type: "area" }),
type: "area" as const,
primary: areaName || area.area_id,
area: area,
icon: area.icon || undefined,
search_labels: [
area.area_id,
...(areaName ? [areaName] : []),
...area.aliases,
],
};
})
);
});
const floorAreasItems = floorAreas.map((area) => {
const areaName = computeAreaName(area);
items.push(
...hierarchy.areas.map((areaId) => {
const area = haAreas[areaId];
const areaName = computeAreaName(area) || area.area_id;
return {
id: formatId({ id: area.area_id, type: "area" }),
type: "area" as const,
primary: areaName || area.area_id,
primary: areaName,
area: area,
icon: area.icon || undefined,
search_labels: [
area.area_id,
...(areaName ? [areaName] : []),
...area.aliases,
],
search_labels: [area.area_id, areaName, ...area.aliases],
};
});
if (nested && floor) {
(floorItem as unknown as FloorNestedComboBoxItem).areas = floorAreasItems;
} else {
items.push(...floorAreasItems);
}
});
const unassignedAreaItems = hierarchy.areas.map((areaId) => {
const area = haAreas[areaId];
const areaName = computeAreaName(area) || area.area_id;
return {
id: formatId({ id: area.area_id, type: "area" }),
type: "area" as const,
primary: areaName,
area: area,
icon: area.icon || undefined,
search_labels: [area.area_id, areaName, ...area.aliases],
};
});
if (nested && unassignedAreaItems.length) {
items.push({
areas: unassignedAreaItems,
} as UnassignedAreasFloorComboBoxItem);
} else {
items.push(...unassignedAreaItems);
}
})
);
return items;
};

View File

@@ -1,9 +1,7 @@
import { stringCompare } from "../common/string/compare";
import type { HomeAssistant } from "../types";
import type { DeviceRegistryEntry } from "./device_registry";
import type {
EntityRegistryDisplayEntry,
EntityRegistryEntry,
} from "./entity_registry";
import type { EntityRegistryEntry } from "./entity_registry";
import type { RegistryEntry } from "./registry";
export { subscribeAreaRegistry } from "./ws-area_registry";
@@ -20,10 +18,7 @@ export interface AreaRegistryEntry extends RegistryEntry {
temperature_entity_id: string | null;
}
export type AreaEntityLookup = Record<
string,
(EntityRegistryEntry | EntityRegistryDisplayEntry)[]
>;
export type AreaEntityLookup = Record<string, EntityRegistryEntry[]>;
export type AreaDeviceLookup = Record<string, DeviceRegistryEntry[]>;
@@ -74,7 +69,7 @@ export const reorderAreaRegistryEntries = (
});
export const getAreaEntityLookup = (
entities: (EntityRegistryEntry | EntityRegistryDisplayEntry)[]
entities: EntityRegistryEntry[]
): AreaEntityLookup => {
const areaEntityLookup: AreaEntityLookup = {};
for (const entity of entities) {
@@ -104,3 +99,22 @@ export const getAreaDeviceLookup = (
}
return areaDeviceLookup;
};
export const areaCompare =
(entries?: HomeAssistant["areas"], order?: string[]) =>
(a: string, b: string) => {
const indexA = order ? order.indexOf(a) : -1;
const indexB = order ? order.indexOf(b) : -1;
if (indexA === -1 && indexB === -1) {
const nameA = entries?.[a]?.name ?? a;
const nameB = entries?.[b]?.name ?? b;
return stringCompare(nameA, nameB);
}
if (indexA === -1) {
return 1;
}
if (indexB === -1) {
return -1;
}
return indexA - indexB;
};

View File

@@ -114,6 +114,12 @@ export interface StateTrigger extends BaseTrigger {
for?: string | number | ForDict;
}
export interface MqttTrigger extends BaseTrigger {
trigger: "mqtt";
topic: string;
payload?: string;
}
export interface GeoLocationTrigger extends BaseTrigger {
trigger: "geo_location";
source: string;
@@ -121,12 +127,6 @@ export interface GeoLocationTrigger extends BaseTrigger {
event: "enter" | "leave";
}
export interface MqttTrigger extends BaseTrigger {
trigger: "mqtt";
topic: string;
payload?: string;
}
export interface HassTrigger extends BaseTrigger {
trigger: "homeassistant";
event: "start" | "shutdown";

View File

@@ -144,7 +144,9 @@ const tryDescribeTrigger = (
const type = getTriggerObjectId(trigger.trigger);
return (
hass.localize(`component.${domain}.triggers.${type}.name`) ||
hass.localize(
`component.${domain}.triggers.${type}.description_configured`
) ||
hass.localize(
`ui.panel.config.automation.editor.triggers.type.${triggerType as LegacyTrigger["trigger"]}.label`
) ||
@@ -917,7 +919,9 @@ const tryDescribeCondition = (
const type = getConditionObjectId(condition.condition);
return (
hass.localize(`component.${domain}.conditions.${type}.name`) ||
hass.localize(
`component.${domain}.conditions.${type}.description_configured`
) ||
hass.localize(
`ui.panel.config.automation.editor.conditions.type.${conditionType as LegacyCondition["condition"]}.label`
) ||

View File

@@ -137,12 +137,8 @@ const getCalendarDate = (dateObj: any): string | undefined => {
return undefined;
};
export const getCalendars = (
hass: HomeAssistant,
element: Element
): Calendar[] => {
const computedStyles = getComputedStyle(element);
return Object.keys(hass.states)
export const getCalendars = (hass: HomeAssistant): Calendar[] =>
Object.keys(hass.states)
.filter(
(eid) =>
computeDomain(eid) === "calendar" &&
@@ -153,9 +149,8 @@ export const getCalendars = (
.map((eid, idx) => ({
...hass.states[eid],
name: computeStateName(hass.states[eid]),
backgroundColor: getColorByIndex(idx, computedStyles),
backgroundColor: getColorByIndex(idx),
}));
};
export const createCalendarEvent = (
hass: HomeAssistant,

View File

@@ -17,19 +17,11 @@ export const CONDITION_COLLECTIONS: AutomationElementGroupCollection[] = [
},
},
},
{
titleKey:
"ui.panel.config.automation.editor.conditions.groups.helpers.label",
groups: {
helpers: {},
},
},
{
titleKey: "ui.panel.config.automation.editor.conditions.groups.other.label",
groups: {
template: {},
trigger: {},
other: {},
},
},
] as const;

View File

@@ -50,11 +50,7 @@ export type DeviceEntityDisplayLookup = Record<
EntityRegistryDisplayEntry[]
>;
export type DeviceEntityLookup<
T extends EntityRegistryEntry | EntityRegistryDisplayEntry =
| EntityRegistryEntry
| EntityRegistryDisplayEntry,
> = Record<string, T[]>;
export type DeviceEntityLookup = Record<string, EntityRegistryEntry[]>;
export interface DeviceRegistryEntryMutableParams {
area_id?: string | null;
@@ -111,7 +107,7 @@ export const sortDeviceRegistryByName = (
);
export const getDeviceEntityLookup = (
entities: (EntityRegistryEntry | EntityRegistryDisplayEntry)[]
entities: EntityRegistryEntry[]
): DeviceEntityLookup => {
const deviceEntityLookup: DeviceEntityLookup = {};
for (const entity of entities) {

View File

@@ -11,7 +11,7 @@ import {
isLastDayOfMonth,
addYears,
} from "date-fns";
import type { Collection, HassEntity } from "home-assistant-js-websocket";
import type { Collection } from "home-assistant-js-websocket";
import { getCollection } from "home-assistant-js-websocket";
import memoizeOne from "memoize-one";
import {
@@ -1361,37 +1361,3 @@ export const calculateSolarConsumedGauge = (
}
return undefined;
};
/**
* Get current power value from entity state, normalized to kW
* @param stateObj - The entity state object to get power value from
* @returns Power value in kW, or 0 if entity not found or invalid
*/
export const getPowerFromState = (stateObj: HassEntity): number | undefined => {
if (!stateObj) {
return undefined;
}
const value = parseFloat(stateObj.state);
if (isNaN(value)) {
return undefined;
}
// Normalize to kW based on unit of measurement (case-sensitive)
// Supported units: GW, kW, MW, mW, TW, W
const unit = stateObj.attributes.unit_of_measurement;
switch (unit) {
case "W":
return value / 1000;
case "mW":
return value / 1000000;
case "MW":
return value * 1000;
case "GW":
return value * 1000000;
case "TW":
return value * 1000000000;
default:
// Assume kW if no unit or unit is kW
return value;
}
};

View File

@@ -1,14 +0,0 @@
import type { HomeAssistant } from "../types";
export interface ESPHomeEncryptionKey {
encryption_key: string;
}
export const fetchESPHomeEncryptionKey = (
hass: HomeAssistant,
entry_id: string
): Promise<ESPHomeEncryptionKey> =>
hass.callWS({
type: "esphome/get_encryption_key",
entry_id,
});

View File

@@ -1,3 +1,4 @@
import { stringCompare } from "../common/string/compare";
import type { HomeAssistant } from "../types";
import type { AreaRegistryEntry } from "./area_registry";
import type { RegistryEntry } from "./registry";
@@ -74,3 +75,27 @@ export const getFloorAreaLookup = (
}
return floorAreaLookup;
};
export const floorCompare =
(entries?: HomeAssistant["floors"], order?: string[]) =>
(a: string, b: string) => {
const indexA = order ? order.indexOf(a) : -1;
const indexB = order ? order.indexOf(b) : -1;
if (indexA === -1 && indexB === -1) {
const floorA = entries?.[a];
const floorB = entries?.[b];
if (floorA && floorB && floorA.level !== floorB.level) {
return (floorB.level ?? -9999) - (floorA.level ?? -9999);
}
const nameA = floorA?.name ?? a;
const nameB = floorB?.name ?? b;
return stringCompare(nameA, nameB);
}
if (indexA === -1) {
return 1;
}
if (indexB === -1) {
return -1;
}
return indexA - indexB;
};

View File

@@ -47,7 +47,8 @@ export interface HassioFullBackupCreateParams {
confirm_password?: string;
background?: boolean;
}
export interface HassioPartialBackupCreateParams extends HassioFullBackupCreateParams {
export interface HassioPartialBackupCreateParams
extends HassioFullBackupCreateParams {
folders?: string[];
addons?: string[];
homeassistant?: boolean;

View File

@@ -1,8 +1,8 @@
import type { Connection } from "home-assistant-js-websocket";
import { createCollection } from "home-assistant-js-websocket";
import type { LocalizeFunc } from "../common/translations/localize";
import { debounce } from "../common/util/debounce";
import type { HomeAssistant } from "../types";
import { debounce } from "../common/util/debounce";
export const integrationsWithPanel = {
bluetooth: "config/bluetooth",
@@ -25,8 +25,6 @@ export type IntegrationType =
| "entity"
| "system";
export type DomainManifestLookup = Record<string, IntegrationManifest>;
export interface IntegrationManifest {
is_built_in: boolean;
overwrites_built_in?: boolean;

View File

@@ -101,10 +101,7 @@ export const deleteLabelRegistryEntry = (
});
export const getLabels = (
hassStates: HomeAssistant["states"],
hassAreas: HomeAssistant["areas"],
hassDevices: HomeAssistant["devices"],
hassEntities: HomeAssistant["entities"],
hass: HomeAssistant,
labels?: LabelRegistryEntry[],
includeDomains?: string[],
excludeDomains?: string[],
@@ -118,8 +115,8 @@ export const getLabels = (
return [];
}
const devices = Object.values(hassDevices);
const entities = Object.values(hassEntities);
const devices = Object.values(hass.devices);
const entities = Object.values(hass.entities);
let deviceEntityLookup: DeviceEntityDisplayLookup = {};
let inputDevices: DeviceRegistryEntry[] | undefined;
@@ -173,7 +170,7 @@ export const getLabels = (
return false;
}
return deviceEntityLookup[device.id].some((entity) => {
const stateObj = hassStates[entity.entity_id];
const stateObj = hass.states[entity.entity_id];
if (!stateObj) {
return false;
}
@@ -184,9 +181,8 @@ export const getLabels = (
});
});
inputEntities = inputEntities!.filter((entity) => {
const stateObj = hassStates[entity.entity_id];
const stateObj = hass.states[entity.entity_id];
return (
stateObj &&
stateObj.attributes.device_class &&
includeDeviceClasses.includes(stateObj.attributes.device_class)
);
@@ -204,7 +200,7 @@ export const getLabels = (
return false;
}
return deviceEntityLookup[device.id].some((entity) => {
const stateObj = hassStates[entity.entity_id];
const stateObj = hass.states[entity.entity_id];
if (!stateObj) {
return false;
}
@@ -212,7 +208,7 @@ export const getLabels = (
});
});
inputEntities = inputEntities!.filter((entity) => {
const stateObj = hassStates[entity.entity_id];
const stateObj = hass.states[entity.entity_id];
if (!stateObj) {
return false;
}
@@ -249,8 +245,8 @@ export const getLabels = (
if (areaIds) {
areaIds.forEach((areaId) => {
const area = hassAreas[areaId];
area?.labels.forEach((label) => usedLabels.add(label));
const area = hass.areas[areaId];
area.labels.forEach((label) => usedLabels.add(label));
});
}

View File

@@ -18,11 +18,6 @@ export interface LabPreviewFeaturesResponse {
features: LabPreviewFeature[];
}
/**
* Fetch all lab features
* @param hass - The Home Assistant instance
* @returns A promise to fetch the lab features
*/
export const fetchLabFeatures = async (
hass: HomeAssistant
): Promise<LabPreviewFeature[]> => {
@@ -32,15 +27,6 @@ export const fetchLabFeatures = async (
return response.features;
};
/**
* Update a specific lab feature
* @param hass - The Home Assistant instance
* @param domain - The domain of the lab feature
* @param preview_feature - The preview feature of the lab feature
* @param enabled - Whether the lab feature is enabled
* @param create_backup - Whether to create a backup of the lab feature
* @returns A promise to update the lab feature
*/
export const labsUpdatePreviewFeature = (
hass: HomeAssistant,
domain: string,
@@ -79,12 +65,6 @@ const subscribeLabUpdates = (
"labs_updated"
);
/**
* Subscribe to a collection of lab features
* @param conn - The connection to the Home Assistant instance
* @param onChange - The function to call when the lab features change
* @returns The unsubscribe function
*/
export const subscribeLabFeatures = (
conn: Connection,
onChange: (features: LabPreviewFeature[]) => void
@@ -96,27 +76,3 @@ export const subscribeLabFeatures = (
conn,
onChange
);
/**
* Subscribe to a specific lab feature
* @param conn - The connection to the Home Assistant instance
* @param domain - The domain of the lab feature
* @param previewFeature - The preview feature of the lab feature
* @param onChange - The function to call when the lab feature changes
* @returns The unsubscribe function
*/
export const subscribeLabFeature = (
conn: Connection,
domain: string,
previewFeature: string,
onChange: (enabled: boolean) => void
) =>
subscribeLabFeatures(conn, (features) => {
const enabled =
features.find(
(feature) =>
feature.domain === domain &&
feature.preview_feature === previewFeature
)?.enabled ?? false;
onChange(enabled);
});

View File

@@ -18,7 +18,8 @@ export const enum LawnMowerEntityFeature {
}
interface LawnMowerEntityAttributes
extends HassEntityAttributeBase, Record<string, any> {}
extends HassEntityAttributeBase,
Record<string, any> {}
export interface LawnMowerEntity extends HassEntityBase {
attributes: LawnMowerEntityAttributes;

View File

@@ -18,7 +18,8 @@ export interface LovelaceSectionConfig extends LovelaceBaseSectionConfig {
cards?: LovelaceCardConfig[];
}
export interface LovelaceStrategySectionConfig extends LovelaceBaseSectionConfig {
export interface LovelaceStrategySectionConfig
extends LovelaceBaseSectionConfig {
strategy: LovelaceStrategyConfig;
}

View File

@@ -11,7 +11,8 @@ export interface LovelaceConfig extends LovelaceDashboardBaseConfig {
views: LovelaceViewRawConfig[];
}
export interface LovelaceDashboardStrategyConfig extends LovelaceDashboardBaseConfig {
export interface LovelaceDashboardStrategyConfig
extends LovelaceDashboardBaseConfig {
strategy: LovelaceStrategyConfig;
}

View File

@@ -1,10 +1,7 @@
import type { MediaSelectorValue } from "../../selector";
import type { LovelaceBadgeConfig } from "./badge";
import type { LovelaceCardConfig } from "./card";
import type {
LovelaceSectionConfig,
LovelaceSectionRawConfig,
} from "./section";
import type { LovelaceSectionRawConfig } from "./section";
import type { LovelaceStrategyConfig } from "./strategy";
export interface ShowViewConfig {
@@ -36,12 +33,6 @@ export interface LovelaceViewHeaderConfig {
badges_wrap?: "wrap" | "scroll";
}
export interface LovelaceViewSidebarConfig {
sections?: LovelaceSectionConfig[];
content_label?: string;
sidebar_label?: string;
}
export interface LovelaceBaseViewConfig {
index?: number;
title?: string;
@@ -65,8 +56,6 @@ export interface LovelaceViewConfig extends LovelaceBaseViewConfig {
cards?: LovelaceCardConfig[];
sections?: LovelaceSectionRawConfig[];
header?: LovelaceViewHeaderConfig;
// Only used for section view, it should move to a section view config type when the views will have dedicated editor.
sidebar?: LovelaceViewSidebarConfig;
}
export interface LovelaceStrategyViewConfig extends LovelaceBaseViewConfig {

View File

@@ -29,7 +29,8 @@ export interface LovelaceDashboardMutableParams {
title: string;
}
export interface LovelaceDashboardCreateParams extends LovelaceDashboardMutableParams {
export interface LovelaceDashboardCreateParams
extends LovelaceDashboardMutableParams {
url_path: string;
mode: "storage";
}

View File

@@ -219,13 +219,9 @@ const tryDescribeAction = <T extends ActionType>(
if (config.action) {
const [domain, serviceName] = config.action.split(".", 2);
const descriptionPlaceholders =
hass.services[domain][serviceName].description_placeholders;
const service =
hass.localize(
`component.${domain}.services.${serviceName}.name`,
descriptionPlaceholders
) || hass.services[domain][serviceName]?.name;
hass.localize(`component.${domain}.services.${serviceName}.name`) ||
hass.services[domain][serviceName]?.name;
if (config.metadata) {
return hass.localize(

View File

@@ -332,6 +332,7 @@ export interface NumberSelector {
max?: number;
step?: number | "any";
mode?: "box" | "slider";
entity?: { domains: readonly string[] };
unit_of_measurement?: string;
slider_ticks?: boolean;
translation_key?: string;

View File

@@ -1,30 +1,15 @@
import type { HassServiceTarget } from "home-assistant-js-websocket";
import { computeDomain } from "../common/entity/compute_domain";
import type { HaDevicePickerDeviceFilterFunc } from "../components/device/ha-device-picker";
import type { PickerComboBoxItem } from "../components/ha-picker-combo-box";
import type { HomeAssistant } from "../types";
import type { FloorComboBoxItem } from "./area_floor";
import type { AreaRegistryEntry } from "./area_registry";
import type { DevicePickerItem, DeviceRegistryEntry } from "./device_registry";
import type { DeviceRegistryEntry } from "./device_registry";
import type { HaEntityPickerEntityFilterFunc } from "./entity";
import type {
EntityComboBoxItem,
EntityRegistryDisplayEntry,
} from "./entity_registry";
export const TARGET_SEPARATOR = "________";
import type { EntityRegistryDisplayEntry } from "./entity_registry";
export type TargetType = "entity" | "device" | "area" | "label" | "floor";
export type TargetTypeFloorless = Exclude<TargetType, "floor">;
export interface SingleHassServiceTarget {
entity_id?: string;
device_id?: string;
area_id?: string;
floor_id?: string;
label_id?: string;
}
export interface ExtractFromTargetResult {
missing_areas: string[];
missing_devices: string[];
@@ -50,39 +35,6 @@ export const extractFromTarget = async (
target,
});
export const getTriggersForTarget = async (
callWS: HomeAssistant["callWS"],
target: HassServiceTarget,
expandGroup = true
) =>
callWS<string[]>({
type: "get_triggers_for_target",
target,
expand_group: expandGroup,
});
export const getConditionsForTarget = async (
callWS: HomeAssistant["callWS"],
target: HassServiceTarget,
expandGroup = true
) =>
callWS<string[]>({
type: "get_conditions_for_target",
target,
expand_group: expandGroup,
});
export const getServicesForTarget = async (
callWS: HomeAssistant["callWS"],
target: HassServiceTarget,
expandGroup = true
) =>
callWS<string[]>({
type: "get_services_for_target",
target,
expand_group: expandGroup,
});
export const areaMeetsFilter = (
area: AreaRegistryEntry,
devices: HomeAssistant["devices"],
@@ -210,32 +162,3 @@ export const entityRegMeetsFilter = (
}
return true;
};
export const getTargetComboBoxItemType = (
item:
| PickerComboBoxItem
| (FloorComboBoxItem & { last?: boolean | undefined })
| EntityComboBoxItem
| DevicePickerItem
) => {
if (
(item as FloorComboBoxItem).type === "area" ||
(item as FloorComboBoxItem).type === "floor"
) {
return (item as FloorComboBoxItem).type;
}
if ("domain" in item) {
return "device";
}
if ("stateObj" in item) {
return "entity";
}
if (item.id === "___EMPTY_SEARCH___") {
return "empty";
}
return "label";
};

View File

@@ -106,7 +106,8 @@ export interface AutomationTrace extends BaseTrace {
}
export interface AutomationTraceExtended
extends AutomationTrace, BaseTraceExtended {
extends AutomationTrace,
BaseTraceExtended {
config: ManualAutomationConfig;
blueprint_inputs?: BlueprintAutomationConfig;
}

View File

@@ -28,24 +28,18 @@ export const TRIGGER_COLLECTIONS: AutomationElementGroupCollection[] = [
},
},
},
{
titleKey: "ui.panel.config.automation.editor.triggers.groups.helpers.label",
groups: {
helpers: {},
},
},
{
titleKey: "ui.panel.config.automation.editor.triggers.groups.other.label",
groups: {
event: {},
geo_location: {},
homeassistant: {},
mqtt: {},
conversation: {},
tag: {},
template: {},
webhook: {},
persistent_notification: {},
other: {},
},
},
] as const;

View File

@@ -82,12 +82,6 @@ export interface WeatherEntity extends HassEntityBase {
attributes: WeatherEntityAttributes;
}
export const WEATHER_TEMPERATURE_ATTRIBUTES = new Set<string>([
"temperature",
"apparent_temperature",
"dew_point",
]);
export const weatherSVGs = new Set<string>([
"clear-night",
"cloudy",
@@ -262,15 +256,9 @@ export const getWeatherUnit = (
export const getSecondaryWeatherAttribute = (
hass: HomeAssistant,
stateObj: WeatherEntity,
forecast: ForecastAttribute[],
temperatureFractionDigits?: number
forecast: ForecastAttribute[]
): TemplateResult | undefined => {
const extrema = getWeatherExtrema(
hass,
stateObj,
forecast,
temperatureFractionDigits
);
const extrema = getWeatherExtrema(hass, stateObj, forecast);
if (extrema) {
return extrema;
@@ -310,8 +298,7 @@ export const getSecondaryWeatherAttribute = (
const getWeatherExtrema = (
hass: HomeAssistant,
stateObj: WeatherEntity,
forecast: ForecastAttribute[],
temperatureFractionDigits?: number
forecast: ForecastAttribute[]
): TemplateResult | undefined => {
if (!forecast?.length) {
return undefined;
@@ -326,22 +313,13 @@ const getWeatherExtrema = (
break;
}
if (!tempHigh || fc.temperature > tempHigh) {
tempHigh =
temperatureFractionDigits === undefined
? fc.temperature
: round(fc.temperature, temperatureFractionDigits);
tempHigh = fc.temperature;
}
if (fc.templow !== undefined && (!tempLow || fc.templow < tempLow)) {
tempLow =
temperatureFractionDigits === undefined
? fc.templow
: round(fc.templow, temperatureFractionDigits);
if (!tempLow || (fc.templow && fc.templow < tempLow)) {
tempLow = fc.templow;
}
if (!fc.templow && (!tempLow || fc.temperature < tempLow)) {
tempLow =
temperatureFractionDigits === undefined
? fc.temperature
: round(fc.temperature, temperatureFractionDigits);
tempLow = fc.temperature;
}
}

View File

@@ -1,16 +1,15 @@
import { mdiAlertOutline, mdiClose } from "@mdi/js";
import { mdiAlertOutline } from "@mdi/js";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, query, state } from "lit/decorators";
import { classMap } from "lit/directives/class-map";
import { ifDefined } from "lit/directives/if-defined";
import { fireEvent } from "../../common/dom/fire_event";
import "../../components/ha-button";
import "../../components/ha-dialog-footer";
import "../../components/ha-dialog-header";
import "../../components/ha-md-dialog";
import type { HaMdDialog } from "../../components/ha-md-dialog";
import "../../components/ha-svg-icon";
import "../../components/ha-textfield";
import type { HaTextField } from "../../components/ha-textfield";
import "../../components/ha-wa-dialog";
import type { HomeAssistant } from "../../types";
import type { DialogBoxParams } from "./show-dialog-box";
@@ -20,12 +19,12 @@ class DialogBox extends LitElement {
@state() private _params?: DialogBoxParams;
@state() private _open = false;
@state() private _closeState?: "canceled" | "confirmed";
@query("ha-textfield") private _textField?: HaTextField;
@query("ha-md-dialog") private _dialog?: HaMdDialog;
private _closePromise?: Promise<void>;
private _closeResolve?: () => void;
@@ -35,7 +34,6 @@ class DialogBox extends LitElement {
await this._closePromise;
}
this._params = params;
this._open = true;
}
public closeDialog(): boolean {
@@ -62,30 +60,16 @@ class DialogBox extends LitElement {
this.hass.localize("ui.dialogs.generic.default_confirmation_title"));
return html`
<ha-wa-dialog
.hass=${this.hass}
.open=${this._open}
type=${confirmPrompt ? "alert" : "standard"}
?prevent-scrim-close=${confirmPrompt}
<ha-md-dialog
open
.disableCancelAction=${confirmPrompt}
@closed=${this._dialogClosed}
type="alert"
aria-labelledby="dialog-box-title"
aria-describedby="dialog-box-description"
>
<ha-dialog-header slot="header">
${!confirmPrompt
? html`<slot name="headerNavigationIcon" slot="navigationIcon">
<ha-icon-button
data-dialog="close"
.label=${this.hass?.localize("ui.common.close") ?? "Close"}
.path=${mdiClose}
></ha-icon-button
></slot>`
: nothing}
<span
class=${classMap({ title: true, alert: confirmPrompt })}
slot="title"
id="dialog-box-title"
>
<div slot="headline">
<span .title=${dialogTitle} id="dialog-box-title">
${this._params.warning
? html`<ha-svg-icon
.path=${mdiAlertOutline}
@@ -94,13 +78,13 @@ class DialogBox extends LitElement {
: nothing}
${dialogTitle}
</span>
</ha-dialog-header>
<div id="dialog-box-description">
</div>
<div slot="content" id="dialog-box-description">
${this._params.text ? html` <p>${this._params.text}</p> ` : ""}
${this._params.prompt
? html`
<ha-textfield
autofocus
dialogInitialFocus
value=${ifDefined(this._params.defaultValue)}
.placeholder=${this._params.placeholder}
.label=${this._params.inputLabel
@@ -115,11 +99,10 @@ class DialogBox extends LitElement {
`
: ""}
</div>
<ha-dialog-footer slot="footer">
<div slot="actions">
${confirmPrompt
? html`
<ha-button
slot="secondaryAction"
@click=${this._dismiss}
?autofocus=${!this._params.prompt && this._params.destructive}
appearance="plain"
@@ -131,7 +114,6 @@ class DialogBox extends LitElement {
`
: nothing}
<ha-button
slot="primaryAction"
@click=${this._confirm}
?autofocus=${!this._params.prompt && !this._params.destructive}
variant=${this._params.destructive ? "danger" : "brand"}
@@ -140,8 +122,8 @@ class DialogBox extends LitElement {
? this._params.confirmText
: this.hass.localize("ui.common.ok")}
</ha-button>
</ha-dialog-footer>
</ha-wa-dialog>
</div>
</ha-md-dialog>
`;
}
@@ -166,20 +148,20 @@ class DialogBox extends LitElement {
}
private _closeDialog() {
this._open = false;
fireEvent(this, "dialog-closed", { dialog: this.localName });
this._dialog?.close();
this._closePromise = new Promise((resolve) => {
this._closeResolve = resolve;
});
}
private _dialogClosed() {
fireEvent(this, "dialog-closed", { dialog: this.localName });
if (!this._closeState) {
fireEvent(this, "dialog-closed", { dialog: this.localName });
this._cancel();
}
this._closeState = undefined;
this._params = undefined;
this._open = false;
this._closeResolve?.();
this._closeResolve = undefined;
}
@@ -205,14 +187,6 @@ class DialogBox extends LitElement {
ha-textfield {
width: 100%;
}
.title.alert {
padding: 0 var(--ha-space-2);
}
@media all and (min-width: 450px) and (min-height: 500px) {
.title.alert {
padding: 0 var(--ha-space-1);
}
}
`;
}

View File

@@ -32,7 +32,8 @@ export interface PromptDialogParams extends BaseDialogBoxParams {
}
export interface DialogBoxParams
extends ConfirmationDialogParams, PromptDialogParams {
extends ConfirmationDialogParams,
PromptDialogParams {
confirm?: (out?: string) => void;
confirmation?: boolean;
prompt?: boolean;

View File

@@ -1,9 +1,9 @@
import { ancestorsWithProperty } from "../common/dom/ancestors-with-property";
import { deepActiveElement } from "../common/dom/deep-active-element";
import type { HASSDomEvent, ValidHassDomEvent } from "../common/dom/fire_event";
import { mainWindow } from "../common/dom/get_main_window";
import { nextRender } from "../common/util/render-status";
import type { ProvideHassElement } from "../mixins/provide-hass-lit-mixin";
import { ancestorsWithProperty } from "../common/dom/ancestors-with-property";
import { deepActiveElement } from "../common/dom/deep-active-element";
import { nextRender } from "../common/util/render-status";
declare global {
// for fire event
@@ -19,11 +19,10 @@ declare global {
}
}
export interface HassDialog<
T = HASSDomEvents[ValidHassDomEvent],
> extends HTMLElement {
export interface HassDialog<T = HASSDomEvents[ValidHassDomEvent]>
extends HTMLElement {
showDialog(params: T);
closeDialog?: (historyState?: any) => boolean;
closeDialog?: () => boolean;
}
interface ShowDialogParams<T> {
@@ -144,32 +143,27 @@ export const showDialog = async (
return true;
};
export const closeDialog = async (
dialogTag: string,
historyState?: any
): Promise<boolean> => {
export const closeDialog = async (dialogTag: string): Promise<boolean> => {
if (!(dialogTag in LOADED)) {
return true;
}
const dialogElement = await LOADED[dialogTag].element;
if (dialogElement.closeDialog) {
return dialogElement.closeDialog(historyState) !== false;
return dialogElement.closeDialog() !== false;
}
return true;
};
// called on back()
export const closeLastDialog = async (historyState?: any) => {
export const closeLastDialog = async () => {
if (OPEN_DIALOG_STACK.length) {
const lastDialog = OPEN_DIALOG_STACK.pop() as DialogState;
const closed = await closeDialog(lastDialog.dialogTag, historyState);
const lastDialog = OPEN_DIALOG_STACK.pop();
const closed = await closeDialog(lastDialog!.dialogTag);
if (!closed) {
// if the dialog was not closed, put it back on the stack
OPEN_DIALOG_STACK.push(lastDialog);
} else if (
OPEN_DIALOG_STACK.length &&
mainWindow.history.state?.opensDialog
) {
OPEN_DIALOG_STACK.push(lastDialog!);
}
if (OPEN_DIALOG_STACK.length && mainWindow.history.state?.opensDialog) {
// if there are more dialogs open, push a new state so back() will close the next top dialog
mainWindow.history.pushState(
{ dialog: OPEN_DIALOG_STACK[OPEN_DIALOG_STACK.length - 1].dialogTag },

View File

@@ -1,5 +1,5 @@
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { property, state } from "lit/decorators";
import { slugify } from "../../../common/string/slugify";
import "../../../components/buttons/ha-progress-button";
import "../../../components/ha-camera-stream";
@@ -9,7 +9,6 @@ import type { HomeAssistant } from "../../../types";
import { fileDownload } from "../../../util/file_download";
import { showToast } from "../../../util/toast";
@customElement("more-info-camera")
class MoreInfoCamera extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@@ -113,6 +112,8 @@ class MoreInfoCamera extends LitElement {
`;
}
customElements.define("more-info-camera", MoreInfoCamera);
declare global {
interface HTMLElementTagNameMap {
"more-info-camera": MoreInfoCamera;

View File

@@ -7,7 +7,7 @@ import {
} from "@mdi/js";
import type { CSSResultGroup, PropertyValues } from "lit";
import { LitElement, css, html, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { property, state } from "lit/decorators";
import { stopPropagation } from "../../../common/dom/stop_propagation";
import { supportsFeature } from "../../../common/entity/supports-feature";
import "../../../components/ha-attribute-icon";
@@ -32,7 +32,6 @@ import { moreInfoControlStyle } from "../components/more-info-control-style";
type MainControl = "temperature" | "humidity";
@customElement("more-info-climate")
class MoreInfoClimate extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@@ -568,6 +567,8 @@ class MoreInfoClimate extends LitElement {
}
}
customElements.define("more-info-climate", MoreInfoClimate);
declare global {
interface HTMLElementTagNameMap {
"more-info-climate": MoreInfoClimate;

View File

@@ -1,7 +1,7 @@
import type { HassEntity } from "home-assistant-js-websocket";
import type { CSSResultGroup, PropertyValues } from "lit";
import { LitElement, css, html, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { property, state } from "lit/decorators";
import { dynamicElement } from "../../../common/dom/dynamic-element-directive";
import type { GroupEntity } from "../../../data/group";
import { computeGroupDomain } from "../../../data/group";
@@ -13,7 +13,6 @@ import {
importMoreInfoControl,
} from "../state_more_info_control";
@customElement("more-info-group")
class MoreInfoGroup extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@@ -107,6 +106,8 @@ class MoreInfoGroup extends LitElement {
}
}
customElements.define("more-info-group", MoreInfoGroup);
declare global {
interface HTMLElementTagNameMap {
"more-info-group": MoreInfoGroup;

View File

@@ -1,7 +1,7 @@
import { mdiPower, mdiTuneVariant } from "@mdi/js";
import type { CSSResultGroup, PropertyValues } from "lit";
import { LitElement, css, html, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { property, state } from "lit/decorators";
import { stopPropagation } from "../../../common/dom/stop_propagation";
import { supportsFeature } from "../../../common/entity/supports-feature";
import "../../../components/ha-control-select-menu";
@@ -15,7 +15,6 @@ import type { HomeAssistant } from "../../../types";
import "../components/ha-more-info-control-select-container";
import { moreInfoControlStyle } from "../components/more-info-control-style";
@customElement("more-info-humidifier")
class MoreInfoHumidifier extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@@ -250,6 +249,8 @@ class MoreInfoHumidifier extends LitElement {
}
}
customElements.define("more-info-humidifier", MoreInfoHumidifier);
declare global {
interface HTMLElementTagNameMap {
"more-info-humidifier": MoreInfoHumidifier;

View File

@@ -1,10 +1,9 @@
import type { PropertyValues } from "lit";
import { css, html, LitElement } from "lit";
import { customElement, property, state } from "lit/decorators";
import { property, state } from "lit/decorators";
import "../components/ha-spinner";
import "../components/ha-button";
@customElement("ha-init-page")
class HaInitPage extends LitElement {
@property({ type: Boolean }) public error = false;
@@ -121,6 +120,8 @@ class HaInitPage extends LitElement {
`;
}
customElements.define("ha-init-page", HaInitPage);
declare global {
interface HTMLElementTagNameMap {
"ha-init-page": HaInitPage;

View File

@@ -128,8 +128,6 @@ class HassSubpage extends LitElement {
ha-menu-button,
ha-icon-button-arrow-prev,
::slotted([slot="toolbar-icon"]) {
display: flex;
align-items: center;
pointer-events: auto;
color: var(--sidebar-icon-color);
}
@@ -145,6 +143,7 @@ class HassSubpage extends LitElement {
-webkit-box-orient: vertical;
overflow: hidden;
text-overflow: ellipsis;
padding-bottom: 1px;
}
.content {

View File

@@ -621,9 +621,9 @@ export class HaTabsSubpageDataTable extends KeyboardShortcutMixin(LitElement) {
} else if (this._sortDirection === "asc") {
this._sortDirection = "desc";
} else {
this._sortDirection = "asc";
this._sortDirection = null;
}
this._sortColumn = columnId;
this._sortColumn = this._sortDirection === null ? undefined : columnId;
fireEvent(this, "sorting-changed", {
column: columnId,

View File

@@ -1,6 +1,6 @@
import { mdiClose } from "@mdi/js";
import { html, LitElement, nothing } from "lit";
import { customElement, property, query, state } from "lit/decorators";
import { property, query, state } from "lit/decorators";
import type { LocalizeKeys } from "../common/translations/localize";
import "../components/ha-button";
import "../components/ha-icon-button";
@@ -26,7 +26,6 @@ export interface ToastActionParams {
| { translationKey: LocalizeKeys; args?: Record<string, string> };
}
@customElement("notification-manager")
class NotificationManager extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@@ -116,6 +115,8 @@ class NotificationManager extends LitElement {
}
}
customElements.define("notification-manager", NotificationManager);
declare global {
interface HTMLElementTagNameMap {
"notification-manager": NotificationManager;

View File

@@ -90,7 +90,9 @@ class OnboardingRestoreBackupCloudLogin extends LitElement {
this._email = this._cloudLoginElement.emailField.value;
}
await import("../../panels/config/cloud/forgot-password/cloud-forgot-password-card");
await import(
"../../panels/config/cloud/forgot-password/cloud-forgot-password-card"
);
this._view = "forgot-password";
}

View File

@@ -3,7 +3,7 @@ import { TZDate } from "@date-fns/tz";
import { addDays, isSameDay } from "date-fns";
import type { CSSResultGroup } from "lit";
import { LitElement, css, html, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { property, state } from "lit/decorators";
import { formatDate } from "../../common/datetime/format_date";
import { formatDateTime } from "../../common/datetime/format_date_time";
import { formatTime } from "../../common/datetime/format_time";
@@ -26,7 +26,6 @@ import type { CalendarEventDetailDialogParams } from "./show-dialog-calendar-eve
import { showCalendarEventEditDialog } from "./show-dialog-calendar-event-editor";
import { resolveTimeZone } from "../../common/datetime/resolve-time-zone";
@customElement("dialog-calendar-event-detail")
class DialogCalendarEventDetail extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@@ -272,3 +271,8 @@ declare global {
"dialog-calendar-event-detail": DialogCalendarEventDetail;
}
}
customElements.define(
"dialog-calendar-event-detail",
DialogCalendarEventDetail
);

View File

@@ -87,7 +87,7 @@ class PanelCalendar extends LitElement {
public willUpdate(changedProps: PropertyValues): void {
super.willUpdate(changedProps);
if (!this.hasUpdated) {
this._calendars = getCalendars(this.hass, this);
this._calendars = getCalendars(this.hass);
}
}
@@ -243,7 +243,7 @@ class PanelCalendar extends LitElement {
manifest: await fetchIntegrationManifest(this.hass, "local_calendar"),
dialogClosedCallback: ({ flowFinished }) => {
if (flowFinished) {
this._calendars = getCalendars(this.hass, this);
this._calendars = getCalendars(this.hass);
}
},
});

View File

@@ -13,7 +13,6 @@ import { generateLovelaceViewStrategy } from "../lovelace/strategies/get-strateg
import type { Lovelace } from "../lovelace/types";
import "../lovelace/views/hui-view";
import "../lovelace/views/hui-view-container";
import "../lovelace/views/hui-view-background";
const CLIMATE_LOVELACE_VIEW_CONFIG: LovelaceStrategyViewConfig = {
strategy: {
@@ -116,7 +115,6 @@ class PanelClimate extends LitElement {
this._lovelace
? html`
<hui-view-container .hass=${this.hass}>
<hui-view-background .hass=${this.hass}> </hui-view-background>
<hui-view
.hass=${this.hass}
.narrow=${this.narrow}

View File

@@ -1,7 +1,7 @@
import type { HassEntity } from "home-assistant-js-websocket";
import type { CSSResultGroup } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { property, state } from "lit/decorators";
import { fireEvent } from "../../../common/dom/fire_event";
import "../../../components/entity/ha-entity-picker";
import type { HaEntityPicker } from "../../../components/entity/ha-entity-picker";
@@ -40,7 +40,6 @@ const SENSOR_DOMAINS = ["sensor"];
const TEMPERATURE_DEVICE_CLASSES = [SENSOR_DEVICE_CLASS_TEMPERATURE];
const HUMIDITY_DEVICE_CLASSES = [SENSOR_DEVICE_CLASS_HUMIDITY];
@customElement("dialog-area-registry-detail")
class DialogAreaDetail extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@@ -427,3 +426,5 @@ declare global {
"dialog-area-registry-detail": DialogAreaDetail;
}
}
customElements.define("dialog-area-registry-detail", DialogAreaDetail);

View File

@@ -1,496 +0,0 @@
import { mdiClose, mdiDragHorizontalVariant, mdiTextureBox } from "@mdi/js";
import type { CSSResultGroup } from "lit";
import { LitElement, css, html, nothing } from "lit";
import { customElement, property, query, state } from "lit/decorators";
import { repeat } from "lit/directives/repeat";
import {
type AreasFloorHierarchy,
getAreasFloorHierarchy,
getAreasOrder,
getFloorOrder,
} from "../../../common/areas/areas-floor-hierarchy";
import { fireEvent } from "../../../common/dom/fire_event";
import "../../../components/ha-button";
import "../../../components/ha-dialog-header";
import "../../../components/ha-floor-icon";
import "../../../components/ha-icon";
import "../../../components/ha-icon-button";
import "../../../components/ha-md-dialog";
import type { HaMdDialog } from "../../../components/ha-md-dialog";
import "../../../components/ha-md-list";
import "../../../components/ha-md-list-item";
import "../../../components/ha-sortable";
import "../../../components/ha-svg-icon";
import type { AreaRegistryEntry } from "../../../data/area_registry";
import {
reorderAreaRegistryEntries,
updateAreaRegistryEntry,
} from "../../../data/area_registry";
import { reorderFloorRegistryEntries } from "../../../data/floor_registry";
import { haStyle, haStyleDialog } from "../../../resources/styles";
import type { HomeAssistant } from "../../../types";
import { showToast } from "../../../util/toast";
import type { AreasFloorsOrderDialogParams } from "./show-dialog-areas-floors-order";
const UNASSIGNED_FLOOR = "__unassigned__";
interface FloorChange {
areaId: string;
floorId: string | null;
}
@customElement("dialog-areas-floors-order")
class DialogAreasFloorsOrder extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@state() private _open = false;
@state() private _hierarchy?: AreasFloorHierarchy;
@state() private _saving = false;
@query("ha-md-dialog") private _dialog?: HaMdDialog;
public async showDialog(
_params: AreasFloorsOrderDialogParams
): Promise<void> {
this._open = true;
this._computeHierarchy();
}
private _computeHierarchy(): void {
this._hierarchy = getAreasFloorHierarchy(
Object.values(this.hass.floors),
Object.values(this.hass.areas)
);
}
public closeDialog(): void {
this._dialog?.close();
}
private _dialogClosed(): void {
this._open = false;
this._hierarchy = undefined;
this._saving = false;
fireEvent(this, "dialog-closed", { dialog: this.localName });
}
protected render() {
if (!this._open || !this._hierarchy) {
return nothing;
}
const hasFloors = this._hierarchy.floors.length > 0;
const dialogTitle = this.hass.localize(
hasFloors
? "ui.panel.config.areas.dialog.reorder_floors_areas_title"
: "ui.panel.config.areas.dialog.reorder_areas_title"
);
return html`
<ha-md-dialog open @closed=${this._dialogClosed}>
<ha-dialog-header slot="headline">
<ha-icon-button
slot="navigationIcon"
.label=${this.hass.localize("ui.common.close")}
.path=${mdiClose}
@click=${this.closeDialog}
></ha-icon-button>
<span slot="title" .title=${dialogTitle}>${dialogTitle}</span>
</ha-dialog-header>
<div slot="content" class="content">
<ha-sortable
handle-selector=".floor-handle"
draggable-selector=".floor"
@item-moved=${this._floorMoved}
invert-swap
>
<div class="floors">
${repeat(
this._hierarchy.floors,
(floor) => floor.id,
(floor) => this._renderFloor(floor)
)}
</div>
</ha-sortable>
${this._renderUnassignedAreas()}
</div>
<div slot="actions">
<ha-button @click=${this.closeDialog} appearance="plain">
${this.hass.localize("ui.common.cancel")}
</ha-button>
<ha-button @click=${this._save} .disabled=${this._saving}>
${this.hass.localize("ui.common.save")}
</ha-button>
</div>
</ha-md-dialog>
`;
}
private _renderFloor(floor: { id: string; areas: string[] }) {
const floorEntry = this.hass.floors[floor.id];
if (!floorEntry) {
return nothing;
}
return html`
<div class="floor">
<div class="floor-header">
<ha-floor-icon .floor=${floorEntry}></ha-floor-icon>
<span class="floor-name">${floorEntry.name}</span>
<ha-svg-icon
class="floor-handle"
.path=${mdiDragHorizontalVariant}
></ha-svg-icon>
</div>
<ha-sortable
handle-selector=".area-handle"
draggable-selector="ha-md-list-item"
@item-moved=${this._areaMoved}
@item-added=${this._areaAdded}
group="areas"
.floor=${floor.id}
>
<ha-md-list>
${floor.areas.length > 0
? floor.areas.map((areaId) => this._renderArea(areaId))
: html`<p class="empty">
${this.hass.localize(
"ui.panel.config.areas.dialog.empty_floor"
)}
</p>`}
</ha-md-list>
</ha-sortable>
</div>
`;
}
private _renderUnassignedAreas() {
const hasFloors = this._hierarchy!.floors.length > 0;
return html`
<div class="floor unassigned">
${hasFloors
? html`<div class="floor-header">
<span class="floor-name">
${this.hass.localize(
"ui.panel.config.areas.dialog.other_areas"
)}
</span>
</div>`
: nothing}
<ha-sortable
handle-selector=".area-handle"
draggable-selector="ha-md-list-item"
@item-moved=${this._areaMoved}
@item-added=${this._areaAdded}
group="areas"
.floor=${UNASSIGNED_FLOOR}
>
<ha-md-list>
${this._hierarchy!.areas.length > 0
? this._hierarchy!.areas.map((areaId) => this._renderArea(areaId))
: html`<p class="empty">
${this.hass.localize(
"ui.panel.config.areas.dialog.empty_unassigned"
)}
</p>`}
</ha-md-list>
</ha-sortable>
</div>
`;
}
private _renderArea(areaId: string) {
const area = this.hass.areas[areaId];
if (!area) {
return nothing;
}
return html`
<ha-md-list-item .sortableData=${area}>
${area.icon
? html`<ha-icon slot="start" .icon=${area.icon}></ha-icon>`
: html`<ha-svg-icon
slot="start"
.path=${mdiTextureBox}
></ha-svg-icon>`}
<span slot="headline">${area.name}</span>
<ha-svg-icon
class="area-handle"
slot="end"
.path=${mdiDragHorizontalVariant}
></ha-svg-icon>
</ha-md-list-item>
`;
}
private _floorMoved(ev: CustomEvent): void {
ev.stopPropagation();
if (!this._hierarchy) {
return;
}
const { oldIndex, newIndex } = ev.detail;
const newFloors = [...this._hierarchy.floors];
const [movedFloor] = newFloors.splice(oldIndex, 1);
newFloors.splice(newIndex, 0, movedFloor);
this._hierarchy = {
...this._hierarchy,
floors: newFloors,
};
}
private _areaMoved(ev: CustomEvent): void {
ev.stopPropagation();
if (!this._hierarchy) {
return;
}
const { floor } = ev.currentTarget as HTMLElement & { floor: string };
const { oldIndex, newIndex } = ev.detail;
const floorId = floor === UNASSIGNED_FLOOR ? null : floor;
if (floorId === null) {
// Reorder unassigned areas
const newAreas = [...this._hierarchy.areas];
const [movedArea] = newAreas.splice(oldIndex, 1);
newAreas.splice(newIndex, 0, movedArea);
this._hierarchy = {
...this._hierarchy,
areas: newAreas,
};
} else {
// Reorder areas within a floor
this._hierarchy = {
...this._hierarchy,
floors: this._hierarchy.floors.map((f) => {
if (f.id === floorId) {
const newAreas = [...f.areas];
const [movedArea] = newAreas.splice(oldIndex, 1);
newAreas.splice(newIndex, 0, movedArea);
return { ...f, areas: newAreas };
}
return f;
}),
};
}
}
private _areaAdded(ev: CustomEvent): void {
ev.stopPropagation();
if (!this._hierarchy) {
return;
}
const { floor } = ev.currentTarget as HTMLElement & { floor: string };
const { data: area, index } = ev.detail as {
data: AreaRegistryEntry;
index: number;
};
const newFloorId = floor === UNASSIGNED_FLOOR ? null : floor;
// Update hierarchy
const newUnassignedAreas = this._hierarchy.areas.filter(
(id) => id !== area.area_id
);
if (newFloorId === null) {
// Add to unassigned at the specified index
newUnassignedAreas.splice(index, 0, area.area_id);
}
this._hierarchy = {
...this._hierarchy,
floors: this._hierarchy.floors.map((f) => {
if (f.id === newFloorId) {
// Add to new floor at the specified index
const newAreas = [...f.areas];
newAreas.splice(index, 0, area.area_id);
return { ...f, areas: newAreas };
}
// Remove from old floor
return {
...f,
areas: f.areas.filter((id) => id !== area.area_id),
};
}),
areas: newUnassignedAreas,
};
}
private _computeFloorChanges(): FloorChange[] {
if (!this._hierarchy) {
return [];
}
const changes: FloorChange[] = [];
// Check areas assigned to floors
for (const floor of this._hierarchy.floors) {
for (const areaId of floor.areas) {
const originalFloorId = this.hass.areas[areaId]?.floor_id ?? null;
if (floor.id !== originalFloorId) {
changes.push({ areaId, floorId: floor.id });
}
}
}
// Check unassigned areas
for (const areaId of this._hierarchy.areas) {
const originalFloorId = this.hass.areas[areaId]?.floor_id ?? null;
if (originalFloorId !== null) {
changes.push({ areaId, floorId: null });
}
}
return changes;
}
private async _save(): Promise<void> {
if (!this._hierarchy || this._saving) {
return;
}
this._saving = true;
try {
const areaOrder = getAreasOrder(this._hierarchy);
const floorOrder = getFloorOrder(this._hierarchy);
// Update floor assignments for areas that changed floors
const floorChanges = this._computeFloorChanges();
const floorChangePromises = floorChanges.map(({ areaId, floorId }) =>
updateAreaRegistryEntry(this.hass, areaId, {
floor_id: floorId,
})
);
await Promise.all(floorChangePromises);
// Reorder areas and floors
await reorderAreaRegistryEntries(this.hass, areaOrder);
await reorderFloorRegistryEntries(this.hass, floorOrder);
this.closeDialog();
} catch (err: any) {
showToast(this, {
message:
err.message ||
this.hass.localize("ui.panel.config.areas.dialog.reorder_failed"),
});
this._saving = false;
}
}
static get styles(): CSSResultGroup {
return [
haStyle,
haStyleDialog,
css`
ha-md-dialog {
min-width: 600px;
max-height: 90%;
--dialog-content-padding: 8px 24px;
}
@media all and (max-width: 600px), all and (max-height: 500px) {
ha-md-dialog {
--md-dialog-container-shape: 0;
min-width: 100%;
min-height: 100%;
}
}
.floors {
display: flex;
flex-direction: column;
gap: 16px;
}
.floor {
border: 1px solid var(--divider-color);
border-radius: var(
--ha-card-border-radius,
var(--ha-border-radius-lg)
);
overflow: hidden;
}
.floor.unassigned {
margin-top: 16px;
}
.floor-header {
display: flex;
align-items: center;
padding: 12px 16px;
background-color: var(--secondary-background-color);
gap: 12px;
}
.floor-name {
flex: 1;
font-weight: var(--ha-font-weight-medium);
}
.floor-handle {
cursor: grab;
color: var(--secondary-text-color);
}
ha-md-list {
padding: 0;
--md-list-item-leading-space: 16px;
--md-list-item-trailing-space: 16px;
display: flex;
flex-direction: column;
}
ha-md-list-item {
--md-list-item-one-line-container-height: 48px;
--md-list-item-container-shape: 0;
}
ha-md-list-item.sortable-ghost {
border-radius: calc(
var(--ha-card-border-radius, var(--ha-border-radius-lg)) - 1px
);
box-shadow: inset 0 0 0 2px var(--primary-color);
}
.area-handle {
cursor: grab;
color: var(--secondary-text-color);
}
.empty {
text-align: center;
color: var(--secondary-text-color);
font-style: italic;
margin: 0;
padding: 12px 16px;
order: 1;
}
ha-md-list:has(ha-md-list-item) .empty {
display: none;
}
.content {
padding-top: 16px;
padding-bottom: 16px;
}
`,
];
}
}
declare global {
interface HTMLElementTagNameMap {
"dialog-areas-floors-order": DialogAreasFloorsOrder;
}
}

View File

@@ -1,7 +1,7 @@
import { mdiTextureBox } from "@mdi/js";
import type { CSSResultGroup } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { property, state } from "lit/decorators";
import { repeat } from "lit/directives/repeat";
import memoizeOne from "memoize-one";
import { fireEvent } from "../../../common/dom/fire_event";
@@ -27,7 +27,6 @@ import type { HomeAssistant } from "../../../types";
import { showAreaRegistryDetailDialog } from "./show-dialog-area-registry-detail";
import type { FloorRegistryDetailDialogParams } from "./show-dialog-floor-registry-detail";
@customElement("dialog-floor-registry-detail")
class DialogFloorDetail extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@@ -145,10 +144,6 @@ class DialogFloorDetail extends LitElement {
"ui.panel.config.floors.editor.level"
)}
type="number"
.helper=${this.hass.localize(
"ui.panel.config.floors.editor.level_helper"
)}
helperPersistent
></ha-textfield>
<ha-icon-picker
@@ -362,3 +357,5 @@ declare global {
"dialog-floor-registry-detail": DialogFloorDetail;
}
}
customElements.define("dialog-floor-registry-detail", DialogFloorDetail);

View File

@@ -2,10 +2,10 @@ import type { ActionDetail } from "@material/mwc-list";
import {
mdiDelete,
mdiDotsVertical,
mdiDragHorizontalVariant,
mdiHelpCircle,
mdiPencil,
mdiPlus,
mdiSort,
} from "@mdi/js";
import {
css,
@@ -21,6 +21,7 @@ import memoizeOne from "memoize-one";
import {
getAreasFloorHierarchy,
getAreasOrder,
getFloorOrder,
type AreasFloorHierarchy,
} from "../../../common/areas/areas-floor-hierarchy";
import { formatListWithAnds } from "../../../common/string/format-list";
@@ -41,6 +42,7 @@ import type { FloorRegistryEntry } from "../../../data/floor_registry";
import {
createFloorRegistryEntry,
deleteFloorRegistryEntry,
reorderFloorRegistryEntries,
updateFloorRegistryEntry,
} from "../../../data/floor_registry";
import {
@@ -56,7 +58,6 @@ import {
loadAreaRegistryDetailDialog,
showAreaRegistryDetailDialog,
} from "./show-dialog-area-registry-detail";
import { showAreasFloorsOrderDialog } from "./show-dialog-areas-floors-order";
import { showFloorRegistryDetailDialog } from "./show-dialog-floor-registry-detail";
const UNASSIGNED_FLOOR = "__unassigned__";
@@ -83,8 +84,6 @@ export class HaConfigAreasDashboard extends LitElement {
@property({ attribute: false }) public route!: Route;
private _searchParms = new URLSearchParams(window.location.search);
@state() private _hierarchy?: AreasFloorHierarchy;
private _blockHierarchyUpdate = false;
@@ -168,9 +167,7 @@ export class HaConfigAreasDashboard extends LitElement {
.hass=${this.hass}
.narrow=${this.narrow}
.isWide=${this.isWide}
.backPath=${this._searchParms.has("historyBack")
? undefined
: "/config"}
back-path="/config"
.tabs=${configSections.areas}
.route=${this.route}
has-fab
@@ -182,84 +179,87 @@ export class HaConfigAreasDashboard extends LitElement {
@click=${this._showHelp}
></ha-icon-button>
<div class="container">
<div class="floors">
${this._hierarchy.floors.map(({ areas, id }) => {
const floor = this.hass.floors[id];
if (!floor) {
return nothing;
}
return html`
<div class="floor">
<div class="header">
<h2>
<ha-floor-icon .floor=${floor}></ha-floor-icon>
${floor.name}
</h2>
<div class="actions">
<ha-button-menu
.floor=${floor}
@action=${this._handleFloorAction}
>
<ha-icon-button
slot="trigger"
.path=${mdiDotsVertical}
></ha-icon-button>
<ha-list-item graphic="icon"
><ha-svg-icon
.path=${mdiSort}
slot="graphic"
></ha-svg-icon
>${this.hass.localize(
"ui.panel.config.areas.picker.reorder"
)}</ha-list-item
<ha-sortable
handle-selector=".handle"
draggable-selector=".floor"
@item-moved=${this._floorMoved}
.options=${SORT_OPTIONS}
group="floors"
invert-swap
>
<div class="floors">
${this._hierarchy.floors.map(({ areas, id }) => {
const floor = this.hass.floors[id];
if (!floor) {
return nothing;
}
return html`
<div class="floor">
<div class="header">
<h2>
<ha-floor-icon .floor=${floor}></ha-floor-icon>
${floor.name}
</h2>
<div class="actions">
<ha-svg-icon
class="handle"
.path=${mdiDragHorizontalVariant}
></ha-svg-icon>
<ha-button-menu
.floor=${floor}
@action=${this._handleFloorAction}
>
<li divider role="separator"></li>
<ha-list-item graphic="icon"
><ha-svg-icon
.path=${mdiPencil}
slot="graphic"
></ha-svg-icon
>${this.hass.localize(
"ui.panel.config.areas.picker.floor.edit_floor"
)}</ha-list-item
>
<ha-list-item class="warning" graphic="icon"
><ha-svg-icon
class="warning"
.path=${mdiDelete}
slot="graphic"
></ha-svg-icon
>${this.hass.localize(
"ui.panel.config.areas.picker.floor.delete_floor"
)}</ha-list-item
>
</ha-button-menu>
<ha-icon-button
slot="trigger"
.path=${mdiDotsVertical}
></ha-icon-button>
<ha-list-item graphic="icon"
><ha-svg-icon
.path=${mdiPencil}
slot="graphic"
></ha-svg-icon
>${this.hass.localize(
"ui.panel.config.areas.picker.floor.edit_floor"
)}</ha-list-item
>
<ha-list-item class="warning" graphic="icon"
><ha-svg-icon
class="warning"
.path=${mdiDelete}
slot="graphic"
></ha-svg-icon
>${this.hass.localize(
"ui.panel.config.areas.picker.floor.delete_floor"
)}</ha-list-item
>
</ha-button-menu>
</div>
</div>
<ha-sortable
handle-selector="a"
draggable-selector="a"
@item-added=${this._areaAdded}
@item-moved=${this._areaMoved}
group="areas"
.options=${SORT_OPTIONS}
.floor=${floor.floor_id}
>
<div class="areas">
${areas.map((areaId) => {
const area = this.hass.areas[areaId];
if (!area) {
return nothing;
}
const stats = areasStats.get(area.area_id);
return this._renderArea(area, stats);
})}
</div>
</ha-sortable>
</div>
<ha-sortable
handle-selector="a"
draggable-selector="a"
@item-added=${this._areaAdded}
@item-moved=${this._areaMoved}
group="areas"
.options=${SORT_OPTIONS}
.floor=${floor.floor_id}
>
<div class="areas">
${areas.map((areaId) => {
const area = this.hass.areas[areaId];
if (!area) {
return nothing;
}
const stats = areasStats.get(area.area_id);
return this._renderArea(area, stats);
})}
</div>
</ha-sortable>
</div>
`;
})}
</div>
`;
})}
</div>
</ha-sortable>
${this._hierarchy.areas.length
? html`
@@ -267,30 +267,9 @@ export class HaConfigAreasDashboard extends LitElement {
<div class="header">
<h2>
${this.hass.localize(
this._hierarchy.floors.length
? "ui.panel.config.areas.picker.other_areas"
: "ui.panel.config.areas.picker.header"
"ui.panel.config.areas.picker.unassigned_areas"
)}
</h2>
<div class="actions">
<ha-button-menu
@action=${this._handleUnassignedAreasAction}
>
<ha-icon-button
slot="trigger"
.path=${mdiDotsVertical}
></ha-icon-button>
<ha-list-item graphic="icon"
><ha-svg-icon
.path=${mdiSort}
slot="graphic"
></ha-svg-icon
>${this.hass.localize(
"ui.panel.config.areas.picker.reorder"
)}</ha-list-item
>
</ha-button-menu>
</div>
</div>
<ha-sortable
handle-selector="a"
@@ -412,6 +391,51 @@ export class HaConfigAreasDashboard extends LitElement {
});
}
private async _floorMoved(ev) {
ev.stopPropagation();
if (!this.hass || !this._hierarchy) {
return;
}
const { oldIndex, newIndex } = ev.detail;
const reorderFloors = (
floors: AreasFloorHierarchy["floors"],
oldIdx: number,
newIdx: number
) => {
const newFloors = [...floors];
const [movedFloor] = newFloors.splice(oldIdx, 1);
newFloors.splice(newIdx, 0, movedFloor);
return newFloors;
};
// Optimistically update UI
this._hierarchy = {
...this._hierarchy,
floors: reorderFloors(this._hierarchy.floors, oldIndex, newIndex),
};
const areaOrder = getAreasOrder(this._hierarchy);
const floorOrder = getFloorOrder(this._hierarchy);
// Block hierarchy updates for 500ms to avoid flickering
// because of multiple async updates
this._blockHierarchyUpdateFor(500);
try {
await reorderAreaRegistryEntries(this.hass, areaOrder);
await reorderFloorRegistryEntries(this.hass, floorOrder);
} catch {
showToast(this, {
message: this.hass.localize(
"ui.panel.config.areas.picker.floor_reorder_failed"
),
});
// Revert on error
this._computeHierarchy();
}
}
private async _areaMoved(ev) {
ev.stopPropagation();
if (!this.hass || !this._hierarchy) {
@@ -537,23 +561,14 @@ export class HaConfigAreasDashboard extends LitElement {
const floor = (ev.currentTarget as any).floor;
switch (ev.detail.index) {
case 0:
this._showReorderDialog();
break;
case 1:
this._editFloor(floor);
break;
case 2:
case 1:
this._deleteFloor(floor);
break;
}
}
private _handleUnassignedAreasAction(ev: CustomEvent<ActionDetail>) {
if (ev.detail.index === 0) {
this._showReorderDialog();
}
}
private _createFloor() {
this._openFloorDialog();
}
@@ -583,10 +598,6 @@ export class HaConfigAreasDashboard extends LitElement {
this._openAreaDialog();
}
private _showReorderDialog() {
showAreasFloorsOrderDialog(this, {});
}
private _showHelp() {
showAlertDialog(this, {
title: this.hass.localize("ui.panel.config.areas.caption"),

View File

@@ -1,17 +0,0 @@
import { fireEvent } from "../../../common/dom/fire_event";
export interface AreasFloorsOrderDialogParams {}
export const loadAreasFloorsOrderDialog = () =>
import("./dialog-areas-floors-order");
export const showAreasFloorsOrderDialog = (
element: HTMLElement,
params: AreasFloorsOrderDialogParams
): void => {
fireEvent(element, "show-dialog", {
dialogTag: "dialog-areas-floors-order",
dialogImport: loadAreasFloorsOrderDialog,
dialogParams: params,
});
};

View File

@@ -1,4 +1,3 @@
import "@home-assistant/webawesome/dist/components/divider/divider";
import { consume } from "@lit/context";
import {
mdiAlertCircleCheck,
@@ -33,11 +32,11 @@ import { copyToClipboard } from "../../../../common/util/copy-clipboard";
import "../../../../components/ha-automation-row";
import type { HaAutomationRow } from "../../../../components/ha-automation-row";
import "../../../../components/ha-card";
import "../../../../components/ha-dropdown";
import "../../../../components/ha-dropdown-item";
import type { HaDropdownItem } from "../../../../components/ha-dropdown-item";
import "../../../../components/ha-expansion-panel";
import "../../../../components/ha-icon-button";
import "../../../../components/ha-md-button-menu";
import "../../../../components/ha-md-divider";
import "../../../../components/ha-md-menu-item";
import "../../../../components/ha-service-icon";
import "../../../../components/ha-tooltip";
import {
@@ -289,12 +288,15 @@ export default class HaAutomationActionRow extends LitElement {
</ha-tooltip>`
: nothing}
<ha-dropdown
<ha-md-button-menu
quick
slot="icons"
@click=${preventDefaultStopPropagation}
@keydown=${stopPropagation}
@wa-select=${this._handleDropdownSelect}
placement="bottom-end"
@closed=${stopPropagation}
positioning="fixed"
anchor-corner="end-end"
menu-corner="start-end"
>
<ha-icon-button
slot="trigger"
@@ -302,24 +304,30 @@ export default class HaAutomationActionRow extends LitElement {
.path=${mdiDotsVertical}
></ha-icon-button>
<ha-dropdown-item value="run">
<ha-svg-icon slot="icon" .path=${mdiPlay}></ha-svg-icon>
<ha-md-menu-item .clickAction=${this._runAction}>
<ha-svg-icon slot="start" .path=${mdiPlay}></ha-svg-icon>
${this._renderOverflowLabel(
this.hass.localize("ui.panel.config.automation.editor.actions.run")
)}
</ha-dropdown-item>
<ha-dropdown-item value="rename" .disabled=${this.disabled}>
<ha-svg-icon slot="icon" .path=${mdiRenameBox}></ha-svg-icon>
</ha-md-menu-item>
<ha-md-menu-item
.clickAction=${this._renameAction}
.disabled=${this.disabled}
>
<ha-svg-icon slot="start" .path=${mdiRenameBox}></ha-svg-icon>
${this._renderOverflowLabel(
this.hass.localize(
"ui.panel.config.automation.editor.triggers.rename"
)
)}
</ha-dropdown-item>
<wa-divider></wa-divider>
<ha-dropdown-item value="duplicate" .disabled=${this.disabled}>
</ha-md-menu-item>
<ha-md-divider role="separator" tabindex="-1"></ha-md-divider>
<ha-md-menu-item
.clickAction=${this._duplicateAction}
.disabled=${this.disabled}
>
<ha-svg-icon
slot="icon"
slot="start"
.path=${mdiPlusCircleMultipleOutline}
></ha-svg-icon>
@@ -328,10 +336,13 @@ export default class HaAutomationActionRow extends LitElement {
"ui.panel.config.automation.editor.actions.duplicate"
)
)}
</ha-dropdown-item>
</ha-md-menu-item>
<ha-dropdown-item value="copy" .disabled=${this.disabled}>
<ha-svg-icon slot="icon" .path=${mdiContentCopy}></ha-svg-icon>
<ha-md-menu-item
.clickAction=${this._copyAction}
.disabled=${this.disabled}
>
<ha-svg-icon slot="start" .path=${mdiContentCopy}></ha-svg-icon>
${this._renderOverflowLabel(
this.hass.localize(
"ui.panel.config.automation.editor.triggers.copy"
@@ -340,6 +351,7 @@ export default class HaAutomationActionRow extends LitElement {
<span
>${isMac
? html`<ha-svg-icon
slot="start"
.path=${mdiAppleKeyboardCommand}
></ha-svg-icon>`
: this.hass.localize(
@@ -350,10 +362,13 @@ export default class HaAutomationActionRow extends LitElement {
<span>C</span>
</span>`
)}
</ha-dropdown-item>
</ha-md-menu-item>
<ha-dropdown-item value="cut" .disabled=${this.disabled}>
<ha-svg-icon slot="icon" .path=${mdiContentCut}></ha-svg-icon>
<ha-md-menu-item
.clickAction=${this._cutAction}
.disabled=${this.disabled}
>
<ha-svg-icon slot="start" .path=${mdiContentCut}></ha-svg-icon>
${this._renderOverflowLabel(
this.hass.localize(
"ui.panel.config.automation.editor.triggers.cut"
@@ -362,6 +377,7 @@ export default class HaAutomationActionRow extends LitElement {
<span
>${isMac
? html`<ha-svg-icon
slot="start"
.path=${mdiAppleKeyboardCommand}
></ha-svg-icon>`
: this.hass.localize(
@@ -372,48 +388,51 @@ export default class HaAutomationActionRow extends LitElement {
<span>X</span>
</span>`
)}
</ha-dropdown-item>
</ha-md-menu-item>
${!this.optionsInSidebar
? html`
<ha-dropdown-item
value="move_up"
<ha-md-menu-item
.clickAction=${this._moveUp}
.disabled=${this.disabled || !!this.first}
>
${this.hass.localize(
"ui.panel.config.automation.editor.move_up"
)}
<ha-svg-icon slot="icon" .path=${mdiArrowUp}></ha-svg-icon
></ha-dropdown-item>
<ha-dropdown-item
value="move_down"
<ha-svg-icon slot="start" .path=${mdiArrowUp}></ha-svg-icon
></ha-md-menu-item>
<ha-md-menu-item
.clickAction=${this._moveDown}
.disabled=${this.disabled || !!this.last}
>
${this.hass.localize(
"ui.panel.config.automation.editor.move_down"
)}
<ha-svg-icon slot="icon" .path=${mdiArrowDown}></ha-svg-icon
></ha-dropdown-item>
<ha-svg-icon slot="start" .path=${mdiArrowDown}></ha-svg-icon
></ha-md-menu-item>
`
: nothing}
<ha-dropdown-item
value="toggle_yaml_mode"
<ha-md-menu-item
.clickAction=${this._toggleYamlMode}
.disabled=${!this._uiModeAvailable || !!this._warnings}
>
<ha-svg-icon slot="icon" .path=${mdiPlaylistEdit}></ha-svg-icon>
<ha-svg-icon slot="start" .path=${mdiPlaylistEdit}></ha-svg-icon>
${this._renderOverflowLabel(
this.hass.localize(
`ui.panel.config.automation.editor.edit_${!this._yamlMode ? "yaml" : "ui"}`
)
)}
</ha-dropdown-item>
</ha-md-menu-item>
<wa-divider></wa-divider>
<ha-md-divider role="separator" tabindex="-1"></ha-md-divider>
<ha-dropdown-item value="disable" .disabled=${this.disabled}>
<ha-md-menu-item
.clickAction=${this._onDisable}
.disabled=${this.disabled}
>
<ha-svg-icon
slot="icon"
slot="start"
.path=${this.action.enabled === false
? mdiPlayCircleOutline
: mdiStopCircleOutline}
@@ -424,15 +443,15 @@ export default class HaAutomationActionRow extends LitElement {
`ui.panel.config.automation.editor.actions.${this.action.enabled === false ? "enable" : "disable"}`
)
)}
</ha-dropdown-item>
<ha-dropdown-item
value="delete"
variant="danger"
</ha-md-menu-item>
<ha-md-menu-item
class="warning"
.clickAction=${this._onDelete}
.disabled=${this.disabled}
>
<ha-svg-icon
class="warning"
slot="icon"
slot="start"
.path=${mdiDelete}
></ha-svg-icon>
@@ -444,6 +463,7 @@ export default class HaAutomationActionRow extends LitElement {
<span
>${isMac
? html`<ha-svg-icon
slot="start"
.path=${mdiAppleKeyboardCommand}
></ha-svg-icon>`
: this.hass.localize(
@@ -458,8 +478,8 @@ export default class HaAutomationActionRow extends LitElement {
>
</span>`
)}
</ha-dropdown-item>
</ha-dropdown>
</ha-md-menu-item>
</ha-md-button-menu>
${!this.optionsInSidebar
? html`${this._warnings
@@ -870,47 +890,6 @@ export default class HaAutomationActionRow extends LitElement {
this._automationRowElement?.focus();
}
private _handleDropdownSelect(ev: CustomEvent<{ item: HaDropdownItem }>) {
const action = ev.detail?.item?.value;
if (!action) {
return;
}
switch (action) {
case "run":
this._runAction();
break;
case "rename":
this._renameAction();
break;
case "duplicate":
this._duplicateAction();
break;
case "copy":
this._copyAction();
break;
case "cut":
this._cutAction();
break;
case "move_up":
this._moveUp();
break;
case "move_down":
this._moveDown();
break;
case "toggle_yaml_mode":
this._toggleYamlMode(ev.target as HTMLElement);
break;
case "disable":
this._onDisable();
break;
case "delete":
this._onDelete();
break;
}
}
static styles = [rowStyles, overflowStyles];
}

View File

@@ -1,6 +1,5 @@
import { mdiDragHorizontalVariant, mdiPlus } from "@mdi/js";
import deepClone from "deep-clone-simple";
import type { HassServiceTarget } from "home-assistant-js-websocket";
import type { PropertyValues } from "lit";
import { html, LitElement, nothing } from "lit";
import { customElement, property, queryAll, state } from "lit/decorators";
@@ -214,7 +213,7 @@ export default class HaAutomationAction extends LitElement {
});
}
private _addAction = (action: string, target?: HassServiceTarget) => {
private _addAction = (action: string) => {
let actions: Action[];
if (action === PASTE_VALUE) {
actions = this.actions.concat(deepClone(this._clipboard!.action));
@@ -224,7 +223,6 @@ export default class HaAutomationAction extends LitElement {
actions = this.actions.concat({
action: getValueFromDynamic(action),
metadata: {},
target,
});
} else {
const elClass = customElements.get(

File diff suppressed because it is too large Load Diff

View File

@@ -1,391 +0,0 @@
import {
mdiInformationOutline,
mdiLabel,
mdiPlus,
mdiTextureBox,
} from "@mdi/js";
import { LitElement, css, html, nothing, type TemplateResult } from "lit";
import {
customElement,
eventOptions,
property,
query,
state,
} from "lit/decorators";
import { classMap } from "lit/directives/class-map";
import { repeat } from "lit/directives/repeat";
import memoizeOne from "memoize-one";
import { fireEvent } from "../../../../common/dom/fire_event";
import { stopPropagation } from "../../../../common/dom/stop_propagation";
import "../../../../components/entity/state-badge";
import "../../../../components/ha-domain-icon";
import "../../../../components/ha-floor-icon";
import "../../../../components/ha-icon-next";
import "../../../../components/ha-md-list";
import "../../../../components/ha-md-list-item";
import "../../../../components/ha-svg-icon";
import "../../../../components/ha-tooltip";
import type { ConfigEntry } from "../../../../data/config_entries";
import type { HomeAssistant } from "../../../../types";
import type { AddAutomationElementListItem } from "../add-automation-element-dialog";
type Target = [string, string | undefined, string | undefined];
@customElement("ha-automation-add-items")
export class HaAutomationAddItems extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public items?: {
title: string;
items: AddAutomationElementListItem[];
}[];
@property() public error?: string;
@property({ attribute: "select-label" }) public selectLabel!: string;
@property({ attribute: "empty-label" }) public emptyLabel!: string;
@property({ attribute: false }) public target?: Target;
@property({ attribute: false }) public getLabel!: (
id: string
) => { name: string; icon?: string } | undefined;
@property({ attribute: false }) public configEntryLookup: Record<
string,
ConfigEntry
> = {};
@property({ type: Boolean, attribute: "tooltip-description" })
public tooltipDescription = false;
@property({ type: Boolean, reflect: true }) scrollable = false;
@state() private _itemsScrolled = false;
@query(".items")
private _itemsDiv!: HTMLDivElement;
protected render() {
return html`<div
class=${classMap({
items: true,
blank: this.error || !this.items || !this.items.length,
error: this.error,
scrolled: this._itemsScrolled,
})}
@scroll=${this._onItemsScroll}
>
${!this.items && !this.error
? this.selectLabel
: this.error
? html`${this.error}
<div>${this._renderTarget(this.target)}</div>`
: this.items && !this.items.length
? html`${this.emptyLabel}
${this.target
? html`<div>${this._renderTarget(this.target)}</div>`
: nothing}`
: repeat(
this.items,
(_, index) => `item-group-${index}`,
(itemGroup) =>
this._renderItemList(itemGroup.title, itemGroup.items)
)}
</div>`;
}
private _renderItemList(title, items?: AddAutomationElementListItem[]) {
if (!items || !items.length) {
return nothing;
}
return html`
<div class="items-title">${title}</div>
<ha-md-list>
${repeat(
items,
(item) => item.key,
(item) => html`
<ha-md-list-item
interactive
type="button"
.value=${item.key}
@click=${this._selected}
>
<div slot="headline" class=${this.target ? "item-headline" : ""}>
${item.name}${this._renderTarget(this.target)}
</div>
${!this.tooltipDescription && item.description
? html`<div slot="supporting-text">${item.description}</div>`
: nothing}
${item.icon
? html`<span slot="start">${item.icon}</span>`
: item.iconPath
? html`<ha-svg-icon
slot="start"
.path=${item.iconPath}
></ha-svg-icon>`
: nothing}
${this.tooltipDescription && item.description
? html`<ha-svg-icon
tabindex="0"
id=${`description-tooltip-${item.key}`}
slot="end"
.path=${mdiInformationOutline}
@click=${stopPropagation}
></ha-svg-icon>
<ha-tooltip
.for=${`description-tooltip-${item.key}`}
@wa-show=${stopPropagation}
@wa-hide=${stopPropagation}
@wa-after-hide=${stopPropagation}
@wa-after-show=${stopPropagation}
>${item.description}</ha-tooltip
> `
: nothing}
<ha-svg-icon
slot="end"
class="plus"
.path=${mdiPlus}
></ha-svg-icon>
</ha-md-list-item>
`
)}
</ha-md-list>
`;
}
private _renderTarget = memoizeOne((target?: Target) => {
if (!target) {
return nothing;
}
return html`<div class="selected-target">
${this._getSelectedTargetIcon(target[0], target[1])}
<div class="label">${target[2]}</div>
</div>`;
});
private _getSelectedTargetIcon(
targetType: string,
targetId: string | undefined
): TemplateResult | typeof nothing {
if (!targetId) {
return nothing;
}
if (targetType === "floor") {
return html`<ha-floor-icon
.floor=${this.hass.floors[targetId]}
></ha-floor-icon>`;
}
if (targetType === "area" && this.hass.areas[targetId]) {
const area = this.hass.areas[targetId];
if (area.icon) {
return html`<ha-icon .icon=${area.icon}></ha-icon>`;
}
return html`<ha-svg-icon .path=${mdiTextureBox}></ha-svg-icon>`;
}
if (targetType === "device" && this.hass.devices[targetId]) {
const device = this.hass.devices[targetId];
const configEntry = device.primary_config_entry
? this.configEntryLookup[device.primary_config_entry]
: undefined;
const domain = configEntry?.domain;
if (domain) {
return html`<ha-domain-icon
slot="start"
.hass=${this.hass}
.domain=${domain}
brand-fallback
></ha-domain-icon>`;
}
}
if (targetType === "entity" && this.hass.states[targetId]) {
const stateObj = this.hass.states[targetId];
if (stateObj) {
return html`<state-badge
.stateObj=${stateObj}
.hass=${this.hass}
.stateColor=${false}
></state-badge>`;
}
}
if (targetType === "label") {
const label = this.getLabel(targetId);
if (label?.icon) {
return html`<ha-icon .icon=${label.icon}></ha-icon>`;
}
return html`<ha-svg-icon .path=${mdiLabel}></ha-svg-icon>`;
}
return nothing;
}
private _selected(ev) {
const item = ev.currentTarget;
fireEvent(this, "value-changed", {
value: item.value,
});
}
@eventOptions({ passive: true })
private _onItemsScroll(ev) {
const top = ev.target.scrollTop ?? 0;
this._itemsScrolled = top > 0;
}
public override scrollTo(options?: ScrollToOptions): void;
public override scrollTo(x: number, y: number): void;
public override scrollTo(
xOrOptions?: number | ScrollToOptions,
y?: number
): void {
if (typeof xOrOptions === "number") {
this._itemsDiv?.scrollTo(xOrOptions, y!);
} else {
this._itemsDiv?.scrollTo(xOrOptions);
}
}
static styles = css`
:host {
display: flex;
}
:host([scrollable]) .items {
overflow: auto;
}
.items {
display: flex;
flex-direction: column;
flex: 1;
}
.items.blank {
border-radius: var(--ha-border-radius-xl);
background-color: var(--ha-color-surface-default);
align-items: center;
color: var(--ha-color-text-secondary);
padding: var(--ha-space-0);
margin: var(--ha-space-0) var(--ha-space-4)
max(var(--safe-area-inset-bottom), var(--ha-space-3));
line-height: var(--ha-line-height-expanded);
justify-content: center;
}
.items.error {
background-color: var(--ha-color-fill-danger-quiet-resting);
color: var(--ha-color-on-danger-normal);
}
.items ha-md-list {
--md-list-item-two-line-container-height: var(--ha-space-12);
--md-list-item-leading-space: var(--ha-space-3);
--md-list-item-trailing-space: var(--md-list-item-leading-space);
--md-list-item-bottom-space: var(--ha-space-2);
--md-list-item-top-space: var(--md-list-item-bottom-space);
--md-list-item-supporting-text-font: var(--ha-font-family-body);
--ha-md-list-item-gap: var(--ha-space-3);
gap: var(--ha-space-2);
padding: var(--ha-space-0) var(--ha-space-4);
}
.items ha-md-list ha-md-list-item {
border-radius: var(--ha-border-radius-lg);
border: 1px solid var(--ha-color-border-neutral-quiet);
}
.items ha-md-list {
padding-bottom: max(var(--safe-area-inset-bottom), var(--ha-space-3));
}
.items .item-headline {
display: flex;
align-items: center;
gap: var(--ha-space-2);
min-height: var(--ha-space-9);
flex-wrap: wrap;
}
.items-title {
position: sticky;
display: flex;
align-items: center;
font-weight: var(--ha-font-weight-medium);
padding-top: var(--ha-space-2);
padding-bottom: var(--ha-space-2);
padding-inline-start: var(--ha-space-8);
padding-inline-end: var(--ha-space-3);
top: 0;
z-index: 1;
background-color: var(--card-background-color);
}
ha-bottom-sheet .items-title {
padding-top: var(--ha-space-3);
}
.scrolled .items-title:first-of-type {
box-shadow: var(--bar-box-shadow);
border-bottom: 1px solid var(--ha-color-border-neutral-quiet);
}
ha-icon-next {
width: var(--ha-space-6);
}
ha-svg-icon.plus {
color: var(--primary-color);
}
.selected-target {
display: inline-flex;
gap: var(--ha-space-1);
justify-content: center;
align-items: center;
border-radius: var(--ha-border-radius-md);
background: var(--ha-color-fill-neutral-normal-resting);
padding: 0 var(--ha-space-2) 0 var(--ha-space-1);
color: var(--ha-color-on-neutral-normal);
overflow: hidden;
}
.selected-target .label {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.selected-target ha-icon,
.selected-target ha-svg-icon,
.selected-target state-badge,
.selected-target ha-domain-icon {
display: flex;
padding: var(--ha-space-1) 0;
}
.selected-target state-badge {
--mdc-icon-size: 24px;
}
.selected-target state-badge,
.selected-target ha-floor-icon {
display: flex;
height: 32px;
width: 32px;
align-items: center;
}
.selected-target ha-domain-icon {
filter: grayscale(100%);
}
`;
}
declare global {
interface HTMLElementTagNameMap {
"ha-automation-add-items": HaAutomationAddItems;
}
}

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