mirror of
https://github.com/home-assistant/frontend.git
synced 2026-09-16 05:49:19 +00:00
Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6ccc78114c |
@@ -36,7 +36,7 @@ jobs:
|
||||
python-version: ${{ env.PYTHON_VERSION }}
|
||||
|
||||
- name: Verify version
|
||||
uses: home-assistant/actions/helpers/verify-version@58bff37c8947f690ace498be413a9b78d6f30f93 # master
|
||||
uses: home-assistant/actions/helpers/verify-version@a7c616ce81ccda50150bf1595786c71b1883fabb # master
|
||||
|
||||
- name: Setup Node and install
|
||||
uses: ./.github/actions/setup
|
||||
|
||||
@@ -12,29 +12,19 @@ const CONNECTIONS: ModbusConnection[] = [
|
||||
{
|
||||
endpoint: ["serial", RS485_PORT],
|
||||
connected: true,
|
||||
source: "config_entry",
|
||||
units: { [FLEXIT_ENTRY_ID]: [1], [SOLAREDGE_ENTRY_ID]: [2] },
|
||||
},
|
||||
{
|
||||
endpoint: ["tcp", "192.168.1.42", 502],
|
||||
connected: true,
|
||||
source: "config_entry",
|
||||
units: { [FRONIUS_ENTRY_ID]: [1] },
|
||||
},
|
||||
// A second inverter behind a gateway that is not answering
|
||||
{
|
||||
endpoint: ["tcp", "modbus-gateway.local", 502],
|
||||
connected: false,
|
||||
source: "config_entry",
|
||||
units: { [FRONIUS_ENTRY_ID]: [2] },
|
||||
},
|
||||
// A YAML hub keeps a link of its own to a device an integration also talks to
|
||||
{
|
||||
endpoint: ["tcp", "192.168.1.42", 502],
|
||||
connected: true,
|
||||
source: "yaml",
|
||||
units: { modbus_hub: [10, 11] },
|
||||
},
|
||||
];
|
||||
|
||||
export const mockModbus = (hass: MockHomeAssistant) => {
|
||||
|
||||
@@ -358,12 +358,6 @@ export const mockHassioSupervisor = (hass: MockHomeAssistant) => {
|
||||
return data;
|
||||
}
|
||||
|
||||
if (msg.endpoint === "/ingress/panels") {
|
||||
// The navigation picker waits for this collection before it enables
|
||||
// itself, so it has to answer even when no add-on exposes a panel.
|
||||
return { panels: {} };
|
||||
}
|
||||
|
||||
if (msg.endpoint === "/store/reload") {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -10,7 +10,6 @@ import { applyThemesOnElement } from "../../src/common/dom/apply_themes_on_eleme
|
||||
import { dynamicElement } from "../../src/common/dom/dynamic-element-directive";
|
||||
import type { HASSDomEvent } from "../../src/common/dom/fire_event";
|
||||
import { setDirectionStyles } from "../../src/common/util/compute_rtl";
|
||||
import { NO_KIOSK_ELEMENTS_HIDDEN } from "../../src/data/kiosk_mode";
|
||||
import "../../src/components/ha-button";
|
||||
import "../../src/components/ha-drawer";
|
||||
import { HaExpansionPanel } from "../../src/components/ha-expansion-panel";
|
||||
@@ -634,7 +633,6 @@ class HaGallery extends LitElement {
|
||||
floors: {},
|
||||
hassUrl: (path) => path,
|
||||
kioskMode: false,
|
||||
kioskElementsHidden: NO_KIOSK_ELEMENTS_HIDDEN,
|
||||
language: "en",
|
||||
loadBackendTranslation: async () => galleryLocalize,
|
||||
loadFragmentTranslation: async () => undefined,
|
||||
|
||||
@@ -57,7 +57,6 @@ This component is based on the webawesome input component.
|
||||
| invalid | Boolean | false | Marks the input as invalid. |
|
||||
| inset-label | Boolean | false | Uses an inset label style where the label stays inside the input. |
|
||||
| validation-message | String | "" | Custom validation message shown when the input is invalid. |
|
||||
| input-id | String | - | Sets the native input's `id` and associated label `for` on the inner `wa-input`. Defaults to `"input"`. |
|
||||
| pattern | String | - | A regular expression pattern to validate input against. |
|
||||
| minlength | Number | - | The minimum length of input that will be considered valid. |
|
||||
| maxlength | Number | - | The maximum length of input that will be considered valid. |
|
||||
|
||||
@@ -21,8 +21,6 @@ import "../../../../src/components/ha-settings-row";
|
||||
import type { BlueprintInput } from "../../../../src/data/blueprint";
|
||||
import type { DeviceRegistryEntry } from "../../../../src/data/device/device_registry";
|
||||
import type { LabelRegistryEntry } from "../../../../src/data/label/label_registry";
|
||||
import { StatisticMeanType } from "../../../../src/data/recorder";
|
||||
import type { SerialPort } from "../../../../src/data/usb";
|
||||
import {
|
||||
showDialog,
|
||||
type ShowDialogParams,
|
||||
@@ -295,72 +293,9 @@ const LABELS: LabelRegistryEntry[] = [
|
||||
},
|
||||
];
|
||||
|
||||
const serialPort = (port: Partial<SerialPort>): SerialPort => ({
|
||||
device: "/dev/ttyUSB0",
|
||||
resolved_device: null,
|
||||
serial_number: null,
|
||||
manufacturer: null,
|
||||
description: null,
|
||||
matching_integrations: [],
|
||||
present: true,
|
||||
...port,
|
||||
});
|
||||
|
||||
// One port per section the picker groups by, relative to the "zha" domain the
|
||||
// serial port selector below is given as its flow context
|
||||
const SERIAL_PORTS: SerialPort[] = [
|
||||
serialPort({
|
||||
device:
|
||||
"/dev/serial/by-id/usb-Nabu_Casa_SkyConnect_v1.0_9e2adbd75b8beb119fe564a0f320645d-if00-port0",
|
||||
description: "SkyConnect v1.0",
|
||||
manufacturer: "Nabu Casa",
|
||||
serial_number: "9e2adbd75b8beb119fe564a0f320645d",
|
||||
vid: "10C4",
|
||||
pid: "EA60",
|
||||
matching_integrations: ["zha"],
|
||||
}),
|
||||
serialPort({
|
||||
device: "esphome-hass://01JQ8Z5X9WQ0/?port_name=UART0",
|
||||
}),
|
||||
serialPort({
|
||||
device: "socket://192.168.1.10:6638",
|
||||
}),
|
||||
serialPort({
|
||||
device: "/dev/serial/by-id/usb-FTDI_FT232R_USB_UART_AB0KVD1L-if00-port0",
|
||||
description: "FT232R USB UART",
|
||||
manufacturer: "FTDI",
|
||||
serial_number: "AB0KVD1L",
|
||||
vid: "0403",
|
||||
pid: "6001",
|
||||
}),
|
||||
serialPort({
|
||||
device: "/dev/ttyS0",
|
||||
description: "ttyS0",
|
||||
manufacturer: "Intel",
|
||||
}),
|
||||
serialPort({ device: "/dev/ttyAMA0" }),
|
||||
serialPort({
|
||||
device:
|
||||
"/dev/serial/by-id/usb-Silicon_Labs_CP2102_USB_to_UART_Bridge_Controller_0001-if00-port0",
|
||||
description: "CP2102 USB to UART Bridge Controller",
|
||||
manufacturer: "Silicon Labs",
|
||||
serial_number: "0001",
|
||||
vid: "10C4",
|
||||
pid: "EA60",
|
||||
matching_integrations: ["matter"],
|
||||
}),
|
||||
];
|
||||
|
||||
const SCHEMAS: {
|
||||
name: string;
|
||||
input: Record<
|
||||
string,
|
||||
| (BlueprintInput & {
|
||||
required?: boolean;
|
||||
context?: Record<string, unknown>;
|
||||
})
|
||||
| null
|
||||
>;
|
||||
input: Record<string, (BlueprintInput & { required?: boolean }) | null>;
|
||||
}[] = [
|
||||
{
|
||||
name: "One of each",
|
||||
@@ -384,14 +319,8 @@ const SCHEMAS: {
|
||||
selector: { config_entry: {} },
|
||||
},
|
||||
duration: { name: "Duration", selector: { duration: {} } },
|
||||
offset: { name: "Offset", selector: { offset: { enable_day: true } } },
|
||||
app: { name: "App", selector: { app: {} } },
|
||||
serial_port: {
|
||||
name: "Serial port",
|
||||
selector: { serial_port: {} },
|
||||
// A config flow passes its own domain as context, which is what the
|
||||
// picker groups recommended and not recommended ports by
|
||||
context: { domain: "zha" },
|
||||
},
|
||||
number_box: {
|
||||
name: "Number Box",
|
||||
selector: {
|
||||
@@ -576,99 +505,6 @@ const SCHEMAS: {
|
||||
},
|
||||
},
|
||||
},
|
||||
addon: { name: "Add-on", selector: { addon: {} } },
|
||||
areas_display: {
|
||||
name: "Areas display",
|
||||
selector: { areas_display: {} },
|
||||
},
|
||||
assist_pipeline: {
|
||||
name: "Assist pipeline",
|
||||
selector: { assist_pipeline: {} },
|
||||
},
|
||||
automation_behavior: {
|
||||
name: "Automation behavior",
|
||||
selector: { automation_behavior: { mode: "trigger" } },
|
||||
},
|
||||
backup_location: {
|
||||
name: "Backup location",
|
||||
selector: { backup_location: {} },
|
||||
},
|
||||
button_toggle: {
|
||||
name: "Button toggle",
|
||||
selector: {
|
||||
button_toggle: {
|
||||
options: [
|
||||
{ label: "Left", value: "left" },
|
||||
{ label: "Center", value: "center" },
|
||||
{ label: "Right", value: "right" },
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
condition: { name: "Condition", selector: { condition: {} } },
|
||||
conversation_agent: {
|
||||
name: "Conversation agent",
|
||||
selector: { conversation_agent: {} },
|
||||
},
|
||||
country: {
|
||||
name: "Country",
|
||||
selector: { country: { countries: ["DE", "GB", "NL", "US"] } },
|
||||
},
|
||||
entity_name: {
|
||||
name: "Entity name",
|
||||
selector: { entity_name: { entity_id: "light.bedroom" } },
|
||||
},
|
||||
file: {
|
||||
name: "File",
|
||||
selector: { file: { accept: "image/png,image/jpeg" } },
|
||||
},
|
||||
language: { name: "Language", selector: { language: {} } },
|
||||
navigation: { name: "Navigation", selector: { navigation: {} } },
|
||||
numeric_threshold: {
|
||||
name: "Numeric threshold",
|
||||
selector: { numeric_threshold: {} },
|
||||
},
|
||||
period: {
|
||||
name: "Period",
|
||||
selector: {
|
||||
period: {
|
||||
options: ["today", "yesterday", "this_week", "this_month"],
|
||||
},
|
||||
},
|
||||
// Without a value that matches one of the options the selector offers
|
||||
// its custom-period editor instead of the list
|
||||
default: { calendar: { period: "day" } },
|
||||
},
|
||||
selector: {
|
||||
name: "Selector",
|
||||
selector: { selector: {} },
|
||||
default: { text: {} },
|
||||
},
|
||||
state_class: { name: "State class", selector: { state_class: {} } },
|
||||
statistic: { name: "Statistic", selector: { statistic: {} } },
|
||||
stt: { name: "Speech-to-text", selector: { stt: {} } },
|
||||
theme: { name: "Theme", selector: { theme: {} } },
|
||||
timezone: { name: "Time zone", selector: { timezone: {} } },
|
||||
trigger: { name: "Trigger", selector: { trigger: {} } },
|
||||
tts: { name: "Text-to-speech", selector: { tts: {} } },
|
||||
tts_voice: {
|
||||
name: "Text-to-speech voice",
|
||||
selector: { tts_voice: { engineId: "tts.cloud", language: "en-US" } },
|
||||
},
|
||||
ui_action: { name: "UI action", selector: { ui_action: {} } },
|
||||
ui_clock_date_format: {
|
||||
name: "Clock date format",
|
||||
selector: { ui_clock_date_format: {} },
|
||||
},
|
||||
ui_color: { name: "UI color", selector: { ui_color: {} } },
|
||||
ui_state_content: {
|
||||
name: "State content",
|
||||
selector: { ui_state_content: { entity_id: "light.bedroom" } },
|
||||
},
|
||||
ui_time_format: {
|
||||
name: "Time format",
|
||||
selector: { ui_time_format: {} },
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -770,98 +606,8 @@ class DemoHaSelector extends LitElement implements ProvideHassElement {
|
||||
mockFloorRegistry(hass, FLOORS);
|
||||
mockLabelRegistry(hass, LABELS);
|
||||
mockHassioSupervisor(hass);
|
||||
hass.addTranslations({
|
||||
"component.matter.title": "Matter",
|
||||
"component.zha.title": "Zigbee Home Automation",
|
||||
});
|
||||
hass.updateHass({
|
||||
config: {
|
||||
...hass.config,
|
||||
components: [...hass.config.components, "usb"],
|
||||
},
|
||||
});
|
||||
hass.mockWS("auth/sign_path", (params) => params);
|
||||
hass.mockWS("media_player/browse_media", this._browseMedia);
|
||||
hass.mockWS("usb/list_serial_ports", () => SERIAL_PORTS);
|
||||
hass.mockWS("recorder/list_statistic_ids", () => [
|
||||
{
|
||||
statistic_id: "sensor.energy_consumption",
|
||||
statistics_unit_of_measurement: "kWh",
|
||||
source: "recorder",
|
||||
name: null,
|
||||
has_sum: true,
|
||||
mean_type: StatisticMeanType.NONE,
|
||||
unit_class: "energy",
|
||||
},
|
||||
{
|
||||
statistic_id: "sensor.outside_temperature",
|
||||
statistics_unit_of_measurement: "\u00b0C",
|
||||
source: "recorder",
|
||||
name: null,
|
||||
has_sum: false,
|
||||
mean_type: StatisticMeanType.ARITHMETIC,
|
||||
unit_class: "temperature",
|
||||
},
|
||||
]);
|
||||
hass.mockWS("assist_pipeline/pipeline/list", () => ({
|
||||
pipelines: [
|
||||
{
|
||||
id: "pipeline_home",
|
||||
name: "Home Assistant",
|
||||
language: "en",
|
||||
conversation_engine: "conversation.home_assistant",
|
||||
conversation_language: "en",
|
||||
stt_engine: "stt.cloud",
|
||||
stt_language: "en-US",
|
||||
tts_engine: "tts.cloud",
|
||||
tts_language: "en-US",
|
||||
tts_voice: "JennyNeural",
|
||||
wake_word_entity: null,
|
||||
wake_word_id: null,
|
||||
},
|
||||
],
|
||||
preferred_pipeline: "pipeline_home",
|
||||
}));
|
||||
hass.mockWS("conversation/agent/list", () => ({
|
||||
agents: [
|
||||
{
|
||||
id: "conversation.home_assistant",
|
||||
name: "Home Assistant",
|
||||
supported_languages: "*",
|
||||
},
|
||||
{
|
||||
id: "conversation.openai",
|
||||
name: "OpenAI Conversation",
|
||||
supported_languages: ["en"],
|
||||
},
|
||||
],
|
||||
}));
|
||||
hass.mockWS("stt/engine/list", () => ({
|
||||
providers: [
|
||||
{
|
||||
engine_id: "stt.cloud",
|
||||
name: "Home Assistant Cloud",
|
||||
supported_languages: ["en-US"],
|
||||
deprecated: false,
|
||||
},
|
||||
],
|
||||
}));
|
||||
hass.mockWS("tts/engine/list", () => ({
|
||||
providers: [
|
||||
{
|
||||
engine_id: "tts.cloud",
|
||||
name: "Home Assistant Cloud",
|
||||
supported_languages: ["en-US"],
|
||||
deprecated: false,
|
||||
},
|
||||
],
|
||||
}));
|
||||
hass.mockWS("tts/engine/voices", () => ({
|
||||
voices: [
|
||||
{ voice_id: "JennyNeural", name: "Jenny" },
|
||||
{ voice_id: "GuyNeural", name: "Guy" },
|
||||
],
|
||||
}));
|
||||
}
|
||||
|
||||
public provideHass(el) {
|
||||
@@ -1031,7 +777,6 @@ class DemoHaSelector extends LitElement implements ProvideHassElement {
|
||||
<ha-selector
|
||||
.hass=${this.hass}
|
||||
.selector=${value!.selector}
|
||||
.context=${value!.context}
|
||||
.key=${key}
|
||||
.label=${this._label ? value!.name : undefined}
|
||||
.value=${data[key] ?? value!.default}
|
||||
|
||||
@@ -21,8 +21,7 @@ const ENTITIES = [
|
||||
state: "on",
|
||||
attributes: {
|
||||
friendly_name: "Dining Room",
|
||||
supported_color_modes: ["brightness"],
|
||||
color_mode: "brightness",
|
||||
supported_features: 1,
|
||||
brightness: 100,
|
||||
},
|
||||
},
|
||||
@@ -31,7 +30,7 @@ const ENTITIES = [
|
||||
state: "off",
|
||||
attributes: {
|
||||
friendly_name: "Dining Room",
|
||||
supported_color_modes: ["brightness"],
|
||||
supported_features: 1,
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -39,7 +38,7 @@ const ENTITIES = [
|
||||
state: "unavailable",
|
||||
attributes: {
|
||||
friendly_name: "Lost Light",
|
||||
supported_color_modes: ["brightness"],
|
||||
supported_features: 1,
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
@@ -11,7 +11,6 @@ import { LawnMowerEntityFeature } from "../../../../src/data/lawn_mower";
|
||||
const ALL_FEATURES =
|
||||
LawnMowerEntityFeature.START_MOWING +
|
||||
LawnMowerEntityFeature.PAUSE +
|
||||
LawnMowerEntityFeature.STOP +
|
||||
LawnMowerEntityFeature.DOCK;
|
||||
|
||||
const ENTITIES = [
|
||||
@@ -50,14 +49,6 @@ const ENTITIES = [
|
||||
supported_features: ALL_FEATURES,
|
||||
},
|
||||
},
|
||||
{
|
||||
entity_id: "lawn_mower.idle",
|
||||
state: "idle",
|
||||
attributes: {
|
||||
friendly_name: "Idle",
|
||||
supported_features: ALL_FEATURES,
|
||||
},
|
||||
},
|
||||
{
|
||||
entity_id: "lawn_mower.error",
|
||||
state: "error",
|
||||
|
||||
+11
-10
@@ -39,7 +39,7 @@
|
||||
"license": "Apache-2.0",
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "8.0.5",
|
||||
"@babel/runtime": "8.0.0",
|
||||
"@braintree/sanitize-url": "7.1.2",
|
||||
"@codemirror/autocomplete": "6.20.3",
|
||||
"@codemirror/commands": "6.11.0",
|
||||
@@ -67,7 +67,7 @@
|
||||
"@fullcalendar/list": "6.1.21",
|
||||
"@fullcalendar/luxon3": "6.1.21",
|
||||
"@fullcalendar/timegrid": "6.1.21",
|
||||
"@home-assistant/webawesome": "3.7.0-ha.1",
|
||||
"@home-assistant/webawesome": "3.7.0-ha.0",
|
||||
"@lezer/highlight": "1.2.3",
|
||||
"@lit-labs/motion": "1.1.0",
|
||||
"@lit-labs/observers": "2.1.0",
|
||||
@@ -84,6 +84,7 @@
|
||||
"@mdi/svg": "7.4.47",
|
||||
"@replit/codemirror-indentation-markers": "6.5.3",
|
||||
"@swc/helpers": "0.5.23",
|
||||
"@thomasloven/round-slider": "0.6.0",
|
||||
"@tsparticles/engine": "4.4.0",
|
||||
"@tsparticles/preset-links": "4.4.0",
|
||||
"@vibrant/color": "4.0.4",
|
||||
@@ -114,7 +115,7 @@
|
||||
"lit-html": "3.3.3",
|
||||
"luxon": "3.7.2",
|
||||
"maplibre-gl": "5.24.0",
|
||||
"marked": "18.0.12",
|
||||
"marked": "18.0.11",
|
||||
"memoize-one": "6.0.0",
|
||||
"node-vibrant": "4.0.4",
|
||||
"object-hash": "3.0.0",
|
||||
@@ -138,10 +139,10 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@ampproject/remapping": "2.3.0",
|
||||
"@babel/core": "8.0.5",
|
||||
"@babel/core": "8.0.1",
|
||||
"@babel/helper-define-polyfill-provider": "1.0.0",
|
||||
"@babel/plugin-transform-runtime": "8.0.1",
|
||||
"@babel/preset-env": "8.0.5",
|
||||
"@babel/preset-env": "8.0.2",
|
||||
"@bundle-stats/plugin-webpack-filter": "4.22.3",
|
||||
"@eslint/js": "10.0.1",
|
||||
"@gfx/zopfli": "1.0.15",
|
||||
@@ -152,7 +153,7 @@
|
||||
"@octokit/rest": "22.0.1",
|
||||
"@playwright/test": "1.62.1",
|
||||
"@rsdoctor/rspack-plugin": "1.6.3",
|
||||
"@rspack/core": "2.2.3",
|
||||
"@rspack/core": "2.2.2",
|
||||
"@rspack/dev-server": "2.2.1",
|
||||
"@types/babel__plugin-transform-runtime": "7.9.5",
|
||||
"@types/chromecast-caf-receiver": "6.0.26",
|
||||
@@ -194,10 +195,10 @@
|
||||
"html-minifier-terser": "7.2.0",
|
||||
"husky": "9.1.7",
|
||||
"jsdom": "30.0.1",
|
||||
"jszip": "3.10.2",
|
||||
"jszip": "3.10.1",
|
||||
"license-checker-rseidelsohn": "5.0.1",
|
||||
"lightningcss": "1.33.0",
|
||||
"lint-staged": "17.5.1",
|
||||
"lint-staged": "17.5.0",
|
||||
"lit-analyzer": "2.0.3",
|
||||
"lodash.merge": "4.6.2",
|
||||
"lodash.template": "4.18.1",
|
||||
@@ -212,8 +213,8 @@
|
||||
"terser-webpack-plugin": "5.6.1",
|
||||
"ts-lit-plugin": "2.0.2",
|
||||
"typescript": "6.0.3",
|
||||
"typescript-eslint": "8.70.0",
|
||||
"vite": "8.3.0",
|
||||
"typescript-eslint": "8.69.0",
|
||||
"vite": "8.2.2",
|
||||
"vite-tsconfig-paths": "6.1.1",
|
||||
"vitest": "5.0.0",
|
||||
"webpack-stats-plugin": "1.1.3",
|
||||
|
||||
@@ -9,34 +9,36 @@ export const createDurationData = (
|
||||
}
|
||||
if (typeof duration !== "object") {
|
||||
if (typeof duration === "string" || isNaN(duration)) {
|
||||
const durationString = duration.toString().trim();
|
||||
// A leading "-" negates the whole period, matching
|
||||
// `cv.time_period_str` in core.
|
||||
const negative = durationString[0] === "-";
|
||||
const parts = durationString
|
||||
.split(":")
|
||||
.map((part) =>
|
||||
negative && part ? -Math.abs(Number(part)) : Number(part)
|
||||
);
|
||||
|
||||
const parts = duration?.toString().split(":") || [];
|
||||
if (parts.length === 1) {
|
||||
return { seconds: parts[0] };
|
||||
return { seconds: Number(parts[0]) };
|
||||
}
|
||||
if (parts.length > 3) {
|
||||
return undefined;
|
||||
}
|
||||
const seconds = parts[2] || 0;
|
||||
const secondsWhole = Math.trunc(seconds);
|
||||
const seconds = Number(parts[2]) || 0;
|
||||
const seconds_whole = Math.floor(seconds);
|
||||
return {
|
||||
hours: parts[0] || 0,
|
||||
minutes: parts[1] || 0,
|
||||
seconds: secondsWhole,
|
||||
milliseconds: Math.trunc(
|
||||
Number((seconds - secondsWhole).toFixed(4)) * 1000
|
||||
hours: Number(parts[0]) || 0,
|
||||
minutes: Number(parts[1]) || 0,
|
||||
seconds: seconds_whole,
|
||||
milliseconds: Math.floor(
|
||||
Number((seconds - seconds_whole).toFixed(4)) * 1000
|
||||
),
|
||||
};
|
||||
}
|
||||
return { seconds: duration };
|
||||
}
|
||||
return duration;
|
||||
if (!("days" in duration)) {
|
||||
return duration;
|
||||
}
|
||||
const { days, minutes, seconds, milliseconds } = duration;
|
||||
let hours = duration.hours || 0;
|
||||
hours = (hours || 0) + (days || 0) * 24;
|
||||
return {
|
||||
hours,
|
||||
minutes,
|
||||
seconds,
|
||||
milliseconds,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
import type { HaDurationData } from "../../components/ha-duration-input";
|
||||
|
||||
export const durationValueToData = (
|
||||
value?: HaDurationData | string | number
|
||||
): HaDurationData | undefined => {
|
||||
if (typeof value === "number") {
|
||||
return { seconds: value };
|
||||
}
|
||||
if (typeof value === "string") {
|
||||
const negative = value.trim()[0] === "-";
|
||||
const parts = value
|
||||
.split(":")
|
||||
.map((p) => (negative && p ? -Math.abs(Number(p)) : Number(p)));
|
||||
|
||||
if (parts.length === 1) {
|
||||
return { seconds: parts[0] };
|
||||
}
|
||||
if (parts.length === 2) {
|
||||
return { hours: parts[0], minutes: parts[1] };
|
||||
}
|
||||
if (parts.length === 3) {
|
||||
return {
|
||||
hours: parts[0],
|
||||
minutes: parts[1],
|
||||
seconds: parts[2],
|
||||
};
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
return value;
|
||||
};
|
||||
@@ -29,7 +29,7 @@ export const FIXED_DOMAIN_STATES = {
|
||||
fan: ["on", "off"],
|
||||
humidifier: ["on", "off"],
|
||||
input_boolean: ["on", "off"],
|
||||
lawn_mower: ["error", "paused", "mowing", "returning", "docked", "idle"],
|
||||
lawn_mower: ["error", "paused", "mowing", "returning", "docked"],
|
||||
light: ["on", "off"],
|
||||
lock: [
|
||||
"jammed",
|
||||
|
||||
@@ -36,7 +36,7 @@ export function stateActive(stateObj: HassEntity, state?: string): boolean {
|
||||
case "person":
|
||||
return compareState !== "not_home";
|
||||
case "lawn_mower":
|
||||
return !["docked", "paused", "idle"].includes(compareState);
|
||||
return !["docked", "paused"].includes(compareState);
|
||||
case "lock":
|
||||
return compareState !== "locked";
|
||||
case "media_player":
|
||||
|
||||
@@ -1,48 +0,0 @@
|
||||
import {
|
||||
DateFormat,
|
||||
FirstWeekday,
|
||||
NumberFormat,
|
||||
TimeFormat,
|
||||
TimeZone,
|
||||
} from "../../data/translation";
|
||||
import { translationMetadata } from "../../resources/translations-metadata";
|
||||
import type { HomeAssistantInternationalization } from "../../types";
|
||||
import type { LocalizeFunc } from "./localize";
|
||||
|
||||
/**
|
||||
* Builds an internationalization context value for the "lite" localize mode
|
||||
* used before a `hass` object exists. Locale falls back to browser defaults and
|
||||
* the lazy loaders are stubs that warn when called.
|
||||
*/
|
||||
export const buildLiteInternationalization = (
|
||||
language: string,
|
||||
localize: LocalizeFunc
|
||||
): HomeAssistantInternationalization => ({
|
||||
language,
|
||||
selectedLanguage: language,
|
||||
locale: {
|
||||
language,
|
||||
number_format: NumberFormat.language,
|
||||
time_format: TimeFormat.language,
|
||||
date_format: DateFormat.language,
|
||||
time_zone: TimeZone.local,
|
||||
first_weekday: FirstWeekday.language,
|
||||
},
|
||||
localize,
|
||||
translationMetadata,
|
||||
loadBackendTranslation: async (category, integrations, configFlow) => {
|
||||
// eslint-disable-next-line no-console
|
||||
console.warn(
|
||||
"loadBackendTranslation called in lite i18n mode; backend strings are not available",
|
||||
{ category, integrations, configFlow }
|
||||
);
|
||||
return localize;
|
||||
},
|
||||
loadFragmentTranslation: async (fragment) => {
|
||||
// eslint-disable-next-line no-console
|
||||
console.warn(
|
||||
`loadFragmentTranslation("${fragment}") called in lite i18n mode; fragment not loaded`
|
||||
);
|
||||
return undefined;
|
||||
},
|
||||
});
|
||||
@@ -44,8 +44,7 @@ export class StateHistoryChartTimeline extends LitElement {
|
||||
@property({ attribute: "show-names", type: Boolean }) public showNames = true;
|
||||
|
||||
// Render each row's name inside the plot (under its bar) instead of in a
|
||||
// left-hand category-label column. Opt-in; used by the history panel and
|
||||
// history-graph card.
|
||||
// left-hand category-label column. Opt-in; used by the history panel.
|
||||
@property({ attribute: "inside-labels", type: Boolean })
|
||||
public insideLabels = false;
|
||||
|
||||
@@ -80,9 +79,7 @@ export class StateHistoryChartTimeline extends LitElement {
|
||||
.options=${this._chartOptions}
|
||||
.height=${`${
|
||||
this.data.length *
|
||||
(this.insideLabels && (this.chunked || this.showNames)
|
||||
? ROW_HEIGHT_INSIDE_LABELS
|
||||
: ROW_HEIGHT) +
|
||||
(this.insideLabels ? ROW_HEIGHT_INSIDE_LABELS : ROW_HEIGHT) +
|
||||
GRID_BOTTOM
|
||||
}px`}
|
||||
.data=${this._chartData as HaECSeries}
|
||||
|
||||
@@ -80,7 +80,7 @@ export class StateHistoryCharts extends LitElement {
|
||||
@property({ attribute: "show-names", type: Boolean }) public showNames = true;
|
||||
|
||||
// Render timeline row names inside the plot (under each bar) instead of in a
|
||||
// left-hand column. Opt-in; used by the history panel and history-graph card.
|
||||
// left-hand column. Opt-in; used by the history panel.
|
||||
@property({ attribute: "inside-labels", type: Boolean })
|
||||
public insideLabels = false;
|
||||
|
||||
|
||||
@@ -35,14 +35,6 @@ export class HaFilterChip extends FilterChip {
|
||||
`,
|
||||
];
|
||||
|
||||
protected getContainerClasses() {
|
||||
const classes = super.getContainerClasses();
|
||||
if (this.noLeadingIcon) {
|
||||
classes["has-icon"] = false;
|
||||
}
|
||||
return classes;
|
||||
}
|
||||
|
||||
protected renderLeadingIcon() {
|
||||
if (this.noLeadingIcon) {
|
||||
// eslint-disable-next-line lit/prefer-nothing
|
||||
|
||||
@@ -103,87 +103,69 @@ export class HaDurationInput extends LitElement {
|
||||
`;
|
||||
}
|
||||
|
||||
// Fold units we do not render into the smallest unit we do, so a value the
|
||||
// user cannot see is not silently dropped on the next edit.
|
||||
private get _data(): HaDurationData | undefined {
|
||||
if (!this.data) {
|
||||
return this.data;
|
||||
}
|
||||
const data = { ...this.data };
|
||||
if (!this.enableDay && data.days) {
|
||||
data.hours = (data.hours || 0) + data.days * 24;
|
||||
delete data.days;
|
||||
}
|
||||
if (!this.enableMillisecond && data.milliseconds) {
|
||||
data.seconds = (data.seconds || 0) + data.milliseconds / 1000;
|
||||
delete data.milliseconds;
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
private get _negative() {
|
||||
return (
|
||||
this._toggleNegative ||
|
||||
(this._data?.days
|
||||
? this._data.days < 0
|
||||
: this._data?.hours
|
||||
? this._data.hours < 0
|
||||
: this._data?.minutes
|
||||
? this._data.minutes < 0
|
||||
: this._data?.seconds
|
||||
? this._data.seconds < 0
|
||||
: this._data?.milliseconds
|
||||
? this._data.milliseconds < 0
|
||||
(this.data?.days
|
||||
? this.data.days < 0
|
||||
: this.data?.hours
|
||||
? this.data.hours < 0
|
||||
: this.data?.minutes
|
||||
? this.data.minutes < 0
|
||||
: this.data?.seconds
|
||||
? this.data.seconds < 0
|
||||
: this.data?.milliseconds
|
||||
? this.data.milliseconds < 0
|
||||
: false)
|
||||
);
|
||||
}
|
||||
|
||||
private get _days() {
|
||||
return this._data?.days
|
||||
return this.data?.days
|
||||
? this.allowNegative
|
||||
? Math.abs(Number(this._data.days))
|
||||
: Number(this._data.days)
|
||||
: this.required || this._data
|
||||
? Math.abs(Number(this.data.days))
|
||||
: Number(this.data.days)
|
||||
: this.required || this.data
|
||||
? 0
|
||||
: NaN;
|
||||
}
|
||||
|
||||
private get _hours() {
|
||||
return this._data?.hours
|
||||
return this.data?.hours
|
||||
? this.allowNegative
|
||||
? Math.abs(Number(this._data.hours))
|
||||
: Number(this._data.hours)
|
||||
: this.required || this._data
|
||||
? Math.abs(Number(this.data.hours))
|
||||
: Number(this.data.hours)
|
||||
: this.required || this.data
|
||||
? 0
|
||||
: NaN;
|
||||
}
|
||||
|
||||
private get _minutes() {
|
||||
return this._data?.minutes
|
||||
return this.data?.minutes
|
||||
? this.allowNegative
|
||||
? Math.abs(Number(this._data.minutes))
|
||||
: Number(this._data.minutes)
|
||||
: this.required || this._data
|
||||
? Math.abs(Number(this.data.minutes))
|
||||
: Number(this.data.minutes)
|
||||
: this.required || this.data
|
||||
? 0
|
||||
: NaN;
|
||||
}
|
||||
|
||||
private get _seconds() {
|
||||
return this._data?.seconds
|
||||
return this.data?.seconds
|
||||
? this.allowNegative
|
||||
? Math.abs(Number(this._data.seconds))
|
||||
: Number(this._data.seconds)
|
||||
: this.required || this._data
|
||||
? Math.abs(Number(this.data.seconds))
|
||||
: Number(this.data.seconds)
|
||||
: this.required || this.data
|
||||
? 0
|
||||
: NaN;
|
||||
}
|
||||
|
||||
private get _milliseconds() {
|
||||
return this._data?.milliseconds
|
||||
return this.data?.milliseconds
|
||||
? this.allowNegative
|
||||
? Math.abs(Number(this._data.milliseconds))
|
||||
: Number(this._data.milliseconds)
|
||||
: this.required || this._data
|
||||
? Math.abs(Number(this.data.milliseconds))
|
||||
: Number(this.data.milliseconds)
|
||||
: this.required || this.data
|
||||
? 0
|
||||
: NaN;
|
||||
}
|
||||
@@ -254,8 +236,8 @@ export class HaDurationInput extends LitElement {
|
||||
ev.stopPropagation();
|
||||
const negative = (ev.detail?.value || ev.target.value) === "-";
|
||||
this._toggleNegative = negative;
|
||||
if (this._data) {
|
||||
const value = { ...this._data };
|
||||
if (this.data) {
|
||||
const value = { ...this.data };
|
||||
FIELDS.forEach((t) => {
|
||||
if (value[t]) {
|
||||
value[t] = negative ? -Math.abs(value[t]) : Math.abs(value[t]);
|
||||
|
||||
@@ -42,6 +42,7 @@ const SELECTOR_FALLBACK_VALUES = {
|
||||
number: (selector) => selector.number?.min ?? 0,
|
||||
numeric_threshold: undefined,
|
||||
object: undefined,
|
||||
offset: undefined,
|
||||
period: undefined,
|
||||
qr_code: undefined,
|
||||
select: undefined,
|
||||
|
||||
@@ -74,6 +74,7 @@ const SELECTOR_INITIAL_VALUES = {
|
||||
};
|
||||
},
|
||||
object: (selector) => (selector.object?.multiple ? [] : ""),
|
||||
offset: () => ({ type: "none" }),
|
||||
period: undefined,
|
||||
qr_code: undefined,
|
||||
select: (selector) => {
|
||||
|
||||
@@ -57,7 +57,6 @@ export class HaFormString extends LitElement implements HaFormElement {
|
||||
.name=${this.schema.name}
|
||||
.autofocus=${!!this.schema.autofocus}
|
||||
.autocomplete=${this.schema.autocomplete}
|
||||
.inputId=${this.schema.name}
|
||||
.validationMessage=${
|
||||
this.schema.required
|
||||
? this.localize?.("ui.common.error_required")
|
||||
|
||||
@@ -1,16 +0,0 @@
|
||||
import { mdiArrowLeft, mdiArrowRight } from "@mdi/js";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { mainWindow } from "../common/dom/get_main_window";
|
||||
import { HaSvgIcon } from "./ha-svg-icon";
|
||||
|
||||
@customElement("ha-icon-arrow-next")
|
||||
export class HaIconArrowNext extends HaSvgIcon {
|
||||
@property() public override path =
|
||||
mainWindow.document.dir === "rtl" ? mdiArrowLeft : mdiArrowRight;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-icon-arrow-next": HaIconArrowNext;
|
||||
}
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
import { mdiArrowLeft, mdiArrowRight } from "@mdi/js";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { mainWindow } from "../common/dom/get_main_window";
|
||||
import { HaSvgIcon } from "./ha-svg-icon";
|
||||
|
||||
@customElement("ha-icon-arrow-prev")
|
||||
export class HaIconArrowPrev extends HaSvgIcon {
|
||||
@property() public override path =
|
||||
mainWindow.document.dir === "rtl" ? mdiArrowRight : mdiArrowLeft;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-icon-arrow-prev": HaIconArrowPrev;
|
||||
}
|
||||
}
|
||||
@@ -1,52 +0,0 @@
|
||||
import { mdiChevronDoubleLeft, mdiChevronDoubleRight } from "@mdi/js";
|
||||
import type { TemplateResult } from "lit";
|
||||
import { html, LitElement } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { mainWindow } from "../common/dom/get_main_window";
|
||||
import "./ha-icon-button";
|
||||
import { consumeLocalize } from "../common/decorators/consume-context-entry";
|
||||
import type { LocalizeFunc } from "../common/translations/localize";
|
||||
|
||||
@customElement("ha-icon-button-first")
|
||||
export class HaIconButtonFirst extends LitElement {
|
||||
@property({ type: Boolean }) public disabled = false;
|
||||
|
||||
@property() public label?: string;
|
||||
|
||||
@property() href?: string;
|
||||
|
||||
@property() target?: "_blank" | "_parent" | "_self" | "_top";
|
||||
|
||||
@property() rel?: string;
|
||||
|
||||
@property() download?: string;
|
||||
|
||||
@state() private _icon =
|
||||
mainWindow.document.dir === "rtl"
|
||||
? mdiChevronDoubleRight
|
||||
: mdiChevronDoubleLeft;
|
||||
|
||||
@state()
|
||||
@consumeLocalize()
|
||||
private _localize!: LocalizeFunc;
|
||||
|
||||
protected render(): TemplateResult {
|
||||
return html`
|
||||
<ha-icon-button
|
||||
.disabled=${this.disabled}
|
||||
.label=${this.label || this._localize("ui.common.first") || "First"}
|
||||
.path=${this._icon}
|
||||
.href=${this.href}
|
||||
.target=${this.target}
|
||||
.rel=${this.rel}
|
||||
.download=${this.download}
|
||||
></ha-icon-button>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-icon-button-first": HaIconButtonFirst;
|
||||
}
|
||||
}
|
||||
@@ -1,52 +0,0 @@
|
||||
import { mdiChevronDoubleLeft, mdiChevronDoubleRight } from "@mdi/js";
|
||||
import type { TemplateResult } from "lit";
|
||||
import { html, LitElement } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { mainWindow } from "../common/dom/get_main_window";
|
||||
import "./ha-icon-button";
|
||||
import { consumeLocalize } from "../common/decorators/consume-context-entry";
|
||||
import type { LocalizeFunc } from "../common/translations/localize";
|
||||
|
||||
@customElement("ha-icon-button-last")
|
||||
export class HaIconButtonLast extends LitElement {
|
||||
@property({ type: Boolean }) public disabled = false;
|
||||
|
||||
@property() public label?: string;
|
||||
|
||||
@property() href?: string;
|
||||
|
||||
@property() target?: "_blank" | "_parent" | "_self" | "_top";
|
||||
|
||||
@property() rel?: string;
|
||||
|
||||
@property() download?: string;
|
||||
|
||||
@state() private _icon =
|
||||
mainWindow.document.dir === "rtl"
|
||||
? mdiChevronDoubleLeft
|
||||
: mdiChevronDoubleRight;
|
||||
|
||||
@state()
|
||||
@consumeLocalize()
|
||||
private _localize!: LocalizeFunc;
|
||||
|
||||
protected render(): TemplateResult {
|
||||
return html`
|
||||
<ha-icon-button
|
||||
.disabled=${this.disabled}
|
||||
.label=${this.label || this._localize("ui.common.last") || "Last"}
|
||||
.path=${this._icon}
|
||||
.href=${this.href}
|
||||
.target=${this.target}
|
||||
.rel=${this.rel}
|
||||
.download=${this.download}
|
||||
></ha-icon-button>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-icon-button-last": HaIconButtonLast;
|
||||
}
|
||||
}
|
||||
@@ -28,11 +28,6 @@ const LAWN_MOWER_ACTIONS: Partial<
|
||||
service: "start_mowing",
|
||||
feature: LawnMowerEntityFeature.START_MOWING,
|
||||
},
|
||||
idle: {
|
||||
action: "start_mowing",
|
||||
service: "start_mowing",
|
||||
feature: LawnMowerEntityFeature.START_MOWING,
|
||||
},
|
||||
returning: {
|
||||
action: "pause",
|
||||
service: "pause",
|
||||
|
||||
@@ -65,9 +65,7 @@ class HaMenuButton extends LitElement {
|
||||
}
|
||||
const hasNotifications =
|
||||
this._hasNotifications &&
|
||||
(this._narrow ||
|
||||
this._ui.dockedSidebar === "always_hidden" ||
|
||||
this._ui.kioskElementsHidden.has("sidebar"));
|
||||
(this._narrow || this._ui.dockedSidebar === "always_hidden");
|
||||
return html`
|
||||
<ha-icon-button
|
||||
.label=${this._localize("ui.sidebar.sidebar_toggle")}
|
||||
@@ -94,14 +92,9 @@ class HaMenuButton extends LitElement {
|
||||
this._unsubNotifications = undefined;
|
||||
}
|
||||
|
||||
// A hidden sidebar is only reachable as an overlay, so it needs this
|
||||
// button on wide screens too, unless the button itself is hidden.
|
||||
const showButton =
|
||||
!!this._ui &&
|
||||
!this._ui.kioskElementsHidden.has("sidebar_button") &&
|
||||
(this._narrow ||
|
||||
this._ui.dockedSidebar === "always_hidden" ||
|
||||
this._ui.kioskElementsHidden.has("sidebar"));
|
||||
this._ui?.kioskMode === false &&
|
||||
(this._narrow || this._ui.dockedSidebar === "always_hidden");
|
||||
|
||||
this._show = showButton || this._alwaysVisible;
|
||||
|
||||
|
||||
@@ -45,11 +45,8 @@ export class HaChooseSelector extends LitElement {
|
||||
}
|
||||
if (
|
||||
changedProperties.has("value") &&
|
||||
typeof this.value === "object" &&
|
||||
this.value !== null &&
|
||||
"active_choice" in this.value &&
|
||||
this.value.active_choice in this.selector.choose.choices &&
|
||||
this.value.active_choice !== this._activeChoice
|
||||
changedProperties.get("value")?.active_choice &&
|
||||
changedProperties.get("value")?.active_choice !== this._activeChoice
|
||||
) {
|
||||
this._setActiveChoice();
|
||||
}
|
||||
@@ -153,14 +150,14 @@ export class HaChooseSelector extends LitElement {
|
||||
if (this.value === null || this.value === undefined) {
|
||||
return undefined;
|
||||
}
|
||||
return typeof this.value === "object" && "active_choice" in this.value
|
||||
return typeof this.value === "object"
|
||||
? this.value[choice || this.value.active_choice]
|
||||
: this.value;
|
||||
}
|
||||
|
||||
private _setActiveChoice() {
|
||||
if (this.value) {
|
||||
if (typeof this.value === "object" && "active_choice" in this.value) {
|
||||
if (typeof this.value === "object") {
|
||||
if (this.value.active_choice in this.selector.choose.choices) {
|
||||
this._activeChoice = this.value.active_choice;
|
||||
return;
|
||||
@@ -202,22 +199,6 @@ export class HaChooseSelector extends LitElement {
|
||||
];
|
||||
return;
|
||||
}
|
||||
if (
|
||||
typeofValue === "object" &&
|
||||
!Array.isArray(this.value) &&
|
||||
Object.keys(this.value).length > 0 &&
|
||||
Object.keys(this.value).every((key) =>
|
||||
["days", "hours", "minutes", "seconds", "milliseconds"].includes(
|
||||
key
|
||||
)
|
||||
) &&
|
||||
selectorTypes.includes("duration")
|
||||
) {
|
||||
this._activeChoice = Object.keys(this.selector.choose.choices)[
|
||||
selectorTypes.indexOf("duration")
|
||||
];
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
this._activeChoice = Object.keys(this.selector.choose.choices)[0];
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { html, LitElement } from "lit";
|
||||
import { customElement, property, query } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { durationValueToData } from "../../common/datetime/duration_value_to_data";
|
||||
import type { DurationSelector } from "../../data/selector";
|
||||
import "../ha-duration-input";
|
||||
import type { HaDurationData, HaDurationInput } from "../ha-duration-input";
|
||||
import { createDurationData } from "../../common/datetime/create_duration_data";
|
||||
|
||||
@customElement("ha-selector-duration")
|
||||
export class HaTimeDuration extends LitElement {
|
||||
@@ -27,10 +27,7 @@ export class HaTimeDuration extends LitElement {
|
||||
return this._input?.reportValidity() ?? true;
|
||||
}
|
||||
|
||||
private _data = memoizeOne(
|
||||
(value?: HaDurationData | string | number): HaDurationData | undefined =>
|
||||
createDurationData(value)
|
||||
);
|
||||
private _data = memoizeOne(durationValueToData);
|
||||
|
||||
protected render() {
|
||||
return html`
|
||||
|
||||
@@ -0,0 +1,182 @@
|
||||
import {
|
||||
mdiClockMinusOutline,
|
||||
mdiClockOutline,
|
||||
mdiClockPlusOutline,
|
||||
} from "@mdi/js";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, query } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { durationValueToData } from "../../common/datetime/duration_value_to_data";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import type { LocalizeFunc } from "../../common/translations/localize";
|
||||
import type {
|
||||
OffsetSelector,
|
||||
OffsetSelectorValue,
|
||||
OffsetType,
|
||||
} from "../../data/selector";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import "../ha-duration-input";
|
||||
import type { HaDurationData, HaDurationInput } from "../ha-duration-input";
|
||||
import "../ha-input-helper-text";
|
||||
import "../ha-select";
|
||||
|
||||
const OFFSET_TYPES: { value: OffsetType; iconPath: string }[] = [
|
||||
{ value: "none", iconPath: mdiClockOutline },
|
||||
{ value: "before", iconPath: mdiClockMinusOutline },
|
||||
{ value: "after", iconPath: mdiClockPlusOutline },
|
||||
];
|
||||
|
||||
const DEFAULT_DURATION: HaDurationData = { hours: 0, minutes: 0, seconds: 0 };
|
||||
|
||||
const isOffsetValue = (value: unknown): value is OffsetSelectorValue =>
|
||||
typeof value === "object" && value !== null && "type" in value;
|
||||
|
||||
@customElement("ha-selector-offset")
|
||||
export class HaOffsetSelector extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public selector!: OffsetSelector;
|
||||
|
||||
@property({ attribute: false }) public value?: OffsetSelectorValue;
|
||||
|
||||
@property() public label?: string;
|
||||
|
||||
@property() public helper?: string;
|
||||
|
||||
@property({ type: Boolean }) public disabled = false;
|
||||
|
||||
@property({ type: Boolean }) public required = true;
|
||||
|
||||
@query("ha-duration-input", true) private _durationInput?: HaDurationInput;
|
||||
|
||||
public reportValidity(): boolean {
|
||||
return this._durationInput?.reportValidity() ?? true;
|
||||
}
|
||||
|
||||
private get _value(): OffsetSelectorValue | undefined {
|
||||
return isOffsetValue(this.value) ? this.value : undefined;
|
||||
}
|
||||
|
||||
private _duration = memoizeOne(durationValueToData);
|
||||
|
||||
private _typeOptions = memoizeOne((localize: LocalizeFunc) =>
|
||||
OFFSET_TYPES.map(({ value, iconPath }) => ({
|
||||
value,
|
||||
iconPath,
|
||||
label: localize(`ui.components.selectors.offset.${value}`),
|
||||
}))
|
||||
);
|
||||
|
||||
protected render() {
|
||||
const type = this._value?.type ?? "none";
|
||||
return html`
|
||||
<div class="container">
|
||||
${
|
||||
this.label
|
||||
? html`<label>${this.label}${this.required ? "*" : ""}</label>`
|
||||
: nothing
|
||||
}
|
||||
<div class="inputs">
|
||||
<ha-select
|
||||
.value=${type}
|
||||
.options=${this._typeOptions(this.hass.localize)}
|
||||
.disabled=${this.disabled}
|
||||
@selected=${this._typeChanged}
|
||||
></ha-select>
|
||||
${
|
||||
type !== "none"
|
||||
? html`<div class="value-row">
|
||||
<span class="value-label"
|
||||
>${this.hass.localize(
|
||||
"ui.components.selectors.offset.duration"
|
||||
)}${this.required ? "*" : ""}</span
|
||||
>
|
||||
<ha-duration-input
|
||||
.data=${this._duration(this._value?.duration)}
|
||||
.disabled=${this.disabled}
|
||||
.required=${this.required}
|
||||
.enableDay=${this.selector.offset?.enable_day}
|
||||
.enableMillisecond=${
|
||||
this.selector.offset?.enable_millisecond
|
||||
}
|
||||
@value-changed=${this._durationChanged}
|
||||
></ha-duration-input>
|
||||
</div>`
|
||||
: nothing
|
||||
}
|
||||
</div>
|
||||
${
|
||||
this.helper
|
||||
? html`<ha-input-helper-text>${this.helper}</ha-input-helper-text>`
|
||||
: nothing
|
||||
}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
private _durationChanged(ev: CustomEvent<{ value?: HaDurationData }>) {
|
||||
ev.stopPropagation();
|
||||
const type = this._value?.type;
|
||||
if (!type || type === "none") {
|
||||
return;
|
||||
}
|
||||
fireEvent(this, "value-changed", {
|
||||
value: { type, duration: ev.detail.value ?? DEFAULT_DURATION },
|
||||
});
|
||||
}
|
||||
|
||||
private _typeChanged(ev: CustomEvent<{ value?: string }>) {
|
||||
ev.stopPropagation();
|
||||
const type = ev.detail.value as OffsetType | undefined;
|
||||
if (!type || type === this._value?.type) {
|
||||
return;
|
||||
}
|
||||
fireEvent(this, "value-changed", {
|
||||
value:
|
||||
type === "none"
|
||||
? { type }
|
||||
: {
|
||||
type,
|
||||
duration:
|
||||
this._duration(this._value?.duration) ?? DEFAULT_DURATION,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
.container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--ha-space-2);
|
||||
}
|
||||
|
||||
label {
|
||||
display: block;
|
||||
font-weight: var(--ha-font-weight-medium);
|
||||
margin-bottom: var(--ha-space-1);
|
||||
}
|
||||
|
||||
.inputs,
|
||||
.value-row {
|
||||
--ha-input-padding-bottom: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--ha-space-2);
|
||||
}
|
||||
|
||||
.value-label {
|
||||
font-size: var(--ha-font-size-s);
|
||||
color: var(--secondary-text-color);
|
||||
}
|
||||
|
||||
ha-select {
|
||||
width: 100%;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-selector-offset": HaOffsetSelector;
|
||||
}
|
||||
}
|
||||
@@ -43,6 +43,7 @@ const LOAD_ELEMENTS = {
|
||||
number: () => import("./ha-selector-number"),
|
||||
numeric_threshold: () => import("./ha-selector-numeric-threshold"),
|
||||
object: () => import("./ha-selector-object"),
|
||||
offset: () => import("./ha-selector-offset"),
|
||||
period: () => import("./ha-selector-period"),
|
||||
qr_code: () => import("./ha-selector-qr-code"),
|
||||
select: () => import("./ha-selector-select"),
|
||||
|
||||
@@ -77,7 +77,6 @@ export type InputType =
|
||||
* @attr {boolean} invalid - Marks the input as invalid.
|
||||
* @attr {boolean} inset-label - Uses an inset label style where the label stays inside the input.
|
||||
* @attr {string} validation-message - Custom validation message shown when the input is invalid.
|
||||
* @attr {string} input-id - Sets the native input's `id` (and associated label `for`) on the inner `wa-input`.
|
||||
*/
|
||||
@customElement("ha-input")
|
||||
export class HaInput extends WaInputMixin(LitElement) {
|
||||
@@ -125,10 +124,6 @@ export class HaInput extends WaInputMixin(LitElement) {
|
||||
@property({ type: Boolean, attribute: "inset-label" })
|
||||
public insetLabel = false;
|
||||
|
||||
/** Sets the native input id (and label `for`) on the inner `wa-input`. */
|
||||
@property({ attribute: "input-id" })
|
||||
public inputId?: string;
|
||||
|
||||
@query("wa-input")
|
||||
private _input?: WaInput;
|
||||
|
||||
@@ -165,39 +160,6 @@ export class HaInput extends WaInputMixin(LitElement) {
|
||||
this._input?.stepDown();
|
||||
}
|
||||
|
||||
/**
|
||||
* Copies a native input value that was written without `input`/`change` events
|
||||
* (password-manager autofill) into the component so labels and form data update.
|
||||
*/
|
||||
public syncFromNativeInput(): boolean {
|
||||
const native = this._getNativeInput();
|
||||
if (!native) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const nativeValue = native.value;
|
||||
if ((this.value ?? "") === nativeValue) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (this._input) {
|
||||
this._input.value = nativeValue;
|
||||
}
|
||||
this._handleInput();
|
||||
this.dispatchEvent(new Event("input", { bubbles: true, composed: true }));
|
||||
return true;
|
||||
}
|
||||
|
||||
public override reportValidity(): boolean {
|
||||
this.syncFromNativeInput();
|
||||
return super.reportValidity();
|
||||
}
|
||||
|
||||
public override connectedCallback(): void {
|
||||
super.connectedCallback();
|
||||
this.addEventListener("focusin", this._syncFromNativeInput);
|
||||
}
|
||||
|
||||
protected override async firstUpdated(
|
||||
changedProperties: PropertyValues<this>
|
||||
): Promise<void> {
|
||||
@@ -213,7 +175,6 @@ export class HaInput extends WaInputMixin(LitElement) {
|
||||
|
||||
public override disconnectedCallback(): void {
|
||||
super.disconnectedCallback();
|
||||
this.removeEventListener("focusin", this._syncFromNativeInput);
|
||||
this._startSlotResizeObserver?.disconnect();
|
||||
}
|
||||
|
||||
@@ -254,7 +215,6 @@ export class HaInput extends WaInputMixin(LitElement) {
|
||||
.inputmode=${this.inputmode || undefined}
|
||||
.name=${this.name}
|
||||
.disabled=${this.disabled}
|
||||
.inputId=${this.inputId || "input"}
|
||||
class=${classMap({
|
||||
input: true,
|
||||
invalid: this.invalid || this._invalid,
|
||||
@@ -386,14 +346,6 @@ export class HaInput extends WaInputMixin(LitElement) {
|
||||
this.passwordVisible = !this.passwordVisible;
|
||||
}
|
||||
|
||||
private _getNativeInput(): HTMLInputElement | undefined {
|
||||
return this._input?.input ?? undefined;
|
||||
}
|
||||
|
||||
private _syncFromNativeInput = (): void => {
|
||||
this.syncFromNativeInput();
|
||||
};
|
||||
|
||||
static styles = [
|
||||
waInputStyles,
|
||||
css`
|
||||
|
||||
@@ -1327,9 +1327,6 @@ export class HaMap extends ReactiveElement {
|
||||
.maplibregl-popup-anchor-top .maplibregl-popup-tip {
|
||||
border-bottom-color: rgba(80, 80, 80, 0.9) !important;
|
||||
}
|
||||
.maplibregl-ctrl-bottom-left {
|
||||
direction: ltr;
|
||||
}
|
||||
.dark .leaflet-bar a {
|
||||
background-color: #1c1c1c;
|
||||
color: #ffffff;
|
||||
@@ -1380,7 +1377,6 @@ export class HaMap extends ReactiveElement {
|
||||
transparent
|
||||
) !important;
|
||||
text-shadow: none !important;
|
||||
direction: ltr;
|
||||
}
|
||||
/* the theme tokens follow the page, so forced modes need the opposite values */
|
||||
#map.forced-light .leaflet-control-scale-line {
|
||||
|
||||
@@ -1,181 +0,0 @@
|
||||
import { getDeviceAreaId } from "../../common/entity/context/get_device_context";
|
||||
import type {
|
||||
ExtractFromTargetResultReferenced,
|
||||
TargetType,
|
||||
} from "../../data/target";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
|
||||
export interface TargetSubRows {
|
||||
nextType: TargetType;
|
||||
rows: string[];
|
||||
rowEntries?: ExtractFromTargetResultReferenced[];
|
||||
deviceRows: string[];
|
||||
deviceRowEntries?: ExtractFromTargetResultReferenced[];
|
||||
entityRows: string[];
|
||||
}
|
||||
|
||||
const emptyEntries = (): ExtractFromTargetResultReferenced => ({
|
||||
referenced_areas: [],
|
||||
referenced_devices: [],
|
||||
referenced_entities: [],
|
||||
});
|
||||
|
||||
export const computeTargetSubRows = (
|
||||
type: TargetType,
|
||||
itemId: string,
|
||||
entries: ExtractFromTargetResultReferenced,
|
||||
entityRegistry: HomeAssistant["entities"],
|
||||
devices: HomeAssistant["devices"]
|
||||
): TargetSubRows => {
|
||||
let nextType: TargetType =
|
||||
type === "floor" ? "area" : type === "area" ? "device" : "entity";
|
||||
|
||||
if (type === "label") {
|
||||
if (entries.referenced_areas.length) {
|
||||
nextType = "area";
|
||||
} else if (entries.referenced_devices.length) {
|
||||
nextType = "device";
|
||||
}
|
||||
}
|
||||
|
||||
const deviceOf = (entityId: string) => entityRegistry[entityId]?.device_id;
|
||||
const parentOf = (deviceId: string) => devices[deviceId]?.parent_device_id;
|
||||
// An entity belongs to a device row when it is on that device or on one of
|
||||
// its child devices, so the row can nest the children below it.
|
||||
const belongsTo = (entityId: string, deviceId: string) => {
|
||||
const entityDevice = deviceOf(entityId);
|
||||
return (
|
||||
entityDevice === deviceId ||
|
||||
(!!entityDevice && parentOf(entityDevice) === deviceId)
|
||||
);
|
||||
};
|
||||
const entitiesOf = (deviceId: string) =>
|
||||
entries.referenced_entities.filter((entityId) =>
|
||||
belongsTo(entityId, deviceId)
|
||||
);
|
||||
const rootsOf = (deviceIds: string[]) =>
|
||||
deviceIds.filter((deviceId) => {
|
||||
const parentId = parentOf(deviceId);
|
||||
return !parentId || !deviceIds.includes(parentId);
|
||||
});
|
||||
|
||||
const childDevices =
|
||||
type === "device"
|
||||
? [
|
||||
...new Set(
|
||||
entries.referenced_entities
|
||||
.map(deviceOf)
|
||||
.filter(
|
||||
(deviceId): deviceId is string =>
|
||||
!!deviceId &&
|
||||
deviceId !== itemId &&
|
||||
parentOf(deviceId) === itemId
|
||||
)
|
||||
),
|
||||
]
|
||||
: [];
|
||||
|
||||
const rows =
|
||||
nextType === "area"
|
||||
? entries.referenced_areas
|
||||
: nextType === "device" && type !== "label"
|
||||
? rootsOf(entries.referenced_devices)
|
||||
: type === "device"
|
||||
? entries.referenced_entities.filter(
|
||||
(entityId) => !childDevices.includes(deviceOf(entityId) || "")
|
||||
)
|
||||
: type !== "label"
|
||||
? entries.referenced_entities
|
||||
: [];
|
||||
|
||||
const devicesInAreas: string[] = [];
|
||||
|
||||
const rowEntries =
|
||||
nextType === "entity"
|
||||
? undefined
|
||||
: rows.map((rowItem) => {
|
||||
const nextEntries = emptyEntries();
|
||||
|
||||
if (nextType === "area") {
|
||||
const areaDevices = entries.referenced_devices.filter(
|
||||
(deviceId) => {
|
||||
const device = devices[deviceId];
|
||||
return (
|
||||
!!device &&
|
||||
getDeviceAreaId(device, devices) === rowItem &&
|
||||
entries.referenced_entities.some((entityId) =>
|
||||
belongsTo(entityId, deviceId)
|
||||
)
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
devicesInAreas.push(...areaDevices);
|
||||
|
||||
nextEntries.referenced_devices = rootsOf(areaDevices);
|
||||
nextEntries.referenced_entities =
|
||||
entries.referenced_entities.filter((entityId) => {
|
||||
const entity = entityRegistry[entityId];
|
||||
if (!entity) {
|
||||
return false;
|
||||
}
|
||||
return (
|
||||
entity.area_id === rowItem ||
|
||||
!entity.device_id ||
|
||||
areaDevices.includes(entity.device_id)
|
||||
);
|
||||
});
|
||||
|
||||
return nextEntries;
|
||||
}
|
||||
|
||||
nextEntries.referenced_entities = entitiesOf(rowItem);
|
||||
|
||||
return nextEntries;
|
||||
});
|
||||
|
||||
const entityRows =
|
||||
type === "label"
|
||||
? entries.referenced_entities.filter((entityId) => {
|
||||
const entity = entityRegistry[entityId];
|
||||
if (!entity) {
|
||||
return false;
|
||||
}
|
||||
return (
|
||||
entity.labels.includes(itemId) &&
|
||||
!entries.referenced_devices.includes(entity.device_id || "")
|
||||
);
|
||||
})
|
||||
: nextType === "device"
|
||||
? entries.referenced_entities.filter(
|
||||
(entityId) => entityRegistry[entityId]?.area_id === itemId
|
||||
)
|
||||
: [];
|
||||
|
||||
const deviceRows =
|
||||
type === "label"
|
||||
? rootsOf(
|
||||
entries.referenced_devices.filter(
|
||||
(deviceId) =>
|
||||
!devicesInAreas.includes(deviceId) &&
|
||||
devices[deviceId]?.labels.includes(itemId)
|
||||
)
|
||||
)
|
||||
: childDevices;
|
||||
|
||||
const deviceRowEntries = deviceRows.length
|
||||
? deviceRows.map((deviceId) => ({
|
||||
...emptyEntries(),
|
||||
referenced_entities: entitiesOf(deviceId),
|
||||
}))
|
||||
: undefined;
|
||||
|
||||
return {
|
||||
nextType,
|
||||
rows,
|
||||
rowEntries,
|
||||
deviceRows,
|
||||
deviceRowEntries,
|
||||
entityRows,
|
||||
};
|
||||
};
|
||||
@@ -1,5 +1,7 @@
|
||||
import { consume } from "@lit/context";
|
||||
import {
|
||||
mdiChevronLeft,
|
||||
mdiChevronRight,
|
||||
mdiClose,
|
||||
mdiDevices,
|
||||
mdiHome,
|
||||
@@ -27,10 +29,7 @@ import {
|
||||
} from "../../common/entity/compute_device_name";
|
||||
import { computeDomain } from "../../common/entity/compute_domain";
|
||||
import { computeEntityName } from "../../common/entity/compute_entity_name";
|
||||
import {
|
||||
getDeviceArea,
|
||||
getDeviceAreaId,
|
||||
} from "../../common/entity/context/get_device_context";
|
||||
import { getDeviceArea } from "../../common/entity/context/get_device_context";
|
||||
import { getEntityContext } from "../../common/entity/context/get_entity_context";
|
||||
import { computeRTL } from "../../common/util/compute_rtl";
|
||||
import type { AreaRegistryEntry } from "../../data/area/area_registry";
|
||||
@@ -60,12 +59,10 @@ import type { HaDevicePickerDeviceFilterFunc } from "../device/ha-device-picker"
|
||||
import { floorDefaultIconPath } from "../ha-floor-icon";
|
||||
import "../ha-button";
|
||||
import "../ha-icon-button";
|
||||
import "../ha-icon-next";
|
||||
import "../ha-state-icon";
|
||||
import "../ha-svg-icon";
|
||||
import "../item/ha-list-item-base";
|
||||
import "../item/ha-list-item-button";
|
||||
import { computeTargetSubRows } from "./compute-target-sub-rows";
|
||||
import { showTargetDetailsDialog } from "./dialog/show-dialog-target-details";
|
||||
|
||||
@customElement("ha-target-picker-item-row")
|
||||
@@ -316,7 +313,19 @@ export class HaTargetPickerItemRow extends LitElement {
|
||||
></ha-icon-button>
|
||||
`
|
||||
: this.subEntry && this.type === "entity"
|
||||
? html` <ha-icon-next slot="end"></ha-icon-next> `
|
||||
? html`
|
||||
<ha-svg-icon
|
||||
.path=${
|
||||
computeRTL(
|
||||
this.hass.language,
|
||||
this.hass.translationMetadata.translations
|
||||
)
|
||||
? mdiChevronLeft
|
||||
: mdiChevronRight
|
||||
}
|
||||
slot="end"
|
||||
></ha-svg-icon>
|
||||
`
|
||||
: nothing
|
||||
}
|
||||
`;
|
||||
@@ -392,25 +401,125 @@ export class HaTargetPickerItemRow extends LitElement {
|
||||
return this._renderEmptyEntries();
|
||||
}
|
||||
|
||||
const {
|
||||
nextType,
|
||||
rows,
|
||||
rowEntries,
|
||||
deviceRows,
|
||||
deviceRowEntries,
|
||||
entityRows,
|
||||
} = computeTargetSubRows(
|
||||
this.type,
|
||||
this.itemId,
|
||||
entries,
|
||||
this.hass.entities,
|
||||
this.hass.devices
|
||||
);
|
||||
let nextType: TargetType =
|
||||
this.type === "floor"
|
||||
? "area"
|
||||
: this.type === "area"
|
||||
? "device"
|
||||
: "entity";
|
||||
|
||||
if (this.type === "label") {
|
||||
if (entries?.referenced_areas.length) {
|
||||
nextType = "area";
|
||||
} else if (entries?.referenced_devices.length) {
|
||||
nextType = "device";
|
||||
}
|
||||
}
|
||||
|
||||
const rows1 =
|
||||
(nextType === "area"
|
||||
? entries?.referenced_areas
|
||||
: nextType === "device" && this.type !== "label"
|
||||
? entries?.referenced_devices
|
||||
: this.type !== "label"
|
||||
? entries?.referenced_entities
|
||||
: []) || [];
|
||||
|
||||
const devicesInAreas = [] as string[];
|
||||
|
||||
const rows1Entries =
|
||||
nextType === "entity"
|
||||
? undefined
|
||||
: rows1.map((rowItem) => {
|
||||
const nextEntries = {
|
||||
referenced_areas: [] as string[],
|
||||
referenced_devices: [] as string[],
|
||||
referenced_entities: [] as string[],
|
||||
};
|
||||
|
||||
if (nextType === "area") {
|
||||
nextEntries.referenced_devices =
|
||||
entries?.referenced_devices.filter(
|
||||
(device_id) =>
|
||||
this.hass.devices?.[device_id]?.area_id === rowItem &&
|
||||
entries?.referenced_entities.some(
|
||||
(entity_id) =>
|
||||
this.hass.entities?.[entity_id]?.device_id === device_id
|
||||
)
|
||||
) || ([] as string[]);
|
||||
|
||||
devicesInAreas.push(...nextEntries.referenced_devices);
|
||||
|
||||
nextEntries.referenced_entities =
|
||||
entries?.referenced_entities.filter((entity_id) => {
|
||||
const entity = this.hass.entities[entity_id];
|
||||
if (!entity) {
|
||||
return false;
|
||||
}
|
||||
return (
|
||||
entity.area_id === rowItem ||
|
||||
!entity.device_id ||
|
||||
nextEntries.referenced_devices.includes(entity.device_id)
|
||||
);
|
||||
}) || ([] as string[]);
|
||||
|
||||
return nextEntries;
|
||||
}
|
||||
|
||||
nextEntries.referenced_entities =
|
||||
entries?.referenced_entities.filter(
|
||||
(entity_id) =>
|
||||
this.hass.entities?.[entity_id]?.device_id === rowItem
|
||||
) || ([] as string[]);
|
||||
|
||||
return nextEntries;
|
||||
});
|
||||
|
||||
const entityRows =
|
||||
this.type === "label" && entries
|
||||
? entries.referenced_entities.filter((entity_id) => {
|
||||
const entity = this.hass.entities[entity_id];
|
||||
if (!entity) {
|
||||
return false;
|
||||
}
|
||||
return (
|
||||
entity.labels.includes(this.itemId) &&
|
||||
!entries.referenced_devices.includes(entity.device_id || "")
|
||||
);
|
||||
})
|
||||
: nextType === "device" && entries
|
||||
? entries.referenced_entities.filter(
|
||||
(entity_id) =>
|
||||
this.hass.entities[entity_id]?.area_id === this.itemId
|
||||
)
|
||||
: [];
|
||||
|
||||
const deviceRows =
|
||||
this.type === "label" && entries
|
||||
? entries.referenced_devices.filter(
|
||||
(device_id) =>
|
||||
!devicesInAreas.includes(device_id) &&
|
||||
this.hass.devices[device_id]?.labels.includes(this.itemId)
|
||||
)
|
||||
: [];
|
||||
|
||||
const deviceRowsEntries =
|
||||
deviceRows.length === 0
|
||||
? undefined
|
||||
: deviceRows.map((device_id) => ({
|
||||
referenced_areas: [] as string[],
|
||||
referenced_devices: [] as string[],
|
||||
referenced_entities:
|
||||
entries?.referenced_entities.filter(
|
||||
(entity_id) =>
|
||||
this.hass.entities?.[entity_id]?.device_id === device_id
|
||||
) || ([] as string[]),
|
||||
}));
|
||||
|
||||
const nextSubLevel = this.subLevel + 1;
|
||||
|
||||
return html`
|
||||
${rows.map(
|
||||
${rows1.map(
|
||||
(itemId, index) => html`
|
||||
<ha-target-picker-item-row
|
||||
sub-entry
|
||||
@@ -419,7 +528,7 @@ export class HaTargetPickerItemRow extends LitElement {
|
||||
.hass=${this.hass}
|
||||
.type=${nextType}
|
||||
.itemId=${itemId}
|
||||
.parentEntries=${rowEntries?.[index]}
|
||||
.parentEntries=${rows1Entries?.[index]}
|
||||
.hideContext=${this.hideContext || this.type !== "label"}
|
||||
expand
|
||||
></ha-target-picker-item-row>
|
||||
@@ -434,7 +543,7 @@ export class HaTargetPickerItemRow extends LitElement {
|
||||
.hass=${this.hass}
|
||||
type="device"
|
||||
.itemId=${itemId}
|
||||
.parentEntries=${deviceRowEntries?.[index]}
|
||||
.parentEntries=${deviceRowsEntries?.[index]}
|
||||
.hideContext=${this.hideContext || this.type !== "label"}
|
||||
expand
|
||||
></ha-target-picker-item-row>
|
||||
@@ -515,53 +624,32 @@ export class HaTargetPickerItemRow extends LitElement {
|
||||
|
||||
let referencedDevices = entries.referenced_devices;
|
||||
const hiddenDeviceIds: string[] = [];
|
||||
// Parents kept only so a matching child device can nest under them;
|
||||
// their own entities stay filtered out like a hidden device's.
|
||||
const groupOnlyDeviceIds = new Set<string>();
|
||||
if (
|
||||
this.type === "floor" ||
|
||||
this.type === "area" ||
|
||||
this.type === "label"
|
||||
) {
|
||||
const matchingDeviceIds = new Set(
|
||||
referencedDevices.filter((device_id) => {
|
||||
const device = this.hass.devices[device_id];
|
||||
return (
|
||||
!!device &&
|
||||
!hiddenAreaIds.includes(
|
||||
getDeviceAreaId(device, this.hass.devices) || ""
|
||||
) &&
|
||||
deviceMeetsFilter(
|
||||
device,
|
||||
this.hass.entities,
|
||||
this.deviceFilter,
|
||||
this.includeDomains,
|
||||
this.includeDeviceClasses,
|
||||
this.hass.states,
|
||||
this.entityFilter,
|
||||
!this.primaryEntitiesOnly
|
||||
)
|
||||
);
|
||||
})
|
||||
);
|
||||
const parentsOfMatching = new Set(
|
||||
[...matchingDeviceIds].map(
|
||||
(device_id) => this.hass.devices[device_id].parent_device_id
|
||||
)
|
||||
);
|
||||
referencedDevices = referencedDevices.filter((device_id) => {
|
||||
// Absent from the registry is not a filter decision: drop the id
|
||||
// without marking it hidden, like the area filtering above.
|
||||
if (!this.hass.devices[device_id]) {
|
||||
const device = this.hass.devices[device_id];
|
||||
if (!device) {
|
||||
return false;
|
||||
}
|
||||
if (matchingDeviceIds.has(device_id)) {
|
||||
return true;
|
||||
}
|
||||
if (parentsOfMatching.has(device_id)) {
|
||||
groupOnlyDeviceIds.add(device_id);
|
||||
if (
|
||||
!hiddenAreaIds.includes(device.area_id || "") &&
|
||||
deviceMeetsFilter(
|
||||
device,
|
||||
this.hass.entities,
|
||||
this.deviceFilter,
|
||||
this.includeDomains,
|
||||
this.includeDeviceClasses,
|
||||
this.hass.states,
|
||||
this.entityFilter,
|
||||
!this.primaryEntitiesOnly
|
||||
)
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
hiddenDeviceIds.push(device_id);
|
||||
return false;
|
||||
});
|
||||
@@ -575,10 +663,7 @@ export class HaTargetPickerItemRow extends LitElement {
|
||||
if (!entity) {
|
||||
return false;
|
||||
}
|
||||
if (
|
||||
hiddenDeviceIds.includes(entity.device_id || "") ||
|
||||
groupOnlyDeviceIds.has(entity.device_id || "")
|
||||
) {
|
||||
if (hiddenDeviceIds.includes(entity.device_id || "")) {
|
||||
return false;
|
||||
}
|
||||
if (
|
||||
|
||||
@@ -115,7 +115,6 @@ const updateUi = (
|
||||
value.panelUrl !== hass.panelUrl ||
|
||||
value.dockedSidebar !== hass.dockedSidebar ||
|
||||
value.kioskMode !== hass.kioskMode ||
|
||||
value.kioskElementsHidden !== hass.kioskElementsHidden ||
|
||||
value.enableShortcuts !== hass.enableShortcuts ||
|
||||
value.vibrate !== hass.vibrate ||
|
||||
value.suspendWhenHidden !== hass.suspendWhenHidden
|
||||
@@ -127,7 +126,6 @@ const updateUi = (
|
||||
panelUrl: hass.panelUrl,
|
||||
dockedSidebar: hass.dockedSidebar,
|
||||
kioskMode: hass.kioskMode,
|
||||
kioskElementsHidden: hass.kioskElementsHidden,
|
||||
enableShortcuts: hass.enableShortcuts,
|
||||
vibrate: hass.vibrate,
|
||||
suspendWhenHidden: hass.suspendWhenHidden,
|
||||
|
||||
@@ -1,12 +1,9 @@
|
||||
import type { EntityNameType } from "../common/entity/compute_entity_name_display";
|
||||
|
||||
export type EntityIdPart = EntityNameType;
|
||||
export type EntityIdPart = "area" | "device" | "entity" | "floor";
|
||||
|
||||
export type EntityIdFormat = EntityIdPart[];
|
||||
|
||||
export const DEFAULT_ENTITY_ID_FORMAT: EntityIdFormat = [
|
||||
"area",
|
||||
"parent_device",
|
||||
"device",
|
||||
"entity",
|
||||
];
|
||||
|
||||
@@ -1,132 +0,0 @@
|
||||
/**
|
||||
* The parts of the UI a kiosk client can hide.
|
||||
*
|
||||
* A client names them over the external bus (`kiosk_mode/set`) as either
|
||||
* `excluded_elements` (hide exactly these) or `included_elements` (hide
|
||||
* everything except these), so the same set can be described from either side.
|
||||
*/
|
||||
export const KIOSK_ELEMENTS = [
|
||||
/** The sidebar itself: never docked, only reachable as an overlay. */
|
||||
"sidebar",
|
||||
/**
|
||||
* The hamburger button that opens the sidebar, in every panel's toolbar.
|
||||
* In the app panel it lives in `app_panel_header`, so hiding that header
|
||||
* hides this button too, whatever this element is set to.
|
||||
*/
|
||||
"sidebar_button",
|
||||
/**
|
||||
* The dashboard view tabs. The current view's title is shown instead.
|
||||
* Only outside edit mode: in edit mode the tabs are how views are selected,
|
||||
* moved, edited and added, so they stay. Hide `dashboard_edit_button` as well
|
||||
* to keep that button from being a way back to them.
|
||||
*/
|
||||
"dashboard_tabs",
|
||||
/** The "+" menu that adds a device, automation, area or person. */
|
||||
"dashboard_add_button",
|
||||
/** The search button that opens the quick bar. */
|
||||
"dashboard_search_button",
|
||||
/** The Assist button. */
|
||||
"dashboard_assist_button",
|
||||
/** The pencil that switches the dashboard into edit mode. */
|
||||
"dashboard_edit_button",
|
||||
/**
|
||||
* The header the app panel draws above an ingress add-on iframe, including
|
||||
* its sidebar button. With it hidden the iframe takes the full height; a
|
||||
* client can still open the sidebar with `sidebar/show` or `sidebar/toggle`.
|
||||
*/
|
||||
"app_panel_header",
|
||||
] as const;
|
||||
|
||||
export type KioskElement = (typeof KIOSK_ELEMENTS)[number];
|
||||
|
||||
const KIOSK_ELEMENTS_SET: ReadonlySet<string> = new Set(KIOSK_ELEMENTS);
|
||||
|
||||
/**
|
||||
* Nothing hidden. Shared instance so context consumers that compare by
|
||||
* reference don't see a change every time kiosk mode is switched off.
|
||||
*/
|
||||
export const NO_KIOSK_ELEMENTS_HIDDEN: ReadonlySet<KioskElement> = new Set();
|
||||
|
||||
/**
|
||||
* What a client gets when it enables kiosk mode without naming any elements.
|
||||
* This is the set kiosk mode hid back when it was a single boolean, so a client
|
||||
* that only sends `enable` keeps the behavior it has today.
|
||||
*/
|
||||
export const DEFAULT_KIOSK_ELEMENTS_HIDDEN: ReadonlySet<KioskElement> = new Set(
|
||||
[
|
||||
"sidebar",
|
||||
"sidebar_button",
|
||||
"dashboard_add_button",
|
||||
"dashboard_search_button",
|
||||
"dashboard_edit_button",
|
||||
"app_panel_header",
|
||||
]
|
||||
);
|
||||
|
||||
/**
|
||||
* Who asked for kiosk mode. Each source's request is kept on its own and the
|
||||
* hidden elements are their union, so one source switching kiosk mode off
|
||||
* doesn't undo what another still asks for.
|
||||
*
|
||||
* - `external_app`: the companion app, over `kiosk_mode/set`.
|
||||
* - `app_panel`: an ingress add-on, via `home-assistant/subscribe-properties`.
|
||||
*/
|
||||
export type KioskModeSource = "external_app" | "app_panel";
|
||||
|
||||
export interface KioskModeParams {
|
||||
enable: boolean;
|
||||
/** Defaults to `external_app`. */
|
||||
source?: KioskModeSource;
|
||||
/** Hide exactly these. Mutually exclusive with `includedElements`. */
|
||||
excludedElements?: readonly string[];
|
||||
/** Hide everything except these. Mutually exclusive with `excludedElements`. */
|
||||
includedElements?: readonly string[];
|
||||
}
|
||||
|
||||
const isKioskElement = (value: string): value is KioskElement =>
|
||||
KIOSK_ELEMENTS_SET.has(value);
|
||||
|
||||
/**
|
||||
* Resolve a kiosk request into the elements to hide.
|
||||
*
|
||||
* Element names this frontend doesn't know are ignored rather than rejected, so
|
||||
* a newer client naming an element an older frontend has never heard of still
|
||||
* gets the rest of its request honored.
|
||||
*/
|
||||
export const resolveKioskElementsHidden = ({
|
||||
enable,
|
||||
excludedElements,
|
||||
includedElements,
|
||||
}: KioskModeParams): ReadonlySet<KioskElement> => {
|
||||
if (!enable) {
|
||||
return NO_KIOSK_ELEMENTS_HIDDEN;
|
||||
}
|
||||
if (excludedElements) {
|
||||
return new Set(excludedElements.filter(isKioskElement));
|
||||
}
|
||||
if (includedElements) {
|
||||
const shown = new Set(includedElements);
|
||||
return new Set(KIOSK_ELEMENTS.filter((element) => !shown.has(element)));
|
||||
}
|
||||
return DEFAULT_KIOSK_ELEMENTS_HIDDEN;
|
||||
};
|
||||
|
||||
/**
|
||||
* Combine the elements each kiosk source hides into the set the UI hides.
|
||||
*
|
||||
* Returns a source's set unchanged when it is the only one hiding anything, and
|
||||
* the shared empty set when none is, so context consumers comparing by
|
||||
* reference only see a change when the combined result actually changes shape.
|
||||
*/
|
||||
export const combineKioskElementsHidden = (
|
||||
requests: Iterable<ReadonlySet<KioskElement>>
|
||||
): ReadonlySet<KioskElement> => {
|
||||
const nonEmpty = [...requests].filter((request) => request.size > 0);
|
||||
if (nonEmpty.length === 0) {
|
||||
return NO_KIOSK_ELEMENTS_HIDDEN;
|
||||
}
|
||||
if (nonEmpty.length === 1) {
|
||||
return nonEmpty[0];
|
||||
}
|
||||
return new Set(nonEmpty.flatMap((request) => [...request]));
|
||||
};
|
||||
@@ -5,13 +5,12 @@ import type {
|
||||
import { UNAVAILABLE } from "./entity/entity";
|
||||
|
||||
export type LawnMowerEntityState =
|
||||
"paused" | "mowing" | "returning" | "docked" | "idle" | "error";
|
||||
"paused" | "mowing" | "returning" | "docked" | "error";
|
||||
|
||||
export enum LawnMowerEntityFeature {
|
||||
START_MOWING = 1,
|
||||
PAUSE = 2,
|
||||
DOCK = 4,
|
||||
STOP = 8,
|
||||
}
|
||||
|
||||
interface LawnMowerEntityAttributes
|
||||
@@ -39,13 +38,6 @@ export function canPause(stateObj: LawnMowerEntity): boolean {
|
||||
return stateObj.state !== "paused";
|
||||
}
|
||||
|
||||
export function canStop(stateObj: LawnMowerEntity): boolean {
|
||||
if (stateObj.state === UNAVAILABLE) {
|
||||
return false;
|
||||
}
|
||||
return !["docked", "idle"].includes(stateObj.state);
|
||||
}
|
||||
|
||||
export function canDock(stateObj: LawnMowerEntity): boolean {
|
||||
if (stateObj.state === UNAVAILABLE) {
|
||||
return false;
|
||||
|
||||
@@ -69,13 +69,6 @@ export const isLocalMediaSourceContentId = (mediaId: string) =>
|
||||
export const isImageUploadMediaSourceContentId = (mediaId: string) =>
|
||||
mediaId.startsWith("media-source://image_upload");
|
||||
|
||||
export const getImageEntityIdFromMediaSourceContentId = (
|
||||
mediaId: string
|
||||
): string | undefined =>
|
||||
mediaId.startsWith("media-source://image/image.")
|
||||
? mediaId.slice("media-source://image/".length)
|
||||
: undefined;
|
||||
|
||||
export const uploadLocalMedia = async (
|
||||
hass: HomeAssistant,
|
||||
media_content_id: string,
|
||||
|
||||
+1
-8
@@ -3,17 +3,10 @@ import type { HomeAssistant } from "../types";
|
||||
/** Transport with host and port, or with the serial device path. */
|
||||
export type ModbusEndpoint = [string, string, number] | [string, string];
|
||||
|
||||
/** Whether a connection belongs to a config entry or to a YAML hub. */
|
||||
export type ModbusConnectionSource = "config_entry" | "yaml";
|
||||
|
||||
export interface ModbusConnection {
|
||||
endpoint: ModbusEndpoint;
|
||||
connected: boolean;
|
||||
source: ModbusConnectionSource;
|
||||
/**
|
||||
* The unit IDs each holder uses, keyed by config entry ID, or by hub name
|
||||
* when `source` is `"yaml"`.
|
||||
*/
|
||||
/** The unit IDs each config entry holds, keyed by config entry ID. */
|
||||
units: Record<string, number[]>;
|
||||
}
|
||||
|
||||
|
||||
@@ -22,6 +22,7 @@ import type {
|
||||
EntityRegistryEntry,
|
||||
} from "./entity/entity_registry";
|
||||
import type { EntitySources } from "./entity/entity_sources";
|
||||
import type { HaDurationData } from "../components/ha-duration-input";
|
||||
|
||||
export type ThresholdMode = "crossed" | "changed" | "is";
|
||||
|
||||
@@ -63,6 +64,7 @@ export type Selector =
|
||||
| NumberSelector
|
||||
| NumericThresholdSelector
|
||||
| ObjectSelector
|
||||
| OffsetSelector
|
||||
| PeriodSelector
|
||||
| AssistPipelineSelector
|
||||
| QRCodeSelector
|
||||
@@ -446,6 +448,20 @@ export interface ObjectSelector {
|
||||
} | null;
|
||||
}
|
||||
|
||||
export type OffsetType = "none" | "before" | "after";
|
||||
|
||||
export interface OffsetSelector {
|
||||
offset: {
|
||||
enable_day?: boolean;
|
||||
enable_millisecond?: boolean;
|
||||
} | null;
|
||||
}
|
||||
|
||||
export interface OffsetSelectorValue {
|
||||
type: OffsetType;
|
||||
duration?: HaDurationData | string | number;
|
||||
}
|
||||
|
||||
export type PeriodKey =
|
||||
| "today"
|
||||
| "yesterday"
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import { ensureArray } from "../../common/array/ensure-array";
|
||||
import { computeAreaName } from "../../common/entity/compute_area_name";
|
||||
import { durationValueToData } from "../../common/datetime/duration_value_to_data";
|
||||
import { formatDurationLong } from "../../common/datetime/format_duration";
|
||||
import { DEFAULT_ENTITY_NAME } from "../../common/entity/compute_entity_name_display";
|
||||
import { blankBeforeUnit } from "../../common/translations/blank_before_unit";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import type { Selector } from "../selector";
|
||||
import type { OffsetSelectorValue, Selector } from "../selector";
|
||||
|
||||
export const formatSelectorValue = (
|
||||
hass: HomeAssistant,
|
||||
@@ -102,13 +104,6 @@ export const formatSelectorValue = (
|
||||
.join(", ");
|
||||
}
|
||||
|
||||
if ("media" in selector) {
|
||||
const media = ensureArray(value);
|
||||
return media
|
||||
.map((item) => item.metadata?.title || item.media_content_id)
|
||||
.join(", ");
|
||||
}
|
||||
|
||||
if ("object" in selector) {
|
||||
const { fields } = selector.object ?? {};
|
||||
const items = ensureArray(value);
|
||||
@@ -130,6 +125,21 @@ export const formatSelectorValue = (
|
||||
.join(", ");
|
||||
}
|
||||
|
||||
if ("offset" in selector) {
|
||||
const { type, duration } = value as OffsetSelectorValue;
|
||||
const durationData = durationValueToData(duration);
|
||||
if (type === "none" || !durationData) {
|
||||
return "";
|
||||
}
|
||||
const formattedDuration = formatDurationLong(hass.locale, durationData);
|
||||
if (!formattedDuration) {
|
||||
return "";
|
||||
}
|
||||
return hass.localize(`ui.components.selectors.offset.summary.${type}`, {
|
||||
duration: formattedDuration,
|
||||
});
|
||||
}
|
||||
|
||||
return ensureArray(value)
|
||||
.map((v) =>
|
||||
v != null && typeof v === "object" ? JSON.stringify(v) : String(v)
|
||||
|
||||
@@ -1,14 +1,16 @@
|
||||
import { mdiArrowRight } from "@mdi/js";
|
||||
import { ERR_CONNECTION_LOST } from "home-assistant-js-websocket";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { styleMap } from "lit/directives/style-map";
|
||||
import { formatNumericDuration } from "../../common/datetime/format_duration";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import { computeRTL } from "../../common/util/compute_rtl";
|
||||
import "../../components/ha-alert";
|
||||
import "../../components/ha-button";
|
||||
import "../../components/ha-dialog-footer";
|
||||
import "../../components/ha-dialog";
|
||||
import "../../components/ha-svg-icon";
|
||||
import "../../components/ha-icon-arrow-next";
|
||||
import type { HttpConfig } from "../../data/http";
|
||||
import {
|
||||
HTTP_CONFIG_FIELDS,
|
||||
@@ -156,6 +158,10 @@ export class DialogHttpPendingConfig
|
||||
|
||||
const changes = this._changedFields;
|
||||
const { stable, pending } = this._params.state;
|
||||
const rtl = computeRTL(
|
||||
this.hass.language,
|
||||
this.hass.translationMetadata.translations
|
||||
);
|
||||
|
||||
return html`
|
||||
<ha-dialog
|
||||
@@ -227,7 +233,12 @@ export class DialogHttpPendingConfig
|
||||
<span class="old"
|
||||
>${this._formatValue(key, stable[key])}</span
|
||||
>
|
||||
<ha-icon-arrow-next></ha-icon-arrow-next>
|
||||
<ha-svg-icon
|
||||
.path=${mdiArrowRight}
|
||||
style=${styleMap({
|
||||
transform: rtl ? "scaleX(-1)" : "",
|
||||
})}
|
||||
></ha-svg-icon>
|
||||
<span class="new"
|
||||
>${this._formatValue(key, pending![key])}</span
|
||||
>
|
||||
@@ -392,7 +403,7 @@ export class DialogHttpPendingConfig
|
||||
.values .new {
|
||||
color: var(--primary-text-color);
|
||||
}
|
||||
.values ha-icon-arrow-next {
|
||||
.values ha-svg-icon {
|
||||
--mdc-icon-size: 18px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { consume } from "@lit/context";
|
||||
import { mdiHomeImportOutline, mdiPause, mdiPlay, mdiStop } from "@mdi/js";
|
||||
import { mdiHomeImportOutline, mdiPause, mdiPlay } from "@mdi/js";
|
||||
import type { CSSResultGroup } from "lit";
|
||||
import { LitElement, css, html, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
@@ -29,7 +29,6 @@ import {
|
||||
LawnMowerEntityFeature,
|
||||
canDock,
|
||||
canStartMowing,
|
||||
canStop,
|
||||
isMowing,
|
||||
} from "../../../data/lawn_mower";
|
||||
import "../../../state-control/lawn_mower/ha-state-control-lawn_mower-status";
|
||||
@@ -174,14 +173,6 @@ class MoreInfoLawnMower extends LitElement {
|
||||
}
|
||||
}
|
||||
|
||||
private _handleStop() {
|
||||
if (!this.stateObj) return;
|
||||
forwardHaptic(this, "light");
|
||||
this._api.callService("lawn_mower", "stop", {
|
||||
entity_id: this.stateObj.entity_id,
|
||||
});
|
||||
}
|
||||
|
||||
private _handleDock() {
|
||||
if (!this.stateObj) return;
|
||||
forwardHaptic(this, "light");
|
||||
@@ -197,11 +188,9 @@ class MoreInfoLawnMower extends LitElement {
|
||||
|
||||
const stateObj = this.stateObj;
|
||||
const isUnavailable = stateObj.state === UNAVAILABLE;
|
||||
const supportsStop = supportsFeature(stateObj, LawnMowerEntityFeature.STOP);
|
||||
const supportsDock = supportsFeature(stateObj, LawnMowerEntityFeature.DOCK);
|
||||
|
||||
const hasAnyCommand =
|
||||
this._supportsStartPause || supportsStop || supportsDock;
|
||||
const hasAnyCommand = this._supportsStartPause || supportsDock;
|
||||
|
||||
return html`
|
||||
<ha-more-info-state-header .stateObj=${this.stateObj}>
|
||||
@@ -233,21 +222,6 @@ class MoreInfoLawnMower extends LitElement {
|
||||
`
|
||||
: nothing
|
||||
}
|
||||
${
|
||||
supportsStop
|
||||
? html`
|
||||
<ha-control-button
|
||||
.label=${this._i18n.localize(
|
||||
"ui.dialogs.more_info_control.lawn_mower.stop"
|
||||
)}
|
||||
@click=${this._handleStop}
|
||||
.disabled=${isUnavailable || !canStop(stateObj)}
|
||||
>
|
||||
<ha-svg-icon .path=${mdiStop}></ha-svg-icon>
|
||||
</ha-control-button>
|
||||
`
|
||||
: nothing
|
||||
}
|
||||
${
|
||||
supportsDock
|
||||
? html`
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { consume } from "@lit/context";
|
||||
import {
|
||||
mdiChevronRight,
|
||||
mdiFan,
|
||||
mdiHomeImportOutline,
|
||||
mdiMapMarker,
|
||||
@@ -24,7 +25,6 @@ import "../../../components/ha-control-select-menu";
|
||||
import type { HaDropdownSelectEvent } from "../../../components/ha-dropdown";
|
||||
import "../../../components/ha-icon";
|
||||
import "../../../components/ha-svg-icon";
|
||||
import "../../../components/ha-icon-next";
|
||||
import {
|
||||
apiContext,
|
||||
entitiesContext,
|
||||
@@ -489,7 +489,7 @@ class MoreInfoVacuum extends LitElement {
|
||||
</p>
|
||||
</div>
|
||||
<div class="icon">
|
||||
<ha-icon-next></ha-icon-next>
|
||||
<ha-svg-icon .path=${mdiChevronRight}></ha-svg-icon>
|
||||
</div>
|
||||
</button>
|
||||
`
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { mdiMenuDown } from "@mdi/js";
|
||||
import { mdiChevronLeft, mdiMenuDown } from "@mdi/js";
|
||||
import type { CSSResultGroup, PropertyValues } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
@@ -9,7 +9,6 @@ import { formatLanguageCode } from "../../common/language/format_language";
|
||||
import "../../components/chips/ha-assist-chip";
|
||||
import "../../components/ha-dialog";
|
||||
import "../../components/ha-dropdown";
|
||||
import "../../components/ha-icon-button-prev";
|
||||
import type { HaDropdownSelectEvent } from "../../components/ha-dropdown";
|
||||
import "../../components/ha-dropdown-item";
|
||||
import { getLanguageOptions } from "../../components/ha-language-picker";
|
||||
@@ -153,11 +152,12 @@ export class HaVoiceAssistantSetupDialog extends LitElement {
|
||||
hideNavigationIcon
|
||||
? html`<span slot="headerNavigationIcon"></span>`
|
||||
: this._previousSteps.length
|
||||
? html`<ha-icon-button-prev
|
||||
? html`<ha-icon-button
|
||||
slot="headerNavigationIcon"
|
||||
.label=${this.hass.localize("ui.common.back") ?? "Back"}
|
||||
.path=${mdiChevronLeft}
|
||||
@click=${this._goToPreviousStep}
|
||||
></ha-icon-button-prev>`
|
||||
></ha-icon-button>`
|
||||
: nothing
|
||||
}
|
||||
${
|
||||
|
||||
@@ -98,27 +98,7 @@ export const handleExternalMessage = (
|
||||
} else if (msg.command === "bar_code/aborted") {
|
||||
barCodeListeners.forEach((listener) => listener(msg));
|
||||
} else if (msg.command === "kiosk_mode/set") {
|
||||
const { enable, excluded_elements, included_elements } = msg.payload;
|
||||
// The two lists are two ways of describing the same set, so a message
|
||||
// carrying both has no single meaning we could act on.
|
||||
if (excluded_elements && included_elements) {
|
||||
bus.fireMessage({
|
||||
id: msg.id,
|
||||
type: "result",
|
||||
success: false,
|
||||
error: {
|
||||
code: "invalid_format",
|
||||
message:
|
||||
"Pass either excluded_elements or included_elements, not both",
|
||||
},
|
||||
});
|
||||
return true;
|
||||
}
|
||||
fireEvent(window, "hass-kiosk-mode", {
|
||||
enable,
|
||||
excludedElements: excluded_elements,
|
||||
includedElements: included_elements,
|
||||
});
|
||||
fireEvent(window, "hass-kiosk-mode", { enable: msg.payload.enable });
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -325,29 +325,8 @@ export interface EMIncomingMessageKioskModeSet {
|
||||
id: number;
|
||||
type: "command";
|
||||
command: "kiosk_mode/set";
|
||||
/**
|
||||
* Element names are listed in `KIOSK_ELEMENTS`. Two of them depend on
|
||||
* another element:
|
||||
* - `app_panel_header` holds the app panel's sidebar button, so hiding the
|
||||
* header hides that button even when `sidebar_button` is not hidden.
|
||||
* - `dashboard_tabs` is only hidden outside edit mode, where the tabs are the
|
||||
* view editor; hide `dashboard_edit_button` too to keep them out of reach.
|
||||
*/
|
||||
payload: {
|
||||
enable: boolean;
|
||||
/**
|
||||
* Hide exactly these parts of the UI. Mutually exclusive with
|
||||
* `included_elements`; sending both fails the command. Plain strings, as a
|
||||
* newer client may name elements this frontend doesn't know; those are
|
||||
* ignored (see `resolveKioskElementsHidden`).
|
||||
*/
|
||||
excluded_elements?: string[];
|
||||
/**
|
||||
* Hide every part of the UI except these. Mutually exclusive with
|
||||
* `excluded_elements`; sending both fails the command. Unknown names are
|
||||
* ignored, as for `excluded_elements`.
|
||||
*/
|
||||
included_elements?: string[];
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -24,11 +24,6 @@ export class MockLawnMowerEntity extends MockBaseEntity {
|
||||
return;
|
||||
}
|
||||
|
||||
if (service === "stop") {
|
||||
this.update({ state: "idle" });
|
||||
return;
|
||||
}
|
||||
|
||||
if (service === "dock") {
|
||||
this._transition("returning", "docked", TRANSITION_MS);
|
||||
return;
|
||||
|
||||
@@ -24,7 +24,6 @@ import {
|
||||
} from "../data/context";
|
||||
import { updateHassGroups } from "../data/context/updateContext";
|
||||
import type { IconCategory } from "../data/icons";
|
||||
import { NO_KIOSK_ELEMENTS_HIDDEN } from "../data/kiosk_mode";
|
||||
import type { EntityRegistryDisplayEntry } from "../data/entity/entity_registry";
|
||||
import {
|
||||
DateFormat,
|
||||
@@ -465,7 +464,6 @@ export const provideHass = (
|
||||
vibrate: true,
|
||||
debugConnection: false,
|
||||
kioskMode: false,
|
||||
kioskElementsHidden: NO_KIOSK_ELEMENTS_HIDDEN,
|
||||
suspendWhenHidden: false,
|
||||
// @ts-ignore
|
||||
async callService(domain, service, data, target, _notify, returnResponse) {
|
||||
|
||||
@@ -60,9 +60,7 @@ export class HaInitPage extends LitElement {
|
||||
: nothing
|
||||
}
|
||||
`
|
||||
: html`<p
|
||||
class=${classMap({ "loading-text": !this.migration, rtl: document.dir === "rtl" })}
|
||||
>
|
||||
: html`<p class=${classMap({ "loading-text": !this.migration })}>
|
||||
${
|
||||
this.migration
|
||||
? html`<span class="migration-text"
|
||||
@@ -126,9 +124,6 @@ export class HaInitPage extends LitElement {
|
||||
.loading-text {
|
||||
opacity: 0.66;
|
||||
}
|
||||
.rtl {
|
||||
direction: rtl;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
|
||||
@@ -51,9 +51,7 @@ export class HomeAssistantMain extends LitElement {
|
||||
|
||||
protected render(): TemplateResult {
|
||||
const sidebarNarrow =
|
||||
this._sidebarNarrow ||
|
||||
this._externalSidebar ||
|
||||
this.hass.kioskElementsHidden.has("sidebar");
|
||||
this._sidebarNarrow || this._externalSidebar || this.hass.kioskMode;
|
||||
|
||||
const isPanelReady =
|
||||
this.hass.panels && this.hass.userData && this.hass.systemData;
|
||||
@@ -107,7 +105,7 @@ export class HomeAssistantMain extends LitElement {
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (this._sidebarNarrow || this.hass.kioskElementsHidden.has("sidebar")) {
|
||||
if (this._sidebarNarrow || this.hass.kioskMode) {
|
||||
this._drawerOpen = ev.detail?.open ?? !this._drawerOpen;
|
||||
} else {
|
||||
fireEvent(this, "hass-dock-sidebar", {
|
||||
@@ -146,9 +144,7 @@ export class HomeAssistantMain extends LitElement {
|
||||
|
||||
this.toggleAttribute(
|
||||
"modal",
|
||||
this._sidebarNarrow ||
|
||||
this._externalSidebar ||
|
||||
this.hass.kioskElementsHidden.has("sidebar")
|
||||
this._sidebarNarrow || this._externalSidebar || this.hass.kioskMode
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -17,7 +17,6 @@ import { applyThemesOnElement } from "../common/dom/apply_themes_on_element";
|
||||
import type { HASSDomEvent } from "../common/dom/fire_event";
|
||||
import { mainWindow } from "../common/dom/get_main_window";
|
||||
import { navigate } from "../common/navigate";
|
||||
import { buildLiteInternationalization } from "../common/translations/lite-internationalization";
|
||||
import {
|
||||
addSearchParam,
|
||||
extractSearchParam,
|
||||
@@ -25,7 +24,6 @@ import {
|
||||
} from "../common/url/search-params";
|
||||
import { subscribeOne } from "../common/util/subscribe-one";
|
||||
import "../components/ha-card";
|
||||
import "../components/progress/ha-progress-bar";
|
||||
import type { AuthUrlSearchParams } from "../data/auth";
|
||||
import { hassUrl } from "../data/auth";
|
||||
import { saveFrontendSystemData } from "../data/frontend";
|
||||
@@ -42,6 +40,7 @@ import { HassElement } from "../state/hass-element";
|
||||
import type { HomeAssistant, ValueChangedEvent } from "../types";
|
||||
import { storeState } from "../util/ha-pref-storage";
|
||||
import { registerServiceWorker } from "../util/register-service-worker";
|
||||
import "../components/progress/ha-progress-bar";
|
||||
import "./onboarding-analytics";
|
||||
import "./onboarding-create-user";
|
||||
import "./onboarding-loading";
|
||||
@@ -166,7 +165,9 @@ class HaOnboarding extends litLocalizeLiteMixin(HassElement) {
|
||||
}
|
||||
|
||||
if (this._init) {
|
||||
return html`<onboarding-welcome></onboarding-welcome>`;
|
||||
return html`<onboarding-welcome
|
||||
.localize=${this.localize}
|
||||
></onboarding-welcome>`;
|
||||
}
|
||||
|
||||
const step = this._curStep()!;
|
||||
@@ -229,20 +230,6 @@ class HaOnboarding extends litLocalizeLiteMixin(HassElement) {
|
||||
import("../components/ha-language-picker");
|
||||
}
|
||||
|
||||
protected willUpdate(changedProps: PropertyValues<this>) {
|
||||
super.willUpdate(changedProps);
|
||||
// Before `hass` connects, feed the context providers from the lite localize
|
||||
// state so context-consuming components render on the onboarding screens.
|
||||
if (
|
||||
!this.hass &&
|
||||
(changedProps.has("localize") || changedProps.has("language"))
|
||||
) {
|
||||
this._provideLiteInternationalization(
|
||||
buildLiteInternationalization(this.language, this.localize)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
protected updated(changedProps: PropertyValues) {
|
||||
super.updated(changedProps);
|
||||
if (changedProps.has("_page")) {
|
||||
|
||||
@@ -1,36 +1,36 @@
|
||||
import "@home-assistant/webawesome/dist/components/divider/divider";
|
||||
import type { CSSResultGroup, TemplateResult } from "lit";
|
||||
import { LitElement, css, html } from "lit";
|
||||
import { customElement, state } from "lit/decorators";
|
||||
import { consumeLocalize } from "../common/decorators/consume-context-entry";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import type { LocalizeFunc } from "../common/translations/localize";
|
||||
import "../components/ha-button";
|
||||
import "../components/ha-icon-next";
|
||||
import "../components/item/ha-list-item-button";
|
||||
import "../components/list/ha-list-base";
|
||||
import type { HomeAssistant } from "../types";
|
||||
import { onBoardingStyles } from "./styles";
|
||||
|
||||
@customElement("onboarding-welcome")
|
||||
class OnboardingWelcome extends LitElement {
|
||||
@state()
|
||||
@consumeLocalize()
|
||||
private _localize!: LocalizeFunc;
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public localize!: LocalizeFunc;
|
||||
|
||||
protected render(): TemplateResult {
|
||||
return html`
|
||||
<h1>${this._localize("ui.panel.page-onboarding.welcome.header")}</h1>
|
||||
<p>${this._localize("ui.panel.page-onboarding.intro")}</p>
|
||||
<h1>${this.localize("ui.panel.page-onboarding.welcome.header")}</h1>
|
||||
<p>${this.localize("ui.panel.page-onboarding.intro")}</p>
|
||||
|
||||
<ha-button @click=${this._start} class="start">
|
||||
${this._localize("ui.panel.page-onboarding.welcome.start")}
|
||||
${this.localize("ui.panel.page-onboarding.welcome.start")}
|
||||
</ha-button>
|
||||
|
||||
<div class="divider">
|
||||
<wa-divider></wa-divider>
|
||||
<div>
|
||||
<span
|
||||
>${this._localize(
|
||||
>${this.localize(
|
||||
"ui.panel.page-onboarding.welcome.or_restore"
|
||||
)}</span
|
||||
>
|
||||
@@ -40,10 +40,10 @@ class OnboardingWelcome extends LitElement {
|
||||
<ha-list-base>
|
||||
<ha-list-item-button @click=${this._restoreBackupUpload}>
|
||||
<div slot="headline">
|
||||
${this._localize("ui.panel.page-onboarding.restore.upload_backup")}
|
||||
${this.localize("ui.panel.page-onboarding.restore.upload_backup")}
|
||||
</div>
|
||||
<div slot="supporting-text">
|
||||
${this._localize(
|
||||
${this.localize(
|
||||
"ui.panel.page-onboarding.restore.options.upload_description"
|
||||
)}
|
||||
</div>
|
||||
@@ -52,7 +52,7 @@ class OnboardingWelcome extends LitElement {
|
||||
<ha-list-item-button @click=${this._restoreBackupCloud}>
|
||||
<div slot="headline">Home Assistant Cloud</div>
|
||||
<div slot="supporting-text">
|
||||
${this._localize(
|
||||
${this.localize(
|
||||
"ui.panel.page-onboarding.restore.ha-cloud.description"
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -49,6 +49,8 @@ class HaPanelApp extends LitElement {
|
||||
|
||||
@state() private _loadingMessage?: string;
|
||||
|
||||
@state() private _kioskMode = false;
|
||||
|
||||
@state() private _iframeLoaded = false;
|
||||
|
||||
// Set when the addon signals (via subscribe-properties) that it handles the
|
||||
@@ -81,6 +83,11 @@ class HaPanelApp extends LitElement {
|
||||
) {
|
||||
this._sendPropertiesToIframe();
|
||||
}
|
||||
|
||||
const oldHass = changedProps.get("hass") as HomeAssistant | undefined;
|
||||
if (oldHass && oldHass.kioskMode !== this.hass.kioskMode) {
|
||||
this._kioskMode = this.hass.kioskMode;
|
||||
}
|
||||
}
|
||||
|
||||
public connectedCallback() {
|
||||
@@ -103,10 +110,7 @@ class HaPanelApp extends LitElement {
|
||||
this._fetchDataTimeout = undefined;
|
||||
}
|
||||
if (this._enabledKioskMode) {
|
||||
fireEvent(window, "hass-kiosk-mode", {
|
||||
enable: false,
|
||||
source: "app_panel",
|
||||
});
|
||||
fireEvent(window, "hass-kiosk-mode", { enable: false });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -117,35 +121,18 @@ class HaPanelApp extends LitElement {
|
||||
></hass-loading-screen>`;
|
||||
}
|
||||
|
||||
// The sidebar button only exists inside this header, so hiding the header
|
||||
// hides the button too (documented on `app_panel_header`). Keeping it would
|
||||
// need a floating control over the add-on, which kiosk mode never had.
|
||||
const hideHeader = this.hass.kioskElementsHidden.has("app_panel_header");
|
||||
const sidebarHidden = this.hass.kioskElementsHidden.has("sidebar");
|
||||
const hideMenuButton = this.hass.kioskElementsHidden.has("sidebar_button");
|
||||
|
||||
// Make sure this all is 1 template so hiding toolbar doesn't reload iframe
|
||||
return html`
|
||||
${
|
||||
!hideHeader &&
|
||||
(this.narrow ||
|
||||
this.hass.dockedSidebar === "always_hidden" ||
|
||||
sidebarHidden)
|
||||
!this._kioskMode &&
|
||||
(this.narrow || this.hass.dockedSidebar === "always_hidden")
|
||||
? html`
|
||||
<div class="header">
|
||||
${
|
||||
hideMenuButton
|
||||
? nothing
|
||||
: html`
|
||||
<ha-icon-button
|
||||
.label=${this.hass.localize(
|
||||
"ui.sidebar.sidebar_toggle"
|
||||
)}
|
||||
.path=${mdiMenu}
|
||||
@click=${this._toggleMenu}
|
||||
></ha-icon-button>
|
||||
`
|
||||
}
|
||||
<ha-icon-button
|
||||
.label=${this.hass.localize("ui.sidebar.sidebar_toggle")}
|
||||
.path=${mdiMenu}
|
||||
@click=${this._toggleMenu}
|
||||
></ha-icon-button>
|
||||
<div class="main-title">${this._addon.name}</div>
|
||||
</div>
|
||||
`
|
||||
@@ -154,7 +141,7 @@ class HaPanelApp extends LitElement {
|
||||
<iframe
|
||||
class=${classMap({
|
||||
loaded: this._iframeLoaded,
|
||||
"kiosk-mode": hideHeader,
|
||||
"kiosk-mode": this._kioskMode,
|
||||
"handle-safe-area": this._handleSafeArea,
|
||||
})}
|
||||
title=${this._addon.name}
|
||||
@@ -196,10 +183,7 @@ class HaPanelApp extends LitElement {
|
||||
this._iframeLoaded = false;
|
||||
// Reset state when switching apps
|
||||
if (this._enabledKioskMode) {
|
||||
fireEvent(window, "hass-kiosk-mode", {
|
||||
enable: false,
|
||||
source: "app_panel",
|
||||
});
|
||||
fireEvent(window, "hass-kiosk-mode", { enable: false });
|
||||
this._enabledKioskMode = false;
|
||||
}
|
||||
this._iframeSubscribeUpdates = false;
|
||||
@@ -445,14 +429,9 @@ class HaPanelApp extends LitElement {
|
||||
// safe area itself; we then forward the inset values below.
|
||||
this._handleSafeArea = !!data.handleSafeArea;
|
||||
this._sendPropertiesToIframe();
|
||||
// Kept apart from the companion app's own kiosk request, so it adds
|
||||
// to that selection and leaves it in place when the addon lets go.
|
||||
if (data.kioskMode && !this._enabledKioskMode) {
|
||||
if (data.kioskMode && !this.hass.kioskMode) {
|
||||
this._enabledKioskMode = true;
|
||||
fireEvent(window, "hass-kiosk-mode", {
|
||||
enable: true,
|
||||
source: "app_panel",
|
||||
});
|
||||
fireEvent(window, "hass-kiosk-mode", { enable: true });
|
||||
}
|
||||
break;
|
||||
|
||||
@@ -460,10 +439,7 @@ class HaPanelApp extends LitElement {
|
||||
this._iframeSubscribeUpdates = false;
|
||||
this._handleSafeArea = false;
|
||||
if (this._enabledKioskMode) {
|
||||
fireEvent(window, "hass-kiosk-mode", {
|
||||
enable: false,
|
||||
source: "app_panel",
|
||||
});
|
||||
fireEvent(window, "hass-kiosk-mode", { enable: false });
|
||||
this._enabledKioskMode = false;
|
||||
}
|
||||
break;
|
||||
|
||||
@@ -82,9 +82,6 @@ export class HAFullCalendar extends LitElement {
|
||||
|
||||
@property({ attribute: "add-fab-style" }) public addFabStyle = "on_top";
|
||||
|
||||
@property({ attribute: "auto-height", type: Boolean }) public autoHeight =
|
||||
false;
|
||||
|
||||
@property({ attribute: false }) public events: CalendarEvent[] = [];
|
||||
|
||||
@property({ attribute: false }) public calendars: CalendarData[] = [];
|
||||
@@ -317,10 +314,6 @@ export class HAFullCalendar extends LitElement {
|
||||
this.calendar!.setOption("eventDisplay", this.eventDisplay);
|
||||
}
|
||||
|
||||
if (changedProps.has("autoHeight")) {
|
||||
this.calendar.setOption("height", this._height);
|
||||
}
|
||||
|
||||
const oldHass = changedProps.get("hass") as HomeAssistant;
|
||||
|
||||
if (oldHass && oldHass.language !== this.hass.language) {
|
||||
@@ -352,7 +345,6 @@ export class HAFullCalendar extends LitElement {
|
||||
: this.hass.config.time_zone,
|
||||
firstDay: firstWeekdayIndex(this.hass.locale),
|
||||
initialView,
|
||||
height: this._height,
|
||||
eventDisplay: this.eventDisplay,
|
||||
eventTimeFormat: {
|
||||
hour: useAmPm(this.hass.locale) ? "numeric" : "2-digit",
|
||||
@@ -372,10 +364,6 @@ export class HAFullCalendar extends LitElement {
|
||||
this._fireViewChanged();
|
||||
}
|
||||
|
||||
private get _height(): CalendarOptions["height"] {
|
||||
return this.autoHeight ? "auto" : defaultFullCalendarConfig.height;
|
||||
}
|
||||
|
||||
// Return if there are calendars that support creating events
|
||||
private get _hasMutableCalendars(): boolean {
|
||||
return this.calendars.some((selCal) => {
|
||||
|
||||
@@ -175,6 +175,8 @@ class SupervisorAppsCardContent extends LitElement {
|
||||
line-height: var(--ha-line-height-condensed);
|
||||
}
|
||||
.footer {
|
||||
border-top: var(--ha-border-width-sm) solid
|
||||
var(--ha-color-border-neutral-quiet);
|
||||
padding-top: var(--ha-space-2);
|
||||
display: flex;
|
||||
gap: var(--ha-space-2);
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { startOfYesterday } from "date-fns";
|
||||
import { consume } from "@lit/context";
|
||||
import {
|
||||
mdiChevronRight,
|
||||
mdiDelete,
|
||||
mdiDevices,
|
||||
mdiDotsVertical,
|
||||
@@ -38,7 +39,6 @@ import type { HaDropdownSelectEvent } from "../../../components/ha-dropdown";
|
||||
import "../../../components/ha-dropdown-item";
|
||||
import "../../../components/ha-icon-button";
|
||||
import "../../../components/ha-icon-next";
|
||||
import "../../../components/ha-icon-button-next";
|
||||
import "../../../components/ha-list";
|
||||
import "../../../components/ha-svg-icon";
|
||||
import "../../../components/ha-tooltip";
|
||||
@@ -604,11 +604,12 @@ class HaConfigAreaPage extends LitElement {
|
||||
back: "1",
|
||||
})}"
|
||||
>
|
||||
<ha-icon-button-next
|
||||
<ha-icon-button
|
||||
.path=${mdiChevronRight}
|
||||
.label=${this.hass.localize(
|
||||
"ui.dialogs.more_info_control.show_more"
|
||||
)}
|
||||
></ha-icon-button-next>
|
||||
></ha-icon-button>
|
||||
</a>
|
||||
</div>
|
||||
<ha-logbook
|
||||
|
||||
+186
-237
@@ -20,7 +20,6 @@ import { fireEvent } from "../../../../common/dom/fire_event";
|
||||
import { computeAreaName } from "../../../../common/entity/compute_area_name";
|
||||
import { computeDeviceName } from "../../../../common/entity/compute_device_name";
|
||||
import { computeEntityNameList } from "../../../../common/entity/compute_entity_name_display";
|
||||
import { getDeviceAreaId } from "../../../../common/entity/context/get_device_context";
|
||||
import { stringCompare } from "../../../../common/string/compare";
|
||||
import "../../../../components/ha-floor-icon";
|
||||
import "../../../../components/ha-icon";
|
||||
@@ -31,7 +30,10 @@ import "../../../../components/ha-svg-icon";
|
||||
import "../../../../components/item/ha-list-item-button";
|
||||
import "../../../../components/item/ha-row-item";
|
||||
import "../../../../components/list/ha-list-base";
|
||||
import { getAreaEntityLookup } from "../../../../data/area/area_registry";
|
||||
import {
|
||||
getAreaDeviceLookup,
|
||||
getAreaEntityLookup,
|
||||
} from "../../../../data/area/area_registry";
|
||||
import {
|
||||
getAreasNestedInFloors,
|
||||
type AreaFloorValue,
|
||||
@@ -49,10 +51,7 @@ import {
|
||||
registriesContext,
|
||||
statesContext,
|
||||
} from "../../../../data/context";
|
||||
import {
|
||||
getDeviceEntityLookup,
|
||||
type DeviceRegistryEntry,
|
||||
} from "../../../../data/device/device_registry";
|
||||
import { getDeviceEntityLookup } from "../../../../data/device/device_registry";
|
||||
import {
|
||||
domainToName,
|
||||
type DomainManifestLookup,
|
||||
@@ -84,7 +83,6 @@ interface Level2Entries {
|
||||
interface Level3Entries {
|
||||
open: boolean;
|
||||
entities: string[];
|
||||
devices?: Record<string, Level3Entries>;
|
||||
}
|
||||
|
||||
@customElement("ha-automation-add-from-target")
|
||||
@@ -283,19 +281,23 @@ export default class HaAutomationAddFromTarget extends LitElement {
|
||||
}
|
||||
|
||||
if (valueId && valueType === "device") {
|
||||
const entry = this._getDeviceEntry(entries, valueId);
|
||||
if (!entry) {
|
||||
return nothing;
|
||||
const areaId = this._registries.devices[valueId]?.area_id;
|
||||
if (areaId) {
|
||||
const floorId = this._registries.areas[areaId]?.floor_id || "";
|
||||
const { entities } =
|
||||
entries[`floor${TARGET_SEPARATOR}${floorId}`].areas![
|
||||
`area${TARGET_SEPARATOR}${areaId}`
|
||||
].devices![valueId];
|
||||
|
||||
return entities.length ? this._renderEntities(entities) : nothing;
|
||||
}
|
||||
const numberOfDevices = Object.keys(entry.devices ?? {}).length;
|
||||
return html`
|
||||
${numberOfDevices ? this._renderDevices(entry.devices!) : nothing}
|
||||
${
|
||||
entry.entities.length
|
||||
? this._renderEntities(entry.entities)
|
||||
: nothing
|
||||
}
|
||||
`;
|
||||
|
||||
const device = this._registries.devices[valueId];
|
||||
const isService = device.entry_type === "service";
|
||||
const { entities } =
|
||||
entries[`${isService ? "service" : "area"}${TARGET_SEPARATOR}`]
|
||||
.devices![valueId];
|
||||
return entities.length ? this._renderEntities(entities) : nothing;
|
||||
}
|
||||
|
||||
if (valueType === "device" || valueType === "helper") {
|
||||
@@ -678,8 +680,7 @@ export default class HaAutomationAddFromTarget extends LitElement {
|
||||
stringCompare(deviceNameA, deviceNameB, this.hass.locale.language)
|
||||
)
|
||||
.map(([deviceId, deviceName, domain]) => {
|
||||
const { open, entities, devices: children } = devices[deviceId];
|
||||
const numberOfChildren = Object.keys(children ?? {}).length;
|
||||
const { open, entities } = devices[deviceId];
|
||||
|
||||
return this._renderItem(
|
||||
deviceName || deviceId,
|
||||
@@ -687,15 +688,10 @@ export default class HaAutomationAddFromTarget extends LitElement {
|
||||
false,
|
||||
this._getSelectedTargetId(this.value) ===
|
||||
`device${TARGET_SEPARATOR}${deviceId}`,
|
||||
!open && !!(entities.length || numberOfChildren),
|
||||
!open && !!entities.length,
|
||||
open,
|
||||
domain ? this._renderDomainIcon(domain) : undefined,
|
||||
open
|
||||
? html`
|
||||
${numberOfChildren ? this._renderDevices(children!) : nothing}
|
||||
${this._renderEntities(entities)}
|
||||
`
|
||||
: undefined
|
||||
open ? this._renderEntities(entities) : undefined
|
||||
);
|
||||
});
|
||||
|
||||
@@ -903,16 +899,8 @@ export default class HaAutomationAddFromTarget extends LitElement {
|
||||
// #region memoized data helpers
|
||||
|
||||
private _getAreaDeviceLookupMemoized = memoizeOne(
|
||||
(devices: HomeAssistant["devices"]) => {
|
||||
const lookup: Record<string, DeviceRegistryEntry[]> = {};
|
||||
for (const device of Object.values(devices)) {
|
||||
const areaId = getDeviceAreaId(device, devices);
|
||||
if (areaId) {
|
||||
(lookup[areaId] ??= []).push(device);
|
||||
}
|
||||
}
|
||||
return lookup;
|
||||
}
|
||||
(devices: HomeAssistant["devices"]) =>
|
||||
getAreaDeviceLookup(Object.values(devices))
|
||||
);
|
||||
|
||||
private _getAreaEntityLookupMemoized = memoizeOne(
|
||||
@@ -986,18 +974,32 @@ export default class HaAutomationAddFromTarget extends LitElement {
|
||||
|
||||
private _loadUnassignedDevices() {
|
||||
const unassignedDevices = Object.values(this._registries.devices).filter(
|
||||
(device) =>
|
||||
!device.disabled_by &&
|
||||
!getDeviceAreaId(device, this._registries.devices)
|
||||
(device) => !device.area_id
|
||||
);
|
||||
|
||||
const devices = this._buildDeviceEntries(
|
||||
unassignedDevices.filter((device) => device.entry_type !== "service")
|
||||
);
|
||||
const devices: Record<string, Level3Entries> = {};
|
||||
|
||||
const services = this._buildDeviceEntries(
|
||||
unassignedDevices.filter((device) => device.entry_type === "service")
|
||||
);
|
||||
const services: Record<string, Level3Entries> = {};
|
||||
|
||||
unassignedDevices.forEach(({ id: deviceId, entry_type }) => {
|
||||
const device = this._registries.devices[deviceId];
|
||||
if (!device || device.disabled_by) {
|
||||
return;
|
||||
}
|
||||
const deviceEntry = {
|
||||
open: false,
|
||||
entities:
|
||||
this._getDeviceEntityLookupMemoized(this._registries.entities)[
|
||||
deviceId
|
||||
]?.map((entity) => entity.entity_id) || [],
|
||||
};
|
||||
if (entry_type === "service") {
|
||||
services[deviceId] = deviceEntry;
|
||||
return;
|
||||
}
|
||||
|
||||
devices[deviceId] = deviceEntry;
|
||||
});
|
||||
|
||||
if (Object.keys(devices).length) {
|
||||
this._entries = {
|
||||
@@ -1077,21 +1079,31 @@ export default class HaAutomationAddFromTarget extends LitElement {
|
||||
|
||||
private _loadArea(area: FloorComboBoxItem) {
|
||||
const [, id] = area.id.split(TARGET_SEPARATOR, 2);
|
||||
const referenced_devices = (
|
||||
this._getAreaDeviceLookupMemoized(this._registries.devices)[id] || []
|
||||
).filter((device) => !device.disabled_by);
|
||||
const referenced_devices =
|
||||
this._getAreaDeviceLookupMemoized(this._registries.devices)[id] || [];
|
||||
const referenced_entities =
|
||||
this._getAreaEntityLookupMemoized(this._registries.entities)[id] || [];
|
||||
|
||||
const devices = this._buildDeviceEntries(referenced_devices);
|
||||
const areaDeviceIds = new Set(
|
||||
referenced_devices.map((device) => device.id)
|
||||
);
|
||||
const devices: Record<string, Level3Entries> = {};
|
||||
|
||||
referenced_devices.forEach(({ id: deviceId }) => {
|
||||
const device = this._registries.devices[deviceId];
|
||||
if (!device || device.disabled_by) {
|
||||
return;
|
||||
}
|
||||
devices[deviceId] = {
|
||||
open: false,
|
||||
entities:
|
||||
this._getDeviceEntityLookupMemoized(this._registries.entities)[
|
||||
deviceId
|
||||
]?.map((entity) => entity.entity_id) || [],
|
||||
};
|
||||
});
|
||||
|
||||
const entities: string[] = [];
|
||||
|
||||
referenced_entities.forEach((entity) => {
|
||||
if (!entity.device_id || !areaDeviceIds.has(entity.device_id)) {
|
||||
if (!entity.device_id || !devices[entity.device_id]) {
|
||||
entities.push(entity.entity_id);
|
||||
}
|
||||
});
|
||||
@@ -1103,160 +1115,6 @@ export default class HaAutomationAddFromTarget extends LitElement {
|
||||
};
|
||||
}
|
||||
|
||||
private _buildDeviceEntries(
|
||||
devices: DeviceRegistryEntry[]
|
||||
): Record<string, Level3Entries> {
|
||||
const deviceEntityLookup = this._getDeviceEntityLookupMemoized(
|
||||
this._registries.entities
|
||||
);
|
||||
const entryFor = (deviceId: string): Level3Entries => ({
|
||||
open: false,
|
||||
entities:
|
||||
deviceEntityLookup[deviceId]?.map((entity) => entity.entity_id) || [],
|
||||
});
|
||||
|
||||
const deviceIds = new Set(devices.map((device) => device.id));
|
||||
const nested = (device: DeviceRegistryEntry) =>
|
||||
!!device.parent_device_id && deviceIds.has(device.parent_device_id);
|
||||
|
||||
const roots = devices.filter((device) => !nested(device));
|
||||
const children = devices.filter(nested);
|
||||
|
||||
const entries: Record<string, Level3Entries> = {};
|
||||
for (const device of roots) {
|
||||
entries[device.id] = entryFor(device.id);
|
||||
}
|
||||
for (const device of children) {
|
||||
(entries[device.parent_device_id!].devices ??= {})[device.id] = entryFor(
|
||||
device.id
|
||||
);
|
||||
}
|
||||
return entries;
|
||||
}
|
||||
|
||||
private _deviceGroupPath(
|
||||
device: DeviceRegistryEntry
|
||||
): [level1: string, level2?: string] {
|
||||
const areaId = getDeviceAreaId(device, this._registries.devices);
|
||||
if (areaId) {
|
||||
return [
|
||||
`floor${TARGET_SEPARATOR}${this._registries.areas[areaId]?.floor_id || ""}`,
|
||||
`area${TARGET_SEPARATOR}${areaId}`,
|
||||
];
|
||||
}
|
||||
return [
|
||||
`${device.entry_type === "service" ? "service" : "area"}${TARGET_SEPARATOR}`,
|
||||
];
|
||||
}
|
||||
|
||||
private _deviceGroup(
|
||||
entries: Record<string, Level1Entries>,
|
||||
device: DeviceRegistryEntry
|
||||
): Record<string, Level3Entries> | undefined {
|
||||
const [level1, level2] = this._deviceGroupPath(device);
|
||||
const entry = entries[level1];
|
||||
return level2 ? entry?.areas?.[level2]?.devices : entry?.devices;
|
||||
}
|
||||
|
||||
private _deviceChain(
|
||||
group: Record<string, Level3Entries> | undefined,
|
||||
device: DeviceRegistryEntry
|
||||
): string[] {
|
||||
const parentId = device.parent_device_id;
|
||||
return parentId && group?.[parentId]?.devices?.[device.id]
|
||||
? [parentId, device.id]
|
||||
: [device.id];
|
||||
}
|
||||
|
||||
private _getDeviceEntry(
|
||||
entries: Record<string, Level1Entries>,
|
||||
deviceId: string
|
||||
): Level3Entries | undefined {
|
||||
const device = this._registries.devices[deviceId];
|
||||
if (!device) {
|
||||
return undefined;
|
||||
}
|
||||
const group = this._deviceGroup(entries, device);
|
||||
let level = group;
|
||||
let entry: Level3Entries | undefined;
|
||||
for (const key of this._deviceChain(group, device)) {
|
||||
entry = level?.[key];
|
||||
level = entry?.devices;
|
||||
}
|
||||
return entry;
|
||||
}
|
||||
|
||||
private _setDeviceOpen(
|
||||
deviceId: string,
|
||||
deviceOpen: boolean | undefined,
|
||||
openAncestors: boolean
|
||||
) {
|
||||
const device = this._registries.devices[deviceId];
|
||||
if (!device) {
|
||||
return;
|
||||
}
|
||||
const [level1, level2] = this._deviceGroupPath(device);
|
||||
const group = this._deviceGroup(this._entries, device);
|
||||
if (!group) {
|
||||
return;
|
||||
}
|
||||
const chain = this._deviceChain(group, device);
|
||||
|
||||
const updateGroup = (
|
||||
devices: Record<string, Level3Entries>,
|
||||
[key, ...rest]: string[]
|
||||
): Record<string, Level3Entries> => {
|
||||
const entry = devices[key];
|
||||
if (!entry) {
|
||||
return devices;
|
||||
}
|
||||
const isTarget = !rest.length;
|
||||
return {
|
||||
...devices,
|
||||
[key]: {
|
||||
...entry,
|
||||
open: isTarget
|
||||
? (deviceOpen ?? entry.open)
|
||||
: openAncestors || entry.open,
|
||||
devices:
|
||||
isTarget || !entry.devices
|
||||
? entry.devices
|
||||
: updateGroup(entry.devices, rest),
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
const level1Entry = this._entries[level1];
|
||||
if (!level2) {
|
||||
this._entries = {
|
||||
...this._entries,
|
||||
[level1]: {
|
||||
...level1Entry,
|
||||
open: openAncestors || level1Entry.open,
|
||||
devices: updateGroup(group, chain),
|
||||
},
|
||||
};
|
||||
return;
|
||||
}
|
||||
|
||||
const level2Entry = level1Entry.areas![level2];
|
||||
this._entries = {
|
||||
...this._entries,
|
||||
[level1]: {
|
||||
...level1Entry,
|
||||
open: openAncestors || level1Entry.open,
|
||||
areas: {
|
||||
...level1Entry.areas,
|
||||
[level2]: {
|
||||
...level2Entry,
|
||||
open: openAncestors || level2Entry.open,
|
||||
devices: updateGroup(group, chain),
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
private _expandTreeToItem(type: string, id: string) {
|
||||
if (type === "floor" || type === "label") {
|
||||
return;
|
||||
@@ -1264,26 +1122,39 @@ export default class HaAutomationAddFromTarget extends LitElement {
|
||||
|
||||
if (type === "entity") {
|
||||
const deviceId = this._registries.entities[id]?.device_id;
|
||||
if (deviceId && this._registries.devices[deviceId]) {
|
||||
this._setDeviceOpen(deviceId, true, true);
|
||||
return;
|
||||
}
|
||||
const device = deviceId ? this._registries.devices[deviceId] : undefined;
|
||||
const deviceAreaId = (deviceId && device?.area_id) || undefined;
|
||||
|
||||
const entity = this._registries.entities[id];
|
||||
if (!deviceAreaId) {
|
||||
let floor: string;
|
||||
let area: string;
|
||||
const entity = this._registries.entities[id];
|
||||
|
||||
if (entity?.area_id) {
|
||||
const floor = `floor${TARGET_SEPARATOR}${this._registries.areas[entity.area_id]?.floor_id || ""}`;
|
||||
const area = `area${TARGET_SEPARATOR}${entity.area_id}`;
|
||||
const floorEntry = this._entries[floor];
|
||||
if (!deviceId && entity.area_id) {
|
||||
floor = `floor${TARGET_SEPARATOR}${this._registries.areas[entity.area_id]?.floor_id || ""}`;
|
||||
area = `area${TARGET_SEPARATOR}${entity.area_id}`;
|
||||
} else if (!deviceId) {
|
||||
const domain = id.split(".", 1)[0];
|
||||
const isHelper =
|
||||
this.manifests![domain]?.integration_type === "helper";
|
||||
|
||||
floor = isHelper
|
||||
? `helper${TARGET_SEPARATOR}`
|
||||
: `device${TARGET_SEPARATOR}`;
|
||||
area = `${isHelper ? "helper_" : "entity_"}${domain}${TARGET_SEPARATOR}`;
|
||||
} else {
|
||||
floor = `${device!.entry_type === "service" ? "service" : "area"}${TARGET_SEPARATOR}`;
|
||||
area = deviceId;
|
||||
}
|
||||
this._entries = {
|
||||
...this._entries,
|
||||
[floor]: {
|
||||
...floorEntry,
|
||||
...this._entries[floor],
|
||||
open: true,
|
||||
areas: {
|
||||
...floorEntry.areas,
|
||||
devices: {
|
||||
...this._entries[floor].devices!,
|
||||
[area]: {
|
||||
...floorEntry.areas![area],
|
||||
...this._entries[floor].devices![area],
|
||||
open: true,
|
||||
},
|
||||
},
|
||||
@@ -1292,23 +1163,26 @@ export default class HaAutomationAddFromTarget extends LitElement {
|
||||
return;
|
||||
}
|
||||
|
||||
const domain = id.split(".", 1)[0];
|
||||
const isHelper = this.manifests![domain]?.integration_type === "helper";
|
||||
const group = isHelper
|
||||
? `helper${TARGET_SEPARATOR}`
|
||||
: `device${TARGET_SEPARATOR}`;
|
||||
const domainGroup = `${isHelper ? "helper_" : "entity_"}${domain}${TARGET_SEPARATOR}`;
|
||||
const groupEntry = this._entries[group];
|
||||
const floor = `floor${TARGET_SEPARATOR}${this._registries.areas[deviceAreaId]?.floor_id || ""}`;
|
||||
const area = `area${TARGET_SEPARATOR}${deviceAreaId}`;
|
||||
|
||||
this._entries = {
|
||||
...this._entries,
|
||||
[group]: {
|
||||
...groupEntry,
|
||||
[floor]: {
|
||||
...this._entries[floor],
|
||||
open: true,
|
||||
devices: {
|
||||
...groupEntry.devices,
|
||||
[domainGroup]: {
|
||||
...groupEntry.devices![domainGroup],
|
||||
areas: {
|
||||
...this._entries[floor].areas!,
|
||||
[area]: {
|
||||
...this._entries[floor].areas![area],
|
||||
open: true,
|
||||
devices: {
|
||||
...this._entries[floor].areas![area].devices,
|
||||
[deviceId!]: {
|
||||
...this._entries[floor].areas![area].devices![deviceId!],
|
||||
open: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -1317,7 +1191,38 @@ export default class HaAutomationAddFromTarget extends LitElement {
|
||||
}
|
||||
|
||||
if (type === "device") {
|
||||
this._setDeviceOpen(id, undefined, true);
|
||||
const deviceAreaId = this._registries.devices[id]?.area_id;
|
||||
|
||||
if (!deviceAreaId) {
|
||||
const device = this._registries.devices[id];
|
||||
const floor = `${device.entry_type === "service" ? "service" : "area"}${TARGET_SEPARATOR}`;
|
||||
this._entries = {
|
||||
...this._entries,
|
||||
[floor]: {
|
||||
...this._entries[floor],
|
||||
open: true,
|
||||
},
|
||||
};
|
||||
return;
|
||||
}
|
||||
|
||||
const floor = `floor${TARGET_SEPARATOR}${this._registries.areas[deviceAreaId]?.floor_id || ""}`;
|
||||
const area = `area${TARGET_SEPARATOR}${deviceAreaId}`;
|
||||
|
||||
this._entries = {
|
||||
...this._entries,
|
||||
[floor]: {
|
||||
...this._entries[floor],
|
||||
open: true,
|
||||
areas: {
|
||||
...this._entries[floor].areas!,
|
||||
[area]: {
|
||||
...this._entries[floor].areas![area],
|
||||
open: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1443,7 +1348,51 @@ export default class HaAutomationAddFromTarget extends LitElement {
|
||||
}
|
||||
|
||||
if (type === "device" && id) {
|
||||
this._setDeviceOpen(id, open, false);
|
||||
const areaId = this._registries.devices[id]?.area_id;
|
||||
if (areaId) {
|
||||
const areaTargetId = `area${TARGET_SEPARATOR}${this._registries.devices[id]?.area_id ?? ""}`;
|
||||
const floorId = `floor${TARGET_SEPARATOR}${(areaId && this._registries.areas[areaId]?.floor_id) || ""}`;
|
||||
|
||||
this._entries = {
|
||||
...this._entries,
|
||||
[floorId]: {
|
||||
...this._entries[floorId],
|
||||
areas: {
|
||||
...this._entries[floorId].areas,
|
||||
[areaTargetId]: {
|
||||
...this._entries[floorId].areas![areaTargetId],
|
||||
devices: {
|
||||
...this._entries[floorId].areas![areaTargetId].devices,
|
||||
[id]: {
|
||||
...this._entries[floorId].areas![areaTargetId].devices[id],
|
||||
open,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
return;
|
||||
}
|
||||
|
||||
const deviceType =
|
||||
this._registries.devices[id]?.entry_type === "service"
|
||||
? "service"
|
||||
: "area";
|
||||
const floorId = `${deviceType}${TARGET_SEPARATOR}`;
|
||||
this._entries = {
|
||||
...this._entries,
|
||||
[floorId]: {
|
||||
...this._entries[floorId],
|
||||
devices: {
|
||||
...this._entries[floorId].devices,
|
||||
[id]: {
|
||||
...this._entries[floorId].devices![id],
|
||||
open,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -8,7 +8,6 @@ import type { TriggerElement } from "../ha-automation-trigger-row";
|
||||
import type { HaDurationData } from "../../../../../components/ha-duration-input";
|
||||
import "../../../../../components/ha-form/ha-form";
|
||||
import { createDurationData } from "../../../../../common/datetime/create_duration_data";
|
||||
import { durationDataToSeconds } from "../../../../../common/datetime/duration_to_seconds";
|
||||
import type { LocalizeFunc } from "../../../../../common/translations/localize";
|
||||
import type { SchemaUnion } from "../../../../../components/ha-form/types";
|
||||
|
||||
@@ -83,21 +82,13 @@ export class HaCalendarTrigger extends LitElement implements TriggerElement {
|
||||
const schema = this._schema(this.hass.localize);
|
||||
// Convert from string representation to ha form duration representation
|
||||
const trigger_offset = this.trigger.offset;
|
||||
// Copy, `createDurationData` returns the input object as-is for dict values.
|
||||
const duration: HaDurationData = { ...createDurationData(trigger_offset)! };
|
||||
const duration: HaDurationData = createDurationData(trigger_offset)!;
|
||||
let offset_type = "after";
|
||||
if (durationDataToSeconds(duration) < 0) {
|
||||
// A negative offset negates the whole period, and the sign is shown by
|
||||
// the separate before/after select instead.
|
||||
if (duration.days) {
|
||||
duration.days = Math.abs(duration.days);
|
||||
}
|
||||
duration.hours = Math.abs(duration.hours ?? 0);
|
||||
duration.minutes = Math.abs(duration.minutes ?? 0);
|
||||
duration.seconds = Math.abs(duration.seconds ?? 0);
|
||||
if (duration.milliseconds) {
|
||||
duration.milliseconds = Math.abs(duration.milliseconds);
|
||||
}
|
||||
if (
|
||||
(typeof trigger_offset === "object" && duration!.hours! < 0) ||
|
||||
(typeof trigger_offset === "string" && trigger_offset.startsWith("-"))
|
||||
) {
|
||||
duration.hours = Math.abs(duration.hours!);
|
||||
offset_type = "before";
|
||||
}
|
||||
const data = {
|
||||
@@ -122,12 +113,11 @@ export class HaCalendarTrigger extends LitElement implements TriggerElement {
|
||||
// Convert back to duration string representation
|
||||
const duration = ev.detail.value.offset;
|
||||
const offsetType = ev.detail.value.offset_type === "before" ? "-" : "";
|
||||
const h = (duration.days ?? 0) * 24 + (duration.hours ?? 0);
|
||||
const m = duration.minutes ?? 0;
|
||||
const s = (duration.seconds ?? 0) + (duration.milliseconds ?? 0) / 1000;
|
||||
const newTrigger = {
|
||||
...ev.detail.value,
|
||||
offset: `${offsetType}${h}:${m}:${s}`,
|
||||
offset: `${offsetType}${duration.hours ?? 0}:${duration.minutes ?? 0}:${
|
||||
duration.seconds ?? 0
|
||||
}`,
|
||||
};
|
||||
delete newTrigger.offset_type;
|
||||
fireEvent(this, "value-changed", { value: newTrigger });
|
||||
|
||||
@@ -2,7 +2,9 @@ import { css, html, LitElement } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { ensureArray } from "../../../../../common/array/ensure-array";
|
||||
import { createDurationData } from "../../../../../common/datetime/create_duration_data";
|
||||
import { fireEvent } from "../../../../../common/dom/fire_event";
|
||||
import { hasTemplate } from "../../../../../common/string/has-template";
|
||||
import "../../../../../components/ha-form/ha-form";
|
||||
import type { SchemaUnion } from "../../../../../components/ha-form/types";
|
||||
import type { NumericStateTrigger } from "../../../../../data/automation";
|
||||
@@ -196,6 +198,21 @@ export class HaNumericStateTrigger extends LitElement {
|
||||
};
|
||||
}
|
||||
|
||||
private _wrapForValue(
|
||||
forValue: NumericStateTrigger["for"]
|
||||
): Record<string, unknown> | undefined {
|
||||
if (forValue === undefined) {
|
||||
return undefined;
|
||||
}
|
||||
if (typeof forValue === "string" && hasTemplate(forValue)) {
|
||||
return { active_choice: "template", template: forValue };
|
||||
}
|
||||
return {
|
||||
active_choice: "duration",
|
||||
duration: createDurationData(forValue),
|
||||
};
|
||||
}
|
||||
|
||||
private _unwrapForValue(
|
||||
forValue: Record<string, unknown> | undefined
|
||||
): NumericStateTrigger["for"] {
|
||||
@@ -223,6 +240,7 @@ export class HaNumericStateTrigger extends LitElement {
|
||||
private _data = memoizeOne((trigger: NumericStateTrigger) => ({
|
||||
...trigger,
|
||||
entity_id: ensureArray(trigger.entity_id),
|
||||
for: this._wrapForValue(trigger.for),
|
||||
}));
|
||||
|
||||
public render() {
|
||||
|
||||
@@ -15,6 +15,7 @@ import {
|
||||
union,
|
||||
} from "superstruct";
|
||||
import { ensureArray } from "../../../../../common/array/ensure-array";
|
||||
import { createDurationData } from "../../../../../common/datetime/create_duration_data";
|
||||
import { fireEvent } from "../../../../../common/dom/fire_event";
|
||||
import { hasTemplate } from "../../../../../common/string/has-template";
|
||||
import type { LocalizeFunc } from "../../../../../common/translations/localize";
|
||||
@@ -219,6 +220,21 @@ export class HaStateTrigger extends LitElement implements TriggerElement {
|
||||
return true;
|
||||
}
|
||||
|
||||
private _wrapForValue(
|
||||
forValue: StateTrigger["for"]
|
||||
): Record<string, unknown> | undefined {
|
||||
if (forValue === undefined) {
|
||||
return undefined;
|
||||
}
|
||||
if (typeof forValue === "string" && hasTemplate(forValue)) {
|
||||
return { active_choice: "template", template: forValue };
|
||||
}
|
||||
return {
|
||||
active_choice: "duration",
|
||||
duration: createDurationData(forValue),
|
||||
};
|
||||
}
|
||||
|
||||
private _unwrapForValue(
|
||||
forValue: Record<string, unknown> | undefined
|
||||
): StateTrigger["for"] {
|
||||
@@ -235,6 +251,7 @@ export class HaStateTrigger extends LitElement implements TriggerElement {
|
||||
const data = {
|
||||
...this.trigger,
|
||||
entity_id: ensureArray(this.trigger.entity_id),
|
||||
for: this._wrapForValue(this.trigger.for),
|
||||
};
|
||||
|
||||
data.to = this._normalizeStates(this.trigger.to, data.attribute);
|
||||
|
||||
@@ -3,7 +3,9 @@ import { customElement, property } from "lit/decorators";
|
||||
import type { TemplateTrigger } from "../../../../../data/automation";
|
||||
import type { HomeAssistant } from "../../../../../types";
|
||||
import "../../../../../components/ha-form/ha-form";
|
||||
import { createDurationData } from "../../../../../common/datetime/create_duration_data";
|
||||
import { fireEvent } from "../../../../../common/dom/fire_event";
|
||||
import { hasTemplate } from "../../../../../common/string/has-template";
|
||||
import type { SchemaUnion } from "../../../../../components/ha-form/types";
|
||||
|
||||
const SCHEMA = [
|
||||
@@ -35,6 +37,21 @@ export class HaTemplateTrigger extends LitElement {
|
||||
return { trigger: "template", value_template: "" };
|
||||
}
|
||||
|
||||
private _wrapForValue(
|
||||
forValue: TemplateTrigger["for"]
|
||||
): Record<string, unknown> | undefined {
|
||||
if (forValue === undefined) {
|
||||
return undefined;
|
||||
}
|
||||
if (typeof forValue === "string" && hasTemplate(forValue)) {
|
||||
return { active_choice: "template", template: forValue };
|
||||
}
|
||||
return {
|
||||
active_choice: "duration",
|
||||
duration: createDurationData(forValue),
|
||||
};
|
||||
}
|
||||
|
||||
private _unwrapForValue(
|
||||
forValue: Record<string, unknown> | undefined
|
||||
): TemplateTrigger["for"] {
|
||||
@@ -50,6 +67,7 @@ export class HaTemplateTrigger extends LitElement {
|
||||
protected render() {
|
||||
const data = {
|
||||
...this.trigger,
|
||||
for: this._wrapForValue(this.trigger.for),
|
||||
};
|
||||
|
||||
return html`
|
||||
|
||||
@@ -5,13 +5,10 @@ import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, state } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { consumeLocalize } from "../../../common/decorators/consume-context-entry";
|
||||
import { ENTITY_NAME_TYPES } from "../../../common/entity/compute_entity_name_display";
|
||||
import type { LocalizeFunc } from "../../../common/translations/localize";
|
||||
import { debounce } from "../../../common/util/debounce";
|
||||
import type { HaProgressButton } from "../../../components/buttons/ha-progress-button";
|
||||
import "../../../components/buttons/ha-progress-button";
|
||||
import "../../../components/chips/ha-chip-set";
|
||||
import "../../../components/chips/ha-filter-chip";
|
||||
import "../../../components/ha-alert";
|
||||
import "../../../components/ha-button";
|
||||
import "../../../components/ha-card";
|
||||
@@ -35,11 +32,6 @@ import "./ha-entity-id-format-editor";
|
||||
const EXAMPLE_DOMAIN = "sensor";
|
||||
const PREVIEW_DEBOUNCE_MS = 200;
|
||||
|
||||
interface EntityIdExample {
|
||||
name: string;
|
||||
parts: Partial<Record<EntityIdPart, string>>;
|
||||
}
|
||||
|
||||
@customElement("ha-config-entity-id-format")
|
||||
export class HaConfigEntityIdFormat extends LitElement {
|
||||
@state()
|
||||
@@ -58,12 +50,10 @@ export class HaConfigEntityIdFormat extends LitElement {
|
||||
|
||||
@state() private _error?: string;
|
||||
|
||||
@state() private _previews?: string[];
|
||||
@state() private _preview?: string;
|
||||
|
||||
@state() private _previewError = false;
|
||||
|
||||
@state() private _selectedExample = 0;
|
||||
|
||||
protected async firstUpdated(changedProps: PropertyValues<this>) {
|
||||
super.firstUpdated(changedProps);
|
||||
try {
|
||||
@@ -89,49 +79,12 @@ export class HaConfigEntityIdFormat extends LitElement {
|
||||
}
|
||||
|
||||
private _examples = memoizeOne(
|
||||
(localize: LocalizeFunc): EntityIdExample[] => [
|
||||
{
|
||||
name: localize(
|
||||
"ui.panel.config.entity_id_format.card.examples.thermostat.name"
|
||||
),
|
||||
parts: {
|
||||
floor: localize(
|
||||
"ui.panel.config.entity_id_format.card.examples.thermostat.floor"
|
||||
),
|
||||
area: localize(
|
||||
"ui.panel.config.entity_id_format.card.examples.thermostat.area"
|
||||
),
|
||||
device: localize(
|
||||
"ui.panel.config.entity_id_format.card.examples.thermostat.device"
|
||||
),
|
||||
entity: localize(
|
||||
"ui.panel.config.entity_id_format.card.examples.thermostat.entity"
|
||||
),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: localize(
|
||||
"ui.panel.config.entity_id_format.card.examples.power_strip.name"
|
||||
),
|
||||
parts: {
|
||||
floor: localize(
|
||||
"ui.panel.config.entity_id_format.card.examples.power_strip.floor"
|
||||
),
|
||||
area: localize(
|
||||
"ui.panel.config.entity_id_format.card.examples.power_strip.area"
|
||||
),
|
||||
parent_device: localize(
|
||||
"ui.panel.config.entity_id_format.card.examples.power_strip.parent_device"
|
||||
),
|
||||
device: localize(
|
||||
"ui.panel.config.entity_id_format.card.examples.power_strip.device"
|
||||
),
|
||||
entity: localize(
|
||||
"ui.panel.config.entity_id_format.card.examples.power_strip.entity"
|
||||
),
|
||||
},
|
||||
},
|
||||
]
|
||||
(localize: LocalizeFunc): Record<EntityIdPart, string> => ({
|
||||
area: localize("ui.panel.config.entity_id_format.card.examples.area"),
|
||||
device: localize("ui.panel.config.entity_id_format.card.examples.device"),
|
||||
entity: localize("ui.panel.config.entity_id_format.card.examples.entity"),
|
||||
floor: localize("ui.panel.config.entity_id_format.card.examples.floor"),
|
||||
})
|
||||
);
|
||||
|
||||
private _debouncedUpdatePreview = debounce(
|
||||
@@ -141,16 +94,12 @@ export class HaConfigEntityIdFormat extends LitElement {
|
||||
|
||||
private async _updatePreview() {
|
||||
const examples = this._examples(this._localize);
|
||||
const fullName = this._format!.map((part) => examples[part])
|
||||
.filter(Boolean)
|
||||
.join(" ");
|
||||
try {
|
||||
this._previews = await Promise.all(
|
||||
examples.map(async (example) => {
|
||||
const fullName = this._format!.map((part) => example.parts[part])
|
||||
.filter(Boolean)
|
||||
.join(" ");
|
||||
const { slug } = await fetchSlug(this._api, fullName);
|
||||
return slug;
|
||||
})
|
||||
);
|
||||
const { slug } = await fetchSlug(this._api, fullName);
|
||||
this._preview = slug;
|
||||
this._previewError = false;
|
||||
} catch (_err: any) {
|
||||
this._previewError = true;
|
||||
@@ -197,6 +146,7 @@ export class HaConfigEntityIdFormat extends LitElement {
|
||||
)}
|
||||
@value-changed=${this._formatChanged}
|
||||
></ha-entity-id-format-editor>
|
||||
${this._renderPreview()}
|
||||
`
|
||||
: nothing
|
||||
}
|
||||
@@ -219,82 +169,28 @@ export class HaConfigEntityIdFormat extends LitElement {
|
||||
</ha-progress-button>
|
||||
</div>
|
||||
</ha-card>
|
||||
${this._format ? this._renderExamples() : nothing}
|
||||
`;
|
||||
}
|
||||
|
||||
private _renderExamples() {
|
||||
const examples = this._examples(this._localize);
|
||||
const example = examples[this._selectedExample];
|
||||
const parts = ENTITY_NAME_TYPES.filter((part) => example.parts[part]);
|
||||
private _renderPreview() {
|
||||
return html`
|
||||
<ha-card
|
||||
outlined
|
||||
.header=${this._localize(
|
||||
"ui.panel.config.entity_id_format.card.preview"
|
||||
)}
|
||||
>
|
||||
<div class="card-content examples">
|
||||
<ha-chip-set>
|
||||
${examples.map(
|
||||
(item, index) => html`
|
||||
<ha-filter-chip
|
||||
no-leading-icon
|
||||
data-index=${index}
|
||||
.selected=${index === this._selectedExample}
|
||||
.label=${item.name}
|
||||
@click=${this._selectExample}
|
||||
></ha-filter-chip>
|
||||
`
|
||||
)}
|
||||
</ha-chip-set>
|
||||
${
|
||||
this._previewError
|
||||
? html`<ha-alert alert-type="error">
|
||||
${this._localize(
|
||||
"ui.panel.config.entity_id_format.card.preview_error"
|
||||
)}
|
||||
</ha-alert>`
|
||||
: html`<code
|
||||
>${EXAMPLE_DOMAIN}.${
|
||||
this._previews?.[this._selectedExample] ?? "…"
|
||||
}</code
|
||||
>`
|
||||
}
|
||||
<dl>
|
||||
${parts.map((part) => {
|
||||
const used = this._format!.includes(part);
|
||||
return html`
|
||||
<dt>
|
||||
${this._localize(
|
||||
`ui.components.entity.entity-name-picker.types.${part}`
|
||||
)}
|
||||
</dt>
|
||||
<dd class=${used ? "" : "unused"}>
|
||||
${
|
||||
used
|
||||
? example.parts[part]
|
||||
: this._localize(
|
||||
"ui.panel.config.entity_id_format.card.unused_part",
|
||||
{ name: example.parts[part] }
|
||||
)
|
||||
}
|
||||
</dd>
|
||||
`;
|
||||
})}
|
||||
</dl>
|
||||
</div>
|
||||
</ha-card>
|
||||
<div class="preview">
|
||||
<span class="preview-label">
|
||||
${this._localize("ui.panel.config.entity_id_format.card.preview")}
|
||||
</span>
|
||||
${
|
||||
this._previewError
|
||||
? html`<ha-alert alert-type="error">
|
||||
${this._localize(
|
||||
"ui.panel.config.entity_id_format.card.preview_error"
|
||||
)}
|
||||
</ha-alert>`
|
||||
: html`<code>${EXAMPLE_DOMAIN}.${this._preview ?? "…"}</code>`
|
||||
}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
private _selectExample(ev: Event) {
|
||||
ev.preventDefault();
|
||||
this._selectedExample = Number(
|
||||
(ev.currentTarget as HTMLElement).dataset.index
|
||||
);
|
||||
}
|
||||
|
||||
private _formatChanged(ev: CustomEvent) {
|
||||
this._format = ev.detail.value;
|
||||
}
|
||||
@@ -329,36 +225,24 @@ export class HaConfigEntityIdFormat extends LitElement {
|
||||
haStyle,
|
||||
css`
|
||||
:host {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--ha-space-5);
|
||||
display: block;
|
||||
}
|
||||
.description {
|
||||
margin-top: 0;
|
||||
color: var(--secondary-text-color);
|
||||
}
|
||||
.examples {
|
||||
.preview {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--ha-space-3);
|
||||
gap: var(--ha-space-1);
|
||||
margin-top: var(--ha-space-5);
|
||||
}
|
||||
.examples dl {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) minmax(0, 2fr);
|
||||
column-gap: var(--ha-space-3);
|
||||
margin: 0;
|
||||
overflow-wrap: anywhere;
|
||||
.preview-label {
|
||||
font-weight: 500;
|
||||
}
|
||||
.examples dt,
|
||||
.examples .unused {
|
||||
color: var(--secondary-text-color);
|
||||
}
|
||||
.examples dd {
|
||||
margin: 0;
|
||||
}
|
||||
.examples code {
|
||||
.preview code {
|
||||
display: block;
|
||||
padding: var(--ha-space-2);
|
||||
padding: var(--ha-space-3);
|
||||
border-radius: var(--ha-border-radius-md);
|
||||
background-color: var(--secondary-background-color);
|
||||
font-family: var(--ha-font-family-code);
|
||||
@@ -368,7 +252,6 @@ export class HaConfigEntityIdFormat extends LitElement {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding-block-end: var(--ha-space-4);
|
||||
}
|
||||
`,
|
||||
];
|
||||
|
||||
@@ -6,7 +6,6 @@ import { repeat } from "lit/directives/repeat";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { consumeLocalize } from "../../../common/decorators/consume-context-entry";
|
||||
import { fireEvent } from "../../../common/dom/fire_event";
|
||||
import { ENTITY_NAME_TYPES } from "../../../common/entity/compute_entity_name_display";
|
||||
import type { LocalizeFunc } from "../../../common/translations/localize";
|
||||
import "../../../components/chips/ha-assist-chip";
|
||||
import "../../../components/chips/ha-chip-set";
|
||||
@@ -23,6 +22,8 @@ import type {
|
||||
} from "../../../data/entity_id_format";
|
||||
import type { ValueChangedEvent } from "../../../types";
|
||||
|
||||
const STRUCTURAL_TYPES = ["area", "device", "entity", "floor"] as const;
|
||||
|
||||
const REQUIRED_TYPES: readonly EntityIdPart[] = ["device", "entity"];
|
||||
|
||||
const rowRenderer: RenderItemFunction<PickerComboBoxItem> = (item) => html`
|
||||
@@ -38,7 +39,7 @@ const decodeType = (value: string): EntityIdPart | undefined => {
|
||||
value.startsWith("___") && value.endsWith("___")
|
||||
? value.slice(3, -3)
|
||||
: value;
|
||||
return (ENTITY_NAME_TYPES as readonly string[]).includes(type)
|
||||
return (STRUCTURAL_TYPES as readonly string[]).includes(type)
|
||||
? (type as EntityIdPart)
|
||||
: undefined;
|
||||
};
|
||||
@@ -64,7 +65,6 @@ export class HaEntityIdFormatEditor extends LitElement {
|
||||
<div class="container">
|
||||
${this.label ? html`<label>${this.label}</label>` : nothing}
|
||||
<ha-generic-picker
|
||||
no-sort
|
||||
.disabled=${this.disabled}
|
||||
.getItems=${this._getFilteredItems}
|
||||
.rowRenderer=${rowRenderer}
|
||||
@@ -221,7 +221,7 @@ export class HaEntityIdFormatEditor extends LitElement {
|
||||
|
||||
private _getItems = memoizeOne(
|
||||
(localize: LocalizeFunc): PickerComboBoxItem[] =>
|
||||
ENTITY_NAME_TYPES.map((type) => {
|
||||
STRUCTURAL_TYPES.map((type) => {
|
||||
const primary = localize(
|
||||
`ui.components.entity.entity-name-picker.types.${type}`
|
||||
);
|
||||
|
||||
@@ -3,6 +3,7 @@ import "@home-assistant/webawesome/dist/components/divider/divider";
|
||||
import { consume } from "@lit/context";
|
||||
import {
|
||||
mdiCog,
|
||||
mdiChevronRight,
|
||||
mdiDelete,
|
||||
mdiDotsVertical,
|
||||
mdiDownload,
|
||||
@@ -47,7 +48,6 @@ import type { HaDropdownSelectEvent } from "../../../components/ha-dropdown";
|
||||
import "../../../components/ha-dropdown-item";
|
||||
import "../../../components/ha-icon";
|
||||
import "../../../components/ha-icon-button";
|
||||
import "../../../components/ha-icon-button-next";
|
||||
import "../../../components/ha-icon-next";
|
||||
import "../../../components/item/ha-list-item-base";
|
||||
import "../../../components/item/ha-list-item-button";
|
||||
@@ -959,11 +959,12 @@ export class HaConfigDevicePage extends LitElement {
|
||||
back: "1",
|
||||
})}"
|
||||
>
|
||||
<ha-icon-button-next
|
||||
<ha-icon-button
|
||||
.path=${mdiChevronRight}
|
||||
.label=${this.hass.localize(
|
||||
"ui.dialogs.more_info_control.show_more"
|
||||
)}
|
||||
></ha-icon-button-next>
|
||||
></ha-icon-button>
|
||||
</a>
|
||||
</div>
|
||||
<ha-logbook
|
||||
|
||||
@@ -3,6 +3,7 @@ import { consume } from "@lit/context";
|
||||
import {
|
||||
mdiAlertCircle,
|
||||
mdiCancel,
|
||||
mdiChevronRight,
|
||||
mdiDelete,
|
||||
mdiDotsVertical,
|
||||
mdiEye,
|
||||
@@ -68,7 +69,6 @@ import "../../../components/ha-filter-states";
|
||||
import "../../../components/ha-filter-voice-assistants";
|
||||
import "../../../components/ha-icon";
|
||||
import "../../../components/ha-icon-button";
|
||||
import "../../../components/ha-icon-next";
|
||||
import "../../../components/ha-sub-menu";
|
||||
import "../../../components/ha-svg-icon";
|
||||
import "../../../components/ha-tooltip";
|
||||
@@ -906,7 +906,10 @@ export class HaConfigEntities extends LitElement {
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.automation.picker.bulk_actions.add_label"
|
||||
)}
|
||||
<ha-icon-next slot="end"></ha-icon-next>
|
||||
<ha-svg-icon
|
||||
slot="end"
|
||||
.path=${mdiChevronRight}
|
||||
></ha-svg-icon>
|
||||
${this._renderLabelItems("submenu")}
|
||||
</ha-dropdown-item>
|
||||
<wa-divider></wa-divider>`
|
||||
|
||||
@@ -678,6 +678,7 @@ class HaConfigIntegrationsDashboard extends KeyboardShortcutMixin(
|
||||
([domain, items]) =>
|
||||
html`<ha-integration-card
|
||||
data-domain=${domain}
|
||||
.hass=${this.hass}
|
||||
.domain=${domain}
|
||||
.items=${items}
|
||||
.manifest=${this._manifests[domain]}
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
type IntegrationManifest,
|
||||
} from "../../../data/integration";
|
||||
import type { HomeAssistant } from "../../../types";
|
||||
import "./ha-integration-header";
|
||||
import "../../../components/ha-card";
|
||||
import { brandsUrl } from "../../../util/brands-url";
|
||||
import { haStyle } from "../../../resources/styles";
|
||||
|
||||
@@ -1,251 +0,0 @@
|
||||
import { mdiFileCodeOutline, mdiPackageVariant, mdiWeb } from "@mdi/js";
|
||||
import { consume, type ContextType } from "@lit/context";
|
||||
import type { TemplateResult } from "lit";
|
||||
import { LitElement, css, html, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { consumeLocalize } from "../../../common/decorators/consume-context-entry";
|
||||
import { transform } from "../../../common/decorators/transform";
|
||||
import { computeRTL } from "../../../common/util/compute_rtl";
|
||||
import type { LocalizeFunc } from "../../../common/translations/localize";
|
||||
import "../../../components/ha-svg-icon";
|
||||
import "../../../components/ha-tooltip";
|
||||
import {
|
||||
devicesContext,
|
||||
internationalizationContext,
|
||||
} from "../../../data/context";
|
||||
import type { DeviceRegistryEntry } from "../../../data/device/device_registry";
|
||||
import type { EntityRegistryEntry } from "../../../data/entity/entity_registry";
|
||||
import type { IntegrationManifest } from "../../../data/integration";
|
||||
import type { HomeAssistantInternationalization } from "../../../types";
|
||||
import type { ConfigEntryExtended } from "./ha-config-integrations";
|
||||
|
||||
@customElement("ha-integration-card-footer")
|
||||
export class HaIntegrationCardFooter extends LitElement {
|
||||
@property({ attribute: false }) public manifest?: IntegrationManifest;
|
||||
|
||||
@property({ attribute: false }) public items!: ConfigEntryExtended[];
|
||||
|
||||
@property({ attribute: false })
|
||||
public entityRegistryEntries!: EntityRegistryEntry[];
|
||||
|
||||
@property({ attribute: false }) public domainEntities: string[] = [];
|
||||
|
||||
@consume({ context: devicesContext, subscribe: true })
|
||||
private _devices!: ContextType<typeof devicesContext>;
|
||||
|
||||
@state()
|
||||
@consumeLocalize()
|
||||
private _localize!: LocalizeFunc;
|
||||
|
||||
@state()
|
||||
@consume({ context: internationalizationContext, subscribe: true })
|
||||
@transform<HomeAssistantInternationalization, boolean>({
|
||||
transformer: ({ language, translationMetadata }) =>
|
||||
computeRTL(language, translationMetadata.translations),
|
||||
})
|
||||
private _isRTL!: boolean;
|
||||
|
||||
protected render(): TemplateResult | typeof nothing {
|
||||
const devices = this._getDevices(this.items, this._devices);
|
||||
const entitiesCount = devices.length
|
||||
? 0
|
||||
: this._getEntityCount(
|
||||
this.items,
|
||||
this.entityRegistryEntries,
|
||||
this.domainEntities
|
||||
);
|
||||
|
||||
const services = !devices.some((device) => device.entry_type !== "service");
|
||||
|
||||
const hasIcons =
|
||||
Boolean(this.manifest && !this.manifest.is_built_in) ||
|
||||
Boolean(this.manifest?.iot_class?.startsWith("cloud_")) ||
|
||||
Boolean(
|
||||
this.manifest &&
|
||||
!this.manifest.config_flow &&
|
||||
!this.items.every((itm) => itm.source === "system")
|
||||
);
|
||||
|
||||
let countBadge: TemplateResult | typeof nothing = nothing;
|
||||
if (devices.length > 0) {
|
||||
countBadge = html`<span class="count-badge"
|
||||
>${this._localize(
|
||||
`ui.panel.config.integrations.config_entry.${services ? "services" : "devices"}`,
|
||||
{ count: devices.length }
|
||||
)}</span
|
||||
>`;
|
||||
} else if (entitiesCount > 0) {
|
||||
countBadge = html`<span class="count-badge"
|
||||
>${this._localize(
|
||||
`ui.panel.config.integrations.config_entry.entities`,
|
||||
{ count: entitiesCount }
|
||||
)}</span
|
||||
>`;
|
||||
} else if (this.items.find((itm) => itm.source !== "yaml")) {
|
||||
countBadge = html`<span class="count-badge"
|
||||
>${this._localize(`ui.panel.config.integrations.config_entry.entries`, {
|
||||
count: this.items.filter((itm) => itm.source !== "yaml").length,
|
||||
})}</span
|
||||
>`;
|
||||
}
|
||||
|
||||
if (countBadge === nothing && !hasIcons) return nothing;
|
||||
|
||||
const placement = this._isRTL ? "right" : "left";
|
||||
|
||||
return html`
|
||||
<div class="footer">
|
||||
${countBadge}
|
||||
<div class="icons">
|
||||
${
|
||||
this.manifest && !this.manifest.is_built_in
|
||||
? html`<span
|
||||
class="icon ${
|
||||
this.manifest.overwrites_built_in ? "overwrites" : "custom"
|
||||
}"
|
||||
>
|
||||
<ha-svg-icon
|
||||
id="icon-custom"
|
||||
.path=${mdiPackageVariant}
|
||||
></ha-svg-icon>
|
||||
<ha-tooltip for="icon-custom" .placement=${placement}>
|
||||
${this._localize(
|
||||
this.manifest.overwrites_built_in
|
||||
? "ui.panel.config.integrations.config_entry.custom_overwrites_core"
|
||||
: "ui.panel.config.integrations.config_entry.custom_integration"
|
||||
)}
|
||||
</ha-tooltip>
|
||||
</span>`
|
||||
: nothing
|
||||
}
|
||||
${
|
||||
this.manifest && this.manifest.iot_class?.startsWith("cloud_")
|
||||
? html`<span class="icon cloud">
|
||||
<ha-svg-icon id="icon-cloud" .path=${mdiWeb}></ha-svg-icon>
|
||||
<ha-tooltip for="icon-cloud" .placement=${placement}>
|
||||
${this._localize(
|
||||
"ui.panel.config.integrations.config_entry.depends_on_cloud"
|
||||
)}
|
||||
</ha-tooltip>
|
||||
</span>`
|
||||
: nothing
|
||||
}
|
||||
${
|
||||
this.manifest &&
|
||||
!this.manifest?.config_flow &&
|
||||
!this.items.every((itm) => itm.source === "system")
|
||||
? html`<span class="icon yaml">
|
||||
<ha-svg-icon
|
||||
id="icon-yaml"
|
||||
.path=${mdiFileCodeOutline}
|
||||
></ha-svg-icon>
|
||||
<ha-tooltip for="icon-yaml" .placement=${placement}>
|
||||
${this._localize(
|
||||
"ui.panel.config.integrations.config_entry.no_config_flow"
|
||||
)}
|
||||
</ha-tooltip>
|
||||
</span>`
|
||||
: nothing
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
private _getEntityCount = memoizeOne(
|
||||
(
|
||||
configEntry: ConfigEntryExtended[],
|
||||
entityRegistryEntries: EntityRegistryEntry[],
|
||||
domainEntities: string[]
|
||||
): number => {
|
||||
if (!entityRegistryEntries) {
|
||||
return domainEntities.length;
|
||||
}
|
||||
|
||||
const entryIds = configEntry
|
||||
.map((entry) => entry.entry_id)
|
||||
.filter(Boolean);
|
||||
|
||||
if (!entryIds.length) {
|
||||
return domainEntities.length;
|
||||
}
|
||||
|
||||
const entityRegEntities = entityRegistryEntries.filter(
|
||||
(entity) =>
|
||||
entity.config_entry_id && entryIds.includes(entity.config_entry_id)
|
||||
);
|
||||
|
||||
if (entityRegEntities.length === domainEntities.length) {
|
||||
return domainEntities.length;
|
||||
}
|
||||
|
||||
const entityIds = new Set<string>(
|
||||
entityRegEntities.map((reg) => reg.entity_id)
|
||||
);
|
||||
|
||||
for (const entity of domainEntities) {
|
||||
entityIds.add(entity);
|
||||
}
|
||||
|
||||
return entityIds.size;
|
||||
}
|
||||
);
|
||||
|
||||
private _getDevices = memoizeOne(
|
||||
(
|
||||
configEntry: ConfigEntryExtended[],
|
||||
deviceRegistryEntries: ContextType<typeof devicesContext>
|
||||
): DeviceRegistryEntry[] => {
|
||||
if (!deviceRegistryEntries) {
|
||||
return [];
|
||||
}
|
||||
const entryIds = configEntry.map((entry) => entry.entry_id);
|
||||
return Object.values(deviceRegistryEntries).filter((device) =>
|
||||
device.config_entries.some((entryId) => entryIds.includes(entryId))
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
static styles = css`
|
||||
:host {
|
||||
display: block;
|
||||
}
|
||||
.footer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding-top: var(--ha-space-2);
|
||||
min-height: var(--ha-space-8);
|
||||
}
|
||||
.count-badge {
|
||||
color: var(--ha-color-text-secondary);
|
||||
font-size: var(--ha-font-size-m);
|
||||
}
|
||||
.icons {
|
||||
display: flex;
|
||||
margin-inline-start: auto;
|
||||
}
|
||||
.icon {
|
||||
color: var(--label-badge-grey);
|
||||
padding: var(--ha-space-1);
|
||||
margin-inline-start: var(--ha-space-2);
|
||||
}
|
||||
.icon.custom {
|
||||
color: var(--warning-color);
|
||||
}
|
||||
.icon.overwrites {
|
||||
color: var(--error-color);
|
||||
}
|
||||
.icon ha-svg-icon {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
display: block;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-integration-card-footer": HaIntegrationCardFooter;
|
||||
}
|
||||
}
|
||||
@@ -1,14 +1,19 @@
|
||||
import { mdiFileCodeOutline, mdiPackageVariant, mdiWeb } from "@mdi/js";
|
||||
import type { CSSResultGroup, TemplateResult } from "lit";
|
||||
import { LitElement, css, html } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { LitElement, css, html, nothing } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { classMap } from "lit/directives/class-map";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { consumeLocalize } from "../../../common/decorators/consume-context-entry";
|
||||
import type { LocalizeFunc } from "../../../common/translations/localize";
|
||||
import { PROTOCOL_INTEGRATIONS } from "../../../common/integrations/protocolIntegrationPicked";
|
||||
import { computeRTL } from "../../../common/util/compute_rtl";
|
||||
import "../../../components/ha-button";
|
||||
import "../../../components/ha-card";
|
||||
import "../../../components/ha-ripple";
|
||||
import "../../../components/ha-svg-icon";
|
||||
import "../../../components/ha-tooltip";
|
||||
import type { ConfigEntry } from "../../../data/config_entries";
|
||||
import { ERROR_STATES } from "../../../data/config_entries";
|
||||
import type { DeviceRegistryEntry } from "../../../data/device/device_registry";
|
||||
import type { EntityRegistryEntry } from "../../../data/entity/entity_registry";
|
||||
import type {
|
||||
IntegrationLogInfo,
|
||||
@@ -16,15 +21,13 @@ import type {
|
||||
} from "../../../data/integration";
|
||||
import { LogSeverity } from "../../../data/integration";
|
||||
import { haStyle } from "../../../resources/styles";
|
||||
import type { HomeAssistant } from "../../../types";
|
||||
import type { ConfigEntryExtended } from "./ha-config-integrations";
|
||||
import "./ha-integration-card-footer";
|
||||
import "./ha-integration-card-header";
|
||||
import "./ha-integration-header";
|
||||
|
||||
@customElement("ha-integration-card")
|
||||
export class HaIntegrationCard extends LitElement {
|
||||
@state()
|
||||
@consumeLocalize()
|
||||
private _localize!: LocalizeFunc;
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property() public domain!: string;
|
||||
|
||||
@@ -62,42 +65,183 @@ export class HaIntegrationCard extends LitElement {
|
||||
class="ripple-anchor"
|
||||
>
|
||||
<ha-ripple></ha-ripple>
|
||||
<div class="card-content">
|
||||
<ha-integration-card-header
|
||||
.domain=${this.domain}
|
||||
.localizedDomainName=${this.items[0].localized_domain_name}
|
||||
.error=${
|
||||
ERROR_STATES.includes(entryState)
|
||||
? this._localize(
|
||||
`ui.panel.config.integrations.config_entry.state.${entryState}`
|
||||
<ha-integration-header
|
||||
.hass=${this.hass}
|
||||
.domain=${this.domain}
|
||||
.localizedDomainName=${this.items[0].localized_domain_name}
|
||||
.error=${
|
||||
ERROR_STATES.includes(entryState)
|
||||
? this.hass.localize(
|
||||
`ui.panel.config.integrations.config_entry.state.${entryState}`
|
||||
)
|
||||
: undefined
|
||||
}
|
||||
.warning=${
|
||||
entryState !== "loaded" && !ERROR_STATES.includes(entryState)
|
||||
? this.hass.localize(
|
||||
`ui.panel.config.integrations.config_entry.state.${entryState}`
|
||||
)
|
||||
: debugLoggingEnabled
|
||||
? this.hass.localize(
|
||||
"ui.panel.config.integrations.config_entry.debug_logging_enabled"
|
||||
)
|
||||
: undefined
|
||||
}
|
||||
.warning=${
|
||||
entryState !== "loaded" && !ERROR_STATES.includes(entryState)
|
||||
? this._localize(
|
||||
`ui.panel.config.integrations.config_entry.state.${entryState}`
|
||||
)
|
||||
: debugLoggingEnabled
|
||||
? this._localize(
|
||||
"ui.panel.config.integrations.config_entry.debug_logging_enabled"
|
||||
)
|
||||
: undefined
|
||||
}
|
||||
.manifest=${this.manifest}
|
||||
></ha-integration-card-header>
|
||||
<ha-integration-card-footer
|
||||
.manifest=${this.manifest}
|
||||
.items=${this.items}
|
||||
.entityRegistryEntries=${this.entityRegistryEntries}
|
||||
.domainEntities=${this.domainEntities}
|
||||
></ha-integration-card-footer>
|
||||
</div>
|
||||
}
|
||||
.manifest=${this.manifest}
|
||||
>
|
||||
</ha-integration-header>
|
||||
</a>
|
||||
|
||||
${this._renderSingleEntry()}
|
||||
</ha-card>
|
||||
`;
|
||||
}
|
||||
|
||||
private _renderSingleEntry(): TemplateResult {
|
||||
const devices = this._getDevices(this.items, this.hass.devices);
|
||||
const entitiesCount = devices.length
|
||||
? 0
|
||||
: this._getEntityCount(
|
||||
this.items,
|
||||
this.entityRegistryEntries,
|
||||
this.domainEntities
|
||||
);
|
||||
|
||||
const services = !devices.some((device) => device.entry_type !== "service");
|
||||
|
||||
return html`
|
||||
<div class="card-actions">
|
||||
${
|
||||
devices.length > 0
|
||||
? html`<ha-button
|
||||
appearance="plain"
|
||||
href=${
|
||||
devices.length === 1 &&
|
||||
// Always link to device page for protocol integrations to show Add Device button
|
||||
// @ts-expect-error
|
||||
!PROTOCOL_INTEGRATIONS.includes(this.domain)
|
||||
? `/config/devices/device/${devices[0].id}`
|
||||
: `/config/devices/dashboard?historyBack=1&domain=${this.domain}`
|
||||
}
|
||||
>
|
||||
${this.hass.localize(
|
||||
`ui.panel.config.integrations.config_entry.${
|
||||
services ? "services" : "devices"
|
||||
}`,
|
||||
{ count: devices.length }
|
||||
)}
|
||||
</ha-button>`
|
||||
: entitiesCount > 0
|
||||
? html`<ha-button
|
||||
appearance="plain"
|
||||
href=${`/config/entities?historyBack=1&domain=${this.domain}`}
|
||||
>
|
||||
${this.hass.localize(
|
||||
`ui.panel.config.integrations.config_entry.entities`,
|
||||
{ count: entitiesCount }
|
||||
)}
|
||||
</ha-button>`
|
||||
: this.items.find((itm) => itm.source !== "yaml")
|
||||
? html`<ha-button
|
||||
appearance="plain"
|
||||
href=${`/config/integrations/integration/${this.domain}`}
|
||||
>
|
||||
${this.hass.localize(
|
||||
`ui.panel.config.integrations.config_entry.entries`,
|
||||
{
|
||||
count: this.items.filter((itm) => itm.source !== "yaml")
|
||||
.length,
|
||||
}
|
||||
)}
|
||||
</ha-button>`
|
||||
: html`<div class="spacer"></div>`
|
||||
}
|
||||
<div class="icons">
|
||||
${
|
||||
this.manifest && !this.manifest.is_built_in
|
||||
? html`<span
|
||||
class="icon ${
|
||||
this.manifest.overwrites_built_in ? "overwrites" : "custom"
|
||||
}"
|
||||
>
|
||||
<ha-svg-icon
|
||||
id="icon-custom"
|
||||
.path=${mdiPackageVariant}
|
||||
></ha-svg-icon>
|
||||
<ha-tooltip
|
||||
for="icon-custom"
|
||||
.placement=${
|
||||
computeRTL(
|
||||
this.hass.language,
|
||||
this.hass.translationMetadata.translations
|
||||
)
|
||||
? "right"
|
||||
: "left"
|
||||
}
|
||||
>
|
||||
${this.hass.localize(
|
||||
this.manifest.overwrites_built_in
|
||||
? "ui.panel.config.integrations.config_entry.custom_overwrites_core"
|
||||
: "ui.panel.config.integrations.config_entry.custom_integration"
|
||||
)}
|
||||
</ha-tooltip>
|
||||
</span>`
|
||||
: nothing
|
||||
}
|
||||
${
|
||||
this.manifest && this.manifest.iot_class?.startsWith("cloud_")
|
||||
? html`<div class="icon cloud">
|
||||
<ha-svg-icon id="icon-cloud" .path=${mdiWeb}></ha-svg-icon>
|
||||
<ha-tooltip
|
||||
for="icon-cloud"
|
||||
.placement=${
|
||||
computeRTL(
|
||||
this.hass.language,
|
||||
this.hass.translationMetadata.translations
|
||||
)
|
||||
? "right"
|
||||
: "left"
|
||||
}
|
||||
>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.integrations.config_entry.depends_on_cloud"
|
||||
)}
|
||||
</ha-tooltip>
|
||||
</div>`
|
||||
: nothing
|
||||
}
|
||||
${
|
||||
this.manifest &&
|
||||
!this.manifest?.config_flow &&
|
||||
!this.items.every((itm) => itm.source === "system")
|
||||
? html`<div class="icon yaml">
|
||||
<ha-svg-icon
|
||||
id="icon-yaml"
|
||||
.path=${mdiFileCodeOutline}
|
||||
></ha-svg-icon>
|
||||
<ha-tooltip
|
||||
for="icon-yaml"
|
||||
.placement=${
|
||||
computeRTL(
|
||||
this.hass.language,
|
||||
this.hass.translationMetadata.translations
|
||||
)
|
||||
? "right"
|
||||
: "left"
|
||||
}
|
||||
>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.integrations.config_entry.no_config_flow"
|
||||
)}
|
||||
</ha-tooltip>
|
||||
</div>`
|
||||
: nothing
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
private _getState = memoizeOne(
|
||||
(configEntry: ConfigEntry[]): ConfigEntry["state"] => {
|
||||
if (configEntry.length === 1) {
|
||||
@@ -114,6 +258,60 @@ export class HaIntegrationCard extends LitElement {
|
||||
}
|
||||
);
|
||||
|
||||
private _getEntityCount = memoizeOne(
|
||||
(
|
||||
configEntry: ConfigEntry[],
|
||||
entityRegistryEntries: EntityRegistryEntry[],
|
||||
domainEntities: string[]
|
||||
): number => {
|
||||
if (!entityRegistryEntries) {
|
||||
return domainEntities.length;
|
||||
}
|
||||
|
||||
const entryIds = configEntry
|
||||
.map((entry) => entry.entry_id)
|
||||
.filter(Boolean);
|
||||
|
||||
if (!entryIds.length) {
|
||||
return domainEntities.length;
|
||||
}
|
||||
|
||||
const entityRegEntities = entityRegistryEntries.filter(
|
||||
(entity) =>
|
||||
entity.config_entry_id && entryIds.includes(entity.config_entry_id)
|
||||
);
|
||||
|
||||
if (entityRegEntities.length === domainEntities.length) {
|
||||
return domainEntities.length;
|
||||
}
|
||||
|
||||
const entityIds = new Set<string>(
|
||||
entityRegEntities.map((reg) => reg.entity_id)
|
||||
);
|
||||
|
||||
for (const entity of domainEntities) {
|
||||
entityIds.add(entity);
|
||||
}
|
||||
|
||||
return entityIds.size;
|
||||
}
|
||||
);
|
||||
|
||||
private _getDevices = memoizeOne(
|
||||
(
|
||||
configEntry: ConfigEntry[],
|
||||
deviceRegistryEntries: HomeAssistant["devices"]
|
||||
): DeviceRegistryEntry[] => {
|
||||
if (!deviceRegistryEntries) {
|
||||
return [];
|
||||
}
|
||||
const entryIds = configEntry.map((entry) => entry.entry_id);
|
||||
return Object.values(deviceRegistryEntries).filter((device) =>
|
||||
device.config_entries.some((entryId) => entryIds.includes(entryId))
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return [
|
||||
haStyle,
|
||||
@@ -121,24 +319,16 @@ export class HaIntegrationCard extends LitElement {
|
||||
ha-card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
cursor: pointer;
|
||||
--state-color: var(--divider-color, #e0e0e0);
|
||||
--state-message-color: var(--state-color);
|
||||
}
|
||||
ha-card:hover {
|
||||
background-color: var(--ha-color-fill-neutral-quiet-resting);
|
||||
}
|
||||
.ripple-anchor {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
flex-grow: 1;
|
||||
position: relative;
|
||||
outline: none;
|
||||
/* ha-ripple adds a hover overlay that conflicts with ha-card:hover background change;
|
||||
neutralize it so hover matches the apps card (background-color only) */
|
||||
--ha-ripple-hover-color: transparent;
|
||||
}
|
||||
.ripple-anchor:focus-visible:before {
|
||||
position: absolute;
|
||||
@@ -148,11 +338,13 @@ export class HaIntegrationCard extends LitElement {
|
||||
background-color: var(--secondary-text-color);
|
||||
opacity: 0.08;
|
||||
}
|
||||
.card-content {
|
||||
padding: var(--ha-space-4) var(--ha-space-4) var(--ha-space-2);
|
||||
flex: 1;
|
||||
ha-integration-header {
|
||||
height: 100%;
|
||||
}
|
||||
.card-actions {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
.debug-logging {
|
||||
--state-color: var(--warning-color);
|
||||
@@ -184,10 +376,40 @@ export class HaIntegrationCard extends LitElement {
|
||||
--ha-card-border-color: var(--state-color);
|
||||
--text-on-state-color: var(--text-primary-color);
|
||||
}
|
||||
.content {
|
||||
flex: 1;
|
||||
--mdc-list-side-padding-right: 20px;
|
||||
--mdc-list-side-padding-left: 24px;
|
||||
--mdc-list-item-graphic-margin: 24px;
|
||||
}
|
||||
a {
|
||||
text-decoration: none;
|
||||
color: var(--primary-text-color);
|
||||
}
|
||||
.icons {
|
||||
display: flex;
|
||||
}
|
||||
.icon {
|
||||
color: var(--label-badge-grey);
|
||||
padding: 4px;
|
||||
margin-left: 8px;
|
||||
margin-inline-start: 8px;
|
||||
margin-inline-end: initial;
|
||||
}
|
||||
.icon.custom {
|
||||
color: var(--warning-color);
|
||||
}
|
||||
.icon.overwrites {
|
||||
color: var(--error-color);
|
||||
}
|
||||
.icon ha-svg-icon {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
display: block;
|
||||
}
|
||||
.spacer {
|
||||
height: 36px;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
||||
+33
-40
@@ -1,20 +1,18 @@
|
||||
import { mdiAlertCircleOutline, mdiAlertOutline } from "@mdi/js";
|
||||
import { consume } from "@lit/context";
|
||||
import type { TemplateResult } from "lit";
|
||||
import { LitElement, css, html, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { consumeLocalize } from "../../../common/decorators/consume-context-entry";
|
||||
import { transform } from "../../../common/decorators/transform";
|
||||
import type { LocalizeFunc } from "../../../common/translations/localize";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import "../../../components/ha-icon-next";
|
||||
import "../../../components/ha-svg-icon";
|
||||
import { configContext, uiContext } from "../../../data/context";
|
||||
import type { IntegrationManifest } from "../../../data/integration";
|
||||
import { domainToName } from "../../../data/integration";
|
||||
import type { HomeAssistantConfig, HomeAssistantUI } from "../../../types";
|
||||
import type { HomeAssistant } from "../../../types";
|
||||
import { brandsUrl } from "../../../util/brands-url";
|
||||
|
||||
@customElement("ha-integration-card-header")
|
||||
export class HaIntegrationCardHeader extends LitElement {
|
||||
@customElement("ha-integration-header")
|
||||
export class HaIntegrationHeader extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property() public error?: string;
|
||||
|
||||
@property() public warning?: string;
|
||||
@@ -25,28 +23,10 @@ export class HaIntegrationCardHeader extends LitElement {
|
||||
|
||||
@property({ attribute: false }) public manifest?: IntegrationManifest;
|
||||
|
||||
@state()
|
||||
@consumeLocalize()
|
||||
private _localize!: LocalizeFunc;
|
||||
|
||||
@state()
|
||||
@consume({ context: uiContext, subscribe: true })
|
||||
@transform<HomeAssistantUI, boolean | undefined>({
|
||||
transformer: ({ themes }) => themes?.darkMode,
|
||||
})
|
||||
private _darkMode?: boolean;
|
||||
|
||||
@state()
|
||||
@consume({ context: configContext, subscribe: true })
|
||||
@transform<HomeAssistantConfig, string>({
|
||||
transformer: ({ auth }) => auth.data.hassUrl,
|
||||
})
|
||||
private _hassUrl!: string;
|
||||
|
||||
protected render(): TemplateResult {
|
||||
const domainName =
|
||||
this.localizedDomainName ||
|
||||
domainToName(this._localize, this.domain, this.manifest);
|
||||
domainToName(this.hass.localize, this.domain, this.manifest);
|
||||
|
||||
return html`
|
||||
<div class="header">
|
||||
@@ -56,9 +36,9 @@ export class HaIntegrationCardHeader extends LitElement {
|
||||
{
|
||||
domain: this.domain,
|
||||
type: "icon",
|
||||
darkOptimized: this._darkMode,
|
||||
darkOptimized: this.hass.themes?.darkMode,
|
||||
},
|
||||
this._hassUrl
|
||||
this.hass.auth.data.hassUrl
|
||||
)}
|
||||
crossorigin="anonymous"
|
||||
referrerpolicy="no-referrer"
|
||||
@@ -91,6 +71,12 @@ export class HaIntegrationCardHeader extends LitElement {
|
||||
: nothing
|
||||
}
|
||||
</div>
|
||||
<ha-icon-next
|
||||
class="header-button"
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.integrations.config_entry.configure"
|
||||
)}
|
||||
></ha-icon-next>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
@@ -104,31 +90,36 @@ export class HaIntegrationCardHeader extends LitElement {
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
:host {
|
||||
display: block;
|
||||
flex: 1;
|
||||
padding-bottom: var(--ha-space-2);
|
||||
}
|
||||
.header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
position: relative;
|
||||
padding-top: 16px;
|
||||
padding-bottom: 16px;
|
||||
padding-inline-start: 16px;
|
||||
padding-inline-end: 8px;
|
||||
direction: var(--direction);
|
||||
box-sizing: border-box;
|
||||
min-width: 0;
|
||||
}
|
||||
.header img {
|
||||
margin-inline-start: initial;
|
||||
margin-inline-end: var(--ha-space-4);
|
||||
margin-inline-end: 16px;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
direction: var(--direction);
|
||||
}
|
||||
.info {
|
||||
.header .info {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
align-self: center;
|
||||
min-width: 0;
|
||||
}
|
||||
ha-icon-next {
|
||||
color: var(--secondary-text-color);
|
||||
}
|
||||
.primary {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
@@ -146,10 +137,10 @@ export class HaIntegrationCardHeader extends LitElement {
|
||||
.secondary {
|
||||
min-width: 0;
|
||||
--mdc-icon-size: 20px;
|
||||
-webkit-line-clamp: 1;
|
||||
font-size: var(--ha-font-size-s);
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: flex-start;
|
||||
}
|
||||
.secondary > span {
|
||||
position: relative;
|
||||
@@ -159,7 +150,9 @@ export class HaIntegrationCardHeader extends LitElement {
|
||||
white-space: nowrap;
|
||||
}
|
||||
.secondary > ha-svg-icon {
|
||||
margin-inline-end: var(--ha-space-1);
|
||||
margin-right: 4px;
|
||||
margin-inline-end: 4px;
|
||||
margin-inline-start: initial;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.error ha-svg-icon {
|
||||
@@ -173,6 +166,6 @@ export class HaIntegrationCardHeader extends LitElement {
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-integration-card-header": HaIntegrationCardHeader;
|
||||
"ha-integration-header": HaIntegrationHeader;
|
||||
}
|
||||
}
|
||||
+15
-40
@@ -1,6 +1,5 @@
|
||||
import {
|
||||
mdiCableData,
|
||||
mdiFileDocumentOutline,
|
||||
mdiLan,
|
||||
mdiPuzzle,
|
||||
mdiRefresh,
|
||||
@@ -23,10 +22,7 @@ import "../../../../../components/ha-svg-icon";
|
||||
import type { ConfigEntry } from "../../../../../data/config_entries";
|
||||
import { getConfigEntries } from "../../../../../data/config_entries";
|
||||
import { domainToName } from "../../../../../data/integration";
|
||||
import type {
|
||||
ModbusConnection,
|
||||
ModbusConnectionSource,
|
||||
} from "../../../../../data/modbus";
|
||||
import type { ModbusConnection } from "../../../../../data/modbus";
|
||||
import {
|
||||
listModbusConnections,
|
||||
modbusEndpointTarget,
|
||||
@@ -47,8 +43,7 @@ const isKnownTransport = (transport: string): transport is ModbusTransport =>
|
||||
(TRANSPORTS as readonly string[]).includes(transport);
|
||||
|
||||
interface ConnectionHolder {
|
||||
/** A config entry ID, or a hub name on a YAML connection. */
|
||||
id: string;
|
||||
entryId: string;
|
||||
entry?: ConfigEntry;
|
||||
units: number[];
|
||||
}
|
||||
@@ -56,10 +51,9 @@ interface ConnectionHolder {
|
||||
interface ConnectionListItem {
|
||||
icon: string;
|
||||
primary: string;
|
||||
secondary: string;
|
||||
transport: string;
|
||||
state: string;
|
||||
serialDevice?: string;
|
||||
source: ModbusConnectionSource;
|
||||
holders: ConnectionHolder[];
|
||||
connection: ModbusConnection;
|
||||
}
|
||||
@@ -120,34 +114,22 @@ export class ModbusConfigDashboard extends LitElement {
|
||||
);
|
||||
}
|
||||
|
||||
// A YAML hub is a link of its own rather than a hold on a shared
|
||||
// connection, so the same device can be listed twice
|
||||
private _secondaryName(connection: ModbusConnection): string {
|
||||
const transport = this._transportName(connection.endpoint[0]);
|
||||
if (connection.source !== "yaml") {
|
||||
return transport;
|
||||
}
|
||||
return `${transport} • ${this.hass.localize(
|
||||
"ui.panel.config.modbus.configured_in_yaml"
|
||||
)}`;
|
||||
}
|
||||
|
||||
private _connectionListItem(
|
||||
connection: ModbusConnection,
|
||||
entries: Record<string, ConfigEntry>
|
||||
): ConnectionListItem {
|
||||
const [transport] = connection.endpoint;
|
||||
const serialDevice = modbusSerialDevice(connection.endpoint);
|
||||
|
||||
return {
|
||||
icon: serialDevice ? mdiCableData : mdiLan,
|
||||
primary: modbusEndpointTarget(connection.endpoint),
|
||||
secondary: this._secondaryName(connection),
|
||||
transport: this._transportName(transport),
|
||||
state: this._stateName(connection.connected, serialDevice !== undefined),
|
||||
serialDevice,
|
||||
source: connection.source,
|
||||
holders: Object.entries(connection.units).map(([id, units]) => ({
|
||||
id,
|
||||
entry: connection.source === "yaml" ? undefined : entries[id],
|
||||
holders: Object.entries(connection.units).map(([entryId, units]) => ({
|
||||
entryId,
|
||||
entry: entries[entryId],
|
||||
units,
|
||||
})),
|
||||
connection,
|
||||
@@ -179,20 +161,13 @@ export class ModbusConfigDashboard extends LitElement {
|
||||
`;
|
||||
}
|
||||
|
||||
private _renderHolder(
|
||||
holder: ConnectionHolder,
|
||||
source: ModbusConnectionSource
|
||||
): TemplateResult {
|
||||
// A YAML hub is named by the key it is configured under, and an entry
|
||||
// removed while the connection was listed has nowhere to link to
|
||||
private _renderHolder(holder: ConnectionHolder): TemplateResult {
|
||||
// An entry removed while the connection was listed has nowhere to link to
|
||||
if (!holder.entry) {
|
||||
return html`
|
||||
<ha-md-list-item class="holder">
|
||||
<ha-svg-icon
|
||||
slot="start"
|
||||
.path=${source === "yaml" ? mdiFileDocumentOutline : mdiPuzzle}
|
||||
></ha-svg-icon>
|
||||
<div slot="headline">${holder.id}</div>
|
||||
<ha-svg-icon slot="start" .path=${mdiPuzzle}></ha-svg-icon>
|
||||
<div slot="headline">${holder.entryId}</div>
|
||||
${this._renderUnits(holder)}
|
||||
</ha-md-list-item>
|
||||
`;
|
||||
@@ -204,7 +179,7 @@ export class ModbusConfigDashboard extends LitElement {
|
||||
<ha-md-list-item
|
||||
type="link"
|
||||
class="holder"
|
||||
href=${`/config/integrations/integration/${domain}#config_entry=${holder.id}`}
|
||||
href=${`/config/integrations/integration/${domain}#config_entry=${holder.entryId}`}
|
||||
>
|
||||
<img
|
||||
slot="start"
|
||||
@@ -259,10 +234,10 @@ export class ModbusConfigDashboard extends LitElement {
|
||||
<ha-md-list-item class="connection">
|
||||
<ha-svg-icon slot="start" .path=${item.icon}></ha-svg-icon>
|
||||
<div slot="headline">${item.primary}</div>
|
||||
<div slot="supporting-text">${item.secondary}</div>
|
||||
<div slot="supporting-text">${item.transport}</div>
|
||||
${this._renderState(item)}
|
||||
</ha-md-list-item>
|
||||
${item.holders.map((holder) => this._renderHolder(holder, item.source))}
|
||||
${item.holders.map((holder) => this._renderHolder(holder))}
|
||||
${
|
||||
item.serialDevice && isComponentLoaded(this.hass.config, "usb")
|
||||
? this._renderSerialPortLink()
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { consume, type ContextType } from "@lit/context";
|
||||
import { mdiChevronRight } from "@mdi/js";
|
||||
import type { CSSResultGroup, TemplateResult } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
@@ -8,7 +9,7 @@ import type {
|
||||
HASSDomEvent,
|
||||
} from "../../../../../common/dom/fire_event";
|
||||
import "../../../../../components/ha-card";
|
||||
import "../../../../../components/ha-icon-button-next";
|
||||
import "../../../../../components/ha-icon-button";
|
||||
import "../../../../../components/ha-list";
|
||||
import "../../../../../components/input/ha-input-search";
|
||||
import type { HaInputSearch } from "../../../../../components/input/ha-input-search";
|
||||
@@ -185,14 +186,15 @@ export class ZHADeviceEndpointList extends LitElement {
|
||||
${
|
||||
this.showDeviceLink
|
||||
? html`
|
||||
<ha-icon-button-next
|
||||
<ha-icon-button
|
||||
slot="end"
|
||||
.path=${mdiChevronRight}
|
||||
.href=${`/config/devices/device/${deviceEndpoint.dev_id}`}
|
||||
.label=${this._i18n.localize(
|
||||
"ui.panel.config.zha.groups.open_device"
|
||||
)}
|
||||
@click=${this._stopPropagation}
|
||||
></ha-icon-button-next>
|
||||
></ha-icon-button>
|
||||
`
|
||||
: nothing
|
||||
}
|
||||
@@ -212,13 +214,14 @@ export class ZHADeviceEndpointList extends LitElement {
|
||||
${
|
||||
this.showDeviceLink
|
||||
? html`
|
||||
<ha-icon-button-next
|
||||
<ha-icon-button
|
||||
slot="end"
|
||||
.path=${mdiChevronRight}
|
||||
.href=${`/config/devices/device/${deviceEndpoint.dev_id}`}
|
||||
.label=${this._i18n.localize(
|
||||
"ui.panel.config.zha.groups.open_device"
|
||||
)}
|
||||
></ha-icon-button-next>
|
||||
></ha-icon-button>
|
||||
`
|
||||
: nothing
|
||||
}
|
||||
|
||||
+2
-4
@@ -1,4 +1,4 @@
|
||||
import { mdiChevronLeft, mdiChevronRight, mdiClose } from "@mdi/js";
|
||||
import { mdiChevronLeft, mdiClose } from "@mdi/js";
|
||||
import type { UnsubscribeFunc } from "home-assistant-js-websocket";
|
||||
import type { CSSResultGroup, TemplateResult } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
@@ -64,7 +64,6 @@ import "./zwave-js-add-node-loading";
|
||||
import "./zwave-js-add-node-searching-devices";
|
||||
import "./zwave-js-add-node-select-method";
|
||||
import "./zwave-js-add-node-select-security-strategy";
|
||||
import { mainWindow } from "../../../../../../common/dom/get_main_window";
|
||||
|
||||
const INCLUSION_TIMEOUT_MINUTES = 5;
|
||||
|
||||
@@ -185,8 +184,7 @@ class DialogZWaveJSAddNode extends LitElement {
|
||||
(this._step && backButtonStages.includes(this._step)) ||
|
||||
(this._step === "search_devices" && this._supportsSmartStart)
|
||||
) {
|
||||
icon =
|
||||
mainWindow.document.dir === "rtl" ? mdiChevronRight : mdiChevronLeft;
|
||||
icon = mdiChevronLeft;
|
||||
}
|
||||
|
||||
let titleTranslationKey = "title";
|
||||
|
||||
@@ -103,7 +103,6 @@ class HaConfigSectionNetwork extends LitElement {
|
||||
max-width: 600px;
|
||||
}
|
||||
.discovery-card ha-md-list {
|
||||
background: none;
|
||||
padding-top: 0;
|
||||
}
|
||||
`;
|
||||
|
||||
@@ -1,4 +1,10 @@
|
||||
import { mdiInformationOutline } from "@mdi/js";
|
||||
import {
|
||||
mdiChevronDoubleLeft,
|
||||
mdiChevronDoubleRight,
|
||||
mdiChevronLeft,
|
||||
mdiChevronRight,
|
||||
mdiInformationOutline,
|
||||
} from "@mdi/js";
|
||||
import type { HassEvent } from "home-assistant-js-websocket";
|
||||
import type { TemplateResult, PropertyValues } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
@@ -9,10 +15,6 @@ import "../../../../components/ha-alert";
|
||||
import "../../../../components/ha-button";
|
||||
import "../../../../components/ha-card";
|
||||
import "../../../../components/ha-icon-button";
|
||||
import "../../../../components/ha-icon-button-next";
|
||||
import "../../../../components/ha-icon-button-prev";
|
||||
import "../../../../components/ha-icon-button-last";
|
||||
import "../../../../components/ha-icon-button-first";
|
||||
import "../../../../components/ha-svg-icon";
|
||||
import "../../../../components/ha-tooltip";
|
||||
import "../../../../components/ha-yaml-editor";
|
||||
@@ -167,20 +169,22 @@ class EventSubscribeCard extends LitElement {
|
||||
return html`
|
||||
<ha-card class="events-card">
|
||||
<div class="events-toolbar">
|
||||
<ha-icon-button-first
|
||||
<ha-icon-button
|
||||
.path=${mdiChevronDoubleLeft}
|
||||
.disabled=${index >= bufferTotal - 1}
|
||||
.label=${this.hass!.localize(
|
||||
"ui.panel.config.tools.tabs.events.oldest_event"
|
||||
)}
|
||||
@click=${this._showOldest}
|
||||
></ha-icon-button-first>
|
||||
<ha-icon-button-prev
|
||||
></ha-icon-button>
|
||||
<ha-icon-button
|
||||
.path=${mdiChevronLeft}
|
||||
.disabled=${index >= bufferTotal - 1}
|
||||
.label=${this.hass!.localize(
|
||||
"ui.panel.config.tools.tabs.events.older_event"
|
||||
)}
|
||||
@click=${this._showOlder}
|
||||
></ha-icon-button-prev>
|
||||
></ha-icon-button>
|
||||
<div class="event-info">
|
||||
${this.hass!.localize(
|
||||
"ui.panel.config.tools.tabs.events.event_fired",
|
||||
@@ -214,20 +218,22 @@ class EventSubscribeCard extends LitElement {
|
||||
: nothing
|
||||
}
|
||||
</div>
|
||||
<ha-icon-button-next
|
||||
<ha-icon-button
|
||||
.path=${mdiChevronRight}
|
||||
.disabled=${atNewest}
|
||||
.label=${this.hass!.localize(
|
||||
"ui.panel.config.tools.tabs.events.newer_event"
|
||||
)}
|
||||
@click=${this._showNewer}
|
||||
></ha-icon-button-next>
|
||||
<ha-icon-button-last
|
||||
></ha-icon-button>
|
||||
<ha-icon-button
|
||||
.path=${mdiChevronDoubleRight}
|
||||
.disabled=${atNewest}
|
||||
.label=${this.hass!.localize(
|
||||
"ui.panel.config.tools.tabs.events.newest_event"
|
||||
)}
|
||||
@click=${this._showNewest}
|
||||
></ha-icon-button-last>
|
||||
></ha-icon-button>
|
||||
</div>
|
||||
<ha-yaml-editor
|
||||
.value=${event.event}
|
||||
|
||||
@@ -7,7 +7,6 @@ import { styleMap } from "lit/directives/style-map";
|
||||
import { atLeastVersion } from "../../common/config/version";
|
||||
import { navigate } from "../../common/navigate";
|
||||
import { debounce } from "../../common/util/debounce";
|
||||
import { deepEqual } from "../../common/util/deep-equal";
|
||||
import "../../components/ha-button";
|
||||
import "../../components/ha-svg-icon";
|
||||
import { updateAreaRegistryEntry } from "../../data/area/area_registry";
|
||||
@@ -15,12 +14,9 @@ import { updateDeviceRegistryEntry } from "../../data/device/device_registry";
|
||||
import {
|
||||
fetchFrontendSystemData,
|
||||
saveFrontendSystemData,
|
||||
subscribeFrontendSystemData,
|
||||
type HomeFrontendSystemData,
|
||||
type SecurityFrontendSystemData,
|
||||
} from "../../data/frontend";
|
||||
import type { LovelaceDashboardStrategyConfig } from "../../data/lovelace/config/types";
|
||||
import { SubscribeMixin } from "../../mixins/subscribe-mixin";
|
||||
import { mdiHomeAssistant } from "../../resources/home-assistant-logo-svg";
|
||||
import type { HomeAssistant, PanelInfo, Route } from "../../types";
|
||||
import { showToast } from "../../util/toast";
|
||||
@@ -39,7 +35,7 @@ import { showNewOverviewDialog } from "./dialogs/show-dialog-new-overview";
|
||||
import { hasLegacyOverviewPanel } from "../../data/panel";
|
||||
|
||||
@customElement("ha-panel-home")
|
||||
class PanelHome extends SubscribeMixin(LitElement) {
|
||||
class PanelHome extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ type: Boolean, reflect: true }) public narrow = false;
|
||||
@@ -52,36 +48,12 @@ class PanelHome extends SubscribeMixin(LitElement) {
|
||||
|
||||
@state() private _config: FrontendSystemData["home"] = {};
|
||||
|
||||
@state() private _securityConfig: SecurityFrontendSystemData = {};
|
||||
|
||||
@state() private _extraActionItems?: ExtraActionItem[];
|
||||
|
||||
@query(".banner") private _banner?: HTMLElement;
|
||||
|
||||
private _loadConfigPromise?: Promise<void>;
|
||||
|
||||
private _securityConfigRevision = 0;
|
||||
|
||||
public hassSubscribe() {
|
||||
return [
|
||||
subscribeFrontendSystemData(
|
||||
this.hass.connection,
|
||||
"security",
|
||||
({ value }) => {
|
||||
this._securityConfigRevision++;
|
||||
const config = value || {};
|
||||
if (deepEqual(config, this._securityConfig)) {
|
||||
return;
|
||||
}
|
||||
this._securityConfig = config;
|
||||
if (this.hasUpdated) {
|
||||
this._setLovelace();
|
||||
}
|
||||
}
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
private get _showBanner(): boolean {
|
||||
// Don't show if already dismissed
|
||||
if (this._config.welcome_banner_dismissed) {
|
||||
@@ -178,41 +150,16 @@ class PanelHome extends SubscribeMixin(LitElement) {
|
||||
}
|
||||
|
||||
private async _loadConfig() {
|
||||
const securityConfigRevision = this._securityConfigRevision;
|
||||
const [translationsResult, homeResult, securityResult] =
|
||||
await Promise.allSettled([
|
||||
try {
|
||||
const [_, data] = await Promise.all([
|
||||
this.hass.loadFragmentTranslation("lovelace"),
|
||||
fetchFrontendSystemData(this.hass.connection, "home"),
|
||||
fetchFrontendSystemData(this.hass.connection, "security"),
|
||||
]);
|
||||
|
||||
if (translationsResult.status === "rejected") {
|
||||
this._config = data || {};
|
||||
} catch (err) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error(
|
||||
"Failed to load Lovelace translations:",
|
||||
translationsResult.reason
|
||||
);
|
||||
}
|
||||
|
||||
if (homeResult.status === "rejected") {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error("Failed to load home configuration:", homeResult.reason);
|
||||
console.error("Failed to load favorites:", err);
|
||||
this._config = {};
|
||||
} else {
|
||||
this._config = homeResult.value || {};
|
||||
}
|
||||
|
||||
if (securityResult.status === "rejected") {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error(
|
||||
"Failed to load security configuration:",
|
||||
securityResult.reason
|
||||
);
|
||||
if (securityConfigRevision === this._securityConfigRevision) {
|
||||
this._securityConfig = {};
|
||||
}
|
||||
} else if (securityConfigRevision === this._securityConfigRevision) {
|
||||
this._securityConfig = securityResult.value || {};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -396,7 +343,6 @@ class PanelHome extends SubscribeMixin(LitElement) {
|
||||
return {
|
||||
strategy: {
|
||||
type: "home",
|
||||
alert_entities: this._securityConfig.alert_entities,
|
||||
favorite_entities: this._config.favorite_entities,
|
||||
home_panel: true,
|
||||
hide_welcome_message: this._config.hide_welcome_message,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { consume } from "@lit/context";
|
||||
import { mdiHomeImportOutline, mdiPause, mdiPlay, mdiStop } from "@mdi/js";
|
||||
import { mdiHomeImportOutline, mdiPause, mdiPlay } from "@mdi/js";
|
||||
import type { HassEntity } from "home-assistant-js-websocket";
|
||||
import { LitElement, html, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
@@ -20,7 +20,6 @@ import {
|
||||
LawnMowerEntityFeature,
|
||||
canDock,
|
||||
canStartMowing,
|
||||
canStop,
|
||||
} from "../../../data/lawn_mower";
|
||||
import type { HomeAssistant, HomeAssistantApi } from "../../../types";
|
||||
import type { LovelaceCardFeature, LovelaceCardFeatureEditor } from "../types";
|
||||
@@ -47,7 +46,6 @@ export const LAWN_MOWER_COMMANDS_FEATURES: Record<
|
||||
LawnMowerEntityFeature.PAUSE,
|
||||
LawnMowerEntityFeature.START_MOWING,
|
||||
],
|
||||
stop: [LawnMowerEntityFeature.STOP],
|
||||
dock: [LawnMowerEntityFeature.DOCK],
|
||||
};
|
||||
|
||||
@@ -83,12 +81,6 @@ export const LAWN_MOWER_COMMANDS_BUTTONS: Record<
|
||||
!canStartMowing(stateObj),
|
||||
};
|
||||
},
|
||||
stop: (stateObj) => ({
|
||||
translationKey: "stop",
|
||||
icon: mdiStop,
|
||||
serviceName: "stop",
|
||||
disabled: !canStop(stateObj),
|
||||
}),
|
||||
dock: (stateObj) => ({
|
||||
translationKey: "dock",
|
||||
icon: mdiHomeImportOutline,
|
||||
|
||||
@@ -26,14 +26,12 @@ import type {
|
||||
HomeAssistantFormatters,
|
||||
HomeAssistantInternationalization,
|
||||
} from "../../../types";
|
||||
import type { LovelaceCardFeature, LovelaceCardFeatureEditor } from "../types";
|
||||
import type { LovelaceCardFeature } from "../types";
|
||||
import { cardFeatureStyles } from "./common/card-feature-styles";
|
||||
import type {
|
||||
LovelaceCardFeatureContext,
|
||||
TargetHumidityCardFeatureConfig,
|
||||
} from "./types";
|
||||
import "../../../components/ha-control-button-group";
|
||||
import "../../../components/ha-control-number-buttons";
|
||||
|
||||
const supportsTargetHumidityCardFeatureFromState = (stateObj: HassEntity) => {
|
||||
const domain = computeDomain(stateObj.entity_id);
|
||||
@@ -88,15 +86,9 @@ class HuiTargetHumidityCardFeature
|
||||
static getStubConfig(): TargetHumidityCardFeatureConfig {
|
||||
return {
|
||||
type: "target-humidity",
|
||||
style: "slider",
|
||||
};
|
||||
}
|
||||
|
||||
public static async getConfigElement(): Promise<LovelaceCardFeatureEditor> {
|
||||
await import("../editor/config-elements/hui-target-humidity-card-feature-editor");
|
||||
return document.createElement("hui-target-humidity-card-feature-editor");
|
||||
}
|
||||
|
||||
public setConfig(config: TargetHumidityCardFeatureConfig): void {
|
||||
if (!config) {
|
||||
throw new Error("Invalid configuration");
|
||||
@@ -111,6 +103,10 @@ class HuiTargetHumidityCardFeature
|
||||
}
|
||||
}
|
||||
|
||||
private get _step() {
|
||||
return this._stateObj!.attributes.target_humidity_step ?? 1;
|
||||
}
|
||||
|
||||
private get _min() {
|
||||
return this._stateObj!.attributes.min_humidity ?? 0;
|
||||
}
|
||||
@@ -144,34 +140,13 @@ class HuiTargetHumidityCardFeature
|
||||
return nothing;
|
||||
}
|
||||
|
||||
if (this._config.style === "buttons") {
|
||||
return html`
|
||||
<ha-control-button-group>
|
||||
<ha-control-number-buttons
|
||||
.value=${this._stateObj.attributes.humidity}
|
||||
.min=${this._min}
|
||||
.max=${this._max}
|
||||
.disabled=${this._stateObj.state === UNAVAILABLE}
|
||||
.step=${this._stateObj.attributes.target_humidity_step ?? 1}
|
||||
@value-changed=${this._valueChanged}
|
||||
.label=${this._formatters.formatEntityAttributeName(
|
||||
this._stateObj,
|
||||
"humidity"
|
||||
)}
|
||||
unit="%"
|
||||
.locale=${this._locale}
|
||||
></ha-control-number-buttons>
|
||||
</ha-control-button-group>
|
||||
`;
|
||||
}
|
||||
|
||||
return html`
|
||||
<ha-control-slider
|
||||
.value=${this._stateObj.attributes.humidity}
|
||||
.min=${this._min}
|
||||
.max=${this._max}
|
||||
.step=${this._step}
|
||||
.disabled=${this._stateObj!.state === UNAVAILABLE}
|
||||
.step=${this._stateObj.attributes.target_humidity_step ?? 1}
|
||||
@value-changed=${this._valueChanged}
|
||||
.label=${this._formatters.formatEntityAttributeName(
|
||||
this._stateObj,
|
||||
|
||||
@@ -186,7 +186,6 @@ export interface NumericInputCardFeatureConfig {
|
||||
|
||||
export interface TargetHumidityCardFeatureConfig {
|
||||
type: "target-humidity";
|
||||
style?: "buttons" | "slider";
|
||||
}
|
||||
|
||||
export interface TargetTemperatureCardFeatureConfig {
|
||||
@@ -265,7 +264,7 @@ export interface ValvePositionFavoriteCardFeatureConfig {
|
||||
type: "valve-position-favorite";
|
||||
}
|
||||
|
||||
export const LAWN_MOWER_COMMANDS = ["start_pause", "stop", "dock"] as const;
|
||||
export const LAWN_MOWER_COMMANDS = ["start_pause", "dock"] as const;
|
||||
|
||||
export type LawnMowerCommand = (typeof LAWN_MOWER_COMMANDS)[number];
|
||||
|
||||
|
||||
@@ -190,10 +190,6 @@ export class HuiCalendarCard
|
||||
}
|
||||
|
||||
const loading = !this._entityRegistry || !this._eventsLoaded;
|
||||
const rows = this._config.grid_options
|
||||
? this._config.grid_options.rows
|
||||
: this._config.layout_options?.grid_rows;
|
||||
const autoHeight = this.layout === "grid" && rows === "auto";
|
||||
|
||||
const views: FullCalendarView[] = [
|
||||
"dayGridMonth",
|
||||
@@ -210,12 +206,11 @@ export class HuiCalendarCard
|
||||
}
|
||||
<ha-full-calendar
|
||||
class=${classMap({
|
||||
"is-grid": this.layout === "grid" && !autoHeight,
|
||||
"is-grid": this.layout === "grid",
|
||||
"is-panel": this.layout === "panel",
|
||||
"has-title": !!this._config.title,
|
||||
loading: loading,
|
||||
})}
|
||||
?auto-height=${autoHeight}
|
||||
?add-fab=${this._config.show_add_event}
|
||||
add-fab-style=${this._config.show_add_event ? (this._config.add_event_style ?? "below") : nothing}
|
||||
add-fab-size=${this._config.show_add_event && this._config.add_event_style !== "header" ? (this._config.add_event_size ?? "small") : nothing}
|
||||
@@ -408,11 +403,6 @@ export class HuiCalendarCard
|
||||
min-height: var(--calendar-height);
|
||||
}
|
||||
|
||||
ha-full-calendar[auto-height] {
|
||||
--calendar-height: auto;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
ha-full-calendar.loading {
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
@@ -2,7 +2,6 @@ import type { PropertyValues } from "lit";
|
||||
import { LitElement, css, html, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { classMap } from "lit/directives/class-map";
|
||||
import type { HassEntity } from "home-assistant-js-websocket";
|
||||
import { theme2hex } from "../../../common/color/convert-color";
|
||||
import { isComponentLoaded } from "../../../common/config/is_component_loaded";
|
||||
import { createSearchParam } from "../../../common/url/search-params";
|
||||
@@ -106,22 +105,10 @@ export class HuiHistoryGraphCard extends LitElement implements LovelaceCard {
|
||||
}
|
||||
this._names = {};
|
||||
this._entities.forEach((entity) => {
|
||||
// Leave unset so timeline/line charts use computeHistory's Device ▸ Entity
|
||||
// labels. Only YAML `name` overrides that default.
|
||||
if (entity.name === undefined) {
|
||||
return;
|
||||
}
|
||||
const stateObj =
|
||||
this.hass!.states[entity.entity] ??
|
||||
({
|
||||
entity_id: entity.entity,
|
||||
state: "unavailable",
|
||||
attributes: {},
|
||||
} as HassEntity);
|
||||
this._names[entity.entity] = this.hass!.formatEntityName(
|
||||
stateObj,
|
||||
entity.name
|
||||
);
|
||||
const stateObj = this.hass!.states[entity.entity];
|
||||
this._names[entity.entity] = stateObj
|
||||
? this.hass!.formatEntityName(stateObj, entity.name)
|
||||
: entity.entity;
|
||||
});
|
||||
}
|
||||
|
||||
@@ -328,7 +315,6 @@ export class HuiHistoryGraphCard extends LitElement implements LovelaceCard {
|
||||
const columns = this._config.grid_options?.columns ?? 12;
|
||||
const narrow = typeof columns === "number" && columns <= 12;
|
||||
const hasFixedHeight = typeof this._config.grid_options?.rows === "number";
|
||||
const showNames = this._config.show_names !== false;
|
||||
|
||||
return html`
|
||||
<ha-card>
|
||||
@@ -374,8 +360,11 @@ export class HuiHistoryGraphCard extends LitElement implements LovelaceCard {
|
||||
.names=${this._names}
|
||||
up-to-now
|
||||
.hoursToShow=${this._hoursToShow}
|
||||
.showNames=${showNames}
|
||||
?inside-labels=${showNames}
|
||||
.showNames=${
|
||||
this._config.show_names !== undefined
|
||||
? this._config.show_names
|
||||
: true
|
||||
}
|
||||
.logarithmicScale=${this._config.logarithmic_scale || false}
|
||||
.minYAxis=${this._config.min_y_axis}
|
||||
.maxYAxis=${this._config.max_y_axis}
|
||||
|
||||
@@ -2,7 +2,6 @@ import { endOfDay, startOfDay } from "date-fns";
|
||||
import type { UnsubscribeFunc } from "home-assistant-js-websocket";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { classMap } from "lit/directives/class-map";
|
||||
import { styleMap } from "lit/directives/style-map";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { computeCssColor } from "../../../common/color/compute-color";
|
||||
@@ -40,10 +39,6 @@ import {
|
||||
filterLowBatteryEntities,
|
||||
filterUnavailableBatteryEntities,
|
||||
} from "../../maintenance/strategies/maintenance-view-strategy";
|
||||
import {
|
||||
isSecurityAlertActive,
|
||||
resolveSecurityAlertSeverity,
|
||||
} from "../../security/strategies/security-alerts";
|
||||
import type { LovelaceCard, LovelaceGridOptions } from "../types";
|
||||
import { tileCardStyle } from "./tile/tile-card-style";
|
||||
import type { HomeSummaryCard } from "./types";
|
||||
@@ -184,16 +179,6 @@ export class HuiHomeSummaryCard
|
||||
? `${formattedMinTemp}°`
|
||||
: `${formattedMinTemp} - ${formattedMaxTemp}°`;
|
||||
}
|
||||
case "alerts": {
|
||||
const count = (this._config.alert_entities ?? []).filter(
|
||||
(alertEntity) => isSecurityAlertActive(this.hass!, alertEntity.entity)
|
||||
).length;
|
||||
return count
|
||||
? this.hass.localize("ui.card.home-summary.count_active_alerts", {
|
||||
count,
|
||||
})
|
||||
: "";
|
||||
}
|
||||
case "security": {
|
||||
// Alarm and lock status
|
||||
const securityFilters = HOME_SUMMARIES_FILTERS.security.map((filter) =>
|
||||
@@ -217,22 +202,8 @@ export class HuiHomeSummaryCard
|
||||
return s === "disarmed";
|
||||
});
|
||||
|
||||
const warningCount = (this._config.alert_entities ?? []).filter(
|
||||
(alertEntity) =>
|
||||
resolveSecurityAlertSeverity(
|
||||
alertEntity,
|
||||
this.hass!.states[alertEntity.entity]
|
||||
) === "warning" &&
|
||||
isSecurityAlertActive(this.hass!, alertEntity.entity)
|
||||
).length;
|
||||
const warningText = warningCount
|
||||
? this.hass.localize("ui.card.home-summary.count_warnings", {
|
||||
count: warningCount,
|
||||
})
|
||||
: undefined;
|
||||
|
||||
if (!locks.length && !alarms.length) {
|
||||
return warningText ?? "";
|
||||
return "";
|
||||
}
|
||||
|
||||
const unlockedLocks = locks.filter((entityId) => {
|
||||
@@ -240,18 +211,23 @@ export class HuiHomeSummaryCard
|
||||
return s === "unlocked" || s === "jammed" || s === "open";
|
||||
});
|
||||
|
||||
const statusText = unlockedLocks.length
|
||||
? this.hass.localize("ui.card.home-summary.count_locks_unlocked", {
|
||||
if (unlockedLocks.length) {
|
||||
return this.hass.localize(
|
||||
"ui.card.home-summary.count_locks_unlocked",
|
||||
{
|
||||
count: unlockedLocks.length,
|
||||
})
|
||||
: disarmedAlarms.length
|
||||
? this.hass.localize("ui.card.home-summary.count_alarms_disarmed", {
|
||||
count: disarmedAlarms.length,
|
||||
})
|
||||
: warningText
|
||||
? undefined
|
||||
: this.hass.localize("ui.card.home-summary.all_secure");
|
||||
return [warningText, statusText].filter(Boolean).join(", ");
|
||||
}
|
||||
);
|
||||
}
|
||||
if (disarmedAlarms.length) {
|
||||
return this.hass.localize(
|
||||
"ui.card.home-summary.count_alarms_disarmed",
|
||||
{
|
||||
count: disarmedAlarms.length,
|
||||
}
|
||||
);
|
||||
}
|
||||
return this.hass.localize("ui.card.home-summary.all_secure");
|
||||
}
|
||||
case "media_players": {
|
||||
// Playing media
|
||||
@@ -362,29 +338,23 @@ export class HuiHomeSummaryCard
|
||||
return nothing;
|
||||
}
|
||||
|
||||
const summary = this._config.summary;
|
||||
const isAlertsSummary = summary === "alerts";
|
||||
const color = computeCssColor(HOME_SUMMARIES_COLORS[summary]);
|
||||
const color = computeCssColor(HOME_SUMMARIES_COLORS[this._config.summary]);
|
||||
|
||||
const style = {
|
||||
"--tile-color": color,
|
||||
"--ha-alert-color": isAlertsSummary ? color : undefined,
|
||||
};
|
||||
|
||||
const secondary = this._computeSummaryState();
|
||||
const secondaryLoading = this._computeSecondaryLoading(
|
||||
summary,
|
||||
this._config.summary,
|
||||
this._energyData
|
||||
);
|
||||
|
||||
const label = getSummaryLabel(this.hass.localize, summary);
|
||||
const icon = HOME_SUMMARIES_ICONS[summary];
|
||||
const label = getSummaryLabel(this.hass.localize, this._config.summary);
|
||||
const icon = HOME_SUMMARIES_ICONS[this._config.summary];
|
||||
|
||||
return html`
|
||||
<ha-card
|
||||
class=${classMap({ alert: isAlertsSummary })}
|
||||
style=${styleMap(style)}
|
||||
>
|
||||
<ha-card style=${styleMap(style)}>
|
||||
<ha-tile-container
|
||||
.vertical=${Boolean(this._config.vertical)}
|
||||
.interactive=${this._hasCardAction}
|
||||
@@ -411,24 +381,6 @@ export class HuiHomeSummaryCard
|
||||
css`
|
||||
:host {
|
||||
--tile-color: var(--state-inactive-color);
|
||||
--ha-alert-pulse-opacity: 0.3;
|
||||
}
|
||||
ha-card.alert {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
--tile-color: var(--ha-alert-color);
|
||||
}
|
||||
ha-card.alert::before {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
border-radius: var(--ha-card-border-radius, var(--ha-border-radius-lg));
|
||||
background-color: var(--ha-alert-color);
|
||||
content: "";
|
||||
opacity: var(--ha-alert-pulse-opacity);
|
||||
pointer-events: none;
|
||||
}
|
||||
ha-card.alert ha-tile-container {
|
||||
position: relative;
|
||||
}
|
||||
`,
|
||||
];
|
||||
|
||||
@@ -1,15 +1,14 @@
|
||||
import { mdiDotsVertical } from "@mdi/js";
|
||||
import "@thomasloven/round-slider";
|
||||
import type { PropertyValues } from "lit";
|
||||
import { LitElement, css, html, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { classMap } from "lit/directives/class-map";
|
||||
import { styleMap } from "lit/directives/style-map";
|
||||
import { applyThemesOnElement } from "../../../common/dom/apply_themes_on_element";
|
||||
import type { HASSDomEvent } from "../../../common/dom/fire_event";
|
||||
import { fireEvent } from "../../../common/dom/fire_event";
|
||||
import { stateColorBrightness } from "../../../common/entity/state_color";
|
||||
import "../../../components/ha-card";
|
||||
import "../../../components/ha-control-circular-slider";
|
||||
import "../../../components/ha-icon-button";
|
||||
import "../../../components/ha-state-icon";
|
||||
import { UNAVAILABLE, UNKNOWN } from "../../../data/entity/entity";
|
||||
@@ -109,14 +108,12 @@ export class HuiLightCard extends LitElement implements LovelaceCard {
|
||||
<div class="content">
|
||||
<div id="controls">
|
||||
<div id="slider">
|
||||
<ha-control-circular-slider
|
||||
mode="start"
|
||||
.min=${1}
|
||||
.max=${100}
|
||||
.step=${1}
|
||||
<!-- @ts-ignore Round-slider has no tag definition or exported type -->
|
||||
<round-slider
|
||||
min="1"
|
||||
max="100"
|
||||
.value=${brightness}
|
||||
.disabled=${stateObj.state === UNAVAILABLE}
|
||||
prevent-interaction-on-scroll
|
||||
@value-changing=${this._dragEvent}
|
||||
@value-changed=${this._setBrightness}
|
||||
style=${styleMap({
|
||||
@@ -124,7 +121,7 @@ export class HuiLightCard extends LitElement implements LovelaceCard {
|
||||
? "visible"
|
||||
: "hidden",
|
||||
})}
|
||||
></ha-control-circular-slider>
|
||||
></round-slider>
|
||||
<ha-icon-button
|
||||
class="light-button ${classMap({
|
||||
"slider-center": lightSupportsBrightness(stateObj),
|
||||
@@ -194,10 +191,9 @@ export class HuiLightCard extends LitElement implements LovelaceCard {
|
||||
}
|
||||
}
|
||||
|
||||
private _dragEvent(ev: HASSDomEvent<HASSDomEvents["value-changing"]>): void {
|
||||
const { value } = ev.detail;
|
||||
if (typeof value !== "number" || isNaN(value)) return;
|
||||
this.shadowRoot!.querySelector(".brightness")!.innerHTML = `${value} %`;
|
||||
private _dragEvent(e: any): void {
|
||||
this.shadowRoot!.querySelector(".brightness")!.innerHTML =
|
||||
`${e.detail.value} %`;
|
||||
this._showBrightness();
|
||||
this._hideBrightness();
|
||||
}
|
||||
@@ -217,14 +213,10 @@ export class HuiLightCard extends LitElement implements LovelaceCard {
|
||||
}, 500);
|
||||
}
|
||||
|
||||
private _setBrightness(
|
||||
ev: HASSDomEvent<HASSDomEvents["value-changed"]>
|
||||
): void {
|
||||
const { value } = ev.detail;
|
||||
if (typeof value !== "number" || isNaN(value)) return;
|
||||
private _setBrightness(e: any): void {
|
||||
this.hass!.callService("light", "turn_on", {
|
||||
entity_id: this._config!.entity,
|
||||
brightness_pct: value,
|
||||
brightness_pct: e.detail.value,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -300,11 +292,10 @@ export class HuiLightCard extends LitElement implements LovelaceCard {
|
||||
min-width: 100px;
|
||||
}
|
||||
|
||||
ha-control-circular-slider {
|
||||
width: 100%;
|
||||
--control-circular-slider-color: var(--primary-color);
|
||||
--control-circular-slider-background: var(--slider-track-color);
|
||||
--control-circular-slider-background-opacity: 1;
|
||||
round-slider {
|
||||
--round-slider-path-color: var(--slider-track-color);
|
||||
--round-slider-bar-color: var(--primary-color);
|
||||
padding-bottom: 10%;
|
||||
}
|
||||
|
||||
.light-button {
|
||||
|
||||
@@ -3,7 +3,6 @@ import type { EntityNameItem } from "../../../common/entity/compute_entity_name_
|
||||
import type { HaDurationData } from "../../../components/ha-duration-input";
|
||||
import type { MapCardMarkerLabelMode } from "../../../components/map/ha-map";
|
||||
import type { EnergySourceByType } from "../../../data/energy";
|
||||
import type { SecurityAlertEntityConfig } from "../../../data/frontend";
|
||||
import type { ActionConfig } from "../../../data/lovelace/config/action";
|
||||
import type { LovelaceCardConfig } from "../../../data/lovelace/config/card";
|
||||
import type {
|
||||
@@ -715,7 +714,6 @@ export interface HeadingCardConfig extends LovelaceCardConfig {
|
||||
|
||||
export interface HomeSummaryCard extends LovelaceCardConfig {
|
||||
summary: HomeSummary;
|
||||
alert_entities?: SecurityAlertEntityConfig[];
|
||||
vertical?: boolean;
|
||||
tap_action?: ActionConfig;
|
||||
hold_action?: ActionConfig;
|
||||
|
||||
@@ -12,11 +12,9 @@ import { computeDeviceName } from "../../../../common/entity/compute_device_name
|
||||
import { computeDomain } from "../../../../common/entity/compute_domain";
|
||||
import { computeEntityName } from "../../../../common/entity/compute_entity_name";
|
||||
import { computeStateName } from "../../../../common/entity/compute_state_name";
|
||||
import { getDeviceAreaId } from "../../../../common/entity/context/get_device_context";
|
||||
import { getEntityContext } from "../../../../common/entity/context/get_entity_context";
|
||||
import { stringCompare } from "../../../../common/string/compare";
|
||||
import { entityComboBoxKeys } from "../../../../data/entity/entity_picker";
|
||||
import type { DeviceRegistryEntry } from "../../../../data/device/device_registry";
|
||||
import { domainToName } from "../../../../data/integration";
|
||||
import { multiTermSortedSearch } from "../../../../resources/fuseMultiTerm";
|
||||
import type { HomeAssistant } from "../../../../types";
|
||||
@@ -26,7 +24,6 @@ export interface DeviceNode {
|
||||
id: string;
|
||||
name: string;
|
||||
entityIds: string[];
|
||||
children: DeviceNode[];
|
||||
}
|
||||
|
||||
export interface AreaNode {
|
||||
@@ -135,30 +132,6 @@ export function buildEntityTree(input: BuildEntityTreeInput): EntityTree {
|
||||
const unassignedEntityByDomain = new Map<string, string[]>();
|
||||
const searchableEntities: SearchableEntity[] = [];
|
||||
|
||||
const addDeviceEntity = (
|
||||
bucket: Map<string, string[]>,
|
||||
device: DeviceRegistryEntry,
|
||||
entityId: string,
|
||||
areaId: string | undefined
|
||||
) => {
|
||||
const list = bucket.get(device.id) ?? [];
|
||||
list.push(entityId);
|
||||
bucket.set(device.id, list);
|
||||
// A child device nests under its parent when both land in the same
|
||||
// bucket, so the parent needs a row even without entities of its own.
|
||||
const parent = device.parent_device_id
|
||||
? deviceReg[device.parent_device_id]
|
||||
: undefined;
|
||||
if (
|
||||
parent &&
|
||||
getDeviceAreaId(parent, deviceReg) === areaId &&
|
||||
(parent.entry_type === "service") === (device.entry_type === "service") &&
|
||||
!bucket.has(parent.id)
|
||||
) {
|
||||
bucket.set(parent.id, []);
|
||||
}
|
||||
};
|
||||
|
||||
for (const entityId of Object.keys(states)) {
|
||||
const stateObj = states[entityId];
|
||||
if (!stateObj) continue;
|
||||
@@ -209,7 +182,9 @@ export function buildEntityTree(input: BuildEntityTreeInput): EntityTree {
|
||||
const target = isService
|
||||
? unassignedServiceEntities
|
||||
: unassignedDeviceEntities;
|
||||
addDeviceEntity(target, device, entityId, undefined);
|
||||
const list = target.get(device.id) ?? [];
|
||||
list.push(entityId);
|
||||
target.set(device.id, list);
|
||||
} else if (isHelperDomain(domain)) {
|
||||
const list = unassignedHelperByDomain.get(domain) ?? [];
|
||||
list.push(entityId);
|
||||
@@ -225,7 +200,9 @@ export function buildEntityTree(input: BuildEntityTreeInput): EntityTree {
|
||||
const groupUnderDevice = device && !entry?.area_id;
|
||||
if (groupUnderDevice) {
|
||||
const byDevice = areaDeviceEntities.get(areaId) ?? new Map();
|
||||
addDeviceEntity(byDevice, device!, entityId, areaId);
|
||||
const list = byDevice.get(device!.id) ?? [];
|
||||
list.push(entityId);
|
||||
byDevice.set(device!.id, list);
|
||||
areaDeviceEntities.set(areaId, byDevice);
|
||||
} else {
|
||||
const list = areaDirectEntities.get(areaId) ?? [];
|
||||
@@ -240,35 +217,17 @@ export function buildEntityTree(input: BuildEntityTreeInput): EntityTree {
|
||||
return stringCompare(an, bn, language);
|
||||
};
|
||||
|
||||
const sortDeviceNodes = (nodes: DeviceNode[]) => {
|
||||
nodes.sort((a, b) => stringCompare(a.name, b.name, language));
|
||||
nodes.forEach((node) => sortDeviceNodes(node.children));
|
||||
};
|
||||
|
||||
const buildDeviceNodes = (source: Map<string, string[]>): DeviceNode[] => {
|
||||
const nodes = new Map<string, DeviceNode>();
|
||||
for (const [id, ids] of source) {
|
||||
const device = deviceReg[id];
|
||||
nodes.set(id, {
|
||||
id,
|
||||
name: (device ? computeDeviceName(device) : undefined) ?? id,
|
||||
entityIds: ids.sort(sortByName),
|
||||
children: [],
|
||||
});
|
||||
}
|
||||
const roots: DeviceNode[] = [];
|
||||
for (const node of nodes.values()) {
|
||||
const parentId = deviceReg[node.id]?.parent_device_id;
|
||||
const parent = parentId ? nodes.get(parentId) : undefined;
|
||||
if (parent) {
|
||||
parent.children.push(node);
|
||||
} else {
|
||||
roots.push(node);
|
||||
}
|
||||
}
|
||||
sortDeviceNodes(roots);
|
||||
return roots;
|
||||
};
|
||||
const buildDeviceNodes = (source: Map<string, string[]>): DeviceNode[] =>
|
||||
[...source.entries()]
|
||||
.map(([id, ids]) => {
|
||||
const device = deviceReg[id];
|
||||
return {
|
||||
id,
|
||||
name: (device ? computeDeviceName(device) : undefined) ?? id,
|
||||
entityIds: ids.sort(sortByName),
|
||||
};
|
||||
})
|
||||
.sort((a, b) => stringCompare(a.name, b.name, language));
|
||||
|
||||
const buildAreaNode = (areaId: string): AreaNode | undefined => {
|
||||
const area = areaReg[areaId];
|
||||
@@ -366,28 +325,17 @@ export function buildEntityTree(input: BuildEntityTreeInput): EntityTree {
|
||||
};
|
||||
}
|
||||
|
||||
const devicePathToEntity = (
|
||||
devices: DeviceNode[],
|
||||
parentKey: string,
|
||||
entityId: string
|
||||
): string[] | undefined => {
|
||||
for (const device of devices) {
|
||||
const key = deviceKey(parentKey, device.id);
|
||||
if (device.entityIds.includes(entityId)) return [key];
|
||||
const nested = devicePathToEntity(device.children, key, entityId);
|
||||
if (nested) return [key, ...nested];
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
|
||||
export function pathToEntity(tree: EntityTree, entityId: string): string[] {
|
||||
for (const floor of tree.floors) {
|
||||
const fKey = floorKey(floor.id);
|
||||
for (const area of floor.areas) {
|
||||
const aKey = areaKey(fKey, area.id);
|
||||
if (area.directEntityIds.includes(entityId)) return [fKey, aKey];
|
||||
const devicePath = devicePathToEntity(area.devices, aKey, entityId);
|
||||
if (devicePath) return [fKey, aKey, ...devicePath];
|
||||
for (const device of area.devices) {
|
||||
if (device.entityIds.includes(entityId)) {
|
||||
return [fKey, aKey, deviceKey(aKey, device.id)];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -397,15 +345,21 @@ export function pathToEntity(tree: EntityTree, entityId: string): string[] {
|
||||
if (area.directEntityIds.includes(entityId)) {
|
||||
return [otherAreasFloor, aKey];
|
||||
}
|
||||
const devicePath = devicePathToEntity(area.devices, aKey, entityId);
|
||||
if (devicePath) return [otherAreasFloor, aKey, ...devicePath];
|
||||
for (const device of area.devices) {
|
||||
if (device.entityIds.includes(entityId)) {
|
||||
return [otherAreasFloor, aKey, deviceKey(aKey, device.id)];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const section of tree.unassignedSections) {
|
||||
const sKey = unassignedKey(section.id);
|
||||
if (section.devices) {
|
||||
const devicePath = devicePathToEntity(section.devices, sKey, entityId);
|
||||
if (devicePath) return [sKey, ...devicePath];
|
||||
for (const device of section.devices) {
|
||||
if (device.entityIds.includes(entityId)) {
|
||||
return [sKey, deviceKey(sKey, device.id)];
|
||||
}
|
||||
}
|
||||
}
|
||||
if (section.domains) {
|
||||
for (const group of section.domains) {
|
||||
|
||||
@@ -416,18 +416,14 @@ export class HuiSuggestionEntityTree extends LitElement {
|
||||
`;
|
||||
}
|
||||
|
||||
private _renderDevice(
|
||||
device: DeviceNode,
|
||||
parentKey: string,
|
||||
nested = false
|
||||
): TemplateResult {
|
||||
private _renderDevice(device: DeviceNode, parentKey: string): TemplateResult {
|
||||
const key = deviceKey(parentKey, device.id);
|
||||
const expanded = this._isExpanded(key);
|
||||
const domain = this._deviceDomain(device.id);
|
||||
return html`
|
||||
<ha-combo-box-item
|
||||
type="button"
|
||||
class="branch ${nested ? "depth-device-nested" : "depth-device"} device-item"
|
||||
class="branch depth-device device-item"
|
||||
aria-expanded=${expanded}
|
||||
data-node-key=${key}
|
||||
@click=${this._toggleNode}
|
||||
@@ -448,24 +444,11 @@ export class HuiSuggestionEntityTree extends LitElement {
|
||||
</ha-combo-box-item>
|
||||
${
|
||||
expanded
|
||||
? html`
|
||||
${repeat(
|
||||
device.children,
|
||||
(child: DeviceNode) => child.id,
|
||||
(child: DeviceNode) => this._renderDevice(child, key, true)
|
||||
)}
|
||||
${repeat(
|
||||
device.entityIds,
|
||||
(id: string) => id,
|
||||
(id: string) =>
|
||||
this._renderEntity(
|
||||
id,
|
||||
nested
|
||||
? "depth-entity-device-nested"
|
||||
: "depth-entity-device"
|
||||
)
|
||||
)}
|
||||
`
|
||||
? repeat(
|
||||
device.entityIds,
|
||||
(id: string) => id,
|
||||
(id: string) => this._renderEntity(id, "depth-entity-device")
|
||||
)
|
||||
: nothing
|
||||
}
|
||||
`;
|
||||
@@ -667,13 +650,9 @@ export class HuiSuggestionEntityTree extends LitElement {
|
||||
ha-combo-box-item.depth-entity-area {
|
||||
--md-list-item-leading-space: var(--ha-space-12);
|
||||
}
|
||||
ha-combo-box-item.depth-entity-device,
|
||||
ha-combo-box-item.depth-device-nested {
|
||||
ha-combo-box-item.depth-entity-device {
|
||||
--md-list-item-leading-space: var(--ha-space-16);
|
||||
}
|
||||
ha-combo-box-item.depth-entity-device-nested {
|
||||
--md-list-item-leading-space: var(--ha-space-20);
|
||||
}
|
||||
.leading {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
@@ -173,7 +173,6 @@ const EDITABLES_FEATURE_TYPES = new Set<UiFeatureTypes>([
|
||||
"media-player-volume-slider",
|
||||
"numeric-input",
|
||||
"select-options",
|
||||
"target-humidity",
|
||||
"timer-actions",
|
||||
"timer-presets",
|
||||
"trend-graph",
|
||||
|
||||
@@ -1,90 +0,0 @@
|
||||
import { html, nothing, LitElement } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||
import "../../../../components/ha-form/ha-form";
|
||||
import type { SchemaUnion } from "../../../../components/ha-form/types";
|
||||
import type { HomeAssistant } from "../../../../types";
|
||||
import type {
|
||||
TargetHumidityCardFeatureConfig,
|
||||
LovelaceCardFeatureContext,
|
||||
} from "../../card-features/types";
|
||||
import type { LovelaceCardFeatureEditor } from "../../types";
|
||||
import type { LocalizeFunc } from "../../../../common/translations/localize";
|
||||
|
||||
@customElement("hui-target-humidity-card-feature-editor")
|
||||
export class HuiTargetHumidityCardFeatureEditor
|
||||
extends LitElement
|
||||
implements LovelaceCardFeatureEditor
|
||||
{
|
||||
@property({ attribute: false }) public hass?: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public context?: LovelaceCardFeatureContext;
|
||||
|
||||
@state() private _config?: TargetHumidityCardFeatureConfig;
|
||||
|
||||
public setConfig(config: TargetHumidityCardFeatureConfig): void {
|
||||
this._config = config;
|
||||
}
|
||||
|
||||
private _schema = memoizeOne(
|
||||
(localize: LocalizeFunc) =>
|
||||
[
|
||||
{
|
||||
name: "style",
|
||||
selector: {
|
||||
select: {
|
||||
multiple: false,
|
||||
mode: "list",
|
||||
options: ["slider", "buttons"].map((mode) => ({
|
||||
value: mode,
|
||||
label: localize(
|
||||
`ui.panel.lovelace.editor.features.types.target-humidity.style_list.${mode}`
|
||||
),
|
||||
})),
|
||||
},
|
||||
},
|
||||
},
|
||||
] as const
|
||||
);
|
||||
|
||||
protected render() {
|
||||
if (!this.hass || !this._config) {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
const data: TargetHumidityCardFeatureConfig = {
|
||||
style: "slider",
|
||||
...this._config,
|
||||
};
|
||||
|
||||
const schema = this._schema(this.hass.localize);
|
||||
|
||||
return html`
|
||||
<ha-form
|
||||
.hass=${this.hass}
|
||||
.data=${data}
|
||||
.schema=${schema}
|
||||
.computeLabel=${this._computeLabelCallback}
|
||||
@value-changed=${this._valueChanged}
|
||||
></ha-form>
|
||||
`;
|
||||
}
|
||||
|
||||
private _valueChanged(ev: CustomEvent): void {
|
||||
fireEvent(this, "config-changed", { config: ev.detail.value });
|
||||
}
|
||||
|
||||
private _computeLabelCallback = (
|
||||
schema: SchemaUnion<ReturnType<typeof this._schema>>
|
||||
) =>
|
||||
this.hass!.localize(
|
||||
`ui.panel.lovelace.editor.features.types.target-humidity.${schema.name}`
|
||||
);
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"hui-target-humidity-card-feature-editor": HuiTargetHumidityCardFeatureEditor;
|
||||
}
|
||||
}
|
||||
@@ -257,9 +257,7 @@ class HUIRoot extends LitElement {
|
||||
icon: mdiPlus,
|
||||
key: "ui.panel.lovelace.menu.add",
|
||||
visible:
|
||||
!this._editMode &&
|
||||
this.hass.user?.is_admin &&
|
||||
!this.hass.kioskElementsHidden.has("dashboard_add_button"),
|
||||
!this._editMode && this.hass.user?.is_admin && !this.hass.kioskMode,
|
||||
overflow: this.narrow,
|
||||
subItems: [
|
||||
{
|
||||
@@ -302,9 +300,7 @@ class HUIRoot extends LitElement {
|
||||
? "(⌘ + K)"
|
||||
: "(Ctrl + K)"
|
||||
: undefined,
|
||||
visible:
|
||||
!this._editMode &&
|
||||
!this.hass.kioskElementsHidden.has("dashboard_search_button"),
|
||||
visible: !this._editMode && !this.hass.kioskMode,
|
||||
overflow: this.narrow,
|
||||
},
|
||||
{
|
||||
@@ -315,9 +311,7 @@ class HUIRoot extends LitElement {
|
||||
suffix:
|
||||
this.hass.enableShortcuts && !isMobileClient ? "(A)" : undefined,
|
||||
visible:
|
||||
!this._editMode &&
|
||||
this._conversation(this.hass.config.components) &&
|
||||
!this.hass.kioskElementsHidden.has("dashboard_assist_button"),
|
||||
!this._editMode && this._conversation(this.hass.config.components),
|
||||
overflow: this.narrow,
|
||||
},
|
||||
{
|
||||
@@ -353,7 +347,7 @@ class HUIRoot extends LitElement {
|
||||
!this._editMode &&
|
||||
this.hass!.user?.is_admin &&
|
||||
!this.hass!.config.recovery_mode &&
|
||||
!this.hass.kioskElementsHidden.has("dashboard_edit_button") &&
|
||||
!this.hass.kioskMode &&
|
||||
!this.noEdit,
|
||||
overflow: true,
|
||||
overflow_can_promote: true,
|
||||
@@ -555,10 +549,6 @@ class HUIRoot extends LitElement {
|
||||
|
||||
const isSubview = curViewConfig?.subview;
|
||||
const hasTabViews = views.filter((view) => !view.subview).length > 1;
|
||||
// Only honored outside edit mode. In edit mode the tabs are the view
|
||||
// editor (select, move, edit and add views), so hiding them there would
|
||||
// make views uneditable; see `dashboard_tabs` in data/kiosk_mode.
|
||||
const hideTabs = this.hass.kioskElementsHidden.has("dashboard_tabs");
|
||||
|
||||
return html`
|
||||
<div
|
||||
@@ -615,19 +605,13 @@ class HUIRoot extends LitElement {
|
||||
${curViewConfig.title}
|
||||
</div>
|
||||
`
|
||||
: hideTabs
|
||||
? html`
|
||||
: hasTabViews
|
||||
? tabs
|
||||
: html`
|
||||
<div class="main-title">
|
||||
${curViewConfig?.title ?? dashboardTitle}
|
||||
${views[0]?.title ?? dashboardTitle}
|
||||
</div>
|
||||
`
|
||||
: hasTabViews
|
||||
? tabs
|
||||
: html`
|
||||
<div class="main-title">
|
||||
${views[0]?.title ?? dashboardTitle}
|
||||
</div>
|
||||
`
|
||||
}
|
||||
<div class="action-items">
|
||||
${this._renderActionItems()}
|
||||
|
||||
@@ -6,7 +6,6 @@ import { lightEntityFilters } from "../../../../light/strategies/light-view-stra
|
||||
import { securityEntityFilters } from "../../../../security/strategies/security-view-strategy";
|
||||
|
||||
export const HOME_SUMMARIES = [
|
||||
"alerts",
|
||||
"light",
|
||||
"climate",
|
||||
"security",
|
||||
@@ -19,7 +18,6 @@ export const HOME_SUMMARIES = [
|
||||
export type HomeSummary = (typeof HOME_SUMMARIES)[number];
|
||||
|
||||
export const HOME_SUMMARIES_ICONS: Record<HomeSummary, string> = {
|
||||
alerts: "mdi:alarm-plus",
|
||||
light: "mdi:lamps",
|
||||
climate: "mdi:home-thermometer",
|
||||
security: "mdi:security",
|
||||
@@ -30,7 +28,6 @@ export const HOME_SUMMARIES_ICONS: Record<HomeSummary, string> = {
|
||||
};
|
||||
|
||||
export const HOME_SUMMARIES_COLORS: Record<HomeSummary, string> = {
|
||||
alerts: "red",
|
||||
light: "amber",
|
||||
climate: "deep-orange",
|
||||
security: "blue-grey",
|
||||
@@ -41,7 +38,6 @@ export const HOME_SUMMARIES_COLORS: Record<HomeSummary, string> = {
|
||||
};
|
||||
|
||||
export const HOME_SUMMARIES_FILTERS: Record<HomeSummary, EntityFilter[]> = {
|
||||
alerts: [],
|
||||
light: lightEntityFilters,
|
||||
climate: climateEntityFilters,
|
||||
security: securityEntityFilters,
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
import { STATE_NOT_RUNNING } from "home-assistant-js-websocket";
|
||||
import { ReactiveElement } from "lit";
|
||||
import { customElement } from "lit/decorators";
|
||||
import type { SecurityAlertEntityConfig } from "../../../../data/frontend";
|
||||
import type { ShortcutItem } from "../../../../data/home_shortcuts";
|
||||
import type { LovelaceConfig } from "../../../../data/lovelace/config/types";
|
||||
import type { LovelaceViewRawConfig } from "../../../../data/lovelace/config/view";
|
||||
import type { HomeAssistant } from "../../../../types";
|
||||
@@ -16,11 +14,11 @@ import {
|
||||
} from "./helpers/home-summaries";
|
||||
import type { HomeAreaViewStrategyConfig } from "./home-area-view-strategy";
|
||||
import type { HomeOtherDevicesViewStrategyConfig } from "./home-other-devices-view-strategy";
|
||||
import type { ShortcutItem } from "../../../../data/home_shortcuts";
|
||||
import type { HomeOverviewViewStrategyConfig } from "./home-overview-view-strategy";
|
||||
|
||||
export interface HomeDashboardStrategyConfig {
|
||||
type: "home";
|
||||
alert_entities?: SecurityAlertEntityConfig[];
|
||||
favorite_entities?: string[];
|
||||
home_panel?: boolean;
|
||||
hide_welcome_message?: boolean;
|
||||
@@ -105,7 +103,6 @@ export class HomeDashboardStrategy extends ReactiveElement {
|
||||
path: "overview",
|
||||
strategy: {
|
||||
type: "home-overview",
|
||||
alert_entities: config.alert_entities,
|
||||
favorite_entities: config.favorite_entities,
|
||||
home_panel: config.home_panel,
|
||||
hide_welcome_message: config.hide_welcome_message,
|
||||
|
||||
@@ -11,7 +11,6 @@ import { floorDefaultIcon } from "../../../../components/ha-floor-icon";
|
||||
import type { AreaRegistryEntry } from "../../../../data/area/area_registry";
|
||||
import type { EnergyPreferences } from "../../../../data/energy";
|
||||
import { getEnergyPreferences } from "../../../../data/energy";
|
||||
import type { SecurityAlertEntityConfig } from "../../../../data/frontend";
|
||||
import type { LovelaceCardConfig } from "../../../../data/lovelace/config/card";
|
||||
import type {
|
||||
LovelaceSectionConfig,
|
||||
@@ -25,7 +24,6 @@ import type { HomeAssistant } from "../../../../types";
|
||||
import { hasClimateEntities } from "../../../climate/strategies/climate-view-strategy";
|
||||
import type {
|
||||
AreaCardConfig,
|
||||
ConditionalCardConfig,
|
||||
DiscoveredDevicesCardConfig,
|
||||
EmptyStateCardConfig,
|
||||
HeadingCardConfig,
|
||||
@@ -37,24 +35,17 @@ import type {
|
||||
UpdatesCardConfig,
|
||||
} from "../../cards/types";
|
||||
import { computeFavoriteCardConfig } from "../helpers/favorite-cards";
|
||||
import {
|
||||
computeDefaultSecurityAlertVisibility,
|
||||
filterSecurityAlertEntities,
|
||||
resolveSecurityAlertSeverity,
|
||||
} from "../../../security/strategies/security-alerts";
|
||||
import {
|
||||
LARGE_SCREEN_CONDITION,
|
||||
SMALL_SCREEN_CONDITION,
|
||||
} from "../helpers/view-columns-conditions";
|
||||
import type { LovelaceStrategyDependency } from "../types";
|
||||
import type { CommonControlsSectionStrategyConfig } from "../usage_prediction/common-controls-section-strategy";
|
||||
import { generateLovelaceSectionStrategy } from "../get-strategy";
|
||||
import { HOME_SUMMARIES_FILTERS } from "./helpers/home-summaries";
|
||||
import { OTHER_DEVICES_FILTERS } from "./helpers/other-devices-filters";
|
||||
|
||||
export interface HomeOverviewViewStrategyConfig {
|
||||
type: "home-overview";
|
||||
alert_entities?: SecurityAlertEntityConfig[];
|
||||
favorite_entities?: string[];
|
||||
home_panel?: boolean;
|
||||
hide_welcome_message?: boolean;
|
||||
@@ -121,28 +112,6 @@ export class HomeOverviewViewStrategy extends ReactiveElement {
|
||||
"panels",
|
||||
];
|
||||
|
||||
static shouldRegenerate(
|
||||
config: HomeOverviewViewStrategyConfig,
|
||||
oldHass: HomeAssistant,
|
||||
newHass: HomeAssistant
|
||||
) {
|
||||
return (
|
||||
this.registryDependencies.some((key) => oldHass[key] !== newHass[key]) ||
|
||||
(config.alert_entities?.some(
|
||||
(alertEntity) =>
|
||||
resolveSecurityAlertSeverity(
|
||||
alertEntity,
|
||||
oldHass.states[alertEntity.entity]
|
||||
) !==
|
||||
resolveSecurityAlertSeverity(
|
||||
alertEntity,
|
||||
newHass.states[alertEntity.entity]
|
||||
)
|
||||
) ??
|
||||
false)
|
||||
);
|
||||
}
|
||||
|
||||
static async generate(
|
||||
config: HomeOverviewViewStrategyConfig,
|
||||
hass: HomeAssistant
|
||||
@@ -287,25 +256,16 @@ export class HomeOverviewViewStrategy extends ReactiveElement {
|
||||
|
||||
let favoritesSection: LovelaceSectionRawConfig | undefined;
|
||||
if (!config.hide_suggested_entities) {
|
||||
const generatedFavoritesSection = await generateLovelaceSectionStrategy(
|
||||
{
|
||||
strategy: {
|
||||
type: "common-controls",
|
||||
limit: maxCommonControls,
|
||||
include_entities: favoriteEntities,
|
||||
hide_empty: true,
|
||||
heading: favoritesHeadingCard,
|
||||
} satisfies CommonControlsSectionStrategyConfig,
|
||||
column_span: maxColumns,
|
||||
} satisfies LovelaceStrategySectionConfig,
|
||||
hass
|
||||
);
|
||||
if (!generatedFavoritesSection.disabled) {
|
||||
favoritesSection = {
|
||||
...generatedFavoritesSection,
|
||||
column_span: maxColumns,
|
||||
};
|
||||
}
|
||||
favoritesSection = {
|
||||
strategy: {
|
||||
type: "common-controls",
|
||||
limit: maxCommonControls,
|
||||
include_entities: favoriteEntities,
|
||||
hide_empty: true,
|
||||
heading: favoritesHeadingCard,
|
||||
} satisfies CommonControlsSectionStrategyConfig,
|
||||
column_span: maxColumns,
|
||||
} satisfies LovelaceStrategySectionConfig;
|
||||
} else if (favoriteEntities.length > 0) {
|
||||
favoritesSection = {
|
||||
type: "grid",
|
||||
@@ -345,17 +305,6 @@ export class HomeOverviewViewStrategy extends ReactiveElement {
|
||||
hass.panels.maintenance &&
|
||||
findEntities(allEntities, maintenanceFilters).length > 0;
|
||||
|
||||
const alertEntities = config.alert_entities ?? [];
|
||||
const alertSeverityEntities = filterSecurityAlertEntities(
|
||||
alertEntities,
|
||||
hass,
|
||||
"alert"
|
||||
);
|
||||
const alertActiveConditions = alertSeverityEntities.map((alertEntity) => ({
|
||||
condition: "and" as const,
|
||||
conditions: computeDefaultSecurityAlertVisibility(alertEntity.entity),
|
||||
}));
|
||||
|
||||
const weatherFilter = generateEntityFilter(hass, {
|
||||
domain: "weather",
|
||||
entity_category: "none",
|
||||
@@ -401,23 +350,17 @@ export class HomeOverviewViewStrategy extends ReactiveElement {
|
||||
},
|
||||
} satisfies HomeSummaryCard)
|
||||
: undefined,
|
||||
security: () => {
|
||||
if (!hasSecurity) {
|
||||
return undefined;
|
||||
}
|
||||
const card: HomeSummaryCard = {
|
||||
type: "home-summary",
|
||||
summary: "security",
|
||||
tap_action: {
|
||||
action: "navigate",
|
||||
navigation_path: "/security?historyBack=1",
|
||||
},
|
||||
};
|
||||
if (alertEntities.length) {
|
||||
card.alert_entities = alertEntities;
|
||||
}
|
||||
return card;
|
||||
},
|
||||
security: () =>
|
||||
hasSecurity
|
||||
? ({
|
||||
type: "home-summary",
|
||||
summary: "security",
|
||||
tap_action: {
|
||||
action: "navigate",
|
||||
navigation_path: "/security?historyBack=1",
|
||||
},
|
||||
} satisfies HomeSummaryCard)
|
||||
: undefined,
|
||||
media_players: () =>
|
||||
hasMediaPlayers
|
||||
? ({
|
||||
@@ -511,10 +454,6 @@ export class HomeOverviewViewStrategy extends ReactiveElement {
|
||||
}
|
||||
}
|
||||
|
||||
const hasVisibleSummaryCards = summaryCards.some(
|
||||
(card) => !("hide_empty" in card && card.hide_empty)
|
||||
);
|
||||
|
||||
// Build summary cards for sidebar (full width: columns 12)
|
||||
const sidebarSummaryCards = summaryCards.map((card) => ({
|
||||
...card,
|
||||
@@ -535,33 +474,6 @@ export class HomeOverviewViewStrategy extends ReactiveElement {
|
||||
heading_style: "title",
|
||||
};
|
||||
|
||||
const alertsCard: HomeSummaryCard | undefined = alertSeverityEntities.length
|
||||
? ({
|
||||
type: "home-summary",
|
||||
summary: "alerts",
|
||||
alert_entities: alertSeverityEntities,
|
||||
tap_action: {
|
||||
action: "navigate",
|
||||
navigation_path: "/security?historyBack=1",
|
||||
},
|
||||
visibility: [
|
||||
{
|
||||
condition: "or",
|
||||
conditions: alertActiveConditions,
|
||||
},
|
||||
],
|
||||
} satisfies HomeSummaryCard)
|
||||
: undefined;
|
||||
|
||||
const mobileAlertsSection: LovelaceSectionConfig | undefined = alertsCard
|
||||
? {
|
||||
type: "grid",
|
||||
column_span: maxColumns,
|
||||
visibility: [SMALL_SCREEN_CONDITION],
|
||||
cards: [{ ...alertsCard, grid_options: { columns: 6 } }],
|
||||
}
|
||||
: undefined;
|
||||
|
||||
// Mobile summary section (visible on small screens only)
|
||||
const mobileSummarySection: LovelaceSectionConfig | undefined =
|
||||
mobileSummaryCards.length > 0
|
||||
@@ -575,7 +487,7 @@ export class HomeOverviewViewStrategy extends ReactiveElement {
|
||||
|
||||
// Sidebar section
|
||||
const sidebarSection: LovelaceSectionConfig | undefined =
|
||||
sidebarSummaryCards.length > 0 || alertsCard
|
||||
sidebarSummaryCards.length > 0
|
||||
? {
|
||||
type: "grid",
|
||||
cards: [
|
||||
@@ -583,103 +495,67 @@ export class HomeOverviewViewStrategy extends ReactiveElement {
|
||||
...summaryHeadingCard,
|
||||
grid_options: { rows: "auto" }, // Compact style
|
||||
},
|
||||
...(alertsCard
|
||||
? [{ ...alertsCard, grid_options: { columns: 12 } }]
|
||||
: []),
|
||||
...sidebarSummaryCards,
|
||||
],
|
||||
}
|
||||
: undefined;
|
||||
|
||||
const emptyStateCard = {
|
||||
type: "empty-state",
|
||||
icon: "mdi:home-assistant",
|
||||
content_only: true,
|
||||
title: hass.localize("ui.panel.lovelace.strategy.home.welcome_title"),
|
||||
content: hass.localize("ui.panel.lovelace.strategy.home.welcome_content"),
|
||||
...(config.home_panel && hass.user?.is_admin
|
||||
? {
|
||||
buttons: [
|
||||
{
|
||||
icon: "mdi:plus",
|
||||
text: hass.localize(
|
||||
"ui.panel.lovelace.strategy.home.welcome_add_device"
|
||||
),
|
||||
appearance: "filled" as const,
|
||||
variant: "brand" as const,
|
||||
tap_action: {
|
||||
action: "fire-dom-event" as const,
|
||||
home_panel: {
|
||||
type: "add_integration",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
icon: "mdi:home-edit",
|
||||
text: hass.localize(
|
||||
"ui.panel.lovelace.strategy.home.welcome_edit_areas"
|
||||
),
|
||||
appearance: "plain" as const,
|
||||
variant: "brand" as const,
|
||||
tap_action: {
|
||||
action: "navigate" as const,
|
||||
navigation_path: "/config/areas/dashboard",
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
: {}),
|
||||
} as EmptyStateCardConfig;
|
||||
|
||||
// No sections, show empty state
|
||||
if (
|
||||
floorsSections.length === 0 &&
|
||||
!alertsCard &&
|
||||
!favoritesSection &&
|
||||
!hasVisibleSummaryCards
|
||||
) {
|
||||
if (floorsSections.length === 0) {
|
||||
return {
|
||||
type: "panel",
|
||||
cards: [emptyStateCard],
|
||||
cards: [
|
||||
{
|
||||
type: "empty-state",
|
||||
icon: "mdi:home-assistant",
|
||||
content_only: true,
|
||||
title: hass.localize(
|
||||
"ui.panel.lovelace.strategy.home.welcome_title"
|
||||
),
|
||||
content: hass.localize(
|
||||
"ui.panel.lovelace.strategy.home.welcome_content"
|
||||
),
|
||||
...(config.home_panel && hass.user?.is_admin
|
||||
? {
|
||||
buttons: [
|
||||
{
|
||||
icon: "mdi:plus",
|
||||
text: hass.localize(
|
||||
"ui.panel.lovelace.strategy.home.welcome_add_device"
|
||||
),
|
||||
appearance: "filled",
|
||||
variant: "brand",
|
||||
tap_action: {
|
||||
action: "fire-dom-event",
|
||||
home_panel: {
|
||||
type: "add_integration",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
icon: "mdi:home-edit",
|
||||
text: hass.localize(
|
||||
"ui.panel.lovelace.strategy.home.welcome_edit_areas"
|
||||
),
|
||||
appearance: "plain",
|
||||
variant: "brand",
|
||||
tap_action: {
|
||||
action: "navigate",
|
||||
navigation_path: "/config/areas/dashboard",
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
: {}),
|
||||
} as EmptyStateCardConfig,
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
const emptyStateSection: LovelaceSectionConfig | undefined =
|
||||
floorsSections.length === 0 &&
|
||||
!favoritesSection &&
|
||||
!hasVisibleSummaryCards &&
|
||||
alertSeverityEntities.length
|
||||
? {
|
||||
type: "grid",
|
||||
column_span: maxColumns,
|
||||
cards: [
|
||||
{
|
||||
type: "conditional",
|
||||
conditions: [
|
||||
{
|
||||
condition: "not",
|
||||
conditions: [
|
||||
{
|
||||
condition: "or",
|
||||
conditions: alertActiveConditions,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
card: emptyStateCard,
|
||||
} satisfies ConditionalCardConfig,
|
||||
],
|
||||
}
|
||||
: undefined;
|
||||
|
||||
const sections = (
|
||||
[
|
||||
emptyStateSection,
|
||||
mobileAlertsSection,
|
||||
favoritesSection,
|
||||
mobileSummarySection,
|
||||
...floorsSections,
|
||||
] satisfies (LovelaceSectionRawConfig | undefined)[]
|
||||
[favoritesSection, mobileSummarySection, ...floorsSections] satisfies (
|
||||
LovelaceSectionRawConfig | undefined
|
||||
)[]
|
||||
).filter(Boolean) as LovelaceSectionRawConfig[];
|
||||
|
||||
return {
|
||||
|
||||
@@ -4,11 +4,9 @@ import { customElement, property, state } from "lit/decorators";
|
||||
import type { HomeAssistant } from "../../../types";
|
||||
import type { LovelaceViewBackgroundConfig } from "../../../data/lovelace/config/view";
|
||||
import {
|
||||
getImageEntityIdFromMediaSourceContentId,
|
||||
isMediaSourceContentId,
|
||||
resolveMediaSourceWithCache,
|
||||
} from "../../../data/media_source";
|
||||
import { computeImageUrl } from "../../../data/image";
|
||||
|
||||
@customElement("hui-view-background")
|
||||
export class HUIViewBackground extends LitElement {
|
||||
@@ -19,8 +17,6 @@ export class HUIViewBackground extends LitElement {
|
||||
|
||||
@state({ attribute: false }) resolvedImage?: string;
|
||||
|
||||
private _entityId: string | undefined = undefined;
|
||||
|
||||
protected render() {
|
||||
return nothing;
|
||||
}
|
||||
@@ -41,41 +37,17 @@ export class HUIViewBackground extends LitElement {
|
||||
const backgroundImage = this._getBackgroundImage(this.background);
|
||||
|
||||
if (!backgroundImage || !isMediaSourceContentId(backgroundImage)) {
|
||||
this._entityId = undefined;
|
||||
this.resolvedImage = undefined;
|
||||
return;
|
||||
}
|
||||
|
||||
let resolvedUrl: string | undefined;
|
||||
this._entityId = getImageEntityIdFromMediaSourceContentId(backgroundImage);
|
||||
if (this._entityId) {
|
||||
const stateObj = this.hass.states[this._entityId];
|
||||
if (stateObj) {
|
||||
const url = computeImageUrl(stateObj);
|
||||
if (url) {
|
||||
const image = new Image();
|
||||
image.src = this.hass.hassUrl(url);
|
||||
try {
|
||||
await image.decode();
|
||||
const newStateObj = this.hass.states[this._entityId];
|
||||
// Discard if stale
|
||||
if (url !== computeImageUrl(newStateObj)) {
|
||||
return;
|
||||
}
|
||||
resolvedUrl = url;
|
||||
} catch {
|
||||
resolvedUrl = undefined;
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
resolvedUrl = (
|
||||
await resolveMediaSourceWithCache(this.hass, backgroundImage)
|
||||
).url;
|
||||
} catch {
|
||||
resolvedUrl = undefined;
|
||||
}
|
||||
try {
|
||||
resolvedUrl = (
|
||||
await resolveMediaSourceWithCache(this.hass, backgroundImage)
|
||||
).url;
|
||||
} catch {
|
||||
resolvedUrl = undefined;
|
||||
}
|
||||
// Discard if the background changed while resolving
|
||||
if (this._getBackgroundImage(this.background) === backgroundImage) {
|
||||
@@ -159,13 +131,6 @@ export class HUIViewBackground extends LitElement {
|
||||
) {
|
||||
applyTheme = true;
|
||||
}
|
||||
if (
|
||||
this._entityId &&
|
||||
this.hass.states[this._entityId] !== oldHass?.states[this._entityId]
|
||||
) {
|
||||
this._fetchMedia();
|
||||
applyTheme = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (changedProperties.has("background")) {
|
||||
|
||||
@@ -346,9 +346,6 @@ export class HuiViewHeader extends LitElement {
|
||||
width: 100%;
|
||||
max-width: 700px;
|
||||
display: flex;
|
||||
min-height: calc(
|
||||
var(--ha-font-size-xl) * var(--ha-line-height-condensed) + 4px
|
||||
);
|
||||
}
|
||||
|
||||
.heading > * {
|
||||
@@ -356,10 +353,6 @@ export class HuiViewHeader extends LitElement {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.container:not(.edit-mode) .heading:has(> *[hidden]) {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.badges {
|
||||
position: relative;
|
||||
flex: 1;
|
||||
|
||||
@@ -391,19 +391,10 @@ export const getMyRedirects = (): Redirects => ({
|
||||
component: "hassio",
|
||||
redirect: "/config/apps/available",
|
||||
},
|
||||
supervisor_apps: {
|
||||
component: "hassio",
|
||||
redirect: "/config/apps",
|
||||
},
|
||||
supervisor_addons: {
|
||||
component: "hassio",
|
||||
redirect: "/config/apps",
|
||||
},
|
||||
supervisor: {
|
||||
// Supervisor panel was removed in 2026.2, fallback to apps
|
||||
component: "hassio",
|
||||
redirect: "/config/apps",
|
||||
},
|
||||
supervisor_app: {
|
||||
component: "hassio",
|
||||
redirect: "/config/app",
|
||||
@@ -440,6 +431,9 @@ export const getMyRedirects = (): Redirects => ({
|
||||
category: "string?",
|
||||
},
|
||||
},
|
||||
lights: {
|
||||
redirect: "/lights",
|
||||
},
|
||||
});
|
||||
|
||||
const getRedirect = (path: string): Redirect | undefined =>
|
||||
|
||||
@@ -4,12 +4,8 @@ import type {
|
||||
SecurityAlertEntityConfig,
|
||||
SecurityAlertSeverity,
|
||||
} from "../../../data/frontend";
|
||||
import type { HomeAssistant } from "../../../types";
|
||||
import type { StateCondition } from "../../lovelace/common/validate-condition";
|
||||
import type { AlertCardConfig } from "../../lovelace/cards/types";
|
||||
import {
|
||||
checkConditionsMet,
|
||||
type StateCondition,
|
||||
} from "../../lovelace/common/validate-condition";
|
||||
|
||||
const DANGER_BINARY_SENSOR_DEVICE_CLASSES = [
|
||||
"carbon_monoxide",
|
||||
@@ -78,36 +74,12 @@ export const computeDefaultSecurityAlertVisibility = (
|
||||
return [condition];
|
||||
};
|
||||
|
||||
export const resolveSecurityAlertSeverity = (
|
||||
alertEntity: SecurityAlertEntityConfig,
|
||||
stateObj?: HassEntity
|
||||
): SecurityAlertSeverity =>
|
||||
alertEntity.severity ?? computeDefaultSecurityAlertSeverity(stateObj);
|
||||
|
||||
export const isSecurityAlertActive = (
|
||||
hass: HomeAssistant,
|
||||
entityId: string
|
||||
): boolean =>
|
||||
checkConditionsMet(computeDefaultSecurityAlertVisibility(entityId), hass, {});
|
||||
|
||||
export const filterSecurityAlertEntities = (
|
||||
alertEntities: SecurityAlertEntityConfig[],
|
||||
hass: HomeAssistant,
|
||||
severity: SecurityAlertSeverity
|
||||
): SecurityAlertEntityConfig[] =>
|
||||
alertEntities.filter(
|
||||
(alertEntity) =>
|
||||
resolveSecurityAlertSeverity(
|
||||
alertEntity,
|
||||
hass.states[alertEntity.entity]
|
||||
) === severity
|
||||
);
|
||||
|
||||
export const computeSecurityAlertCardConfig = (
|
||||
stateObj: HassEntity | undefined,
|
||||
alertEntity: SecurityAlertEntityConfig
|
||||
): AlertCardConfig => {
|
||||
const severity = resolveSecurityAlertSeverity(alertEntity, stateObj);
|
||||
const severity =
|
||||
alertEntity.severity ?? computeDefaultSecurityAlertSeverity(stateObj);
|
||||
return {
|
||||
type: "alert",
|
||||
entity: alertEntity.entity,
|
||||
|
||||
@@ -18,7 +18,6 @@ import {
|
||||
subscribeFrontendUserData,
|
||||
} from "../data/frontend";
|
||||
import { forwardHaptic } from "../data/haptics";
|
||||
import { NO_KIOSK_ELEMENTS_HIDDEN } from "../data/kiosk_mode";
|
||||
import { serviceCallWillDisconnect } from "../data/service";
|
||||
import {
|
||||
DateFormat,
|
||||
@@ -86,7 +85,6 @@ export const connectionMixin = <T extends Constructor<HassBaseEl>>(
|
||||
localize: () => "",
|
||||
translationMetadata,
|
||||
kioskMode: false,
|
||||
kioskElementsHidden: NO_KIOSK_ELEMENTS_HIDDEN,
|
||||
dockedSidebar: "docked",
|
||||
vibrate: true,
|
||||
debugConnection: __DEV__,
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user