mirror of
https://github.com/home-assistant/frontend.git
synced 2026-09-17 23:50:41 +00:00
Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3c08fdb623 | ||
|
|
434b2a1db2 |
@@ -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
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
diff --git a/round-slider.cjs b/round-slider.cjs
|
||||
index f5abedb900513f18ab914f94900d0cb15b87b73b..0a8fdc22d2362ef67d0d25e72a69119001f06096 100644
|
||||
--- a/round-slider.cjs
|
||||
+++ b/round-slider.cjs
|
||||
@@ -78,6 +78,7 @@ var nt;null===(nt=window.HTMLSlotElement)||void 0===nt||nt.prototype.assignedEle
|
||||
viewBox="${-t.left} ${-t.up} ${t.width} ${t.height}"
|
||||
style="margin: ${this.handleSize*this.handleZoom}px;"
|
||||
?disabled=${this.disabled}
|
||||
+ ?readonly=${!this._showHandle}
|
||||
focusable="false"
|
||||
>
|
||||
<g class="slider">
|
||||
@@ -160,6 +161,16 @@ var nt;null===(nt=window.HTMLSlotElement)||void 0===nt||nt.prototype.assignedEle
|
||||
stroke-linecap: round;
|
||||
cursor: var(--round-slider-handle-cursor, pointer);
|
||||
}
|
||||
+ .shadowpath {
|
||||
+ cursor: var(
|
||||
+ --round-slider-path-cursor,
|
||||
+ var(--round-slider-handle-cursor, pointer)
|
||||
+ );
|
||||
+ }
|
||||
+ svg[disabled] .shadowpath,
|
||||
+ svg[readonly] .shadowpath {
|
||||
+ cursor: default;
|
||||
+ }
|
||||
g.low.handle {
|
||||
stroke: var(--round-slider-low-handle-color);
|
||||
}
|
||||
diff --git a/round-slider.iife.js b/round-slider.iife.js
|
||||
index 8d9e578c5891ebfbdf6cff6bd9f7ea965bb6ab0b..d4b6c33d8620c4316b8890dfa4eb74cd7fb0933c 100644
|
||||
--- a/round-slider.iife.js
|
||||
+++ b/round-slider.iife.js
|
||||
@@ -78,6 +78,7 @@ var ht;null===(ht=window.HTMLSlotElement)||void 0===ht||ht.prototype.assignedEle
|
||||
viewBox="${-t.left} ${-t.up} ${t.width} ${t.height}"
|
||||
style="margin: ${this.handleSize*this.handleZoom}px;"
|
||||
?disabled=${this.disabled}
|
||||
+ ?readonly=${!this._showHandle}
|
||||
focusable="false"
|
||||
>
|
||||
<g class="slider">
|
||||
@@ -160,6 +161,16 @@ var ht;null===(ht=window.HTMLSlotElement)||void 0===ht||ht.prototype.assignedEle
|
||||
stroke-linecap: round;
|
||||
cursor: var(--round-slider-handle-cursor, pointer);
|
||||
}
|
||||
+ .shadowpath {
|
||||
+ cursor: var(
|
||||
+ --round-slider-path-cursor,
|
||||
+ var(--round-slider-handle-cursor, pointer)
|
||||
+ );
|
||||
+ }
|
||||
+ svg[disabled] .shadowpath,
|
||||
+ svg[readonly] .shadowpath {
|
||||
+ cursor: default;
|
||||
+ }
|
||||
g.low.handle {
|
||||
stroke: var(--round-slider-low-handle-color);
|
||||
}
|
||||
diff --git a/round-slider.js b/round-slider.js
|
||||
index 9bfdf73cc3913e10133255b2d346ba36c11d8dc9..6c4b14674d52d28a6ab8a0c38b1cb1768eeabbba 100644
|
||||
--- a/round-slider.js
|
||||
+++ b/round-slider.js
|
||||
@@ -363,6 +363,7 @@ export class RoundSlider extends LitElement {
|
||||
viewBox="${-view.left} ${-view.up} ${view.width} ${view.height}"
|
||||
style="margin: ${this.handleSize * this.handleZoom}px;"
|
||||
?disabled=${this.disabled}
|
||||
+ ?readonly=${!this._showHandle}
|
||||
focusable="false"
|
||||
>
|
||||
<g class="slider">
|
||||
@@ -456,6 +457,16 @@ export class RoundSlider extends LitElement {
|
||||
stroke-linecap: round;
|
||||
cursor: var(--round-slider-handle-cursor, pointer);
|
||||
}
|
||||
+ .shadowpath {
|
||||
+ cursor: var(
|
||||
+ --round-slider-path-cursor,
|
||||
+ var(--round-slider-handle-cursor, pointer)
|
||||
+ );
|
||||
+ }
|
||||
+ svg[disabled] .shadowpath,
|
||||
+ svg[readonly] .shadowpath {
|
||||
+ cursor: default;
|
||||
+ }
|
||||
g.low.handle {
|
||||
stroke: var(--round-slider-low-handle-color);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -9,7 +9,6 @@ import { ifDefined } from "lit/directives/if-defined";
|
||||
import { applyThemesOnElement } from "../../src/common/dom/apply_themes_on_element";
|
||||
import { dynamicElement } from "../../src/common/dom/dynamic-element-directive";
|
||||
import type { HASSDomEvent } from "../../src/common/dom/fire_event";
|
||||
import { computeEntityNameDisplayWithoutContext } from "../../src/common/entity/compute_entity_name_display";
|
||||
import { setDirectionStyles } from "../../src/common/util/compute_rtl";
|
||||
import "../../src/components/ha-button";
|
||||
import "../../src/components/ha-drawer";
|
||||
@@ -683,7 +682,10 @@ class HaGallery extends LitElement {
|
||||
formatEntityAttributeName: (_stateObj, attribute) => attribute,
|
||||
formatEntityAttributeValue: (stateObj, attribute, value) =>
|
||||
value != null ? value : (stateObj.attributes[attribute] ?? ""),
|
||||
formatEntityName: computeEntityNameDisplayWithoutContext,
|
||||
formatEntityName: (stateObj, type) =>
|
||||
typeof type === "string"
|
||||
? type
|
||||
: (stateObj.attributes.friendly_name ?? stateObj.entity_id),
|
||||
} as unknown as HomeAssistant;
|
||||
}
|
||||
|
||||
|
||||
@@ -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",
|
||||
@@ -385,13 +320,6 @@ const SCHEMAS: {
|
||||
},
|
||||
duration: { name: "Duration", selector: { duration: {} } },
|
||||
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 +504,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 +605,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 +776,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,
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
@@ -12,10 +12,7 @@ import "../../components/demo-cards";
|
||||
import { mockIcons } from "../../../../demo/src/stubs/icons";
|
||||
import { ClimateEntityFeature } from "../../../../src/data/climate";
|
||||
import { FanEntityFeature } from "../../../../src/data/fan";
|
||||
import type {
|
||||
GridCardConfig,
|
||||
TileCardConfig,
|
||||
} from "../../../../src/panels/lovelace/cards/types";
|
||||
import type { TileCardConfig } from "../../../../src/panels/lovelace/cards/types";
|
||||
|
||||
const ENTITIES = [
|
||||
{
|
||||
@@ -166,39 +163,6 @@ const ENTITIES = [
|
||||
FanEntityFeature.OSCILLATE,
|
||||
},
|
||||
},
|
||||
{
|
||||
entity_id: "fan.three_speed_fan",
|
||||
state: "on",
|
||||
attributes: {
|
||||
friendly_name: "Desk fan",
|
||||
device_class: "fan",
|
||||
percentage: 67,
|
||||
percentage_step: 100 / 3,
|
||||
supported_features: FanEntityFeature.SET_SPEED,
|
||||
},
|
||||
},
|
||||
{
|
||||
entity_id: "fan.four_speed_fan",
|
||||
state: "on",
|
||||
attributes: {
|
||||
friendly_name: "Bedroom fan",
|
||||
device_class: "fan",
|
||||
percentage: 50,
|
||||
percentage_step: 25,
|
||||
supported_features: FanEntityFeature.SET_SPEED,
|
||||
},
|
||||
},
|
||||
{
|
||||
entity_id: "fan.five_speed_fan",
|
||||
state: "on",
|
||||
attributes: {
|
||||
friendly_name: "Attic fan",
|
||||
device_class: "fan",
|
||||
percentage: 80,
|
||||
percentage_step: 20,
|
||||
supported_features: FanEntityFeature.SET_SPEED,
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
const CONFIGS = [
|
||||
@@ -368,50 +332,6 @@ const CONFIGS = [
|
||||
features: [{ type: "fan-speed" }],
|
||||
},
|
||||
},
|
||||
{
|
||||
heading: "Fan speed feature (3 speeds)",
|
||||
config: {
|
||||
type: "tile",
|
||||
entity: "fan.three_speed_fan",
|
||||
features: [{ type: "fan-speed" }],
|
||||
},
|
||||
},
|
||||
{
|
||||
heading: "Fan speed feature (4 speeds)",
|
||||
config: {
|
||||
type: "tile",
|
||||
entity: "fan.four_speed_fan",
|
||||
features: [{ type: "fan-speed" }],
|
||||
},
|
||||
},
|
||||
{
|
||||
heading: "Fan speed feature (5 speeds)",
|
||||
config: {
|
||||
type: "tile",
|
||||
entity: "fan.five_speed_fan",
|
||||
features: [{ type: "fan-speed" }],
|
||||
},
|
||||
},
|
||||
{
|
||||
heading: "Fan speed feature (half width)",
|
||||
config: {
|
||||
type: "grid",
|
||||
columns: 2,
|
||||
square: false,
|
||||
cards: [
|
||||
{
|
||||
type: "tile",
|
||||
entity: "fan.four_speed_fan",
|
||||
features: [{ type: "fan-speed" }],
|
||||
},
|
||||
{
|
||||
type: "tile",
|
||||
entity: "fan.five_speed_fan",
|
||||
features: [{ type: "fan-speed" }],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
heading: "Fan oscillate feature",
|
||||
config: {
|
||||
@@ -498,7 +418,7 @@ const CONFIGS = [
|
||||
],
|
||||
},
|
||||
},
|
||||
] satisfies DemoCardConfig<TileCardConfig | GridCardConfig>[];
|
||||
] satisfies DemoCardConfig<TileCardConfig>[];
|
||||
|
||||
@customElement("demo-lovelace-tile-card")
|
||||
class DemoTile extends LitElement {
|
||||
|
||||
@@ -21,61 +21,6 @@ const ENTITIES = [
|
||||
FanEntityFeature.SET_SPEED,
|
||||
},
|
||||
},
|
||||
{
|
||||
entity_id: "fan.one_speed_fan",
|
||||
state: "on",
|
||||
attributes: {
|
||||
friendly_name: "One speed fan",
|
||||
device_class: "fan",
|
||||
percentage: 100,
|
||||
percentage_step: 100,
|
||||
supported_features: FanEntityFeature.SET_SPEED,
|
||||
},
|
||||
},
|
||||
{
|
||||
entity_id: "fan.two_speed_fan",
|
||||
state: "on",
|
||||
attributes: {
|
||||
friendly_name: "Two speed fan",
|
||||
device_class: "fan",
|
||||
percentage: 50,
|
||||
percentage_step: 50,
|
||||
supported_features: FanEntityFeature.SET_SPEED,
|
||||
},
|
||||
},
|
||||
{
|
||||
entity_id: "fan.three_speed_fan",
|
||||
state: "on",
|
||||
attributes: {
|
||||
friendly_name: "Three speed fan",
|
||||
device_class: "fan",
|
||||
percentage: 67,
|
||||
percentage_step: 100 / 3,
|
||||
supported_features: FanEntityFeature.SET_SPEED,
|
||||
},
|
||||
},
|
||||
{
|
||||
entity_id: "fan.four_speed_fan",
|
||||
state: "on",
|
||||
attributes: {
|
||||
friendly_name: "Four speed fan",
|
||||
device_class: "fan",
|
||||
percentage: 50,
|
||||
percentage_step: 25,
|
||||
supported_features: FanEntityFeature.SET_SPEED,
|
||||
},
|
||||
},
|
||||
{
|
||||
entity_id: "fan.five_speed_fan",
|
||||
state: "on",
|
||||
attributes: {
|
||||
friendly_name: "Five speed fan",
|
||||
device_class: "fan",
|
||||
percentage: 80,
|
||||
percentage_step: 20,
|
||||
supported_features: FanEntityFeature.SET_SPEED,
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
@customElement("demo-more-info-fan")
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
import { IntersectionController } from "@lit-labs/observers/intersection-controller.js";
|
||||
import { mdiArrowCollapseDown, mdiDownload } from "@mdi/js";
|
||||
import { LitElement, type PropertyValues, css, html, nothing } from "lit";
|
||||
import { customElement, query, state } from "lit/decorators";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import { classMap } from "lit/directives/class-map";
|
||||
import { consumeLocalize } from "../../../src/common/decorators/consume-context-entry";
|
||||
import { fireEvent } from "../../../src/common/dom/fire_event";
|
||||
import type {
|
||||
LandingPageKeys,
|
||||
@@ -35,6 +34,9 @@ const SCHEDULE_FETCH_OBSERVER_LOGS = 5;
|
||||
|
||||
@customElement("landing-page-logs")
|
||||
class LandingPageLogs extends LitElement {
|
||||
@property({ attribute: false })
|
||||
public localize!: LocalizeFunc<LandingPageKeys>;
|
||||
|
||||
@query("ha-ansi-to-html") private _ansiToHtmlElement?: HaAnsiToHtml;
|
||||
|
||||
@query(".logs") private _logElement?: HTMLElement;
|
||||
@@ -42,10 +44,6 @@ class LandingPageLogs extends LitElement {
|
||||
@query("#scroll-bottom-marker")
|
||||
private _scrollBottomMarkerElement?: HTMLElement;
|
||||
|
||||
@state()
|
||||
@consumeLocalize()
|
||||
private _localize!: LocalizeFunc<LandingPageKeys>;
|
||||
|
||||
@state() private _show = false;
|
||||
|
||||
@state() private _scrolledToBottomController =
|
||||
@@ -65,12 +63,12 @@ class LandingPageLogs extends LitElement {
|
||||
return html`
|
||||
<div class="actions">
|
||||
<ha-button appearance="plain" @click=${this._toggleLogDetails}>
|
||||
${this._localize(this._show ? "hide_details" : "show_details")}
|
||||
${this.localize(this._show ? "hide_details" : "show_details")}
|
||||
</ha-button>
|
||||
${
|
||||
this._show
|
||||
? html`<ha-icon-button
|
||||
.label=${this._localize("logs.download_logs")}
|
||||
.label=${this.localize("logs.download_logs")}
|
||||
.path=${mdiDownload}
|
||||
@click=${this._downloadLogs}
|
||||
></ha-icon-button>`
|
||||
@@ -82,14 +80,14 @@ class LandingPageLogs extends LitElement {
|
||||
? html`
|
||||
<ha-alert
|
||||
alert-type="error"
|
||||
.title=${this._localize("logs.fetch_error")}
|
||||
.title=${this.localize("logs.fetch_error")}
|
||||
>
|
||||
<ha-button
|
||||
size="small"
|
||||
variant="danger"
|
||||
@click=${this._startLogStream}
|
||||
>
|
||||
${this._localize("logs.retry")}
|
||||
${this.localize("logs.retry")}
|
||||
</ha-button>
|
||||
</ha-alert>
|
||||
`
|
||||
@@ -117,7 +115,7 @@ class LandingPageLogs extends LitElement {
|
||||
@click=${this._scrollToBottom}
|
||||
>
|
||||
<ha-svg-icon .path=${mdiArrowCollapseDown} slot="start"></ha-svg-icon>
|
||||
${this._localize("logs.scroll_down_button")}
|
||||
${this.localize("logs.scroll_down_button")}
|
||||
<ha-svg-icon .path=${mdiArrowCollapseDown} slot="end"></ha-svg-icon>
|
||||
</ha-button>
|
||||
`;
|
||||
|
||||
@@ -1,37 +1,35 @@
|
||||
import { LitElement, css, html, nothing, type CSSResultGroup } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { consumeLocalize } from "../../../src/common/decorators/consume-context-entry";
|
||||
import { fireEvent } from "../../../src/common/dom/fire_event";
|
||||
import { type CSSResultGroup, LitElement, css, html, nothing } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import type {
|
||||
LandingPageKeys,
|
||||
LocalizeFunc,
|
||||
} from "../../../src/common/translations/localize";
|
||||
import "../../../src/components/ha-alert";
|
||||
import "../../../src/components/ha-button";
|
||||
import type { NetworkInterface } from "../../../src/data/hassio/network";
|
||||
import { showAlertDialog } from "../../../src/dialogs/generic/show-dialog-box";
|
||||
import "../../../src/components/ha-alert";
|
||||
import {
|
||||
ALTERNATIVE_DNS_SERVERS,
|
||||
setSupervisorNetworkDns,
|
||||
type NetworkInfo,
|
||||
} from "../data/supervisor";
|
||||
import { showAlertDialog } from "../../../src/dialogs/generic/show-dialog-box";
|
||||
import type { NetworkInterface } from "../../../src/data/hassio/network";
|
||||
import { fireEvent } from "../../../src/common/dom/fire_event";
|
||||
|
||||
@customElement("landing-page-network")
|
||||
class LandingPageNetwork extends LitElement {
|
||||
@property({ attribute: false })
|
||||
public localize!: LocalizeFunc<LandingPageKeys>;
|
||||
|
||||
@property({ attribute: false }) public networkInfo?: NetworkInfo;
|
||||
|
||||
@property({ type: Boolean }) public error = false;
|
||||
|
||||
@state()
|
||||
@consumeLocalize()
|
||||
private _localize!: LocalizeFunc<LandingPageKeys>;
|
||||
|
||||
protected render() {
|
||||
if (this.error) {
|
||||
return html`
|
||||
<ha-alert alert-type="error">
|
||||
<p>${this._localize("network_issue.error_get_network_info")}</p>
|
||||
<p>${this.localize("network_issue.error_get_network_info")}</p>
|
||||
</ha-alert>
|
||||
`;
|
||||
}
|
||||
@@ -49,21 +47,19 @@ class LandingPageNetwork extends LitElement {
|
||||
return html`
|
||||
<ha-alert
|
||||
alert-type="warning"
|
||||
.title=${this._localize("network_issue.title")}
|
||||
.title=${this.localize("network_issue.title")}
|
||||
>
|
||||
<p>
|
||||
${this._localize("network_issue.description", {
|
||||
${this.localize("network_issue.description", {
|
||||
dns: dnsPrimaryInterfaceNameservers || "?",
|
||||
})}
|
||||
</p>
|
||||
<p>${this._localize("network_issue.resolve_different")}</p>
|
||||
<p>${this.localize("network_issue.resolve_different")}</p>
|
||||
${
|
||||
!dnsPrimaryInterfaceNameservers
|
||||
? html`
|
||||
<p>
|
||||
<b
|
||||
>${this._localize("network_issue.no_primary_interface")}
|
||||
</b>
|
||||
<b>${this.localize("network_issue.no_primary_interface")} </b>
|
||||
</p>
|
||||
`
|
||||
: nothing
|
||||
@@ -76,7 +72,7 @@ class LandingPageNetwork extends LitElement {
|
||||
.index=${key}
|
||||
.disabled=${!dnsPrimaryInterfaceNameservers}
|
||||
@click=${this._setDns}
|
||||
>${this._localize(translationKey)}</ha-button
|
||||
>${this.localize(translationKey)}</ha-button
|
||||
>`
|
||||
)}
|
||||
</div>
|
||||
@@ -122,12 +118,12 @@ class LandingPageNetwork extends LitElement {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error(err);
|
||||
showAlertDialog(this, {
|
||||
title: this._localize("network_issue.failed"),
|
||||
title: this.localize("network_issue.failed"),
|
||||
warning: true,
|
||||
text: `${this._localize(
|
||||
text: `${this.localize(
|
||||
"network_issue.set_dns_failed"
|
||||
)}${err?.message ? ` ${this._localize("network_issue.error")}: ${err.message}` : ""}`,
|
||||
confirmText: this._localize("network_issue.close"),
|
||||
)}${err?.message ? ` ${this.localize("network_issue.error")}: ${err.message}` : ""}`,
|
||||
confirmText: this.localize("network_issue.close"),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,7 +9,6 @@ import "../../src/components/ha-spinner";
|
||||
import "../../src/components/ha-svg-icon";
|
||||
import "../../src/components/progress/ha-progress-bar";
|
||||
import { makeDialogManager } from "../../src/dialogs/make-dialog-manager";
|
||||
import { provideLiteI18nMixin } from "../../src/mixins/provide-lite-i18n-mixin";
|
||||
import "../../src/onboarding/onboarding-welcome-links";
|
||||
import { onBoardingStyles } from "../../src/onboarding/styles";
|
||||
import { haStyle } from "../../src/resources/styles";
|
||||
@@ -29,7 +28,7 @@ const SCHEDULE_FETCH_NETWORK_INFO_SECONDS = 5;
|
||||
const SCHEDULE_FETCH_JOBS_INFO_SECONDS = 2;
|
||||
|
||||
@customElement("ha-landing-page")
|
||||
class HaLandingPage extends provideLiteI18nMixin(LandingPageBaseElement) {
|
||||
class HaLandingPage extends LandingPageBaseElement {
|
||||
@property({ attribute: false }) public translationFragment = "landing-page";
|
||||
|
||||
@state() private _supervisorError = false;
|
||||
@@ -83,6 +82,7 @@ class HaLandingPage extends provideLiteI18nMixin(LandingPageBaseElement) {
|
||||
networkIssue || this._networkInfoError
|
||||
? html`
|
||||
<landing-page-network
|
||||
.localize=${this.localize}
|
||||
.networkInfo=${this._networkInfo}
|
||||
.error=${this._networkInfoError}
|
||||
@dns-set=${this._fetchSupervisorInfo}
|
||||
@@ -103,11 +103,13 @@ class HaLandingPage extends provideLiteI18nMixin(LandingPageBaseElement) {
|
||||
: nothing
|
||||
}
|
||||
<landing-page-logs
|
||||
.localize=${this.localize}
|
||||
@landing-page-error=${this._showError}
|
||||
></landing-page-logs>
|
||||
</div>
|
||||
</ha-card>
|
||||
<onboarding-welcome-links
|
||||
.localize=${this.localize}
|
||||
.mobileApp=${this._mobileApp}
|
||||
></onboarding-welcome-links>
|
||||
<div class="footer">
|
||||
|
||||
+18
-18
@@ -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",
|
||||
@@ -52,15 +52,15 @@
|
||||
"@codemirror/view": "6.43.11",
|
||||
"@date-fns/tz": "1.5.0",
|
||||
"@egjs/hammerjs": "2.0.17",
|
||||
"@formatjs/intl-datetimeformat": "7.6.2",
|
||||
"@formatjs/intl-displaynames": "7.3.14",
|
||||
"@formatjs/intl-durationformat": "0.11.0",
|
||||
"@formatjs/intl-getcanonicallocales": "3.2.12",
|
||||
"@formatjs/intl-listformat": "8.3.14",
|
||||
"@formatjs/intl-locale": "5.3.11",
|
||||
"@formatjs/intl-numberformat": "9.4.1",
|
||||
"@formatjs/intl-pluralrules": "6.3.14",
|
||||
"@formatjs/intl-relativetimeformat": "12.3.14",
|
||||
"@formatjs/intl-datetimeformat": "7.6.1",
|
||||
"@formatjs/intl-displaynames": "7.3.13",
|
||||
"@formatjs/intl-durationformat": "0.10.18",
|
||||
"@formatjs/intl-getcanonicallocales": "3.2.11",
|
||||
"@formatjs/intl-listformat": "8.3.13",
|
||||
"@formatjs/intl-locale": "5.3.10",
|
||||
"@formatjs/intl-numberformat": "9.4.0",
|
||||
"@formatjs/intl-pluralrules": "6.3.13",
|
||||
"@formatjs/intl-relativetimeformat": "12.3.13",
|
||||
"@fullcalendar/core": "6.1.21",
|
||||
"@fullcalendar/daygrid": "6.1.21",
|
||||
"@fullcalendar/interaction": "6.1.21",
|
||||
@@ -84,6 +84,7 @@
|
||||
"@mdi/svg": "7.4.47",
|
||||
"@replit/codemirror-indentation-markers": "6.5.3",
|
||||
"@swc/helpers": "0.5.23",
|
||||
"@thomasloven/round-slider": "patch:@thomasloven/round-slider@npm%3A0.6.0#~/.yarn/patches/@thomasloven-round-slider-npm-0.6.0-d7176d9db7.patch",
|
||||
"@tsparticles/engine": "4.4.0",
|
||||
"@tsparticles/preset-links": "4.4.0",
|
||||
"@vibrant/color": "4.0.4",
|
||||
@@ -103,7 +104,7 @@
|
||||
"echarts-extension-chart2music": "0.1.1",
|
||||
"element-internals-polyfill": "3.0.2",
|
||||
"fuse.js": "7.5.0",
|
||||
"hls.js": "1.7.3",
|
||||
"hls.js": "1.7.2",
|
||||
"home-assistant-js-websocket": "9.6.0",
|
||||
"idb-keyval": "6.3.0",
|
||||
"intl-messageformat": "11.2.14",
|
||||
@@ -116,7 +117,6 @@
|
||||
"maplibre-gl": "5.24.0",
|
||||
"marked": "18.0.12",
|
||||
"memoize-one": "6.0.0",
|
||||
"nanoid": "6.0.1",
|
||||
"node-vibrant": "4.0.4",
|
||||
"object-hash": "3.0.0",
|
||||
"punycode": "2.3.1",
|
||||
@@ -139,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 +152,7 @@
|
||||
"@octokit/plugin-retry": "8.1.1",
|
||||
"@octokit/rest": "22.0.1",
|
||||
"@playwright/test": "1.62.1",
|
||||
"@rsdoctor/rspack-plugin": "1.6.4",
|
||||
"@rsdoctor/rspack-plugin": "1.6.3",
|
||||
"@rspack/core": "2.2.3",
|
||||
"@rspack/dev-server": "2.2.1",
|
||||
"@types/babel__plugin-transform-runtime": "7.9.5",
|
||||
@@ -195,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",
|
||||
@@ -214,7 +214,7 @@
|
||||
"ts-lit-plugin": "2.0.2",
|
||||
"typescript": "6.0.3",
|
||||
"typescript-eslint": "8.70.0",
|
||||
"vite": "8.3.0",
|
||||
"vite": "8.2.2",
|
||||
"vite-tsconfig-paths": "6.1.1",
|
||||
"vitest": "5.0.0",
|
||||
"webpack-stats-plugin": "1.1.3",
|
||||
|
||||
+18
-20
@@ -23,7 +23,6 @@ import type {
|
||||
} from "../data/data_entry_flow";
|
||||
import "./ha-auth-form";
|
||||
import type { HaAuthForm } from "./ha-auth-form";
|
||||
import { consumeLocalize } from "../common/decorators/consume-context-entry";
|
||||
|
||||
type State = "loading" | "error" | "step";
|
||||
|
||||
@@ -37,14 +36,12 @@ export class HaAuthFlow extends LitElement {
|
||||
|
||||
@property({ attribute: false }) public oauth2State?: string;
|
||||
|
||||
@property({ attribute: false }) public localize!: LocalizeFunc;
|
||||
|
||||
@property({ attribute: false }) public step?: DataEntryFlowStep;
|
||||
|
||||
@property({ attribute: false }) public initStoreToken = false;
|
||||
|
||||
@state()
|
||||
@consumeLocalize()
|
||||
private _localize!: LocalizeFunc;
|
||||
|
||||
@state() private _storeToken = false;
|
||||
|
||||
@state() private _state: State = "loading";
|
||||
@@ -187,8 +184,8 @@ export class HaAuthFlow extends LitElement {
|
||||
>
|
||||
${
|
||||
this.step.type === "form"
|
||||
? this._localize("ui.panel.page-authorize.form.next")
|
||||
: this._localize("ui.panel.page-authorize.form.start_over")
|
||||
? this.localize("ui.panel.page-authorize.form.next")
|
||||
: this.localize("ui.panel.page-authorize.form.start_over")
|
||||
}
|
||||
</ha-button>
|
||||
</div>
|
||||
@@ -196,20 +193,20 @@ export class HaAuthFlow extends LitElement {
|
||||
case "error":
|
||||
return html`
|
||||
<ha-alert alert-type="error">
|
||||
${this._localize("ui.panel.page-authorize.form.error", {
|
||||
${this.localize("ui.panel.page-authorize.form.error", {
|
||||
error: this._errorMessage,
|
||||
})}
|
||||
</ha-alert>
|
||||
<div class="action">
|
||||
<ha-button @click=${this._startOver}>
|
||||
${this._localize("ui.panel.page-authorize.form.start_over")}
|
||||
${this.localize("ui.panel.page-authorize.form.start_over")}
|
||||
</ha-button>
|
||||
</div>
|
||||
`;
|
||||
case "loading":
|
||||
return html`
|
||||
<ha-alert alert-type="info">
|
||||
${this._localize("ui.panel.page-authorize.form.working")}
|
||||
${this.localize("ui.panel.page-authorize.form.working")}
|
||||
</ha-alert>
|
||||
`;
|
||||
default:
|
||||
@@ -221,8 +218,8 @@ export class HaAuthFlow extends LitElement {
|
||||
switch (step.type) {
|
||||
case "abort":
|
||||
return html`
|
||||
${this._localize("ui.panel.page-authorize.abort_intro")}:
|
||||
${this._localize(
|
||||
${this.localize("ui.panel.page-authorize.abort_intro")}:
|
||||
${this.localize(
|
||||
`ui.panel.page-authorize.form.providers.${step.handler[0]}.abort.${step.reason}`
|
||||
)}
|
||||
`;
|
||||
@@ -231,14 +228,15 @@ export class HaAuthFlow extends LitElement {
|
||||
<h1>
|
||||
${
|
||||
!["select_mfa_module", "mfa"].includes(step.step_id)
|
||||
? this._localize("ui.panel.page-authorize.welcome_home")
|
||||
: this._localize("ui.panel.page-authorize.just_checking")
|
||||
? this.localize("ui.panel.page-authorize.welcome_home")
|
||||
: this.localize("ui.panel.page-authorize.just_checking")
|
||||
}
|
||||
</h1>
|
||||
${this._computeStepDescription(step)}
|
||||
${keyed(
|
||||
step.step_id,
|
||||
html`<ha-auth-form
|
||||
.localize=${this.localize}
|
||||
.data=${this._stepData!}
|
||||
.schema=${autocompleteLoginFields(step.data_schema)}
|
||||
.error=${step.errors}
|
||||
@@ -258,7 +256,7 @@ export class HaAuthFlow extends LitElement {
|
||||
.checked=${this._storeToken}
|
||||
@change=${this._storeTokenChanged}
|
||||
>
|
||||
${this._localize("ui.panel.page-authorize.store_token")}
|
||||
${this.localize("ui.panel.page-authorize.store_token")}
|
||||
</ha-checkbox>
|
||||
`
|
||||
: ""
|
||||
@@ -268,7 +266,7 @@ export class HaAuthFlow extends LitElement {
|
||||
href="https://www.home-assistant.io/docs/locked_out/#forgot-password"
|
||||
target="_blank"
|
||||
rel="noreferrer noopener"
|
||||
>${this._localize("ui.panel.page-authorize.forgot_password")}</a
|
||||
>${this.localize("ui.panel.page-authorize.forgot_password")}</a
|
||||
>
|
||||
</div>
|
||||
`;
|
||||
@@ -338,13 +336,13 @@ export class HaAuthFlow extends LitElement {
|
||||
private _computeStepDescription(step: DataEntryFlowStepForm) {
|
||||
const resourceKey =
|
||||
`ui.panel.page-authorize.form.providers.${step.handler[0]}.step.${step.step_id}.description` as const;
|
||||
return this._localize(resourceKey, step.description_placeholders);
|
||||
return this.localize(resourceKey, step.description_placeholders);
|
||||
}
|
||||
|
||||
private _computeLabelCallback(step: DataEntryFlowStepForm) {
|
||||
// Returns a callback for ha-form to calculate labels per schema object
|
||||
return (schema) =>
|
||||
this._localize(
|
||||
this.localize(
|
||||
`ui.panel.page-authorize.form.providers.${step.handler[0]}.step.${step.step_id}.data.${schema.name}`
|
||||
);
|
||||
}
|
||||
@@ -352,13 +350,13 @@ export class HaAuthFlow extends LitElement {
|
||||
private _computeErrorCallback(step: DataEntryFlowStepForm) {
|
||||
// Returns a callback for ha-form to calculate error messages
|
||||
return (error) =>
|
||||
this._localize(
|
||||
this.localize(
|
||||
`ui.panel.page-authorize.form.providers.${step.handler[0]}.error.${error}`
|
||||
);
|
||||
}
|
||||
|
||||
private _unknownError() {
|
||||
return this._localize("ui.panel.page-authorize.form.unknown_error");
|
||||
return this.localize("ui.panel.page-authorize.form.unknown_error");
|
||||
}
|
||||
|
||||
private _startOver() {
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
/* eslint-disable lit/prefer-static-styles */
|
||||
import { html } from "lit";
|
||||
import { customElement, state } from "lit/decorators";
|
||||
import { consumeLocalize } from "../common/decorators/consume-context-entry";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import type { LocalizeFunc } from "../common/translations/localize";
|
||||
import { HaForm } from "../components/ha-form/ha-form";
|
||||
import "./ha-auth-form-string";
|
||||
@@ -10,13 +9,11 @@ const localizeBaseKey = "ui.panel.page-authorize.form";
|
||||
|
||||
@customElement("ha-auth-form")
|
||||
export class HaAuthForm extends HaForm {
|
||||
@state()
|
||||
@consumeLocalize()
|
||||
private _localize!: LocalizeFunc;
|
||||
@property({ attribute: false }) public localize?: LocalizeFunc;
|
||||
|
||||
protected getFormProperties(): Record<string, any> {
|
||||
return {
|
||||
localize: this._localize,
|
||||
localize: this.localize,
|
||||
localizeBaseKey,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -12,7 +12,6 @@ import "../components/ha-svg-icon";
|
||||
import type { AuthProvider, AuthUrlSearchParams } from "../data/auth";
|
||||
import { fetchAuthProviders } from "../data/auth";
|
||||
import { litLocalizeLiteMixin } from "../mixins/lit-localize-lite-mixin";
|
||||
import { provideLiteI18nMixin } from "../mixins/provide-lite-i18n-mixin";
|
||||
import { registerServiceWorker } from "../util/register-service-worker";
|
||||
import "./ha-auth-flow";
|
||||
|
||||
@@ -24,9 +23,7 @@ const appNames = {
|
||||
};
|
||||
|
||||
@customElement("ha-authorize")
|
||||
export class HaAuthorize extends provideLiteI18nMixin(
|
||||
litLocalizeLiteMixin(LitElement)
|
||||
) {
|
||||
export class HaAuthorize extends litLocalizeLiteMixin(LitElement) {
|
||||
@property({ attribute: false }) public clientId?: string;
|
||||
|
||||
@property({ attribute: false }) public redirectUri?: string;
|
||||
@@ -186,12 +183,14 @@ export class HaAuthorize extends provideLiteI18nMixin(
|
||||
.redirectUri=${this.redirectUri}
|
||||
.oauth2State=${this.oauth2State}
|
||||
.authProvider=${this._authProvider}
|
||||
.localize=${this.localize}
|
||||
.initStoreToken=${this._preselectStoreToken}
|
||||
></ha-auth-flow>
|
||||
${
|
||||
inactiveProviders!.length > 0
|
||||
? html`
|
||||
<ha-pick-auth-provider
|
||||
.localize=${this.localize}
|
||||
.clientId=${this.clientId}
|
||||
.authProviders=${inactiveProviders!}
|
||||
@pick-auth-provider=${this._handleAuthProviderPick}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { css, html, LitElement } from "lit";
|
||||
import { customElement, property, 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-icon-next";
|
||||
@@ -21,15 +20,13 @@ declare global {
|
||||
export class HaPickAuthProvider extends LitElement {
|
||||
@property({ attribute: false }) public authProviders: AuthProvider[] = [];
|
||||
|
||||
@state()
|
||||
@consumeLocalize()
|
||||
private _localize!: LocalizeFunc;
|
||||
@property({ attribute: false }) public localize!: LocalizeFunc;
|
||||
|
||||
protected render() {
|
||||
return html`
|
||||
<h3>
|
||||
<span
|
||||
>${this._localize("ui.panel.page-authorize.pick_auth_provider")}</span
|
||||
>${this.localize("ui.panel.page-authorize.pick_auth_provider")}</span
|
||||
>
|
||||
</h3>
|
||||
<ha-list>
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -1,24 +1,13 @@
|
||||
import type { HassEntity } from "home-assistant-js-websocket";
|
||||
import type {
|
||||
EntityRegistryDisplayEntry,
|
||||
EntityRegistryEntry,
|
||||
} from "../../data/entity/entity_registry";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import { ensureArray } from "../array/ensure-array";
|
||||
import { computeRTL } from "../util/compute_rtl";
|
||||
import { computeAreaName } from "./compute_area_name";
|
||||
import { computeDeviceName } from "./compute_device_name";
|
||||
import {
|
||||
computeEntityEntryName,
|
||||
computeEntityName,
|
||||
entityUseDeviceName,
|
||||
} from "./compute_entity_name";
|
||||
import { computeEntityName, entityUseDeviceName } from "./compute_entity_name";
|
||||
import { computeFloorName } from "./compute_floor_name";
|
||||
import { computeStateName } from "./compute_state_name";
|
||||
import {
|
||||
getEntityContext,
|
||||
getEntityEntryContext,
|
||||
} from "./context/get_entity_context";
|
||||
import { getEntityContext } from "./context/get_entity_context";
|
||||
|
||||
const DEFAULT_SEPARATOR = " ";
|
||||
|
||||
@@ -51,42 +40,6 @@ export interface EntityNameOptions {
|
||||
separator?: string;
|
||||
}
|
||||
|
||||
// Joins items that need no entity context. Returns undefined as soon as one
|
||||
// item references the registries (device, area, ...).
|
||||
const computeTextOnlyName = (
|
||||
items: EntityNameItem[],
|
||||
options?: EntityNameOptions
|
||||
): string | undefined =>
|
||||
items.every((n) => n.type === "text")
|
||||
? items
|
||||
.map((item) => item.text)
|
||||
.join(options?.separator ?? DEFAULT_SEPARATOR)
|
||||
: undefined;
|
||||
|
||||
/**
|
||||
* Name formatter used before the registry-aware one is installed
|
||||
* (see state/connection-mixin and state/state-display-mixin). It honours a
|
||||
* configured string or text-only name and falls back to the friendly name for
|
||||
* anything that needs entity context, so a configured name is never dropped
|
||||
* while the real formatter is still loading.
|
||||
*/
|
||||
export const computeEntityNameDisplayWithoutContext = (
|
||||
stateObj: HassEntity,
|
||||
name: string | EntityNameItem | EntityNameItem[] | undefined,
|
||||
options?: EntityNameOptions
|
||||
): string => {
|
||||
if (typeof name === "string") {
|
||||
return name;
|
||||
}
|
||||
if (!name) {
|
||||
return computeStateName(stateObj);
|
||||
}
|
||||
return (
|
||||
computeTextOnlyName(ensureArray(name), options) ??
|
||||
computeStateName(stateObj)
|
||||
);
|
||||
};
|
||||
|
||||
export const computeEntityNameDisplay = (
|
||||
stateObj: HassEntity,
|
||||
name: string | EntityNameItem | EntityNameItem[] | undefined,
|
||||
@@ -110,9 +63,8 @@ export const computeEntityNameDisplay = (
|
||||
const separator = options?.separator ?? DEFAULT_SEPARATOR;
|
||||
|
||||
// If all items are text, just join them
|
||||
const textOnlyName = computeTextOnlyName(items, options);
|
||||
if (textOnlyName !== undefined) {
|
||||
return textOnlyName;
|
||||
if (items.every((n) => n.type === "text")) {
|
||||
return items.map((item) => item.text).join(separator);
|
||||
}
|
||||
|
||||
const useDeviceName = entityUseDeviceName(stateObj, entities, devices);
|
||||
@@ -150,49 +102,18 @@ export const computeEntityNameList = (
|
||||
areas: HomeAssistant["areas"],
|
||||
floors: HomeAssistant["floors"]
|
||||
): (string | undefined)[] => {
|
||||
const entry = entities[stateObj.entity_id] as
|
||||
EntityRegistryDisplayEntry | undefined;
|
||||
|
||||
if (!entry) {
|
||||
return name.map((item) =>
|
||||
item.type === "entity"
|
||||
? computeStateName(stateObj)
|
||||
: item.type === "text"
|
||||
? item.text
|
||||
: undefined
|
||||
);
|
||||
}
|
||||
|
||||
return computeEntityEntryNameList(
|
||||
entry,
|
||||
name,
|
||||
entities,
|
||||
devices,
|
||||
areas,
|
||||
floors
|
||||
);
|
||||
};
|
||||
|
||||
export const computeEntityEntryNameList = (
|
||||
entry: EntityRegistryDisplayEntry | EntityRegistryEntry,
|
||||
name: EntityNameItem[],
|
||||
entities: HomeAssistant["entities"],
|
||||
devices: HomeAssistant["devices"],
|
||||
areas: HomeAssistant["areas"],
|
||||
floors: HomeAssistant["floors"]
|
||||
): (string | undefined)[] => {
|
||||
const { device, parentDevice, area, floor } = getEntityEntryContext(
|
||||
entry,
|
||||
const { device, parentDevice, area, floor } = getEntityContext(
|
||||
stateObj,
|
||||
entities,
|
||||
devices,
|
||||
areas,
|
||||
floors
|
||||
);
|
||||
|
||||
return name.map((item) => {
|
||||
const names = name.map((item) => {
|
||||
switch (item.type) {
|
||||
case "entity":
|
||||
return computeEntityEntryName(entry, devices);
|
||||
return computeEntityName(stateObj, entities, devices);
|
||||
case "device":
|
||||
return device ? computeDeviceName(device) : undefined;
|
||||
case "parent_device":
|
||||
@@ -207,29 +128,8 @@ export const computeEntityEntryNameList = (
|
||||
return "";
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export const computeEntitySearchLabels = (
|
||||
stateObj: HassEntity,
|
||||
entities: HomeAssistant["entities"],
|
||||
devices: HomeAssistant["devices"],
|
||||
areas: HomeAssistant["areas"],
|
||||
floors: HomeAssistant["floors"]
|
||||
) => {
|
||||
const { device, parentDevice, area } = getEntityContext(
|
||||
stateObj,
|
||||
entities,
|
||||
devices,
|
||||
areas,
|
||||
floors
|
||||
);
|
||||
return {
|
||||
entityName: computeEntityName(stateObj, entities, devices) || null,
|
||||
friendlyName: computeStateName(stateObj) || null,
|
||||
deviceName: (device && computeDeviceName(device)) || null,
|
||||
parentDeviceName: (parentDevice && computeDeviceName(parentDevice)) || null,
|
||||
areaName: (area && computeAreaName(area)) || null,
|
||||
};
|
||||
return names;
|
||||
};
|
||||
|
||||
export interface EntityPickerDisplay {
|
||||
|
||||
@@ -95,7 +95,6 @@ const FIXED_DOMAIN_ATTRIBUTE_STATES = {
|
||||
"door",
|
||||
"garage_door",
|
||||
"gas",
|
||||
"glass_break",
|
||||
"heat",
|
||||
"light",
|
||||
"lock",
|
||||
|
||||
@@ -123,6 +123,8 @@ interface ManagedMarker {
|
||||
draggable?: boolean;
|
||||
onDragEnd?: (location: MapLatLng) => void;
|
||||
dragging?: boolean;
|
||||
/** Pre-drag location; the next update echoing it is ignored (see setLocation) */
|
||||
staleLocation?: MapLatLng;
|
||||
mlMarker?: MapLibreMarker;
|
||||
decoration?: MapItemHandle;
|
||||
removed?: boolean;
|
||||
@@ -131,8 +133,6 @@ interface ManagedMarker {
|
||||
interface ClusterGroup {
|
||||
/** Members are shown in a bubble at their spot instead of an icon */
|
||||
open?: boolean;
|
||||
/** Grouped by key (a zone), so it bubbles even with a single member */
|
||||
keyed?: boolean;
|
||||
members: ManagedMarker[];
|
||||
center: MapLatLng;
|
||||
iconMarker?: MapLibreMarker;
|
||||
@@ -600,10 +600,19 @@ export class MapLibreMapEngine implements MapEngine {
|
||||
},
|
||||
clusterData: options.clusterData,
|
||||
setLocation: (newLocation) => {
|
||||
// The user's hand wins while dragging; the host is the truth otherwise
|
||||
if (managed.dragging) {
|
||||
return;
|
||||
}
|
||||
// Skip one update echoing the pre-drag location (the save has not returned yet)
|
||||
const stale = managed.staleLocation;
|
||||
managed.staleLocation = undefined;
|
||||
if (
|
||||
stale &&
|
||||
newLocation[0] === stale[0] &&
|
||||
newLocation[1] === stale[1]
|
||||
) {
|
||||
return;
|
||||
}
|
||||
managed.location = newLocation;
|
||||
managed.mlMarker?.setLngLat([newLocation[1], newLocation[0]]);
|
||||
},
|
||||
@@ -654,6 +663,7 @@ export class MapLibreMapEngine implements MapEngine {
|
||||
if (managed.draggable) {
|
||||
managed.mlMarker.on("dragstart", () => {
|
||||
managed.dragging = true;
|
||||
managed.staleLocation = managed.location;
|
||||
});
|
||||
managed.mlMarker.on("dragend", () => {
|
||||
managed.dragging = false;
|
||||
@@ -791,8 +801,10 @@ export class MapLibreMapEngine implements MapEngine {
|
||||
resizeHandle?.setAttribute("aria-valuetext", radiusText);
|
||||
};
|
||||
|
||||
// The user's hand wins while dragging; the host is the truth otherwise
|
||||
// Skip one update echoing the pre-edit values (the save has not returned yet)
|
||||
let dragging = false;
|
||||
let staleCenter: MapLatLng | undefined;
|
||||
let staleRadius: number | undefined;
|
||||
|
||||
if (options.resizable) {
|
||||
const east = pointEastOf(center, options.radius);
|
||||
@@ -835,6 +847,8 @@ export class MapLibreMapEngine implements MapEngine {
|
||||
if (keyboardRadius === undefined) {
|
||||
// Host updates treat a key resize like a drag
|
||||
dragging = true;
|
||||
staleCenter = currentCenter;
|
||||
staleRadius = currentRadius;
|
||||
}
|
||||
currentRadius = Math.max(
|
||||
1,
|
||||
@@ -861,11 +875,19 @@ export class MapLibreMapEngine implements MapEngine {
|
||||
[centerMarker, resizeMarker].forEach((handleMarker) => {
|
||||
handleMarker?.on("dragstart", () => {
|
||||
dragging = true;
|
||||
staleCenter = currentCenter;
|
||||
staleRadius = currentRadius;
|
||||
});
|
||||
handleMarker?.on("dragend", () => {
|
||||
dragging = false;
|
||||
});
|
||||
});
|
||||
const isStale = (candidateCenter: MapLatLng, candidateRadius: number) =>
|
||||
staleCenter !== undefined &&
|
||||
staleRadius !== undefined &&
|
||||
candidateCenter[0] === staleCenter[0] &&
|
||||
candidateCenter[1] === staleCenter[1] &&
|
||||
candidateRadius === staleRadius;
|
||||
|
||||
if (options.moveable) {
|
||||
centerMarker.on("drag", () => {
|
||||
@@ -922,6 +944,12 @@ export class MapLibreMapEngine implements MapEngine {
|
||||
if (dragging) {
|
||||
return;
|
||||
}
|
||||
const stale = isStale(newCenter, newRadius);
|
||||
staleCenter = undefined;
|
||||
staleRadius = undefined;
|
||||
if (stale) {
|
||||
return;
|
||||
}
|
||||
currentCenter = newCenter;
|
||||
currentRadius = newRadius;
|
||||
centerMarker.setLngLat([newCenter[1], newCenter[0]]);
|
||||
@@ -1191,16 +1219,13 @@ export class MapLibreMapEngine implements MapEngine {
|
||||
const clusterable = this._markers.filter(
|
||||
(managed) => managed.options.cluster && !managed.removed
|
||||
);
|
||||
// A member or bubble that had focus hands it to the icon replacing it;
|
||||
// read before the bubble holding it is removed
|
||||
// A member that had focus hands it to the icon replacing it; read before
|
||||
// the open bubble holding it is removed
|
||||
const active = (
|
||||
this._map.getContainer().getRootNode() as Document | ShadowRoot
|
||||
).activeElement;
|
||||
const focusedMember = active
|
||||
? (clusterable.find((managed) => managed.element.contains(active)) ??
|
||||
this._clusterGroups.find((group) =>
|
||||
group.iconMarker?.getElement().contains(active)
|
||||
)?.members[0])
|
||||
? clusterable.find((managed) => managed.element.contains(active))
|
||||
: undefined;
|
||||
this._clusterGroups.forEach((group) => group.iconMarker?.remove());
|
||||
|
||||
@@ -1225,7 +1250,6 @@ export class MapLibreMapEngine implements MapEngine {
|
||||
const groups: {
|
||||
seed: { x: number; y: number };
|
||||
members: ManagedMarker[];
|
||||
keyed?: boolean;
|
||||
}[] = [];
|
||||
|
||||
// Keyed groups first; one spread too wide falls through to proximity
|
||||
@@ -1248,10 +1272,8 @@ export class MapLibreMapEngine implements MapEngine {
|
||||
Math.max(...xs) - Math.min(...xs),
|
||||
Math.max(...ys) - Math.min(...ys)
|
||||
);
|
||||
// A keyed group (a zone) bubbles even with a single member, so a lone
|
||||
// person or device in a zone still shows in a bubble pinned to it.
|
||||
if (members.length === 1 || spread <= (groupRadius ?? radius)) {
|
||||
groups.push({ seed: points[0], members, keyed: true });
|
||||
if (members.length > 1 && spread <= (groupRadius ?? radius)) {
|
||||
groups.push({ seed: points[0], members });
|
||||
} else {
|
||||
ungrouped.push(...members);
|
||||
}
|
||||
@@ -1277,7 +1299,6 @@ export class MapLibreMapEngine implements MapEngine {
|
||||
}
|
||||
this._clusterGroups = groups.map((group) => ({
|
||||
members: group.members,
|
||||
keyed: group.keyed,
|
||||
center: [
|
||||
group.members.reduce((sum, m) => sum + m.location[0], 0) /
|
||||
group.members.length,
|
||||
@@ -1289,9 +1310,7 @@ export class MapLibreMapEngine implements MapEngine {
|
||||
|
||||
for (const group of this._clusterGroups) {
|
||||
group.iconMarker = undefined;
|
||||
// A lone non-keyed marker shows plainly; a lone zone occupant falls
|
||||
// through to the bubble path so it renders in a bubble at its zone.
|
||||
if (group.members.length === 1 && !group.keyed) {
|
||||
if (group.members.length === 1) {
|
||||
this._showMarker(group.members[0]);
|
||||
continue;
|
||||
}
|
||||
@@ -1360,10 +1379,8 @@ export class MapLibreMapEngine implements MapEngine {
|
||||
const group = this._clusterGroups.find((candidate) =>
|
||||
candidate.members.includes(focusedMember)
|
||||
);
|
||||
if (group && !group.open) {
|
||||
// A singleton group has no bubble icon; the member shows on its own,
|
||||
// so send focus back to that member rather than to the document.
|
||||
(group.iconMarker?.getElement() ?? focusedMember.element).focus();
|
||||
if (group?.iconMarker && !group.open) {
|
||||
group.iconMarker.getElement().focus();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,73 +0,0 @@
|
||||
import memoizeOne from "memoize-one";
|
||||
import { getColorByIndex } from "../color/colors";
|
||||
import { computeDomain } from "../entity/compute_domain";
|
||||
import type { EntityRegistryEntry } from "../../data/entity/entity_registry";
|
||||
|
||||
/**
|
||||
* Map colors for entities, by registry creation order, so an entity has the
|
||||
* same color on every map. The registry comes from the fullEntitiesContext;
|
||||
* entities without an entry (e.g. YAML zones) get a color derived from their
|
||||
* id.
|
||||
*/
|
||||
|
||||
export const HOME_ZONE_ENTITY_ID = "zone.home";
|
||||
|
||||
/** Domains whose entities are colored by creation order */
|
||||
const ORDERED_DOMAINS = ["zone", "person", "device_tracker"];
|
||||
|
||||
// One index per registry update, shared by every map on the page. Zones,
|
||||
// persons and trackers share one sequence, so a person never has the color
|
||||
// of the zone it is in.
|
||||
const creationIndex = memoizeOne(
|
||||
(entries: EntityRegistryEntry[]): Record<string, number> => {
|
||||
const index: Record<string, number> = {};
|
||||
entries
|
||||
.filter(
|
||||
(entry) =>
|
||||
ORDERED_DOMAINS.includes(computeDomain(entry.entity_id)) &&
|
||||
// The home zone has a fixed color and does not take a palette slot
|
||||
entry.entity_id !== HOME_ZONE_ENTITY_ID
|
||||
)
|
||||
.sort((a, b) => a.created_at - b.created_at || a.id.localeCompare(b.id))
|
||||
.forEach((entry, i) => {
|
||||
index[entry.entity_id] = i;
|
||||
});
|
||||
return index;
|
||||
}
|
||||
);
|
||||
|
||||
// For entities without a registry entry
|
||||
const hashIndex = (entityId: string): number => {
|
||||
let hash = 5381;
|
||||
for (let i = 0; i < entityId.length; i++) {
|
||||
hash = (hash * 33 + entityId.charCodeAt(i)) % 2147483647;
|
||||
}
|
||||
return hash;
|
||||
};
|
||||
|
||||
/** The palette color of an entity on a map */
|
||||
export const entityMapColor = (
|
||||
entityId: string,
|
||||
entries: EntityRegistryEntry[],
|
||||
computedStyles: CSSStyleDeclaration
|
||||
): string =>
|
||||
getColorByIndex(
|
||||
creationIndex(entries)[entityId] ?? hashIndex(entityId),
|
||||
computedStyles
|
||||
);
|
||||
|
||||
/** A zone's color: primary for home, muted for passive, its entity map color otherwise */
|
||||
export const zoneColor = (
|
||||
entityId: string,
|
||||
passive: boolean,
|
||||
entries: EntityRegistryEntry[],
|
||||
computedStyles: CSSStyleDeclaration
|
||||
): string => {
|
||||
if (entityId === HOME_ZONE_ENTITY_ID) {
|
||||
return computedStyles.getPropertyValue("--primary-color");
|
||||
}
|
||||
if (passive) {
|
||||
return computedStyles.getPropertyValue("--secondary-text-color");
|
||||
}
|
||||
return entityMapColor(entityId, entries, computedStyles);
|
||||
};
|
||||
@@ -16,11 +16,7 @@ export const setMarkerAccessibility = (
|
||||
): void => {
|
||||
clearMarkerAccessibility(element);
|
||||
const owned: string[] = [];
|
||||
if (
|
||||
title &&
|
||||
!element.hasAttribute("aria-label") &&
|
||||
!element.hasAttribute("aria-labelledby")
|
||||
) {
|
||||
if (title && !element.hasAttribute("aria-label")) {
|
||||
element.setAttribute("aria-label", title);
|
||||
owned.push("aria-label");
|
||||
}
|
||||
|
||||
@@ -1,67 +0,0 @@
|
||||
import { getContrastedColorHex } from "../color/rgb";
|
||||
|
||||
/** The zone marker: a colored circle with the zone's icon or initials, shared by the map and the zone editor */
|
||||
|
||||
export const ZONE_CIRCLE_SIZE = 36;
|
||||
|
||||
// Content color contrasting the fill; not every theme color parses
|
||||
export const contrastingZoneContent = (color: string): string => {
|
||||
try {
|
||||
return getContrastedColorHex(color.trim());
|
||||
} catch {
|
||||
return "#ffffff";
|
||||
}
|
||||
};
|
||||
|
||||
export const zoneInitials = (name: string): string =>
|
||||
name
|
||||
.split(" ")
|
||||
.map((part) => part[0])
|
||||
.join("")
|
||||
.slice(0, 2);
|
||||
|
||||
export const createZoneMarkerElement = (options: {
|
||||
color: string;
|
||||
icon?: string;
|
||||
/** Path for an ha-svg-icon, when there is no icon name */
|
||||
iconPath?: string;
|
||||
name: string;
|
||||
}): HTMLElement => {
|
||||
const element = document.createElement("div");
|
||||
element.className = "zone-circle";
|
||||
element.style.backgroundColor = options.color;
|
||||
element.style.color = contrastingZoneContent(options.color);
|
||||
if (options.icon) {
|
||||
const icon = document.createElement("ha-icon");
|
||||
icon.setAttribute("icon", options.icon);
|
||||
element.appendChild(icon);
|
||||
} else if (options.iconPath) {
|
||||
const icon = document.createElement("ha-svg-icon");
|
||||
icon.setAttribute("path", options.iconPath);
|
||||
element.appendChild(icon);
|
||||
} else {
|
||||
const initials = document.createElement("span");
|
||||
initials.textContent = zoneInitials(options.name);
|
||||
element.appendChild(initials);
|
||||
}
|
||||
return element;
|
||||
};
|
||||
|
||||
/** Styles for the zone marker, included by ha-map */
|
||||
export const zoneMarkerStyles = `
|
||||
.zone-circle {
|
||||
width: ${ZONE_CIRCLE_SIZE}px;
|
||||
height: ${ZONE_CIRCLE_SIZE}px;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border: 2px solid var(--card-background-color, #fff);
|
||||
box-sizing: border-box;
|
||||
box-shadow: var(--ha-box-shadow-s);
|
||||
overflow: hidden;
|
||||
font-size: var(--ha-font-size-s);
|
||||
font-weight: var(--ha-font-weight-medium);
|
||||
--mdc-icon-size: ${ZONE_CIRCLE_SIZE / 2}px;
|
||||
}
|
||||
`;
|
||||
@@ -33,7 +33,7 @@ export type FormatEntityAttributeNameFunc = (
|
||||
|
||||
export type FormatEntityNameFunc = (
|
||||
stateObj: HassEntity,
|
||||
name: string | EntityNameItem | EntityNameItem[] | undefined,
|
||||
name: EntityNameItem | EntityNameItem[],
|
||||
options?: EntityNameOptions
|
||||
) => string;
|
||||
|
||||
|
||||
@@ -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;
|
||||
},
|
||||
});
|
||||
@@ -90,7 +90,6 @@ export class HaAutomationConditionSummary extends LitElement {
|
||||
></ha-automation-row-options>`
|
||||
: nothing
|
||||
}
|
||||
<slot name="references"></slot>
|
||||
${
|
||||
note
|
||||
? html`
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -53,41 +53,6 @@ export interface StatisticsChartData {
|
||||
yAxisFractionDigits: number;
|
||||
}
|
||||
|
||||
// ECharts stacks a time axis by data index. Merge the sorted point lists so
|
||||
// every stacked line has the same x at every index, padding with null.
|
||||
function alignStackedLines(datasets: LineSeriesOption[]) {
|
||||
const sources = datasets.map((d) => d.data as [number, number | null][]);
|
||||
const cursors = sources.map(() => 0);
|
||||
const counts = sources.map(() => 0);
|
||||
const aligned = sources.map((): [number, number | null][] => []);
|
||||
while (cursors.some((cursor, i) => cursor < sources[i].length)) {
|
||||
let x = Infinity;
|
||||
for (let i = 0; i < sources.length; i++) {
|
||||
const point = sources[i][cursors[i]];
|
||||
if (point && point[0] < x) x = point[0];
|
||||
}
|
||||
let slots = 1;
|
||||
for (let i = 0; i < sources.length; i++) {
|
||||
let count = 0;
|
||||
while (sources[i][cursors[i] + count]?.[0] === x) count++;
|
||||
counts[i] = count;
|
||||
if (count > slots) slots = count;
|
||||
}
|
||||
for (let i = 0; i < sources.length; i++) {
|
||||
const count = counts[i];
|
||||
for (let slot = 0; slot < slots; slot++) {
|
||||
aligned[i].push(
|
||||
count ? sources[i][cursors[i] + Math.min(slot, count - 1)] : [x, null]
|
||||
);
|
||||
}
|
||||
cursors[i] += count;
|
||||
}
|
||||
}
|
||||
datasets.forEach((d, i) => {
|
||||
d.data = aligned[i];
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Transforms raw statistics into ECharts series for `statistics-chart`.
|
||||
* Pure data processing: all environment inputs (current time, theme style,
|
||||
@@ -298,10 +263,8 @@ export function generateStatisticsChartData(
|
||||
),
|
||||
symbol: "none",
|
||||
// minmax sampling operates independently per series, breaking stacking alignment
|
||||
// echarts stacks before it samples, so its lttb keeps stacks aligned
|
||||
// https://github.com/apache/echarts/issues/11879
|
||||
sampling:
|
||||
band || (chartStacked && chartType === "line") ? "lttb" : "minmax",
|
||||
sampling: band && drawBands ? "lttb" : "minmax",
|
||||
animationDurationUpdate: 0,
|
||||
lineStyle: {
|
||||
width: 1.5,
|
||||
@@ -476,15 +439,6 @@ export function generateStatisticsChartData(
|
||||
Array.prototype.push.apply(legendData, statLegendData);
|
||||
});
|
||||
|
||||
if (chartType === "line" && chartStacked) {
|
||||
const stacked = (totalDataSets as LineSeriesOption[]).filter(
|
||||
(d) => d.data?.length
|
||||
);
|
||||
if (stacked.length > 1) {
|
||||
alignStackedLines(stacked);
|
||||
}
|
||||
}
|
||||
|
||||
if (chartType === "bar") {
|
||||
fillDataGapsAndRoundCaps(totalDataSets as BarSeriesOption[], chartStacked);
|
||||
}
|
||||
|
||||
@@ -253,8 +253,6 @@ export class StatisticsChart extends LitElement {
|
||||
}[] = [];
|
||||
for (const param of params) {
|
||||
if (rendered[param.seriesIndex]) continue;
|
||||
// stacked lines are padded with null where a statistic has no data
|
||||
if (!chartIsBar && param.value[1] === null) continue;
|
||||
rendered[param.seriesIndex] = true;
|
||||
|
||||
const statisticId = this._statisticIds[param.seriesIndex];
|
||||
|
||||
@@ -1,100 +1,69 @@
|
||||
import { AssistChip } from "@material/web/chips/internal/assist-chip";
|
||||
import { styles } from "@material/web/chips/internal/assist-styles.cssresult.js";
|
||||
|
||||
import { styles as sharedStyles } from "@material/web/chips/internal/shared-styles.cssresult.js";
|
||||
import { styles as elevatedStyles } from "@material/web/chips/internal/elevated-styles.cssresult.js";
|
||||
|
||||
import { css, html } from "lit";
|
||||
import type { CSSResultGroup } from "lit";
|
||||
import { css } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { HaChipBase } from "./ha-chip-base";
|
||||
|
||||
@customElement("ha-assist-chip")
|
||||
// @ts-ignore
|
||||
export class HaAssistChip extends AssistChip {
|
||||
export class HaAssistChip extends HaChipBase {
|
||||
@property({ type: Boolean, reflect: true }) filled = false;
|
||||
|
||||
@property({ type: Boolean }) active = false;
|
||||
@property({ type: Boolean, reflect: true }) active = false;
|
||||
|
||||
static override styles = [
|
||||
sharedStyles,
|
||||
elevatedStyles,
|
||||
styles,
|
||||
css`
|
||||
:host {
|
||||
--md-sys-color-primary: var(--primary-text-color);
|
||||
--md-sys-color-on-surface: var(--primary-text-color);
|
||||
--md-assist-chip-container-shape: var(
|
||||
--ha-assist-chip-container-shape,
|
||||
16px
|
||||
);
|
||||
--md-assist-chip-outline-color: var(--outline-color);
|
||||
--md-assist-chip-label-text-weight: 400;
|
||||
}
|
||||
/** Material 3 doesn't have a filled chip, so we have to make our own **/
|
||||
.filled {
|
||||
display: flex;
|
||||
pointer-events: none;
|
||||
border-radius: inherit;
|
||||
inset: 0;
|
||||
position: absolute;
|
||||
background-color: var(--ha-assist-chip-filled-container-color);
|
||||
}
|
||||
/** Set the size of mdc icons **/
|
||||
::slotted([slot="icon"]),
|
||||
::slotted([slot="trailing-icon"]) {
|
||||
display: flex;
|
||||
--mdc-icon-size: var(--md-input-chip-icon-size, 18px);
|
||||
font-size: var(--_label-text-size) !important;
|
||||
}
|
||||
@property({ type: Boolean, reflect: true }) elevated = false;
|
||||
|
||||
.trailing.icon ::slotted(*),
|
||||
.trailing.icon svg {
|
||||
margin-inline-end: unset;
|
||||
margin-inline-start: var(--_icon-label-space);
|
||||
}
|
||||
::before {
|
||||
background: var(--ha-assist-chip-container-color, transparent);
|
||||
opacity: var(--ha-assist-chip-container-opacity, 1);
|
||||
}
|
||||
:where(.active)::before {
|
||||
background: var(--ha-assist-chip-active-container-color);
|
||||
opacity: var(--ha-assist-chip-active-container-opacity);
|
||||
}
|
||||
.label {
|
||||
font-family: var(--ha-font-family-body);
|
||||
}
|
||||
`,
|
||||
];
|
||||
|
||||
protected override renderOutline() {
|
||||
if (this.filled) {
|
||||
return html`<span class="filled"></span>`;
|
||||
}
|
||||
|
||||
return super.renderOutline();
|
||||
}
|
||||
|
||||
protected override getContainerClasses() {
|
||||
return {
|
||||
...super.getContainerClasses(),
|
||||
active: this.active,
|
||||
};
|
||||
}
|
||||
|
||||
protected override renderPrimaryContent() {
|
||||
return html`
|
||||
<span class="leading icon" aria-hidden="true">
|
||||
${this.renderLeadingIcon()}
|
||||
</span>
|
||||
<span class="label">${this.label}</span>
|
||||
<span class="touch"></span>
|
||||
<span class="trailing leading icon" aria-hidden="true">
|
||||
${this.renderTrailingIcon()}
|
||||
</span>
|
||||
`;
|
||||
}
|
||||
|
||||
protected renderTrailingIcon() {
|
||||
return html`<slot name="trailing-icon"></slot>`;
|
||||
static override get styles(): CSSResultGroup {
|
||||
return [
|
||||
super.styles,
|
||||
css`
|
||||
:host {
|
||||
--ha-chip-label-color: var(
|
||||
--md-assist-chip-label-text-color,
|
||||
var(--md-sys-color-on-surface, var(--primary-text-color))
|
||||
);
|
||||
--ha-button-border-radius: var(
|
||||
--ha-assist-chip-container-shape,
|
||||
16px
|
||||
);
|
||||
--ha-chip-container-color: var(
|
||||
--ha-assist-chip-container-color,
|
||||
transparent
|
||||
);
|
||||
--ha-chip-container-opacity: var(
|
||||
--ha-assist-chip-container-opacity,
|
||||
1
|
||||
);
|
||||
--ha-chip-outline-color: var(
|
||||
--md-assist-chip-outline-color,
|
||||
var(--outline-color)
|
||||
);
|
||||
--ha-chip-icon-label-space: var(
|
||||
--md-assist-chip-icon-label-space,
|
||||
var(--ha-space-2)
|
||||
);
|
||||
}
|
||||
:host([filled]) {
|
||||
--ha-chip-container-color: var(
|
||||
--ha-assist-chip-filled-container-color
|
||||
);
|
||||
--ha-chip-outline-color: transparent;
|
||||
}
|
||||
:host([active]) {
|
||||
--ha-chip-container-color: var(
|
||||
--ha-assist-chip-active-container-color
|
||||
);
|
||||
--ha-chip-container-opacity: var(
|
||||
--ha-assist-chip-active-container-opacity,
|
||||
1
|
||||
);
|
||||
}
|
||||
.primary {
|
||||
padding-inline-end: var(
|
||||
--md-assist-chip-trailing-space,
|
||||
var(--ha-space-4)
|
||||
);
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,452 @@
|
||||
import { LocalizeController } from "@home-assistant/webawesome/dist/utilities/localize.js";
|
||||
import { mdiClose } from "@mdi/js";
|
||||
import type { CSSResultGroup, PropertyValues, TemplateResult } from "lit";
|
||||
import { css, html, nothing } from "lit";
|
||||
import { property, query, state } from "lit/decorators";
|
||||
import { classMap } from "lit/directives/class-map";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import { HaButton } from "../ha-button";
|
||||
import "../ha-svg-icon";
|
||||
|
||||
export interface ChipFocusOptions extends FocusOptions {
|
||||
trailing?: boolean;
|
||||
}
|
||||
|
||||
export class HaChipBase extends HaButton {
|
||||
@property() label = "";
|
||||
|
||||
@property({ type: Boolean }) removable = false;
|
||||
|
||||
@property({ type: Boolean, attribute: "remove-only" }) removeOnly = false;
|
||||
|
||||
@property({ type: Boolean, reflect: true, attribute: "soft-disabled" })
|
||||
softDisabled = false;
|
||||
|
||||
@property({ type: Boolean, attribute: "always-focusable" })
|
||||
alwaysFocusable = false;
|
||||
|
||||
@property({ type: Boolean, reflect: true, attribute: "has-icon" })
|
||||
hasIcon = false;
|
||||
|
||||
@property({ type: Number, reflect: true, attribute: "tabindex" })
|
||||
override tabIndex = 0;
|
||||
|
||||
@property({ attribute: "aria-label-remove" }) ariaLabelRemove: string | null =
|
||||
null;
|
||||
|
||||
@query(".primary") private _primary?: HTMLButtonElement | HTMLAnchorElement;
|
||||
|
||||
@query(".remove") private _remove?: HTMLButtonElement;
|
||||
|
||||
@state() private _trailingFocused = false;
|
||||
|
||||
@state() private _hasTrailingIcon = false;
|
||||
|
||||
private _chipLocalize = new LocalizeController(this);
|
||||
|
||||
override variant: HaButton["variant"] = "neutral";
|
||||
|
||||
override appearance: HaButton["appearance"] = "outlined";
|
||||
|
||||
override size: HaButton["size"] = "s";
|
||||
|
||||
protected get pressed(): boolean | undefined {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
get focusable() {
|
||||
return !this.disabled || this.alwaysFocusable;
|
||||
}
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.addEventListener("click", this._blockDisabledClick);
|
||||
this.addEventListener("keydown", this._handleKeyDown);
|
||||
}
|
||||
|
||||
override focus(options?: ChipFocusOptions) {
|
||||
if (!this.focusable) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
(options?.trailing || (this.removeOnly && !this.href)) &&
|
||||
this._remove
|
||||
) {
|
||||
this._remove.focus(options);
|
||||
} else {
|
||||
this._primary?.focus(options);
|
||||
}
|
||||
}
|
||||
|
||||
override click() {
|
||||
if (this.removeOnly && !this.href) {
|
||||
this._remove?.click();
|
||||
} else {
|
||||
this._primary?.click();
|
||||
}
|
||||
}
|
||||
|
||||
override blur() {
|
||||
this._primary?.blur();
|
||||
this._remove?.blur();
|
||||
}
|
||||
|
||||
protected override updated(changed: PropertyValues<this>) {
|
||||
super.updated(changed);
|
||||
|
||||
if (changed.has("disabled") || changed.has("alwaysFocusable")) {
|
||||
fireEvent(this, "update-focus", undefined, { composed: false });
|
||||
}
|
||||
}
|
||||
|
||||
override render() {
|
||||
return html`
|
||||
<div class=${classMap({ container: true, "has-remove": this.removable })}>
|
||||
${this._renderPrimaryAction()}
|
||||
${
|
||||
this.removable
|
||||
? html`
|
||||
<span id="remove-label" hidden>
|
||||
${this._chipLocalize.term("remove")}
|
||||
</span>
|
||||
<span id="accessible-label" hidden>${this.ariaLabel}</span>
|
||||
<button
|
||||
class="button remove trailing action"
|
||||
part="remove-button"
|
||||
type="button"
|
||||
?disabled=${this.disabled && !this.alwaysFocusable}
|
||||
aria-disabled=${this.softDisabled ? "true" : nothing}
|
||||
aria-label=${this.ariaLabelRemove || nothing}
|
||||
aria-labelledby=${
|
||||
this.ariaLabelRemove
|
||||
? nothing
|
||||
: this.ariaLabel
|
||||
? "remove-label accessible-label"
|
||||
: "remove-label label"
|
||||
}
|
||||
tabindex=${this.removeOnly && !this.href ? this.tabIndex : -1}
|
||||
@click=${this._handleRemove}
|
||||
@focus=${this._handleRemoveFocus}
|
||||
@blur=${this._handleRemoveBlur}
|
||||
>
|
||||
<span class="icon" aria-hidden="true">
|
||||
<slot name="remove-trailing-icon">
|
||||
<ha-svg-icon .path=${mdiClose}></ha-svg-icon>
|
||||
</slot>
|
||||
</span>
|
||||
</button>
|
||||
`
|
||||
: nothing
|
||||
}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
private _renderPrimaryAction() {
|
||||
if (this.href) {
|
||||
return html`<a
|
||||
class="button primary action"
|
||||
part="base"
|
||||
title=${this.title || nothing}
|
||||
href=${this.href}
|
||||
target=${this.target || nothing}
|
||||
download=${this.download || nothing}
|
||||
rel=${this.rel || nothing}
|
||||
aria-label=${this.ariaLabel || nothing}
|
||||
tabindex=${this._trailingFocused ? -1 : this.tabIndex}
|
||||
@click=${this.handlePrimaryClick}
|
||||
>${this.renderPrimaryContent()}</a
|
||||
>`;
|
||||
}
|
||||
|
||||
if (this.removeOnly) {
|
||||
return html`<span class="content" title=${this.title || nothing}>
|
||||
${this.renderPrimaryContent()}
|
||||
</span>`;
|
||||
}
|
||||
|
||||
return html`<button
|
||||
class=${classMap({
|
||||
button: true,
|
||||
primary: true,
|
||||
action: true,
|
||||
disabled: this.disabled || this.softDisabled,
|
||||
})}
|
||||
part="base"
|
||||
type="button"
|
||||
title=${this.title || nothing}
|
||||
?disabled=${this.disabled && !this.alwaysFocusable}
|
||||
aria-disabled=${
|
||||
this.softDisabled || (this.disabled && this.alwaysFocusable)
|
||||
? "true"
|
||||
: nothing
|
||||
}
|
||||
aria-label=${this.ariaLabel || nothing}
|
||||
aria-pressed=${this.pressed ?? nothing}
|
||||
tabindex=${this._trailingFocused ? -1 : this.tabIndex}
|
||||
@click=${this.handlePrimaryClick}
|
||||
>
|
||||
${this.renderPrimaryContent()}
|
||||
</button>`;
|
||||
}
|
||||
|
||||
protected renderPrimaryContent() {
|
||||
return html`
|
||||
${this.renderLeadingIcon()}
|
||||
<span class="label" id="label" part="label">
|
||||
${this.label || html`<slot></slot>`}
|
||||
</span>
|
||||
<span class="icon" aria-hidden="true" ?hidden=${!this._hasTrailingIcon}>
|
||||
<slot name="trailing-icon" @slotchange=${this._iconChanged}></slot>
|
||||
</span>
|
||||
`;
|
||||
}
|
||||
|
||||
protected renderLeadingIcon(): TemplateResult | typeof nothing {
|
||||
return html`<span class="icon leading" aria-hidden="true">
|
||||
<slot name="icon" @slotchange=${this._iconChanged}></slot>
|
||||
</span>`;
|
||||
}
|
||||
|
||||
protected handlePrimaryClick(event: MouseEvent) {
|
||||
this._blockDisabledClick(event);
|
||||
}
|
||||
|
||||
private _iconChanged(event: Event) {
|
||||
if (!(event.target instanceof HTMLSlotElement)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const hasIcon = event.target.assignedElements({ flatten: true }).length > 0;
|
||||
|
||||
if (event.target.name === "icon") {
|
||||
this.hasIcon = hasIcon;
|
||||
} else {
|
||||
this._hasTrailingIcon = hasIcon;
|
||||
}
|
||||
}
|
||||
|
||||
private _blockDisabledClick(event: MouseEvent) {
|
||||
if (
|
||||
this.softDisabled ||
|
||||
(this.disabled && (!this.href || this.alwaysFocusable))
|
||||
) {
|
||||
event.preventDefault();
|
||||
event.stopImmediatePropagation();
|
||||
}
|
||||
}
|
||||
|
||||
private _handleRemove(event: MouseEvent) {
|
||||
event.stopPropagation();
|
||||
|
||||
if (this.disabled || this.softDisabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
!fireEvent(this, "remove", undefined, {
|
||||
bubbles: false,
|
||||
composed: false,
|
||||
cancelable: true,
|
||||
}).defaultPrevented
|
||||
) {
|
||||
this.remove();
|
||||
}
|
||||
}
|
||||
|
||||
private _handleRemoveFocus() {
|
||||
this._trailingFocused = true;
|
||||
}
|
||||
|
||||
private _handleRemoveBlur() {
|
||||
this._trailingFocused = false;
|
||||
}
|
||||
|
||||
private _handleKeyDown(event: KeyboardEvent) {
|
||||
if (
|
||||
(event.key !== "ArrowLeft" && event.key !== "ArrowRight") ||
|
||||
!this._primary ||
|
||||
!this._remove
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const forwards =
|
||||
(event.key === "ArrowRight") !==
|
||||
(getComputedStyle(this).direction === "rtl");
|
||||
|
||||
if (
|
||||
(forwards && this.shadowRoot?.activeElement === this._primary) ||
|
||||
(!forwards && this.shadowRoot?.activeElement === this._remove)
|
||||
) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
(forwards ? this._remove : this._primary).focus();
|
||||
}
|
||||
}
|
||||
|
||||
static override get styles(): CSSResultGroup {
|
||||
return [
|
||||
super.styles,
|
||||
css`
|
||||
:host {
|
||||
display: inline-flex;
|
||||
max-width: 100%;
|
||||
vertical-align: middle;
|
||||
--ha-button-height: 32px;
|
||||
--ha-button-border-radius: var(--ha-border-radius-pill);
|
||||
--wa-font-weight-action: var(--ha-font-weight-normal);
|
||||
--wa-button-transform-hover: none;
|
||||
--wa-button-transform-active: none;
|
||||
}
|
||||
:host([variant]) {
|
||||
--wa-color-on-quiet: var(
|
||||
--ha-chip-label-color,
|
||||
var(--primary-text-color)
|
||||
);
|
||||
--wa-color-fill-quiet: color-mix(
|
||||
in srgb,
|
||||
var(--ha-chip-label-color, var(--primary-text-color)) 8%,
|
||||
transparent
|
||||
);
|
||||
}
|
||||
.container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
min-width: 0;
|
||||
max-width: 100%;
|
||||
height: var(--ha-button-height);
|
||||
position: relative;
|
||||
border-radius: var(--ha-button-border-radius);
|
||||
}
|
||||
.container::before,
|
||||
.container::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
pointer-events: none;
|
||||
border-radius: inherit;
|
||||
}
|
||||
.container::before {
|
||||
background: var(--ha-chip-container-color, transparent);
|
||||
opacity: var(--ha-chip-container-opacity, 1);
|
||||
}
|
||||
.container::after {
|
||||
border: var(--ha-chip-outline-width, 1px) solid
|
||||
var(--ha-chip-outline-color, var(--outline-color));
|
||||
}
|
||||
.button,
|
||||
.content {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: var(--ha-chip-icon-label-space, var(--ha-space-2));
|
||||
min-width: 0;
|
||||
border: 0;
|
||||
border-radius: inherit;
|
||||
color: var(--ha-chip-label-color, var(--primary-text-color));
|
||||
background: transparent;
|
||||
font-family: var(--ha-font-family-body);
|
||||
font-size: var(--ha-font-size-m);
|
||||
font-weight: var(--ha-font-weight-normal);
|
||||
padding-inline: var(--ha-space-4);
|
||||
position: relative;
|
||||
box-shadow: none;
|
||||
}
|
||||
.button.primary,
|
||||
.content {
|
||||
flex: 1;
|
||||
height: var(--ha-button-height);
|
||||
min-height: var(--ha-button-height);
|
||||
}
|
||||
.label {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
min-width: 0;
|
||||
font-size: var(--ha-chip-label-size, 0.875rem);
|
||||
font-weight: var(
|
||||
--ha-chip-label-weight,
|
||||
var(--ha-font-weight-normal)
|
||||
);
|
||||
line-height: var(--ha-chip-label-line-height, 1.25rem);
|
||||
color: var(--ha-chip-label-color, var(--primary-text-color));
|
||||
}
|
||||
.button.remove {
|
||||
flex: none;
|
||||
padding: 0;
|
||||
width: calc(var(--ha-chip-icon-size, 18px) + 2 * var(--ha-space-2));
|
||||
height: var(--ha-button-height);
|
||||
min-height: var(--ha-button-height);
|
||||
border-start-start-radius: 0;
|
||||
border-end-start-radius: 0;
|
||||
}
|
||||
.has-remove .primary,
|
||||
.has-remove .content {
|
||||
border-start-end-radius: 0;
|
||||
border-end-end-radius: 0;
|
||||
padding-inline-end: 0;
|
||||
}
|
||||
:host([has-icon]) .primary,
|
||||
:host([has-icon]) .content {
|
||||
padding-inline-start: var(--ha-space-2);
|
||||
}
|
||||
.button::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
inset: -8px 0;
|
||||
}
|
||||
slot:not([name]),
|
||||
slot[name="icon"],
|
||||
slot[name="trailing-icon"],
|
||||
slot[name="selected-icon"],
|
||||
slot[name="remove-trailing-icon"] {
|
||||
display: contents;
|
||||
}
|
||||
.icon {
|
||||
display: flex;
|
||||
flex: none;
|
||||
color: var(--ha-chip-label-color, var(--primary-text-color));
|
||||
}
|
||||
:host(:not([has-icon])) .leading {
|
||||
display: none;
|
||||
}
|
||||
.icon[hidden] {
|
||||
display: none;
|
||||
}
|
||||
ha-svg-icon,
|
||||
::slotted([slot]) {
|
||||
flex: none;
|
||||
display: flex;
|
||||
--mdc-icon-size: var(--ha-chip-icon-size, 18px);
|
||||
}
|
||||
:host([disabled]),
|
||||
:host([soft-disabled]) {
|
||||
opacity: 1;
|
||||
}
|
||||
:host([disabled]) .label,
|
||||
:host([disabled]) .icon,
|
||||
:host([soft-disabled]) .label,
|
||||
:host([soft-disabled]) .icon {
|
||||
opacity: 0.38;
|
||||
}
|
||||
:host([disabled][selected]) .container::before,
|
||||
:host([soft-disabled][selected]) .container::before {
|
||||
background-color: var(--primary-text-color);
|
||||
opacity: 0.12;
|
||||
}
|
||||
:host([elevated]) .container {
|
||||
box-shadow: var(--ha-box-shadow-s);
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HASSDomEvents {
|
||||
remove: undefined;
|
||||
"update-focus": undefined;
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,92 @@
|
||||
import { MdChipSet } from "@material/web/chips/chip-set";
|
||||
import { customElement } from "lit/decorators";
|
||||
import { css, html, LitElement } from "lit";
|
||||
import { customElement, queryAssignedElements } from "lit/decorators";
|
||||
import { HaChipBase } from "./ha-chip-base";
|
||||
|
||||
@customElement("ha-chip-set")
|
||||
export class HaChipSet extends MdChipSet {}
|
||||
export class HaChipSet extends LitElement {
|
||||
@queryAssignedElements({ flatten: true }) private _children!: HTMLElement[];
|
||||
|
||||
get chips(): HaChipBase[] {
|
||||
return this._children.filter(
|
||||
(child): child is HaChipBase => child instanceof HaChipBase
|
||||
);
|
||||
}
|
||||
|
||||
override connectedCallback() {
|
||||
super.connectedCallback();
|
||||
this.setAttribute("role", "toolbar");
|
||||
this.addEventListener("keydown", this._handleKeyDown);
|
||||
this.addEventListener("focusin", this._updateTabIndices);
|
||||
this.addEventListener("update-focus", this._updateTabIndices);
|
||||
}
|
||||
|
||||
override disconnectedCallback() {
|
||||
super.disconnectedCallback();
|
||||
this.removeEventListener("keydown", this._handleKeyDown);
|
||||
this.removeEventListener("focusin", this._updateTabIndices);
|
||||
this.removeEventListener("update-focus", this._updateTabIndices);
|
||||
}
|
||||
|
||||
protected render() {
|
||||
return html`<slot @slotchange=${this._updateTabIndices}></slot>`;
|
||||
}
|
||||
|
||||
private _updateTabIndices() {
|
||||
const chips = this.chips;
|
||||
|
||||
const active =
|
||||
chips.find((chip) => chip.focusable && chip.shadowRoot?.activeElement) ||
|
||||
chips.find((chip) => chip.focusable);
|
||||
|
||||
for (const chip of chips) {
|
||||
chip.tabIndex = chip === active ? 0 : -1;
|
||||
}
|
||||
}
|
||||
|
||||
private _handleKeyDown(event: KeyboardEvent) {
|
||||
if (!["ArrowLeft", "ArrowRight", "Home", "End"].includes(event.key)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const chips = this.chips.filter((chip) => chip.focusable);
|
||||
|
||||
if (!chips.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
event.preventDefault();
|
||||
|
||||
const forwards =
|
||||
(event.key === "ArrowRight") !==
|
||||
(getComputedStyle(this).direction === "rtl");
|
||||
|
||||
const current = chips.findIndex((chip) => chip.shadowRoot?.activeElement);
|
||||
|
||||
const index =
|
||||
event.key === "Home"
|
||||
? 0
|
||||
: event.key === "End"
|
||||
? chips.length - 1
|
||||
: current === -1
|
||||
? forwards
|
||||
? 0
|
||||
: chips.length - 1
|
||||
: (current + (forwards ? 1 : -1) + chips.length) % chips.length;
|
||||
|
||||
chips[index].focus({
|
||||
trailing: event.key === "End" || (event.key !== "Home" && !forwards),
|
||||
});
|
||||
this._updateTabIndices();
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
:host {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: var(--ha-space-2);
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
|
||||
@@ -1,54 +1,81 @@
|
||||
import { styles as elevatedStyles } from "@material/web/chips/internal/elevated-styles.cssresult.js";
|
||||
import { FilterChip } from "@material/web/chips/internal/filter-chip";
|
||||
import { styles } from "@material/web/chips/internal/filter-styles.cssresult.js";
|
||||
import { styles as selectableStyles } from "@material/web/chips/internal/selectable-styles.cssresult.js";
|
||||
import { styles as sharedStyles } from "@material/web/chips/internal/shared-styles.cssresult.js";
|
||||
import { styles as trailingIconStyles } from "@material/web/chips/internal/trailing-icon-styles.cssresult.js";
|
||||
import { css, html } from "lit";
|
||||
import { mdiCheck } from "@mdi/js";
|
||||
import type { CSSResultGroup } from "lit";
|
||||
import { css, html, nothing } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { HaChipBase } from "./ha-chip-base";
|
||||
|
||||
@customElement("ha-filter-chip")
|
||||
export class HaFilterChip extends FilterChip {
|
||||
export class HaFilterChip extends HaChipBase {
|
||||
@property({ type: Boolean, reflect: true }) selected = false;
|
||||
|
||||
@property({ type: Boolean, reflect: true }) elevated = false;
|
||||
|
||||
@property({ type: Boolean, reflect: true, attribute: "no-leading-icon" })
|
||||
noLeadingIcon = false;
|
||||
|
||||
static override styles = [
|
||||
sharedStyles,
|
||||
elevatedStyles,
|
||||
trailingIconStyles,
|
||||
selectableStyles,
|
||||
styles,
|
||||
css`
|
||||
:host {
|
||||
--md-sys-color-primary: var(--primary-text-color);
|
||||
--md-sys-color-on-surface: var(--primary-text-color);
|
||||
--md-sys-color-on-surface-variant: var(--primary-text-color);
|
||||
--md-sys-color-on-secondary-container: var(--primary-text-color);
|
||||
--md-filter-chip-container-shape: var(--ha-border-radius-md);
|
||||
--md-filter-chip-outline-color: var(--outline-color);
|
||||
--md-filter-chip-selected-container-color: rgba(
|
||||
var(--rgb-primary-text-color),
|
||||
0.15
|
||||
);
|
||||
--_label-text-font: var(--ha-font-family-body);
|
||||
}
|
||||
`,
|
||||
];
|
||||
|
||||
protected getContainerClasses() {
|
||||
const classes = super.getContainerClasses();
|
||||
if (this.noLeadingIcon) {
|
||||
classes["has-icon"] = false;
|
||||
}
|
||||
return classes;
|
||||
protected override get pressed() {
|
||||
return this.selected;
|
||||
}
|
||||
|
||||
protected renderLeadingIcon() {
|
||||
protected override renderLeadingIcon() {
|
||||
if (this.noLeadingIcon) {
|
||||
// eslint-disable-next-line lit/prefer-nothing
|
||||
return html``;
|
||||
return nothing;
|
||||
}
|
||||
return super.renderLeadingIcon();
|
||||
|
||||
return this.selected
|
||||
? html`<span class="icon" aria-hidden="true">
|
||||
<slot name="selected-icon">
|
||||
<ha-svg-icon .path=${mdiCheck}></ha-svg-icon>
|
||||
</slot>
|
||||
</span>`
|
||||
: super.renderLeadingIcon();
|
||||
}
|
||||
|
||||
protected override handlePrimaryClick(event: MouseEvent) {
|
||||
if (this.disabled || this.softDisabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
const previous = this.selected;
|
||||
this.selected = !this.selected;
|
||||
event.stopPropagation();
|
||||
|
||||
// Dispatch from the chip so listeners see the new selection before deciding
|
||||
// whether to cancel it. Preserve pointer and modifier-key information.
|
||||
const click =
|
||||
typeof PointerEvent !== "undefined" && event instanceof PointerEvent
|
||||
? new PointerEvent("click", event)
|
||||
: new MouseEvent("click", event);
|
||||
|
||||
if (!this.dispatchEvent(click)) {
|
||||
this.selected = previous;
|
||||
event.preventDefault();
|
||||
}
|
||||
}
|
||||
|
||||
static override get styles(): CSSResultGroup {
|
||||
return [
|
||||
super.styles,
|
||||
css`
|
||||
:host {
|
||||
--ha-button-border-radius: var(--ha-border-radius-md);
|
||||
--ha-chip-label-weight: var(--ha-font-weight-medium);
|
||||
}
|
||||
:host([selected]) {
|
||||
--ha-chip-outline-width: var(
|
||||
--md-filter-chip-selected-outline-width,
|
||||
0px
|
||||
);
|
||||
--ha-chip-container-color: var(
|
||||
--md-filter-chip-selected-container-color,
|
||||
rgba(var(--rgb-primary-text-color), 0.15)
|
||||
);
|
||||
}
|
||||
:host([selected]:not([no-leading-icon])) .primary {
|
||||
padding-inline-start: var(--ha-space-2);
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,45 +1,46 @@
|
||||
import { InputChip } from "@material/web/chips/internal/input-chip";
|
||||
import { styles } from "@material/web/chips/internal/input-styles.cssresult.js";
|
||||
import { styles as selectableStyles } from "@material/web/chips/internal/selectable-styles.cssresult.js";
|
||||
import { styles as sharedStyles } from "@material/web/chips/internal/shared-styles.cssresult.js";
|
||||
import { styles as trailingIconStyles } from "@material/web/chips/internal/trailing-icon-styles.cssresult.js";
|
||||
import type { CSSResultGroup } from "lit";
|
||||
import { css } from "lit";
|
||||
import { customElement } from "lit/decorators";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { HaChipBase } from "./ha-chip-base";
|
||||
|
||||
@customElement("ha-input-chip")
|
||||
export class HaInputChip extends InputChip {
|
||||
static override styles = [
|
||||
sharedStyles,
|
||||
trailingIconStyles,
|
||||
selectableStyles,
|
||||
styles,
|
||||
css`
|
||||
:host {
|
||||
max-width: 100%;
|
||||
--md-sys-color-primary: var(--primary-text-color);
|
||||
--md-sys-color-on-surface: var(--primary-text-color);
|
||||
--md-sys-color-on-surface-variant: var(--primary-text-color);
|
||||
--md-sys-color-on-secondary-container: var(--primary-text-color);
|
||||
--md-input-chip-container-shape: 16px;
|
||||
--md-input-chip-outline-color: var(--outline-color);
|
||||
--md-input-chip-selected-container-color: rgba(
|
||||
var(--rgb-primary-text-color),
|
||||
0.15
|
||||
);
|
||||
--ha-input-chip-selected-container-opacity: 1;
|
||||
--md-input-chip-label-text-font: Roboto, sans-serif;
|
||||
--md-input-chip-label-text-weight: 400;
|
||||
}
|
||||
/** Set the size of mdc icons **/
|
||||
::slotted([slot="icon"]) {
|
||||
display: flex;
|
||||
--mdc-icon-size: var(--md-input-chip-icon-size, 18px);
|
||||
}
|
||||
.selected::before {
|
||||
opacity: var(--ha-input-chip-selected-container-opacity);
|
||||
}
|
||||
`,
|
||||
];
|
||||
export class HaInputChip extends HaChipBase {
|
||||
@property({ type: Boolean, reflect: true }) selected = false;
|
||||
|
||||
@property({ type: Boolean, reflect: true }) avatar = false;
|
||||
|
||||
override removable = true;
|
||||
|
||||
static override get styles(): CSSResultGroup {
|
||||
return [
|
||||
super.styles,
|
||||
css`
|
||||
:host {
|
||||
--ha-button-border-radius: 16px;
|
||||
--ha-chip-icon-size: var(--md-input-chip-icon-size, 18px);
|
||||
}
|
||||
:host([selected]) {
|
||||
--ha-chip-container-color: var(
|
||||
--md-input-chip-selected-container-color,
|
||||
rgba(var(--rgb-primary-text-color), 0.15)
|
||||
);
|
||||
--ha-chip-outline-width: var(
|
||||
--md-input-chip-selected-outline-width,
|
||||
0px
|
||||
);
|
||||
--ha-chip-container-opacity: var(
|
||||
--ha-input-chip-selected-container-opacity,
|
||||
1
|
||||
);
|
||||
}
|
||||
:host([avatar]) ::slotted([slot="icon"]) {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
border-radius: var(--ha-border-radius-circle);
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
|
||||
@@ -136,7 +136,9 @@ export class HaEntityNamePicker extends LitElement {
|
||||
${
|
||||
this.helper
|
||||
? html`
|
||||
<ha-input-helper-text> ${this.helper} </ha-input-helper-text>
|
||||
<ha-input-helper-text .disabled=${this.disabled}>
|
||||
${this.helper}
|
||||
</ha-input-helper-text>
|
||||
`
|
||||
: nothing
|
||||
}
|
||||
|
||||
@@ -329,7 +329,11 @@ export class HaStateContentPicker extends LitElement {
|
||||
|
||||
private _renderHelper() {
|
||||
return this.helper
|
||||
? html` <ha-input-helper-text> ${this.helper} </ha-input-helper-text> `
|
||||
? html`
|
||||
<ha-input-helper-text .disabled=${this.disabled}>
|
||||
${this.helper}
|
||||
</ha-input-helper-text>
|
||||
`
|
||||
: nothing;
|
||||
}
|
||||
|
||||
|
||||
@@ -11,10 +11,9 @@ import { customElement, property, query } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { ensureArray } from "../../common/array/ensure-array";
|
||||
import { type HASSDomEvent, fireEvent } from "../../common/dom/fire_event";
|
||||
import {
|
||||
computeEntityPickerDisplay,
|
||||
computeEntitySearchLabels,
|
||||
} from "../../common/entity/compute_entity_name_display";
|
||||
import { computeEntityNameList } from "../../common/entity/compute_entity_name_display";
|
||||
import { computeStateName } from "../../common/entity/compute_state_name";
|
||||
import { computeRTL } from "../../common/util/compute_rtl";
|
||||
import { domainToName } from "../../data/integration";
|
||||
import {
|
||||
getStatisticLabel,
|
||||
@@ -234,6 +233,11 @@ export class HaStatisticPicker extends LitElement {
|
||||
});
|
||||
}
|
||||
|
||||
const isRTL = computeRTL(
|
||||
hass.language,
|
||||
hass.translationMetadata.translations
|
||||
);
|
||||
|
||||
const output: StatisticComboBoxItem[] = [];
|
||||
|
||||
statisticIds.forEach((meta) => {
|
||||
@@ -287,17 +291,31 @@ export class HaStatisticPicker extends LitElement {
|
||||
}
|
||||
const id = meta.statistic_id;
|
||||
|
||||
const { primary, secondary } = computeEntityPickerDisplay(
|
||||
hass,
|
||||
stateObj
|
||||
);
|
||||
const searchLabels = computeEntitySearchLabels(
|
||||
stateObj,
|
||||
hass.entities,
|
||||
hass.devices,
|
||||
hass.areas,
|
||||
hass.floors
|
||||
);
|
||||
const friendlyName = computeStateName(stateObj); // Keep this for search
|
||||
|
||||
const [entityName, deviceName, parentDeviceName, areaName] =
|
||||
computeEntityNameList(
|
||||
stateObj,
|
||||
[
|
||||
{ type: "entity" },
|
||||
{ type: "device" },
|
||||
{ type: "parent_device" },
|
||||
{ type: "area" },
|
||||
],
|
||||
hass.entities,
|
||||
hass.devices,
|
||||
hass.areas,
|
||||
hass.floors
|
||||
);
|
||||
|
||||
const primary = entityName || deviceName || id;
|
||||
const secondary = [
|
||||
areaName,
|
||||
parentDeviceName,
|
||||
entityName ? deviceName : undefined,
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(isRTL ? " ◂ " : " ▸ ");
|
||||
|
||||
const sortingPrefix = `${TYPE_ORDER.indexOf("entity")}`;
|
||||
output.push({
|
||||
@@ -307,12 +325,14 @@ export class HaStatisticPicker extends LitElement {
|
||||
secondary,
|
||||
stateObj: stateObj,
|
||||
type: "entity",
|
||||
sorting_label: [
|
||||
sortingPrefix,
|
||||
searchLabels.deviceName,
|
||||
searchLabels.entityName,
|
||||
].join("_"),
|
||||
search_labels: searchLabels,
|
||||
sorting_label: [sortingPrefix, deviceName, entityName].join("_"),
|
||||
search_labels: {
|
||||
entityName: entityName || null,
|
||||
deviceName: deviceName || null,
|
||||
parentDeviceName: parentDeviceName || null,
|
||||
areaName: areaName || null,
|
||||
friendlyName,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
@@ -387,18 +407,36 @@ export class HaStatisticPicker extends LitElement {
|
||||
const stateObj = this.hass.states[statisticId];
|
||||
|
||||
if (stateObj) {
|
||||
const { primary, secondary } = computeEntityPickerDisplay(
|
||||
this.hass,
|
||||
stateObj
|
||||
);
|
||||
const searchLabels = computeEntitySearchLabels(
|
||||
stateObj,
|
||||
this.hass.entities,
|
||||
this.hass.devices,
|
||||
this.hass.areas,
|
||||
this.hass.floors
|
||||
const [entityName, deviceName, parentDeviceName, areaName] =
|
||||
computeEntityNameList(
|
||||
stateObj,
|
||||
[
|
||||
{ type: "entity" },
|
||||
{ type: "device" },
|
||||
{ type: "parent_device" },
|
||||
{ type: "area" },
|
||||
],
|
||||
this.hass.entities,
|
||||
this.hass.devices,
|
||||
this.hass.areas,
|
||||
this.hass.floors
|
||||
);
|
||||
|
||||
const isRTL = computeRTL(
|
||||
this.hass.language,
|
||||
this.hass.translationMetadata.translations
|
||||
);
|
||||
|
||||
const primary = entityName || deviceName || statisticId;
|
||||
const secondary = [
|
||||
areaName,
|
||||
parentDeviceName,
|
||||
entityName ? deviceName : undefined,
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(isRTL ? " ◂ " : " ▸ ");
|
||||
const friendlyName = computeStateName(stateObj); // Keep this for search
|
||||
|
||||
const sortingPrefix = `${TYPE_ORDER.indexOf("entity")}`;
|
||||
return {
|
||||
id: statisticId,
|
||||
@@ -407,12 +445,15 @@ export class HaStatisticPicker extends LitElement {
|
||||
secondary,
|
||||
stateObj: stateObj,
|
||||
type: "entity",
|
||||
sorting_label: [
|
||||
sortingPrefix,
|
||||
searchLabels.deviceName,
|
||||
searchLabels.entityName,
|
||||
].join("_"),
|
||||
search_labels: { ...searchLabels, statisticId },
|
||||
sorting_label: [sortingPrefix, deviceName, entityName].join("_"),
|
||||
search_labels: {
|
||||
entityName: entityName || null,
|
||||
deviceName: deviceName || null,
|
||||
parentDeviceName: parentDeviceName || null,
|
||||
areaName: areaName || null,
|
||||
friendlyName,
|
||||
statisticId,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import type { CSSResultGroup, TemplateResult } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, 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 type { Analytics, AnalyticsPreferences } from "../data/analytics";
|
||||
@@ -21,15 +20,13 @@ declare global {
|
||||
|
||||
@customElement("ha-analytics")
|
||||
export class HaAnalytics extends LitElement {
|
||||
@property({ attribute: false }) public localize!: LocalizeFunc;
|
||||
|
||||
@property({ attribute: false }) public analytics?: Analytics;
|
||||
|
||||
@property({ attribute: "translation_key_panel" }) public translationKeyPanel:
|
||||
"page-onboarding" | "config" = "config";
|
||||
|
||||
@state()
|
||||
@consumeLocalize()
|
||||
private _localize!: LocalizeFunc;
|
||||
|
||||
protected render(): TemplateResult {
|
||||
const loading = this.analytics === undefined;
|
||||
const baseEnabled = !loading && this.analytics!.preferences.base;
|
||||
@@ -37,12 +34,12 @@ export class HaAnalytics extends LitElement {
|
||||
return html`
|
||||
<ha-row-item>
|
||||
<span slot="headline"
|
||||
>${this._localize(
|
||||
>${this.localize(
|
||||
`ui.panel.${this.translationKeyPanel}.analytics.preferences.base.title`
|
||||
)}</span
|
||||
>
|
||||
<span slot="supporting-text"
|
||||
>${this._localize(
|
||||
>${this.localize(
|
||||
`ui.panel.${this.translationKeyPanel}.analytics.preferences.base.description`
|
||||
)}</span
|
||||
>
|
||||
@@ -54,7 +51,7 @@ export class HaAnalytics extends LitElement {
|
||||
.disabled=${loading}
|
||||
name="base"
|
||||
>
|
||||
${this._localize(
|
||||
${this.localize(
|
||||
`ui.panel.${this.translationKeyPanel}.analytics.preferences.base.title`
|
||||
)}
|
||||
</ha-switch>
|
||||
@@ -63,12 +60,12 @@ export class HaAnalytics extends LitElement {
|
||||
(preference) => html`
|
||||
<ha-row-item>
|
||||
<span slot="headline"
|
||||
>${this._localize(
|
||||
>${this.localize(
|
||||
`ui.panel.${this.translationKeyPanel}.analytics.preferences.${preference}.title`
|
||||
)}</span
|
||||
>
|
||||
<span slot="supporting-text"
|
||||
>${this._localize(
|
||||
>${this.localize(
|
||||
`ui.panel.${this.translationKeyPanel}.analytics.preferences.${preference}.description`
|
||||
)}</span
|
||||
>
|
||||
@@ -80,7 +77,7 @@ export class HaAnalytics extends LitElement {
|
||||
.preference=${preference}
|
||||
name=${preference}
|
||||
>
|
||||
${this._localize(
|
||||
${this.localize(
|
||||
`ui.panel.${this.translationKeyPanel}.analytics.preferences.${preference}.title`
|
||||
)}
|
||||
</ha-switch>
|
||||
@@ -91,7 +88,7 @@ export class HaAnalytics extends LitElement {
|
||||
.for="switch-${preference}"
|
||||
placement="right"
|
||||
>
|
||||
${this._localize(
|
||||
${this.localize(
|
||||
`ui.panel.${this.translationKeyPanel}.analytics.need_base_enabled`
|
||||
)}
|
||||
</ha-tooltip>`
|
||||
@@ -101,12 +98,12 @@ export class HaAnalytics extends LitElement {
|
||||
)}
|
||||
<ha-row-item>
|
||||
<span slot="headline"
|
||||
>${this._localize(
|
||||
>${this.localize(
|
||||
`ui.panel.${this.translationKeyPanel}.analytics.preferences.diagnostics.title`
|
||||
)}</span
|
||||
>
|
||||
<span slot="supporting-text"
|
||||
>${this._localize(
|
||||
>${this.localize(
|
||||
`ui.panel.${this.translationKeyPanel}.analytics.preferences.diagnostics.description`
|
||||
)}</span
|
||||
>
|
||||
@@ -118,7 +115,7 @@ export class HaAnalytics extends LitElement {
|
||||
.disabled=${loading}
|
||||
name="diagnostics"
|
||||
>
|
||||
${this._localize(
|
||||
${this.localize(
|
||||
`ui.panel.${this.translationKeyPanel}.analytics.preferences.diagnostics.title`
|
||||
)}
|
||||
</ha-switch>
|
||||
|
||||
@@ -3,7 +3,8 @@ import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import Fuse from "fuse.js";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { computeEntityPickerDisplay } from "../common/entity/compute_entity_name_display";
|
||||
import { computeEntityNameList } from "../common/entity/compute_entity_name_display";
|
||||
import { computeRTL } from "../common/util/compute_rtl";
|
||||
import type { LocalizeFunc } from "../common/translations/localize";
|
||||
import {
|
||||
multiTermSortedSearch,
|
||||
@@ -182,6 +183,11 @@ export class HaAreaControlsPicker extends LitElement {
|
||||
const allEntityIds = Object.values(controlEntities).flat();
|
||||
const uniqueEntityIds = Array.from(new Set(allEntityIds));
|
||||
|
||||
const isRTL = computeRTL(
|
||||
this.hass.language,
|
||||
this.hass.translationMetadata.translations
|
||||
);
|
||||
|
||||
uniqueEntityIds.forEach((entityId) => {
|
||||
if (isSelected(entityId)) {
|
||||
return;
|
||||
@@ -191,10 +197,29 @@ export class HaAreaControlsPicker extends LitElement {
|
||||
return;
|
||||
}
|
||||
|
||||
const { primary, secondary } = computeEntityPickerDisplay(
|
||||
this.hass!,
|
||||
stateObj
|
||||
);
|
||||
const [entityName, deviceName, parentDeviceName, areaName] =
|
||||
computeEntityNameList(
|
||||
stateObj,
|
||||
[
|
||||
{ type: "entity" },
|
||||
{ type: "device" },
|
||||
{ type: "parent_device" },
|
||||
{ type: "area" },
|
||||
],
|
||||
this.hass!.entities,
|
||||
this.hass!.devices,
|
||||
this.hass!.areas,
|
||||
this.hass!.floors
|
||||
);
|
||||
|
||||
const primary = entityName || deviceName || entityId;
|
||||
const secondary = [
|
||||
areaName,
|
||||
parentDeviceName,
|
||||
entityName ? deviceName : undefined,
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(isRTL ? " ◂ " : " ▸ ");
|
||||
|
||||
entityItems.push({
|
||||
type: "entity",
|
||||
|
||||
@@ -312,7 +312,9 @@ export class HaBaseTimeInput extends LitElement {
|
||||
</div>
|
||||
${
|
||||
this.helper
|
||||
? html`<ha-input-helper-text>${this.helper}</ha-input-helper-text>`
|
||||
? html`<ha-input-helper-text .disabled=${this.disabled}
|
||||
>${this.helper}</ha-input-helper-text
|
||||
>`
|
||||
: nothing
|
||||
}
|
||||
`;
|
||||
|
||||
@@ -198,7 +198,11 @@ export class HaClockDateFormatPicker extends LitElement {
|
||||
|
||||
private _renderHelper() {
|
||||
return this.helper
|
||||
? html` <ha-input-helper-text> ${this.helper} </ha-input-helper-text> `
|
||||
? html`
|
||||
<ha-input-helper-text .disabled=${this.disabled}>
|
||||
${this.helper}
|
||||
</ha-input-helper-text>
|
||||
`
|
||||
: 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]);
|
||||
@@ -272,11 +254,6 @@ export class HaDurationInput extends LitElement {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
/* Full width: bigger touch targets, no ragged right edge in a form. */
|
||||
ha-base-time-input {
|
||||
flex: 1;
|
||||
--time-input-flex: 1;
|
||||
}
|
||||
ha-button-toggle-group {
|
||||
margin: var(--ha-space-2);
|
||||
}
|
||||
|
||||
@@ -27,9 +27,9 @@ declare global {
|
||||
|
||||
@customElement("ha-file-upload")
|
||||
export class HaFileUpload extends LitElement {
|
||||
@state()
|
||||
@consumeLocalize()
|
||||
private _localize!: LocalizeFunc;
|
||||
@property({ attribute: false }) public localize?: LocalizeFunc;
|
||||
|
||||
@state() @consumeLocalize() private _localize?: LocalizeFunc;
|
||||
|
||||
@state()
|
||||
@consume({ context: internationalizationContext, subscribe: true })
|
||||
@@ -92,6 +92,7 @@ export class HaFileUpload extends LitElement {
|
||||
}
|
||||
|
||||
public render(): TemplateResult {
|
||||
const localize = this.localize || this._localize!;
|
||||
return html`
|
||||
${
|
||||
this.uploading
|
||||
@@ -101,13 +102,10 @@ export class HaFileUpload extends LitElement {
|
||||
>${
|
||||
this.uploadingLabel ||
|
||||
(this.value
|
||||
? this._localize(
|
||||
"ui.components.file-upload.uploading_name",
|
||||
{
|
||||
name: this._name,
|
||||
}
|
||||
)
|
||||
: this._localize("ui.components.file-upload.uploading"))
|
||||
? localize("ui.components.file-upload.uploading_name", {
|
||||
name: this._name,
|
||||
})
|
||||
: localize("ui.components.file-upload.uploading"))
|
||||
}</span
|
||||
>
|
||||
${
|
||||
@@ -149,12 +147,12 @@ export class HaFileUpload extends LitElement {
|
||||
slot="start"
|
||||
.path=${this.icon || mdiFileUpload}
|
||||
></ha-svg-icon>
|
||||
${this.label || this._localize("ui.components.file-upload.label")}
|
||||
${this.label || localize("ui.components.file-upload.label")}
|
||||
</ha-button>
|
||||
<span class="secondary"
|
||||
>${
|
||||
this.secondary ||
|
||||
this._localize("ui.components.file-upload.secondary")
|
||||
localize("ui.components.file-upload.secondary")
|
||||
}</span
|
||||
>
|
||||
<span class="supports">${this.supports}</span>`
|
||||
@@ -168,7 +166,7 @@ export class HaFileUpload extends LitElement {
|
||||
</div>
|
||||
<ha-icon-button
|
||||
@click=${this._clearValue}
|
||||
.label=${this.deleteLabel || this._localize("ui.common.delete")}
|
||||
.label=${this.deleteLabel || localize("ui.common.delete")}
|
||||
.path=${mdiDelete}
|
||||
></ha-icon-button>
|
||||
</div>`
|
||||
@@ -187,8 +185,7 @@ export class HaFileUpload extends LitElement {
|
||||
<ha-icon-button
|
||||
@click=${this._clearValue}
|
||||
.label=${
|
||||
this.deleteLabel ||
|
||||
this._localize("ui.common.delete")
|
||||
this.deleteLabel || localize("ui.common.delete")
|
||||
}
|
||||
.path=${mdiDelete}
|
||||
></ha-icon-button>
|
||||
|
||||
@@ -60,9 +60,6 @@ export class HaFilterDevices extends LitElement {
|
||||
|
||||
@property() public type?: keyof RelatedResult;
|
||||
|
||||
@property({ type: Boolean, attribute: "include-disabled-entities" })
|
||||
public includeDisabledEntities = false;
|
||||
|
||||
@property({ type: Boolean, reflect: true }) public expanded = false;
|
||||
|
||||
@property({ type: Boolean }) public narrow = false;
|
||||
@@ -230,14 +227,7 @@ export class HaFilterDevices extends LitElement {
|
||||
for (const deviceId of this.value) {
|
||||
value.push(deviceId);
|
||||
if (this.type) {
|
||||
relatedPromises.push(
|
||||
findRelated(
|
||||
this._api,
|
||||
"device",
|
||||
deviceId,
|
||||
this.includeDisabledEntities
|
||||
)
|
||||
);
|
||||
relatedPromises.push(findRelated(this._api, "device", deviceId));
|
||||
}
|
||||
}
|
||||
const results = await Promise.all(relatedPromises);
|
||||
|
||||
@@ -65,9 +65,6 @@ export class HaFilterFloorAreas extends LitElement {
|
||||
|
||||
@property() public type?: keyof RelatedResult;
|
||||
|
||||
@property({ type: Boolean, attribute: "include-disabled-entities" })
|
||||
public includeDisabledEntities = false;
|
||||
|
||||
@property({ type: Boolean }) public narrow = false;
|
||||
|
||||
@property({ type: Boolean, reflect: true }) public expanded = false;
|
||||
@@ -298,9 +295,7 @@ export class HaFilterFloorAreas extends LitElement {
|
||||
if (this.value.areas) {
|
||||
for (const areaId of this.value.areas) {
|
||||
if (this.type) {
|
||||
relatedPromises.push(
|
||||
findRelated(this._api, "area", areaId, this.includeDisabledEntities)
|
||||
);
|
||||
relatedPromises.push(findRelated(this._api, "area", areaId));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -308,14 +303,7 @@ export class HaFilterFloorAreas extends LitElement {
|
||||
if (this.value.floors) {
|
||||
for (const floorId of this.value.floors) {
|
||||
if (this.type) {
|
||||
relatedPromises.push(
|
||||
findRelated(
|
||||
this._api,
|
||||
"floor",
|
||||
floorId,
|
||||
this.includeDisabledEntities
|
||||
)
|
||||
);
|
||||
relatedPromises.push(findRelated(this._api, "floor", floorId));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -91,7 +91,7 @@ export class HaFormInteger extends LitElement implements HaFormElement {
|
||||
</div>
|
||||
${
|
||||
this.helper
|
||||
? html`<ha-input-helper-text
|
||||
? html`<ha-input-helper-text .disabled=${this.disabled}
|
||||
>${this.helper}</ha-input-helper-text
|
||||
>`
|
||||
: nothing
|
||||
|
||||
@@ -329,7 +329,7 @@ export class HaGenericPicker extends PickerMixin(LitElement) {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
return html`<ha-input-helper-text>
|
||||
return html`<ha-input-helper-text .disabled=${this.disabled}>
|
||||
${
|
||||
showError
|
||||
? html`<span class="error">${this.errorMessage}</span> ${
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,11 @@
|
||||
import type { TemplateResult } from "lit";
|
||||
import { css, html, LitElement } from "lit";
|
||||
import { customElement } from "lit/decorators";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
|
||||
@customElement("ha-input-helper-text")
|
||||
class InputHelperText extends LitElement {
|
||||
@property({ type: Boolean, reflect: true }) disabled = false;
|
||||
|
||||
protected render(): TemplateResult {
|
||||
return html`<slot></slot>`;
|
||||
}
|
||||
@@ -23,6 +25,9 @@ class InputHelperText extends LitElement {
|
||||
);
|
||||
line-height: normal;
|
||||
}
|
||||
:host([disabled]) {
|
||||
color: var(--mdc-text-field-disabled-ink-color, rgba(0, 0, 0, 0.6));
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
|
||||
@@ -50,7 +50,9 @@ class HaLabeledSlider extends LitElement {
|
||||
</div>
|
||||
${
|
||||
this.helper
|
||||
? html`<ha-input-helper-text> ${this.helper} </ha-input-helper-text>`
|
||||
? html`<ha-input-helper-text .disabled=${this.disabled}>
|
||||
${this.helper}
|
||||
</ha-input-helper-text>`
|
||||
: nothing
|
||||
}
|
||||
`;
|
||||
|
||||
@@ -239,7 +239,9 @@ export class HaMediaItemPicker extends LitElement {
|
||||
}
|
||||
${
|
||||
hideEntityPicker && this.helper
|
||||
? html`<ha-input-helper-text>${this.helper}</ha-input-helper-text>`
|
||||
? html`<ha-input-helper-text .disabled=${this.disabled}
|
||||
>${this.helper}</ha-input-helper-text
|
||||
>`
|
||||
: nothing
|
||||
}
|
||||
`;
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
import { OutlinedButton } from "@material/web/button/internal/outlined-button";
|
||||
import { styles as sharedStyles } from "@material/web/button/internal/shared-styles.cssresult.js";
|
||||
import { styles } from "@material/web/button/internal/outlined-styles.cssresult.js";
|
||||
import { css } from "lit";
|
||||
import { customElement } from "lit/decorators";
|
||||
|
||||
@customElement("ha-outlined-button")
|
||||
export class HaOutlinedButton extends OutlinedButton {
|
||||
static override styles = [
|
||||
sharedStyles,
|
||||
styles,
|
||||
css`
|
||||
:host {
|
||||
--ha-icon-display: block;
|
||||
--md-sys-color-primary: var(--primary-text-color);
|
||||
--md-sys-color-outline: var(--outline-color);
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-outlined-button": HaOutlinedButton;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
import { OutlinedField } from "@material/web/field/internal/outlined-field";
|
||||
import { styles } from "@material/web/field/internal/outlined-styles.cssresult.js";
|
||||
import { styles as sharedStyles } from "@material/web/field/internal/shared-styles.cssresult.js";
|
||||
import { css } from "lit";
|
||||
import { customElement } from "lit/decorators";
|
||||
import { literal } from "lit/static-html";
|
||||
|
||||
@customElement("ha-outlined-field")
|
||||
export class HaOutlinedField extends OutlinedField {
|
||||
protected readonly fieldTag = literal`ha-outlined-field`;
|
||||
|
||||
static override styles = [
|
||||
sharedStyles,
|
||||
styles,
|
||||
css`
|
||||
.container::before {
|
||||
display: block;
|
||||
content: "";
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background-color: var(--ha-outlined-field-container-color, transparent);
|
||||
opacity: var(--ha-outlined-field-container-opacity, 1);
|
||||
border-start-start-radius: var(--_container-shape-start-start);
|
||||
border-start-end-radius: var(--_container-shape-start-end);
|
||||
border-end-start-radius: var(--_container-shape-end-start);
|
||||
border-end-end-radius: var(--_container-shape-end-end);
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-outlined-field": HaOutlinedField;
|
||||
}
|
||||
}
|
||||
@@ -164,7 +164,9 @@ export class HaSelect extends LitElement {
|
||||
|
||||
private _renderHelper() {
|
||||
return this.helper
|
||||
? html`<ha-input-helper-text>${this.helper}</ha-input-helper-text>`
|
||||
? html`<ha-input-helper-text .disabled=${this.disabled}
|
||||
>${this.helper}</ha-input-helper-text
|
||||
>`
|
||||
: nothing;
|
||||
}
|
||||
|
||||
|
||||
@@ -72,7 +72,9 @@ export class HaSelectorAutomationBehavior extends LitElement {
|
||||
></ha-select-box>
|
||||
${
|
||||
this.helper
|
||||
? html`<ha-input-helper-text>${this.helper}</ha-input-helper-text>`
|
||||
? html`<ha-input-helper-text .disabled=${this.disabled}
|
||||
>${this.helper}</ha-input-helper-text
|
||||
>`
|
||||
: nothing
|
||||
}
|
||||
`;
|
||||
|
||||
@@ -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];
|
||||
|
||||
@@ -68,7 +68,9 @@ export class HaDateTimeSelector extends LitElement {
|
||||
</div>
|
||||
${
|
||||
this.helper
|
||||
? html`<ha-input-helper-text>${this.helper}</ha-input-helper-text>`
|
||||
? html`<ha-input-helper-text .disabled=${this.disabled}
|
||||
>${this.helper}</ha-input-helper-text
|
||||
>`
|
||||
: ""
|
||||
}
|
||||
`;
|
||||
|
||||
@@ -4,7 +4,6 @@ import memoizeOne from "memoize-one";
|
||||
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 {
|
||||
@@ -28,8 +27,33 @@ export class HaTimeDuration extends LitElement {
|
||||
}
|
||||
|
||||
private _data = memoizeOne(
|
||||
(value?: HaDurationData | string | number): HaDurationData | undefined =>
|
||||
createDurationData(value)
|
||||
(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;
|
||||
}
|
||||
);
|
||||
|
||||
protected render() {
|
||||
|
||||
@@ -143,7 +143,9 @@ export class HaNumberSelector extends LitElement {
|
||||
</div>
|
||||
${
|
||||
!isBox && this.helper
|
||||
? html`<ha-input-helper-text>${this.helper}</ha-input-helper-text>`
|
||||
? html`<ha-input-helper-text .disabled=${this.disabled}
|
||||
>${this.helper}</ha-input-helper-text
|
||||
>`
|
||||
: nothing
|
||||
}
|
||||
`;
|
||||
|
||||
@@ -204,7 +204,9 @@ export class HaObjectSelector extends LitElement {
|
||||
></ha-yaml-editor>
|
||||
${
|
||||
this.helper
|
||||
? html`<ha-input-helper-text>${this.helper}</ha-input-helper-text>`
|
||||
? html`<ha-input-helper-text .disabled=${this.disabled}
|
||||
>${this.helper}</ha-input-helper-text
|
||||
>`
|
||||
: ""
|
||||
} `;
|
||||
}
|
||||
|
||||
@@ -268,7 +268,9 @@ export class HaSelectSelector extends LitElement {
|
||||
|
||||
private _renderHelper() {
|
||||
return this.helper
|
||||
? html`<ha-input-helper-text>${this.helper}</ha-input-helper-text>`
|
||||
? html`<ha-input-helper-text .disabled=${this.disabled}
|
||||
>${this.helper}</ha-input-helper-text
|
||||
>`
|
||||
: "";
|
||||
}
|
||||
|
||||
|
||||
@@ -132,7 +132,9 @@ ${
|
||||
}
|
||||
${
|
||||
this.helper
|
||||
? html`<ha-input-helper-text>${this.helper}</ha-input-helper-text>`
|
||||
? html`<ha-input-helper-text .disabled=${this.disabled}
|
||||
>${this.helper}</ha-input-helper-text
|
||||
>`
|
||||
: nothing
|
||||
}
|
||||
`;
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
import { SubMenu } from "@material/web/menu/internal/submenu/sub-menu";
|
||||
import { styles } from "@material/web/menu/internal/submenu/sub-menu-styles.cssresult.js";
|
||||
import { css } from "lit";
|
||||
import { customElement } from "lit/decorators";
|
||||
|
||||
@customElement("ha-sub-menu")
|
||||
export class HaSubMenu extends SubMenu {
|
||||
async show() {
|
||||
super.show();
|
||||
this.menu.hasOverflow = false;
|
||||
}
|
||||
|
||||
static override styles = [
|
||||
styles,
|
||||
css`
|
||||
:host {
|
||||
--ha-icon-display: block;
|
||||
--md-sys-color-primary: var(--primary-text-color);
|
||||
--md-sys-color-on-primary: var(--primary-text-color);
|
||||
--md-sys-color-secondary: var(--secondary-text-color);
|
||||
--md-sys-color-surface: var(--card-background-color);
|
||||
--md-sys-color-on-surface: var(--primary-text-color);
|
||||
--md-sys-color-on-surface-variant: var(--secondary-text-color);
|
||||
--md-sys-color-secondary-container: rgba(
|
||||
var(--rgb-primary-color),
|
||||
0.15
|
||||
);
|
||||
--md-sys-color-on-secondary-container: var(--text-primary-color);
|
||||
--mdc-icon-size: 16px;
|
||||
|
||||
--md-sys-color-on-primary-container: var(--primary-text-color);
|
||||
--md-sys-color-on-secondary-container: var(--primary-text-color);
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-sub-menu": HaSubMenu;
|
||||
}
|
||||
}
|
||||
@@ -208,7 +208,9 @@ class HaInputMulti extends LitElement {
|
||||
</div>
|
||||
${
|
||||
this.helper
|
||||
? html`<ha-input-helper-text>${this.helper}</ha-input-helper-text>`
|
||||
? html`<ha-input-helper-text .disabled=${this.disabled}
|
||||
>${this.helper}</ha-input-helper-text
|
||||
>`
|
||||
: nothing
|
||||
}
|
||||
`;
|
||||
|
||||
@@ -24,13 +24,11 @@ class HaEntityMarker extends LitElement {
|
||||
|
||||
@property({ attribute: "show-icon", type: Boolean }) public showIcon = false;
|
||||
|
||||
@property({ type: Boolean, reflect: true }) public selected = false;
|
||||
|
||||
protected render() {
|
||||
return html`
|
||||
<div
|
||||
class="marker ${this.entityPicture ? "picture" : ""}"
|
||||
style=${styleMap({ "--ha-marker-selected-color": this.entityColor })}
|
||||
style=${styleMap({ "border-color": this.entityColor })}
|
||||
@click=${this._badgeTap}
|
||||
>
|
||||
${
|
||||
@@ -96,22 +94,13 @@ class HaEntityMarker extends LitElement {
|
||||
height: var(--ha-marker-size, 48px);
|
||||
font-size: var(--ha-marker-font-size, var(--ha-font-size-xl));
|
||||
border-radius: var(--ha-marker-border-radius, 50%);
|
||||
border: var(--ha-marker-border-width, 3px) solid
|
||||
var(--ha-marker-color, var(--card-background-color, #fff));
|
||||
box-shadow: var(--ha-marker-shadow, var(--ha-box-shadow-s));
|
||||
border: 1px solid var(--ha-marker-color, var(--primary-color));
|
||||
color: var(--primary-text-color);
|
||||
background-color: var(
|
||||
--ha-marker-background,
|
||||
var(--card-background-color)
|
||||
);
|
||||
background-color: var(--card-background-color);
|
||||
}
|
||||
.marker.picture {
|
||||
overflow: hidden;
|
||||
}
|
||||
/* A ring in the entity color outside the frame marks the selected marker */
|
||||
:host([selected]) .marker {
|
||||
outline: 3px solid var(--ha-marker-selected-color, var(--primary-color));
|
||||
}
|
||||
.entity-picture {
|
||||
background-size: cover;
|
||||
height: 100%;
|
||||
|
||||
@@ -1,17 +1,14 @@
|
||||
import type { ContextType } from "@lit/context";
|
||||
import { consume } from "@lit/context";
|
||||
import type { PropertyValues, TemplateResult } from "lit";
|
||||
import { css, html, LitElement } from "lit";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { transform } from "../../common/decorators/transform";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import type { HASSDomEvent } from "../../common/dom/fire_event";
|
||||
import { MAP_MAX_ZOOM } from "../../common/map/base-layer";
|
||||
import type { MapLatLng } from "../../common/map/map-engine";
|
||||
import { circleBoundsPoints } from "../../common/map/map-engine";
|
||||
import { internationalizationContext, uiContext } from "../../data/context";
|
||||
import type { Themes } from "../../data/ws-themes";
|
||||
import { internationalizationContext } from "../../data/context";
|
||||
import type { HomeAssistantInternationalization, ThemeMode } from "../../types";
|
||||
import "../ha-alert";
|
||||
import "../ha-input-helper-text";
|
||||
@@ -19,10 +16,6 @@ import "./ha-map";
|
||||
import type { HaMap, HaMapEditableLocation } from "./ha-map";
|
||||
import type { HaIcon } from "../ha-icon";
|
||||
import type { HaSvgIcon } from "../ha-svg-icon";
|
||||
import {
|
||||
createZoneMarkerElement,
|
||||
ZONE_CIRCLE_SIZE,
|
||||
} from "../../common/map/zone-marker";
|
||||
|
||||
declare global {
|
||||
// for fire event
|
||||
@@ -75,15 +68,6 @@ export class HaLocationsEditor extends LitElement {
|
||||
|
||||
@state() private _editingAvailable = true;
|
||||
|
||||
// Marker elements bake in theme colors, so they are rebuilt on a theme
|
||||
// change; narrow the UI context to themes to avoid unrelated rerenders
|
||||
@state()
|
||||
@consume({ context: uiContext, subscribe: true })
|
||||
@transform<ContextType<typeof uiContext>, Themes>({
|
||||
transformer: ({ themes }) => themes,
|
||||
})
|
||||
private _themes?: Themes;
|
||||
|
||||
@state()
|
||||
@consume({ context: internationalizationContext, subscribe: true })
|
||||
private _i18n?: HomeAssistantInternationalization;
|
||||
@@ -124,10 +108,7 @@ export class HaLocationsEditor extends LitElement {
|
||||
return html`
|
||||
<div class="map">
|
||||
<ha-map
|
||||
.editableLocations=${this._editableLocations(
|
||||
this.locations,
|
||||
this._themes
|
||||
)}
|
||||
.editableLocations=${this._editableLocations(this.locations)}
|
||||
.zoom=${this.zoom}
|
||||
.autoFit=${this.autoFit}
|
||||
.themeMode=${this.themeMode}
|
||||
@@ -159,14 +140,7 @@ export class HaLocationsEditor extends LitElement {
|
||||
}
|
||||
|
||||
private _editableLocations = memoizeOne(
|
||||
(
|
||||
locations: MarkerLocation[] | undefined,
|
||||
themes: Themes | undefined
|
||||
): HaMapEditableLocation[] => {
|
||||
if (themes !== this._elementsThemes) {
|
||||
this._elementsThemes = themes;
|
||||
this._elements.clear();
|
||||
}
|
||||
(locations?: MarkerLocation[]): HaMapEditableLocation[] => {
|
||||
const ids = new Set((locations ?? []).map((location) => location.id));
|
||||
for (const id of this._elements.keys()) {
|
||||
if (!ids.has(id)) {
|
||||
@@ -178,9 +152,7 @@ export class HaLocationsEditor extends LitElement {
|
||||
location: [location.latitude, location.longitude],
|
||||
radius: location.radius,
|
||||
element: this._elementFor(location),
|
||||
elementSize: location.radius
|
||||
? [ZONE_CIRCLE_SIZE, ZONE_CIRCLE_SIZE]
|
||||
: [ICON_SIZE, ICON_SIZE],
|
||||
elementSize: [ICON_SIZE, ICON_SIZE],
|
||||
title: location.name,
|
||||
color: location.radius_color,
|
||||
locationEditable: location.location_editable,
|
||||
@@ -193,46 +165,30 @@ export class HaLocationsEditor extends LitElement {
|
||||
// Reused while unchanged, so ha-map moves markers instead of rebuilding them
|
||||
private _elements = new Map<string, { key: string; element?: HTMLElement }>();
|
||||
|
||||
private _elementsThemes?: Themes;
|
||||
|
||||
private _elementFor(location: MarkerLocation): HTMLElement | undefined {
|
||||
const isZone = !!location.radius;
|
||||
const key = JSON.stringify([
|
||||
isZone,
|
||||
location.icon,
|
||||
location.iconPath,
|
||||
location.name,
|
||||
location.radius_color,
|
||||
location.location_editable,
|
||||
]);
|
||||
const cached = this._elements.get(location.id);
|
||||
if (cached?.key === key) {
|
||||
return cached.element;
|
||||
}
|
||||
// The zone marker doubles as the circle's draggable center
|
||||
const element = isZone
|
||||
? this._createZoneMarker(location)
|
||||
: this._createIcon(location);
|
||||
const element = this._createIcon(location);
|
||||
this._elements.set(location.id, { key, element });
|
||||
return element;
|
||||
}
|
||||
|
||||
private _createZoneMarker(location: MarkerLocation): HTMLElement {
|
||||
return createZoneMarkerElement({
|
||||
color:
|
||||
location.radius_color ||
|
||||
getComputedStyle(this).getPropertyValue("--accent-color"),
|
||||
icon: location.icon,
|
||||
iconPath: location.iconPath,
|
||||
name: location.name ?? "",
|
||||
});
|
||||
}
|
||||
|
||||
private _createIcon(location: MarkerLocation): HTMLElement | undefined {
|
||||
if (!location.icon && !location.iconPath) {
|
||||
return undefined;
|
||||
}
|
||||
const el = document.createElement("div");
|
||||
el.className = "named-icon";
|
||||
el.className = `named-icon ${
|
||||
location.location_editable ? "draggable" : ""
|
||||
}`;
|
||||
if (location.name !== undefined) {
|
||||
el.innerText = location.name;
|
||||
}
|
||||
@@ -340,13 +296,12 @@ export class HaLocationsEditor extends LitElement {
|
||||
display: block;
|
||||
height: 100%;
|
||||
}
|
||||
/* Over the map, clear of the zoom control, so a fixed-height host shows it.
|
||||
The control sits at the physical top-left in either direction. */
|
||||
/* Over the map, clear of the zoom control, so a fixed-height host shows it */
|
||||
ha-alert {
|
||||
position: absolute;
|
||||
top: var(--ha-space-2);
|
||||
left: 56px;
|
||||
right: var(--ha-space-2);
|
||||
inset-inline-start: 56px;
|
||||
inset-inline-end: var(--ha-space-2);
|
||||
z-index: 1;
|
||||
}
|
||||
`;
|
||||
|
||||
+69
-312
@@ -1,4 +1,4 @@
|
||||
import { consume, ContextConsumer } from "@lit/context";
|
||||
import { consume } from "@lit/context";
|
||||
import { isToday } from "date-fns";
|
||||
import type { HassConfig, HassEntities } from "home-assistant-js-websocket";
|
||||
import type { PropertyValues } from "lit";
|
||||
@@ -31,23 +31,15 @@ import type {
|
||||
} from "../../common/map/map-engine";
|
||||
import { circleBoundsPoints } from "../../common/map/map-engine";
|
||||
import { editableCircleStyles } from "../../common/map/editable-circle";
|
||||
import { entityMapColor, zoneColor } from "../../common/map/entity-map-colors";
|
||||
import {
|
||||
createZoneMarkerElement,
|
||||
ZONE_CIRCLE_SIZE,
|
||||
zoneMarkerStyles,
|
||||
} from "../../common/map/zone-marker";
|
||||
import { filterXSS } from "../../common/util/xss";
|
||||
import {
|
||||
configContext,
|
||||
connectionContext,
|
||||
formattersContext,
|
||||
fullEntitiesContext,
|
||||
internationalizationContext,
|
||||
statesContext,
|
||||
uiContext,
|
||||
} from "../../data/context";
|
||||
import type { EntityRegistryEntry } from "../../data/entity/entity_registry";
|
||||
import { ensureMapTilesToken } from "../../data/map_tiles";
|
||||
import type {
|
||||
HomeAssistantConfig,
|
||||
@@ -219,37 +211,9 @@ export interface HaMapEntity {
|
||||
unit?: string;
|
||||
name?: string;
|
||||
focus?: boolean;
|
||||
hide_accuracy?: boolean;
|
||||
hide_radius?: boolean;
|
||||
selected?: boolean;
|
||||
}
|
||||
|
||||
// Data carried by entity markers for rendering cluster bubbles
|
||||
interface ClusterData {
|
||||
entityId: string;
|
||||
picture?: string;
|
||||
label: string;
|
||||
showIcon: boolean;
|
||||
unit: string;
|
||||
color?: string;
|
||||
selected: boolean;
|
||||
zoneId?: string;
|
||||
}
|
||||
|
||||
const CLUSTER_AVATAR_SIZE = 32;
|
||||
const CLUSTER_BUBBLE_PADDING = 6;
|
||||
const CLUSTER_BUBBLE_GAP = 4;
|
||||
const CLUSTER_MAX_AVATARS = 3;
|
||||
const CLUSTER_MORE_WIDTH = 28;
|
||||
const CLUSTER_MORE_MAX = 99;
|
||||
const CLUSTER_TAIL_SIZE = 10;
|
||||
// The tail is a rotated square on the bubble's bottom edge, reaching half its diagonal below
|
||||
const CLUSTER_TAIL_HEIGHT = Math.round((CLUSTER_TAIL_SIZE * Math.SQRT2) / 2);
|
||||
const CLUSTER_ZONE_SPACING = 2;
|
||||
const CLUSTER_RADIUS = 40;
|
||||
// Same-zone markers share the zone's bubble while they span at most this many
|
||||
// pixels; further apart they show their actual positions
|
||||
const ZONE_GROUP_RADIUS = 160;
|
||||
|
||||
@customElement("ha-map")
|
||||
export class HaMap extends ReactiveElement {
|
||||
@@ -300,8 +264,6 @@ export class HaMap extends ReactiveElement {
|
||||
|
||||
@property({ attribute: "fit-zones", type: Boolean }) public fitZones = false;
|
||||
|
||||
private _zonePositions: Record<string, MapLatLng> = {};
|
||||
|
||||
@property({ attribute: "theme-mode", type: String })
|
||||
public themeMode: ThemeMode = "auto";
|
||||
|
||||
@@ -334,11 +296,6 @@ export class HaMap extends ReactiveElement {
|
||||
|
||||
private _resizeObserver?: ResizeObserver;
|
||||
|
||||
// Registry creation order decides the palette colors
|
||||
@state() private _entityReg: EntityRegistryEntry[] = [];
|
||||
|
||||
private _registryConsumer?: ContextConsumer<typeof fullEntitiesContext, this>;
|
||||
|
||||
private _entityHandles: MapMarkerHandle[] = [];
|
||||
|
||||
private _zoneHandles: MapItemHandle[] = [];
|
||||
@@ -366,21 +323,6 @@ export class HaMap extends ReactiveElement {
|
||||
this._attachObserver();
|
||||
}
|
||||
|
||||
// Only maps that draw entities ask for the registry; an editor's map, as in
|
||||
// onboarding, never does. The context provider shares one subscription.
|
||||
private _watchRegistry(): void {
|
||||
if (this._registryConsumer || !this.entities?.length) {
|
||||
return;
|
||||
}
|
||||
this._registryConsumer = new ContextConsumer(this, {
|
||||
context: fullEntitiesContext,
|
||||
subscribe: true,
|
||||
callback: (entries) => {
|
||||
this._entityReg = entries;
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
private _handleVisibilityChange = async () => {
|
||||
if (!document.hidden) {
|
||||
setTimeout(() => {
|
||||
@@ -443,7 +385,7 @@ export class HaMap extends ReactiveElement {
|
||||
}
|
||||
}
|
||||
|
||||
if (changedProps.has("clusterMarkers") || changedProps.has("_entityReg")) {
|
||||
if (changedProps.has("clusterMarkers")) {
|
||||
this._drawEntities();
|
||||
}
|
||||
|
||||
@@ -459,11 +401,6 @@ export class HaMap extends ReactiveElement {
|
||||
|
||||
if (changedProps.has("_loaded") || changedProps.has("paths")) {
|
||||
this._drawPaths();
|
||||
// Cluster bubbles show entity colors only while trails are visible
|
||||
const oldPaths = changedProps.get("paths") as HaMapPaths[] | undefined;
|
||||
if (!!oldPaths?.length !== !!this.paths?.length) {
|
||||
this._engine?.refreshClusters();
|
||||
}
|
||||
}
|
||||
|
||||
if (changedProps.has("_loaded") || changedProps.has("editableLocations")) {
|
||||
@@ -496,19 +433,13 @@ export class HaMap extends ReactiveElement {
|
||||
const oldUi = changedProps.get("_ui") as HomeAssistantUI | undefined;
|
||||
if (
|
||||
!changedProps.has("themeMode") &&
|
||||
(!changedProps.has("_ui") || (oldUi && oldUi.themes === this._ui.themes))
|
||||
(!changedProps.has("_ui") ||
|
||||
(oldUi && oldUi.themes?.darkMode === this._ui.themes?.darkMode))
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._updateMapStyle();
|
||||
// Marker, trail and circle colors were resolved from the theme when drawn
|
||||
this._drawEntities();
|
||||
this._drawPaths();
|
||||
if (this._editableHandles.size) {
|
||||
this._removeEditableLocations();
|
||||
this._drawEditableLocations();
|
||||
}
|
||||
}
|
||||
|
||||
private get _darkMode() {
|
||||
@@ -1100,36 +1031,16 @@ export class HaMap extends ReactiveElement {
|
||||
engine.setClustering(null);
|
||||
return;
|
||||
}
|
||||
this._watchRegistry();
|
||||
|
||||
const computedStyles = getComputedStyle(this);
|
||||
// A person's state is "home" for the home zone, the zone name otherwise
|
||||
const zoneByState: Record<string, string> = {};
|
||||
this._zonePositions = {};
|
||||
for (const entity of this.entities) {
|
||||
const stateObj = states[getEntityId(entity)];
|
||||
// A zone that is not drawn cannot anchor a bubble either
|
||||
if (
|
||||
stateObj &&
|
||||
computeStateDomain(stateObj) === "zone" &&
|
||||
(this.renderPassive || !stateObj.attributes.passive)
|
||||
) {
|
||||
zoneByState[
|
||||
stateObj.entity_id === "zone.home"
|
||||
? "home"
|
||||
: computeStateName(stateObj)
|
||||
] = stateObj.entity_id;
|
||||
if (
|
||||
typeof stateObj.attributes.latitude === "number" &&
|
||||
typeof stateObj.attributes.longitude === "number"
|
||||
) {
|
||||
this._zonePositions[stateObj.entity_id] = [
|
||||
stateObj.attributes.latitude,
|
||||
stateObj.attributes.longitude,
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
const zoneColor = computedStyles.getPropertyValue("--accent-color");
|
||||
const passiveZoneColor = computedStyles.getPropertyValue(
|
||||
"--secondary-text-color"
|
||||
);
|
||||
|
||||
const darkPrimaryColor = computedStyles.getPropertyValue(
|
||||
"--dark-primary-color"
|
||||
);
|
||||
|
||||
for (const entity of this.entities) {
|
||||
const stateObj = states[getEntityId(entity)];
|
||||
@@ -1158,29 +1069,26 @@ export class HaMap extends ReactiveElement {
|
||||
continue;
|
||||
}
|
||||
|
||||
const hideRadius = typeof entity !== "string" && entity.hide_radius;
|
||||
// A host-set color wins, except passive zones are always muted
|
||||
const markerColor =
|
||||
!passive && typeof entity !== "string" && entity.color
|
||||
? entity.color
|
||||
: zoneColor(
|
||||
stateObj.entity_id,
|
||||
!!passive,
|
||||
this._entityReg,
|
||||
computedStyles
|
||||
);
|
||||
const zoneMarkerColor = passive ? passiveZoneColor : zoneColor;
|
||||
|
||||
if (!hideRadius && radius) {
|
||||
if (radius) {
|
||||
this._zoneHandles.push(
|
||||
engine.addCircle(position, { radius, color: markerColor })
|
||||
engine.addCircle(position, { radius, color: zoneMarkerColor })
|
||||
);
|
||||
}
|
||||
|
||||
const circleEl = createZoneMarkerElement({
|
||||
color: markerColor,
|
||||
icon,
|
||||
name: title,
|
||||
});
|
||||
// create icon
|
||||
const iconEl = document.createElement("div");
|
||||
iconEl.className = `zone-icon ${this._darkMode ? "dark" : "light"}`;
|
||||
if (icon) {
|
||||
const el = document.createElement("ha-icon");
|
||||
el.setAttribute("icon", icon);
|
||||
iconEl.appendChild(el);
|
||||
} else {
|
||||
const el = document.createElement("span");
|
||||
el.textContent = title;
|
||||
iconEl.appendChild(el);
|
||||
}
|
||||
|
||||
if (this.interactiveZones) {
|
||||
const openMoreInfo = (ev: Event) => {
|
||||
@@ -1189,8 +1097,8 @@ export class HaMap extends ReactiveElement {
|
||||
entityId: stateObj.entity_id,
|
||||
});
|
||||
};
|
||||
circleEl.addEventListener("click", openMoreInfo);
|
||||
circleEl.addEventListener("keydown", (ev) => {
|
||||
iconEl.addEventListener("click", openMoreInfo);
|
||||
iconEl.addEventListener("keydown", (ev) => {
|
||||
if (ev.key === "Enter" || ev.key === " ") {
|
||||
ev.preventDefault();
|
||||
openMoreInfo(ev);
|
||||
@@ -1198,9 +1106,10 @@ export class HaMap extends ReactiveElement {
|
||||
});
|
||||
}
|
||||
|
||||
const zoneIconSize = this._getMarkerSize(computedStyles) / 2;
|
||||
this._zoneHandles.push(
|
||||
engine.addMarker(circleEl, position, {
|
||||
size: [ZONE_CIRCLE_SIZE, ZONE_CIRCLE_SIZE],
|
||||
engine.addMarker(iconEl, position, {
|
||||
size: [zoneIconSize, zoneIconSize],
|
||||
interactive: this.interactiveZones,
|
||||
title,
|
||||
})
|
||||
@@ -1210,7 +1119,7 @@ export class HaMap extends ReactiveElement {
|
||||
this.fitZones &&
|
||||
(typeof entity === "string" || entity.focus !== false)
|
||||
) {
|
||||
if (!hideRadius && radius) {
|
||||
if (radius) {
|
||||
this._focusZonePoints.push(...circleBoundsPoints(position, radius));
|
||||
} else {
|
||||
this._focusZonePoints.push(position);
|
||||
@@ -1254,42 +1163,19 @@ export class HaMap extends ReactiveElement {
|
||||
entityPicture && (typeof entity === "string" || !entity.label_mode)
|
||||
? this._connection.hassUrl(entityPicture)
|
||||
: "";
|
||||
// A host may leave the color to the map
|
||||
const entityColor =
|
||||
(typeof entity !== "string" ? entity.color : undefined) ||
|
||||
entityMapColor(getEntityId(entity), this._entityReg, computedStyles);
|
||||
entityMarker.entityColor = entityColor;
|
||||
if (typeof entity !== "string") {
|
||||
entityMarker.selected = entity.selected ?? false;
|
||||
entityMarker.entityColor = entity.color;
|
||||
}
|
||||
|
||||
const clusterData: ClusterData = {
|
||||
entityId: getEntityId(entity),
|
||||
picture: entityMarker.entityPicture || undefined,
|
||||
label: entityName,
|
||||
showIcon: entityMarker.showIcon,
|
||||
unit: entityMarker.entityUnit ?? "",
|
||||
color: entityColor,
|
||||
selected: typeof entity !== "string" && (entity.selected ?? false),
|
||||
zoneId: ["person", "device_tracker"].includes(
|
||||
computeStateDomain(stateObj)
|
||||
)
|
||||
? zoneByState[stateObj.state]
|
||||
: undefined,
|
||||
};
|
||||
|
||||
const showAccuracy =
|
||||
!!gpsAccuracy && !(typeof entity !== "string" && entity.hide_accuracy);
|
||||
|
||||
const markerSize = this._getMarkerSize(computedStyles);
|
||||
this._entityHandles.push(
|
||||
engine.addMarker(entityMarker, position, {
|
||||
size: [markerSize, markerSize],
|
||||
title,
|
||||
cluster: true,
|
||||
clusterData,
|
||||
decoration: showAccuracy
|
||||
? { radius: gpsAccuracy!, color: entityColor }
|
||||
// create circle around if entity has accuracy
|
||||
decoration: gpsAccuracy
|
||||
? { radius: gpsAccuracy, color: darkPrimaryColor }
|
||||
: undefined,
|
||||
})
|
||||
);
|
||||
@@ -1301,97 +1187,22 @@ export class HaMap extends ReactiveElement {
|
||||
|
||||
engine.setClustering(
|
||||
this.clusterMarkers
|
||||
? {
|
||||
radius: CLUSTER_RADIUS,
|
||||
iconBuilder: this._createClusterBubble,
|
||||
// Everyone in a zone shares its bubble until zooming spreads them
|
||||
groupKey: (marker) => (marker.clusterData as ClusterData)?.zoneId,
|
||||
groupRadius: ZONE_GROUP_RADIUS,
|
||||
}
|
||||
? { radius: CLUSTER_RADIUS, iconBuilder: this._createClusterIcon }
|
||||
: null
|
||||
);
|
||||
}
|
||||
|
||||
// Renders a marker cluster as a bubble of its members' avatars
|
||||
private _createClusterBubble = (
|
||||
members: MapMarkerHandle[]
|
||||
): MapClusterIcon => {
|
||||
const data = members.map((member) => member.clusterData as ClusterData);
|
||||
const shown = data.slice(0, CLUSTER_MAX_AVATARS);
|
||||
const hidden = data.length - shown.length;
|
||||
|
||||
// With history trails shown, colored borders match avatars to trails
|
||||
const showColors = !!this.paths?.length;
|
||||
|
||||
const bubble = document.createElement("div");
|
||||
bubble.className = "cluster-bubble";
|
||||
for (const member of shown) {
|
||||
const avatar = document.createElement("ha-entity-marker");
|
||||
avatar.entityId = member?.entityId;
|
||||
avatar.entityName = member?.label ?? "";
|
||||
avatar.entityUnit = member?.unit ?? "";
|
||||
avatar.showIcon = member?.showIcon ?? false;
|
||||
avatar.entityPicture = member?.picture ?? "";
|
||||
avatar.entityColor = member?.color;
|
||||
if (showColors) {
|
||||
avatar.style.setProperty(
|
||||
"--ha-marker-color",
|
||||
member?.color ?? "var(--primary-color)"
|
||||
);
|
||||
avatar.style.setProperty("--ha-marker-border-width", "2px");
|
||||
}
|
||||
if (member?.selected) {
|
||||
avatar.selected = true;
|
||||
}
|
||||
bubble.appendChild(avatar);
|
||||
}
|
||||
|
||||
let width =
|
||||
shown.length * CLUSTER_AVATAR_SIZE +
|
||||
(shown.length - 1) * CLUSTER_BUBBLE_GAP +
|
||||
2 * CLUSTER_BUBBLE_PADDING;
|
||||
if (hidden > 0) {
|
||||
const more = document.createElement("span");
|
||||
more.className = "more";
|
||||
more.textContent =
|
||||
hidden > CLUSTER_MORE_MAX ? `${CLUSTER_MORE_MAX}+` : `+${hidden}`;
|
||||
bubble.appendChild(more);
|
||||
width += CLUSTER_MORE_WIDTH + CLUSTER_BUBBLE_GAP;
|
||||
}
|
||||
|
||||
// A cluster of one zone's occupants attaches to that zone's marker
|
||||
const zoneId = data[0]?.zoneId;
|
||||
const zonePosition = zoneId ? this._zonePositions[zoneId] : undefined;
|
||||
const atZone =
|
||||
!!zoneId &&
|
||||
!!zonePosition &&
|
||||
data.every((member) => member?.zoneId === zoneId);
|
||||
|
||||
let height = CLUSTER_AVATAR_SIZE + 2 * CLUSTER_BUBBLE_PADDING;
|
||||
let root: HTMLElement = bubble;
|
||||
if (atZone) {
|
||||
root = document.createElement("div");
|
||||
root.className = "cluster-marker";
|
||||
const tail = document.createElement("div");
|
||||
tail.className = "cluster-bubble-tail";
|
||||
root.append(bubble, tail);
|
||||
height += CLUSTER_TAIL_HEIGHT;
|
||||
}
|
||||
|
||||
return {
|
||||
element: root,
|
||||
size: [width, height],
|
||||
// Float above the zone circle, tail pointing at it
|
||||
...(atZone && zonePosition
|
||||
? {
|
||||
location: zonePosition,
|
||||
anchor: [
|
||||
width / 2,
|
||||
height + ZONE_CIRCLE_SIZE / 2 + CLUSTER_ZONE_SPACING,
|
||||
] as [number, number],
|
||||
}
|
||||
: {}),
|
||||
};
|
||||
// Renders a marker cluster as a circle with the member count
|
||||
private _createClusterIcon = (members: MapMarkerHandle[]): MapClusterIcon => {
|
||||
const size = Math.round(
|
||||
this._getMarkerSize(getComputedStyle(this)) * (2 / 3)
|
||||
);
|
||||
const element = document.createElement("div");
|
||||
element.className = "marker-cluster";
|
||||
const count = document.createElement("span");
|
||||
count.textContent = String(members.length);
|
||||
element.appendChild(count);
|
||||
return { element, size: [size, size] };
|
||||
};
|
||||
|
||||
private _drawScaleRuler(): void {
|
||||
@@ -1426,19 +1237,12 @@ export class HaMap extends ReactiveElement {
|
||||
}
|
||||
#map {
|
||||
height: 100%;
|
||||
/* A cluster bubble and its tail cast a single shadow around their
|
||||
combined silhouette (drop-shadow on the wrapper), so no shadow seam
|
||||
appears between the bubble and its tail. */
|
||||
--ha-cluster-shadow: drop-shadow(0 1px 2px rgba(0, 0, 0, 0.08))
|
||||
drop-shadow(0 1px 3px rgba(0, 0, 0, 0.12));
|
||||
}
|
||||
#map.clickable {
|
||||
cursor: pointer;
|
||||
}
|
||||
#map.dark {
|
||||
background: #090909;
|
||||
--ha-cluster-shadow: drop-shadow(0 1px 2px rgba(0, 0, 0, 0.4))
|
||||
drop-shadow(0 1px 3px rgba(0, 0, 0, 0.5));
|
||||
}
|
||||
#map.forced-dark {
|
||||
color: #ffffff;
|
||||
@@ -1459,8 +1263,6 @@ export class HaMap extends ReactiveElement {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
isolation: isolate;
|
||||
filter: var(--ha-cluster-shadow);
|
||||
}
|
||||
.cluster-open-members {
|
||||
display: flex;
|
||||
@@ -1472,13 +1274,7 @@ export class HaMap extends ReactiveElement {
|
||||
max-width: calc(6 * var(--ha-marker-size, 48px) + 5 * 4px + 12px);
|
||||
background: var(--card-background-color, #fff);
|
||||
border-radius: 14px;
|
||||
}
|
||||
/* Both tails are a rotated square whose upper half sits under the bubble;
|
||||
drawn behind it, so it never covers a member's frame or selected ring */
|
||||
.cluster-open-tail,
|
||||
.cluster-bubble-tail {
|
||||
position: relative;
|
||||
z-index: -1;
|
||||
box-shadow: var(--ha-box-shadow-s);
|
||||
}
|
||||
.cluster-open-tail {
|
||||
width: 10px;
|
||||
@@ -1531,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;
|
||||
@@ -1553,65 +1346,17 @@ export class HaMap extends ReactiveElement {
|
||||
.leaflet-pane {
|
||||
z-index: 0 !important;
|
||||
}
|
||||
.cluster-marker {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
isolation: isolate;
|
||||
filter: var(--ha-cluster-shadow);
|
||||
}
|
||||
/* The wrapper carries the shadow around the bubble-plus-tail outline, so
|
||||
the bubble itself drops its own to avoid a seam at the tail. */
|
||||
.cluster-marker .cluster-bubble {
|
||||
filter: none;
|
||||
}
|
||||
.cluster-bubble-tail {
|
||||
width: ${CLUSTER_TAIL_SIZE}px;
|
||||
height: ${CLUSTER_TAIL_SIZE}px;
|
||||
margin-top: ${-CLUSTER_TAIL_SIZE / 2}px;
|
||||
border-radius: 2px;
|
||||
background: var(--card-background-color, #fff);
|
||||
transform: rotate(45deg);
|
||||
}
|
||||
.cluster-bubble {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: ${CLUSTER_BUBBLE_GAP}px;
|
||||
padding: ${CLUSTER_BUBBLE_PADDING}px;
|
||||
box-sizing: border-box;
|
||||
background: var(--card-background-color, #fff);
|
||||
border-radius: 14px;
|
||||
filter: var(--ha-cluster-shadow);
|
||||
--ha-marker-size: ${CLUSTER_AVATAR_SIZE}px;
|
||||
--ha-marker-color: transparent;
|
||||
--ha-marker-border-width: 1px;
|
||||
--ha-marker-shadow: none;
|
||||
--ha-marker-font-size: var(--ha-font-size-s);
|
||||
/* distinguish letter tiles from the bubble background */
|
||||
--ha-marker-background: var(--ha-color-fill-neutral-quiet-resting);
|
||||
}
|
||||
.cluster-bubble .more {
|
||||
flex: none;
|
||||
width: ${CLUSTER_MORE_WIDTH}px;
|
||||
height: ${CLUSTER_AVATAR_SIZE}px;
|
||||
/* Zone icons, sized like the Leaflet divIcon they replaced */
|
||||
.zone-icon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 10px;
|
||||
background: var(--ha-color-fill-neutral-quiet-resting, #f0f0f0);
|
||||
color: var(--primary-text-color, #212121);
|
||||
font-size: var(--ha-font-size-s);
|
||||
font-weight: var(--ha-font-weight-medium);
|
||||
text-align: center;
|
||||
color: var(--primary-text-color);
|
||||
}
|
||||
/* Markers are rounded squares to match the cluster bubble avatars */
|
||||
ha-entity-marker {
|
||||
--ha-marker-border-radius: var(--ha-border-radius-lg);
|
||||
.zone-icon.dark {
|
||||
color: #ffffff;
|
||||
}
|
||||
.cluster-bubble ha-entity-marker {
|
||||
flex: none;
|
||||
--ha-marker-border-radius: 10px;
|
||||
}
|
||||
${unsafeCSS(zoneMarkerStyles)}
|
||||
.leaflet-control,
|
||||
.leaflet-top,
|
||||
.leaflet-bottom {
|
||||
@@ -1632,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 {
|
||||
@@ -1662,6 +1406,19 @@ export class HaMap extends ReactiveElement {
|
||||
ha-icon {
|
||||
--mdc-icon-size: calc(var(--ha-marker-size, 48px) / 2);
|
||||
}
|
||||
|
||||
.marker-cluster {
|
||||
box-sizing: border-box;
|
||||
background-clip: padding-box;
|
||||
background-color: var(--primary-color);
|
||||
border: 3px solid rgba(var(--rgb-primary-color), 0.2);
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: var(--text-primary-color);
|
||||
font-size: var(--ha-font-size-m);
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
|
||||
@@ -7,11 +7,9 @@ import { customElement, property, query } from "lit/decorators";
|
||||
import { styleMap } from "lit/directives/style-map";
|
||||
import { fireEvent, type HASSDomEvent } from "../../common/dom/fire_event";
|
||||
import { computeDomain } from "../../common/entity/compute_domain";
|
||||
import {
|
||||
computeEntityPickerDisplay,
|
||||
computeEntitySearchLabels,
|
||||
} from "../../common/entity/compute_entity_name_display";
|
||||
import { computeEntityNameList } from "../../common/entity/compute_entity_name_display";
|
||||
import { computeStateDomain } from "../../common/entity/compute_state_domain";
|
||||
import { computeStateName } from "../../common/entity/compute_state_name";
|
||||
import { supportsFeature } from "../../common/entity/supports-feature";
|
||||
import {
|
||||
areasContext,
|
||||
@@ -109,6 +107,9 @@ export class HaMediaPlayerPicker extends LitElement {
|
||||
const webBrowserLabel = this._i18n.localize(
|
||||
"ui.components.media-browser.web-browser"
|
||||
);
|
||||
const lang = this._i18n.language || "en";
|
||||
const isRTL =
|
||||
this._i18n.translationMetadata.translations[lang]?.isRTL || false;
|
||||
|
||||
return [
|
||||
{
|
||||
@@ -127,22 +128,34 @@ export class HaMediaPlayerPicker extends LitElement {
|
||||
...Object.values(this._states)
|
||||
.filter(this._filterPlayerEntities)
|
||||
.map<MediaPlayerComboBoxItem>((stateObj) => {
|
||||
const friendlyName = computeStateName(stateObj);
|
||||
const [entityName, deviceName, parentDeviceName, areaName] =
|
||||
computeEntityNameList(
|
||||
stateObj,
|
||||
[
|
||||
{ type: "entity" },
|
||||
{ type: "device" },
|
||||
{ type: "parent_device" },
|
||||
{ type: "area" },
|
||||
],
|
||||
this._entities,
|
||||
this._devices,
|
||||
this._areas,
|
||||
this._floors
|
||||
);
|
||||
const entityId = stateObj.entity_id;
|
||||
const domainName = domainToName(
|
||||
this._i18n.localize,
|
||||
computeDomain(entityId)
|
||||
);
|
||||
const { primary, secondary } = computeEntityPickerDisplay(
|
||||
{
|
||||
entities: this._entities,
|
||||
devices: this._devices,
|
||||
areas: this._areas,
|
||||
floors: this._floors,
|
||||
language: this._i18n.language,
|
||||
translationMetadata: this._i18n.translationMetadata,
|
||||
},
|
||||
stateObj
|
||||
);
|
||||
const primary = entityName || deviceName || entityId;
|
||||
const secondary = [
|
||||
areaName,
|
||||
parentDeviceName,
|
||||
entityName ? deviceName : undefined,
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(isRTL ? " ◂ " : " ▸ ");
|
||||
|
||||
return {
|
||||
id: entityId,
|
||||
@@ -152,14 +165,12 @@ export class HaMediaPlayerPicker extends LitElement {
|
||||
domain_name: domainName,
|
||||
sorting_label: [primary, secondary].filter(Boolean).join("_"),
|
||||
search_labels: {
|
||||
...computeEntitySearchLabels(
|
||||
stateObj,
|
||||
this._entities,
|
||||
this._devices,
|
||||
this._areas,
|
||||
this._floors
|
||||
),
|
||||
entityName: entityName || null,
|
||||
deviceName: deviceName || null,
|
||||
parentDeviceName: parentDeviceName || null,
|
||||
areaName: areaName || null,
|
||||
domainName: domainName || null,
|
||||
friendlyName: friendlyName || null,
|
||||
entityId,
|
||||
},
|
||||
stateObj,
|
||||
|
||||
@@ -7,7 +7,8 @@ import memoizeOne from "memoize-one";
|
||||
|
||||
import { consumeEntityState } from "../../common/decorators/consume-context-entry";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import { computeEntityPickerDisplay } from "../../common/entity/compute_entity_name_display";
|
||||
import { computeEntityNameList } from "../../common/entity/compute_entity_name_display";
|
||||
import { computeRTL } from "../../common/util/compute_rtl";
|
||||
import {
|
||||
areasContext,
|
||||
devicesContext,
|
||||
@@ -53,24 +54,40 @@ class HaMediaPlayerToggle extends LitElement {
|
||||
|
||||
private _computeDisplayData = memoizeOne(
|
||||
(
|
||||
entityId: string,
|
||||
entities: ContextType<typeof entitiesContext>,
|
||||
devices: ContextType<typeof devicesContext>,
|
||||
areas: ContextType<typeof areasContext>,
|
||||
floors: ContextType<typeof floorsContext>,
|
||||
i18n: ContextType<typeof internationalizationContext>,
|
||||
isRTL: boolean,
|
||||
stateObj: HassEntity
|
||||
) =>
|
||||
computeEntityPickerDisplay(
|
||||
{
|
||||
) => {
|
||||
const [entityName, deviceName, parentDeviceName, areaName] =
|
||||
computeEntityNameList(
|
||||
stateObj,
|
||||
[
|
||||
{ type: "entity" },
|
||||
{ type: "device" },
|
||||
{ type: "parent_device" },
|
||||
{ type: "area" },
|
||||
],
|
||||
entities,
|
||||
devices,
|
||||
areas,
|
||||
floors,
|
||||
language: i18n.language,
|
||||
translationMetadata: i18n.translationMetadata,
|
||||
},
|
||||
stateObj
|
||||
)
|
||||
floors
|
||||
);
|
||||
|
||||
const primary = entityName || deviceName || entityId;
|
||||
const secondary = [
|
||||
areaName,
|
||||
parentDeviceName,
|
||||
entityName ? deviceName : undefined,
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(isRTL ? " ◂ " : " ▸ ");
|
||||
|
||||
return { primary, secondary };
|
||||
}
|
||||
);
|
||||
|
||||
protected render() {
|
||||
@@ -87,12 +104,18 @@ class HaMediaPlayerToggle extends LitElement {
|
||||
icon = mdiSpeakerPause;
|
||||
}
|
||||
|
||||
const isRTL = computeRTL(
|
||||
this._i18n.language,
|
||||
this._i18n.translationMetadata.translations
|
||||
);
|
||||
|
||||
const { primary, secondary } = this._computeDisplayData(
|
||||
this.entityId,
|
||||
this._entities,
|
||||
this._devices,
|
||||
this._areas,
|
||||
this._floors,
|
||||
this._i18n,
|
||||
isRTL,
|
||||
stateObj
|
||||
);
|
||||
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { consume } from "@lit/context";
|
||||
import {
|
||||
mdiChevronLeft,
|
||||
mdiChevronRight,
|
||||
mdiClose,
|
||||
mdiDevices,
|
||||
mdiHome,
|
||||
@@ -26,11 +28,12 @@ import {
|
||||
computeDeviceNameDisplay,
|
||||
} from "../../common/entity/compute_device_name";
|
||||
import { computeDomain } from "../../common/entity/compute_domain";
|
||||
import { computeEntityPickerDisplay } from "../../common/entity/compute_entity_name_display";
|
||||
import { computeEntityName } from "../../common/entity/compute_entity_name";
|
||||
import {
|
||||
getDeviceArea,
|
||||
getDeviceAreaId,
|
||||
} 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";
|
||||
import { getConfigEntry } from "../../data/config_entries";
|
||||
@@ -59,7 +62,6 @@ 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";
|
||||
@@ -315,7 +317,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
|
||||
}
|
||||
`;
|
||||
@@ -671,12 +685,40 @@ export class HaTargetPickerItemRow extends LitElement {
|
||||
}
|
||||
if (type === "entity") {
|
||||
const stateObject: HassEntity | undefined = this.hass.states[item];
|
||||
const { primary, secondary } = stateObject
|
||||
? computeEntityPickerDisplay(this.hass, stateObject)
|
||||
: { primary: item, secondary: undefined };
|
||||
const entityName = stateObject
|
||||
? computeEntityName(stateObject, this.hass.entities, this.hass.devices)
|
||||
: item;
|
||||
const { area, device, parentDevice } = stateObject
|
||||
? getEntityContext(
|
||||
stateObject,
|
||||
this.hass.entities,
|
||||
this.hass.devices,
|
||||
this.hass.areas,
|
||||
this.hass.floors
|
||||
)
|
||||
: { area: undefined, device: undefined, parentDevice: undefined };
|
||||
const deviceName = device ? computeDeviceName(device) : undefined;
|
||||
const parentDeviceName = parentDevice
|
||||
? computeDeviceName(parentDevice)
|
||||
: undefined;
|
||||
const areaName = area ? computeAreaName(area) : undefined;
|
||||
const context = [
|
||||
areaName,
|
||||
parentDeviceName,
|
||||
entityName ? deviceName : undefined,
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(
|
||||
computeRTL(
|
||||
this.hass.language,
|
||||
this.hass.translationMetadata.translations
|
||||
)
|
||||
? " ◂ "
|
||||
: " ▸ "
|
||||
);
|
||||
return {
|
||||
name: primary,
|
||||
context: secondary,
|
||||
name: entityName || deviceName || item,
|
||||
context,
|
||||
stateObject,
|
||||
notFound: !stateObject && item !== "all" && item !== "none",
|
||||
};
|
||||
|
||||
@@ -326,7 +326,7 @@ export class HaTracePathDetails extends LitElement {
|
||||
this._entityReg,
|
||||
currentDetail,
|
||||
undefined,
|
||||
undefined,
|
||||
false,
|
||||
this._manifests
|
||||
)
|
||||
: selectedType === "chooseOption"
|
||||
|
||||
@@ -301,7 +301,7 @@ export interface TemplateCondition extends BaseCondition {
|
||||
|
||||
export interface TriggerCondition extends BaseCondition {
|
||||
condition: "trigger";
|
||||
id: string | string[];
|
||||
id: string;
|
||||
}
|
||||
|
||||
type ShorthandBaseCondition = Omit<BaseCondition, "condition">;
|
||||
|
||||
@@ -149,8 +149,6 @@ const formatNumericLimitValue = (
|
||||
export interface DescribeOptions {
|
||||
// Skip the user defined alias and describe the underlying config.
|
||||
ignoreAlias?: boolean;
|
||||
// Rows with trigger-reference chips render the IDs separately.
|
||||
hideTriggerIds?: boolean;
|
||||
}
|
||||
|
||||
export const describeTrigger = (
|
||||
@@ -925,12 +923,6 @@ const tryDescribeCondition = (
|
||||
return condition.alias;
|
||||
}
|
||||
|
||||
if (condition.condition === "trigger" && options?.hideTriggerIds) {
|
||||
return hass.localize(
|
||||
`${conditionsTranslationBaseKey}.trigger.description.summary`
|
||||
);
|
||||
}
|
||||
|
||||
if (!condition.condition) {
|
||||
const shorthands: ("and" | "or" | "not")[] = ["and", "or", "not"];
|
||||
for (const key of shorthands) {
|
||||
|
||||
@@ -12,7 +12,6 @@ export const DOMAIN_DEVICE_CLASSES: Record<string, string[]> = {
|
||||
"door",
|
||||
"garage_door",
|
||||
"gas",
|
||||
"glass_break",
|
||||
"heat",
|
||||
"light",
|
||||
"lock",
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
import type { HassEntity } from "home-assistant-js-websocket";
|
||||
import { getEntityAreaId } from "../../common/entity/context/get_entity_context";
|
||||
import { computeDomain } from "../../common/entity/compute_domain";
|
||||
import {
|
||||
computeEntityPickerDisplay,
|
||||
computeEntitySearchLabels,
|
||||
} from "../../common/entity/compute_entity_name_display";
|
||||
import { computeEntityNameList } from "../../common/entity/compute_entity_name_display";
|
||||
import { computeStateName } from "../../common/entity/compute_state_name";
|
||||
import type { RelatedIdSets } from "../../common/search/related-context";
|
||||
import { caseInsensitiveStringCompare } from "../../common/string/compare";
|
||||
import { computeRTL } from "../../common/util/compute_rtl";
|
||||
import type { PickerComboBoxItem } from "../../components/ha-picker-combo-box";
|
||||
import type { FuseWeightedKey } from "../../resources/fuseMultiTerm";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
@@ -124,11 +123,31 @@ export const getEntities = (
|
||||
|
||||
// These values are the same for every entity, so compute them once instead
|
||||
// of inside the map over (potentially thousands of) entities.
|
||||
const isRTL = computeRTL(
|
||||
hass.language,
|
||||
hass.translationMetadata.translations
|
||||
);
|
||||
const domainNames = new Map<string, string>();
|
||||
|
||||
items = entityIds.map<EntityComboBoxItem>((entityId) => {
|
||||
const stateObj = hass.states[entityId];
|
||||
|
||||
const friendlyName = computeStateName(stateObj); // Keep this for search
|
||||
const [entityName, deviceName, parentDeviceName, areaName] =
|
||||
computeEntityNameList(
|
||||
stateObj,
|
||||
[
|
||||
{ type: "entity" },
|
||||
{ type: "device" },
|
||||
{ type: "parent_device" },
|
||||
{ type: "area" },
|
||||
],
|
||||
hass.entities,
|
||||
hass.devices,
|
||||
hass.areas,
|
||||
hass.floors
|
||||
);
|
||||
|
||||
const domain = computeDomain(entityId);
|
||||
let domainName = domainNames.get(domain);
|
||||
if (domainName === undefined) {
|
||||
@@ -136,7 +155,14 @@ export const getEntities = (
|
||||
domainNames.set(domain, domainName);
|
||||
}
|
||||
|
||||
const { primary, secondary } = computeEntityPickerDisplay(hass, stateObj);
|
||||
const primary = entityName || deviceName || entityId;
|
||||
const secondary = [
|
||||
areaName,
|
||||
parentDeviceName,
|
||||
entityName ? deviceName : undefined,
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(isRTL ? " ◂ " : " ▸ ");
|
||||
|
||||
return {
|
||||
id: `${idPrefix}${entityId}`,
|
||||
@@ -145,14 +171,12 @@ export const getEntities = (
|
||||
domain_name: domainName,
|
||||
sorting_label: [primary, secondary].filter(Boolean).join("_"),
|
||||
search_labels: {
|
||||
...computeEntitySearchLabels(
|
||||
stateObj,
|
||||
hass.entities,
|
||||
hass.devices,
|
||||
hass.areas,
|
||||
hass.floors
|
||||
),
|
||||
entityName: entityName || null,
|
||||
deviceName: deviceName || null,
|
||||
parentDeviceName: parentDeviceName || null,
|
||||
areaName: areaName || null,
|
||||
domainName: domainName || null,
|
||||
friendlyName: friendlyName || null,
|
||||
entityId: entityId,
|
||||
},
|
||||
stateObj: stateObj,
|
||||
|
||||
+23
-45
@@ -36,51 +36,23 @@ export interface FanEntity extends HassEntityBase {
|
||||
|
||||
export type FanDirection = "forward" | "reverse";
|
||||
|
||||
// Named speeds are used when there are icons for every step. Fans with more
|
||||
// steps use numbered speeds ("1", "2", ...) instead.
|
||||
export type FanSpeed = "off" | "low" | "medium" | "high" | "on" | `${number}`;
|
||||
export type FanSpeed = "off" | "low" | "medium" | "high" | "on";
|
||||
|
||||
const FAN_SPEEDS_NAMED: Partial<Record<number, FanSpeed[]>> = {
|
||||
export const FAN_SPEEDS: Partial<Record<number, FanSpeed[]>> = {
|
||||
2: ["off", "on"],
|
||||
3: ["off", "low", "high"],
|
||||
4: ["off", "low", "medium", "high"],
|
||||
};
|
||||
|
||||
export const FAN_SPEED_COUNT_MAX_FOR_BUTTONS = 6;
|
||||
|
||||
export function computeFanSpeedCount(stateObj: FanEntity): number {
|
||||
const step = stateObj.attributes.percentage_step ?? 1;
|
||||
const speedCount = Math.round(100 / step) + 1;
|
||||
return speedCount;
|
||||
}
|
||||
|
||||
export function computeFanSpeeds(stateObj: FanEntity): FanSpeed[] | undefined {
|
||||
const speedCount = computeFanSpeedCount(stateObj);
|
||||
if (speedCount > FAN_SPEED_COUNT_MAX_FOR_BUTTONS) {
|
||||
return undefined;
|
||||
}
|
||||
return (
|
||||
FAN_SPEEDS_NAMED[speedCount] ?? [
|
||||
"off",
|
||||
...Array.from(
|
||||
{ length: speedCount - 1 },
|
||||
(_, index) => `${index + 1}` as const
|
||||
),
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
export const isNumberedFanSpeed = (speed: FanSpeed): speed is `${number}` =>
|
||||
!isNaN(Number(speed));
|
||||
|
||||
export function fanPercentageToSpeed(
|
||||
stateObj: FanEntity,
|
||||
value: number
|
||||
): FanSpeed {
|
||||
const step = stateObj.attributes.percentage_step ?? 1;
|
||||
const speedValue = Math.round(value / step);
|
||||
const speedCount = Math.round(100 / step) + 1;
|
||||
|
||||
const speeds = computeFanSpeeds(stateObj);
|
||||
const speeds = FAN_SPEEDS[speedCount];
|
||||
return speeds?.[speedValue] ?? "off";
|
||||
}
|
||||
|
||||
@@ -89,8 +61,9 @@ export function fanSpeedToPercentage(
|
||||
speed: FanSpeed
|
||||
): number {
|
||||
const step = stateObj.attributes.percentage_step ?? 1;
|
||||
const speedCount = Math.round(100 / step) + 1;
|
||||
|
||||
const speeds = computeFanSpeeds(stateObj);
|
||||
const speeds = FAN_SPEEDS[speedCount];
|
||||
|
||||
if (!speeds) {
|
||||
return 0;
|
||||
@@ -103,22 +76,27 @@ export function fanSpeedToPercentage(
|
||||
return Math.floor(speedValue * step);
|
||||
}
|
||||
|
||||
export function computeFanSpeedCount(stateObj: FanEntity): number {
|
||||
const step = stateObj.attributes.percentage_step ?? 1;
|
||||
const speedCount = Math.round(100 / step) + 1;
|
||||
return speedCount;
|
||||
}
|
||||
|
||||
export function computeFanSpeedIcon(
|
||||
stateObj: FanEntity,
|
||||
speed: FanSpeed
|
||||
): string | undefined {
|
||||
if (speed === "on") {
|
||||
return mdiFan;
|
||||
}
|
||||
if (speed === "off") {
|
||||
return mdiFanOff;
|
||||
}
|
||||
if (isNumberedFanSpeed(speed)) {
|
||||
return undefined;
|
||||
}
|
||||
const index = computeFanSpeeds(stateObj)?.indexOf(speed) ?? 1;
|
||||
return [mdiFanSpeed1, mdiFanSpeed2, mdiFanSpeed3][index - 1];
|
||||
): string {
|
||||
const speedCount = computeFanSpeedCount(stateObj);
|
||||
const speeds = FAN_SPEEDS[speedCount];
|
||||
const index = speeds?.indexOf(speed) ?? 1;
|
||||
|
||||
return speed === "on"
|
||||
? mdiFan
|
||||
: speed === "off"
|
||||
? mdiFanOff
|
||||
: [mdiFanSpeed1, mdiFanSpeed2, mdiFanSpeed3][index - 1];
|
||||
}
|
||||
export const FAN_SPEED_COUNT_MAX_FOR_BUTTONS = 4;
|
||||
|
||||
export function computeFanSpeedStateDisplay(
|
||||
stateObj: FanEntity,
|
||||
|
||||
+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[]>;
|
||||
}
|
||||
|
||||
|
||||
+6
-11
@@ -6,7 +6,7 @@ import { formatListWithAnds } from "../common/string/format-list";
|
||||
import { isTemplate } from "../common/string/has-template";
|
||||
import type { HomeAssistant } from "../types";
|
||||
import type { Condition } from "./automation";
|
||||
import { describeCondition, type DescribeOptions } from "./automation_i18n";
|
||||
import { describeCondition } from "./automation_i18n";
|
||||
import { localizeDeviceAutomationAction } from "./device/device_automation";
|
||||
import type { EntityRegistryEntry } from "./entity/entity_registry";
|
||||
import { domainToName } from "./integration";
|
||||
@@ -47,7 +47,7 @@ export const describeAction = <T extends ActionType>(
|
||||
entityRegistry: EntityRegistryEntry[],
|
||||
action: ActionTypes[T],
|
||||
actionType?: T,
|
||||
options?: DescribeOptions,
|
||||
ignoreAlias = false,
|
||||
manifests?: DomainManifestLookup
|
||||
): string => {
|
||||
try {
|
||||
@@ -56,7 +56,7 @@ export const describeAction = <T extends ActionType>(
|
||||
entityRegistry,
|
||||
action,
|
||||
actionType,
|
||||
options,
|
||||
ignoreAlias,
|
||||
manifests
|
||||
);
|
||||
if (typeof description !== "string") {
|
||||
@@ -79,10 +79,10 @@ const tryDescribeAction = <T extends ActionType>(
|
||||
entityRegistry: EntityRegistryEntry[],
|
||||
action: ActionTypes[T],
|
||||
actionType?: T,
|
||||
options?: DescribeOptions,
|
||||
ignoreAlias = false,
|
||||
manifests?: DomainManifestLookup
|
||||
): string => {
|
||||
if (action.alias && !options?.ignoreAlias) {
|
||||
if (action.alias && !ignoreAlias) {
|
||||
return action.alias;
|
||||
}
|
||||
if (!actionType) {
|
||||
@@ -322,12 +322,7 @@ const tryDescribeAction = <T extends ActionType>(
|
||||
return hass.localize(
|
||||
`${actionTranslationBaseKey}.check_condition.description.full`,
|
||||
{
|
||||
condition: describeCondition(
|
||||
action as Condition,
|
||||
hass,
|
||||
entityRegistry,
|
||||
options
|
||||
),
|
||||
condition: describeCondition(action as Condition, hass, entityRegistry),
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
+1
-3
@@ -38,12 +38,10 @@ export type ItemType =
|
||||
export const findRelated = (
|
||||
hass: Pick<HomeAssistant, "callWS">,
|
||||
itemType: ItemType,
|
||||
itemId: string,
|
||||
includeDisabledEntities = false
|
||||
itemId: string
|
||||
): Promise<RelatedResult> =>
|
||||
hass.callWS<RelatedResult>({
|
||||
type: "search/related",
|
||||
item_type: itemType,
|
||||
item_id: itemId,
|
||||
...(includeDisabledEntities ? { include_disabled_entities: true } : {}),
|
||||
});
|
||||
|
||||
@@ -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,318 +0,0 @@
|
||||
import "@home-assistant/webawesome/dist/components/skeleton/skeleton";
|
||||
import { consume } from "@lit/context";
|
||||
import type { HassConfig } from "home-assistant-js-websocket";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { isComponentLoaded } from "../../../../common/config/is_component_loaded";
|
||||
import { relativeTime } from "../../../../common/datetime/relative_time";
|
||||
import { consumeLocalize } from "../../../../common/decorators/consume-context-entry";
|
||||
import { transform } from "../../../../common/decorators/transform";
|
||||
import { supportsFeature } from "../../../../common/entity/supports-feature";
|
||||
import type { LocalizeFunc } from "../../../../common/translations/localize";
|
||||
import "../../../../components/animation/ha-fade-in";
|
||||
import "../../../../components/ha-switch";
|
||||
import "../../../../components/item/ha-row-item";
|
||||
import type { BackupConfig } from "../../../../data/backup";
|
||||
import { fetchBackupConfig } from "../../../../data/backup";
|
||||
import {
|
||||
apiContext,
|
||||
configContext,
|
||||
internationalizationContext,
|
||||
statesContext,
|
||||
} from "../../../../data/context";
|
||||
import type { EntitySources } from "../../../../data/entity/entity_sources";
|
||||
import { fetchEntitySourcesWithCache } from "../../../../data/entity/entity_sources";
|
||||
import { getSupervisorUpdateConfig } from "../../../../data/supervisor/update";
|
||||
import type { FrontendLocaleData } from "../../../../data/translation";
|
||||
import type { UpdateEntity, UpdateType } from "../../../../data/update";
|
||||
import {
|
||||
getUpdateType,
|
||||
UpdateEntityFeature,
|
||||
updateIsInstalling,
|
||||
} from "../../../../data/update";
|
||||
import type {
|
||||
HomeAssistant,
|
||||
HomeAssistantApi,
|
||||
HomeAssistantConfig,
|
||||
HomeAssistantInternationalization,
|
||||
} from "../../../../types";
|
||||
|
||||
@customElement("ha-more-info-update-backup")
|
||||
export class HaMoreInfoUpdateBackup extends LitElement {
|
||||
@property({ attribute: false }) public stateObj?: UpdateEntity;
|
||||
|
||||
@state()
|
||||
@consumeLocalize()
|
||||
private _localize!: LocalizeFunc;
|
||||
|
||||
@state()
|
||||
@consume({ context: internationalizationContext, subscribe: true })
|
||||
@transform<HomeAssistantInternationalization, FrontendLocaleData>({
|
||||
transformer: ({ locale }) => locale,
|
||||
})
|
||||
private _locale!: FrontendLocaleData;
|
||||
|
||||
@state() private _backupConfigLoading = true;
|
||||
|
||||
@state() private _backupConfig?: BackupConfig;
|
||||
|
||||
@state() private _createBackupLoading = true;
|
||||
|
||||
@state() private _createBackup = false;
|
||||
|
||||
@state() private _entitySources?: EntitySources;
|
||||
|
||||
@consume({ context: apiContext, subscribe: true })
|
||||
private _api!: HomeAssistantApi;
|
||||
|
||||
@consume({ context: statesContext, subscribe: true })
|
||||
private _states!: HomeAssistant["states"];
|
||||
|
||||
@consume({ context: configContext, subscribe: true })
|
||||
@transform<HomeAssistantConfig, HassConfig>({
|
||||
transformer: ({ config }) => config,
|
||||
})
|
||||
private _config!: HassConfig;
|
||||
|
||||
public get createBackup(): boolean {
|
||||
if (
|
||||
!this.stateObj ||
|
||||
!supportsFeature(this.stateObj, UpdateEntityFeature.BACKUP)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
return this._createBackup;
|
||||
}
|
||||
|
||||
protected firstUpdated(): void {
|
||||
this._setupBackup();
|
||||
}
|
||||
|
||||
protected render() {
|
||||
if (
|
||||
!this.stateObj ||
|
||||
!supportsFeature(this.stateObj, UpdateEntityFeature.BACKUP)
|
||||
) {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
const createBackupTexts = this._computeCreateBackupTexts();
|
||||
|
||||
if (!createBackupTexts && !this._backupConfigLoading) {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
return html`
|
||||
<ha-row-item
|
||||
.headline=${createBackupTexts ? createBackupTexts.title : undefined}
|
||||
.supportingText=${
|
||||
createBackupTexts ? createBackupTexts.description : undefined
|
||||
}
|
||||
>
|
||||
${
|
||||
!createBackupTexts
|
||||
? html`<ha-fade-in slot="headline" .delay=${500}
|
||||
><wa-skeleton effect="sheen"></wa-skeleton
|
||||
></ha-fade-in>`
|
||||
: nothing
|
||||
}
|
||||
${
|
||||
this._createBackupLoading
|
||||
? html`<ha-fade-in class="skeleton-end" slot="end" .delay=${500}
|
||||
><wa-skeleton effect="sheen"></wa-skeleton
|
||||
></ha-fade-in>`
|
||||
: html`<ha-switch
|
||||
slot="end"
|
||||
.checked=${this._createBackup}
|
||||
@change=${this._createBackupChanged}
|
||||
.disabled=${updateIsInstalling(this.stateObj)}
|
||||
></ha-switch>`
|
||||
}
|
||||
</ha-row-item>
|
||||
`;
|
||||
}
|
||||
|
||||
private async _setupBackup(): Promise<void> {
|
||||
if (!supportsFeature(this.stateObj!, UpdateEntityFeature.BACKUP)) {
|
||||
this._createBackupLoading = false;
|
||||
this._backupConfigLoading = false;
|
||||
return;
|
||||
}
|
||||
|
||||
let type: UpdateType | undefined = undefined;
|
||||
|
||||
try {
|
||||
this._entitySources = await fetchEntitySourcesWithCache({
|
||||
callWS: this._api.callWS,
|
||||
states: this._states,
|
||||
});
|
||||
type = getUpdateType(this.stateObj!, this._entitySources!);
|
||||
} catch (err) {
|
||||
// ignore error, because the generic backup option remains available
|
||||
// eslint-disable-next-line no-console
|
||||
console.error(err);
|
||||
}
|
||||
|
||||
if (type === undefined) {
|
||||
this._createBackupLoading = false;
|
||||
this._backupConfigLoading = false;
|
||||
return;
|
||||
}
|
||||
|
||||
this._fetchUpdateBackupConfig(type);
|
||||
this._fetchBackupConfig(type);
|
||||
}
|
||||
|
||||
private async _fetchUpdateBackupConfig(type: UpdateType) {
|
||||
try {
|
||||
if (
|
||||
isComponentLoaded(this._config, "hassio") &&
|
||||
["addon", "home_assistant", "home_assistant_os"].includes(type)
|
||||
) {
|
||||
const config = await getSupervisorUpdateConfig(this._api);
|
||||
|
||||
// for home assistant and OS updates
|
||||
if (this._isHaOrOsUpdate(type)) {
|
||||
this._createBackup = config.core_backup_before_update;
|
||||
return;
|
||||
}
|
||||
|
||||
if (type === "addon") {
|
||||
this._createBackup = config.add_on_backup_before_update;
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
// ignore error, because user can still set the config
|
||||
// eslint-disable-next-line no-console
|
||||
console.error(err);
|
||||
this._createBackup = false;
|
||||
} finally {
|
||||
this._createBackupLoading = false;
|
||||
}
|
||||
}
|
||||
|
||||
private async _fetchBackupConfig(type: UpdateType): Promise<void> {
|
||||
try {
|
||||
if (this._isHaOrOsUpdate(type)) {
|
||||
const { config } = await fetchBackupConfig(this._api);
|
||||
this._backupConfig = config;
|
||||
}
|
||||
} catch (err) {
|
||||
// ignore error, because user can still set the config
|
||||
// eslint-disable-next-line no-console
|
||||
console.error(err);
|
||||
} finally {
|
||||
this._backupConfigLoading = false;
|
||||
}
|
||||
}
|
||||
|
||||
private _isHaOrOsUpdate(type: UpdateType): boolean {
|
||||
return ["home_assistant", "home_assistant_os"].includes(type);
|
||||
}
|
||||
|
||||
private _computeCreateBackupTexts():
|
||||
{ title: string; description?: string } | undefined {
|
||||
const updateType = this._entitySources
|
||||
? getUpdateType(this.stateObj!, this._entitySources)
|
||||
: "generic";
|
||||
|
||||
if (this._isHaOrOsUpdate(updateType)) {
|
||||
if (this._backupConfigLoading) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const isBackupConfigValid =
|
||||
!!this._backupConfig &&
|
||||
!!this._backupConfig.automatic_backups_configured &&
|
||||
!!this._backupConfig.create_backup.password &&
|
||||
this._backupConfig.create_backup.agent_ids.length > 0;
|
||||
|
||||
if (!isBackupConfigValid) {
|
||||
return {
|
||||
title: this._localize(
|
||||
"ui.dialogs.more_info_control.update.create_backup.manual"
|
||||
),
|
||||
description: this._localize(
|
||||
"ui.dialogs.more_info_control.update.create_backup.manual_description"
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
const lastAutomaticBackupDate = this._backupConfig
|
||||
?.last_completed_automatic_backup
|
||||
? new Date(this._backupConfig?.last_completed_automatic_backup)
|
||||
: null;
|
||||
const now = new Date();
|
||||
|
||||
return {
|
||||
title: this._localize(
|
||||
"ui.dialogs.more_info_control.update.create_backup.automatic"
|
||||
),
|
||||
description: lastAutomaticBackupDate
|
||||
? this._localize(
|
||||
"ui.dialogs.more_info_control.update.create_backup.automatic_description_last",
|
||||
{
|
||||
relative_time: relativeTime(
|
||||
lastAutomaticBackupDate,
|
||||
this._locale,
|
||||
now,
|
||||
true
|
||||
),
|
||||
}
|
||||
)
|
||||
: this._localize(
|
||||
"ui.dialogs.more_info_control.update.create_backup.automatic_description_none"
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
// App backup
|
||||
if (updateType === "addon") {
|
||||
const version = this.stateObj!.attributes.installed_version;
|
||||
return {
|
||||
title: this._localize(
|
||||
"ui.dialogs.more_info_control.update.create_backup.app"
|
||||
),
|
||||
description: version
|
||||
? this._localize(
|
||||
"ui.dialogs.more_info_control.update.create_backup.app_description",
|
||||
{ version: version }
|
||||
)
|
||||
: undefined,
|
||||
};
|
||||
}
|
||||
|
||||
// Fallback to generic UI
|
||||
return {
|
||||
title: this._localize(
|
||||
"ui.dialogs.more_info_control.update.create_backup.generic"
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
private _createBackupChanged(ev) {
|
||||
this._createBackup = ev.target.checked;
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
:host {
|
||||
display: block;
|
||||
width: 100%;
|
||||
}
|
||||
ha-row-item {
|
||||
width: 100%;
|
||||
--ha-row-item-padding-inline: var(--ha-space-6);
|
||||
}
|
||||
.skeleton-end {
|
||||
width: 48px;
|
||||
height: 24px;
|
||||
display: block;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-more-info-update-backup": HaMoreInfoUpdateBackup;
|
||||
}
|
||||
}
|
||||
@@ -131,10 +131,6 @@ class MoreInfoFan extends LitElement {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
const supportsOnOff =
|
||||
supportsFeature(this.stateObj, FanEntityFeature.TURN_ON) ||
|
||||
supportsFeature(this.stateObj, FanEntityFeature.TURN_OFF);
|
||||
|
||||
const supportsSpeed = supportsFeature(
|
||||
this.stateObj,
|
||||
FanEntityFeature.SET_SPEED
|
||||
@@ -178,15 +174,21 @@ class MoreInfoFan extends LitElement {
|
||||
`
|
||||
}
|
||||
${
|
||||
supportSpeedPercentage && supportsOnOff
|
||||
supportSpeedPercentage
|
||||
? html`
|
||||
<div class="buttons">
|
||||
<ha-outlined-icon-button
|
||||
.disabled=${this.stateObj.state === UNAVAILABLE}
|
||||
@click=${this._toggle}
|
||||
>
|
||||
<ha-svg-icon .path=${mdiPower}></ha-svg-icon>
|
||||
</ha-outlined-icon-button>
|
||||
${
|
||||
supportSpeedPercentage
|
||||
? html`
|
||||
<ha-outlined-icon-button
|
||||
.disabled=${this.stateObj.state === UNAVAILABLE}
|
||||
@click=${this._toggle}
|
||||
>
|
||||
<ha-svg-icon .path=${mdiPower}></ha-svg-icon>
|
||||
</ha-outlined-icon-button>
|
||||
`
|
||||
: nothing
|
||||
}
|
||||
</div>
|
||||
`
|
||||
: nothing
|
||||
|
||||
@@ -1,32 +1,57 @@
|
||||
import "@home-assistant/webawesome/dist/components/skeleton/skeleton";
|
||||
import { consume } from "@lit/context";
|
||||
import type { HassConfig } from "home-assistant-js-websocket";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { isComponentLoaded } from "../../../common/config/is_component_loaded";
|
||||
import { BINARY_STATE_OFF } from "../../../common/const";
|
||||
import { relativeTime } from "../../../common/datetime/relative_time";
|
||||
import { consumeLocalize } from "../../../common/decorators/consume-context-entry";
|
||||
import { transform } from "../../../common/decorators/transform";
|
||||
import { supportsFeature } from "../../../common/entity/supports-feature";
|
||||
import type { LocalizeFunc } from "../../../common/translations/localize";
|
||||
import { sanitizeHttpUrl } from "../../../common/url/sanitize-http-url";
|
||||
import "../../../components/animation/ha-fade-in";
|
||||
import "../../../components/buttons/ha-progress-button";
|
||||
import "../../../components/ha-alert";
|
||||
import "../../../components/ha-button";
|
||||
import "../../../components/ha-faded";
|
||||
import "../../../components/ha-markdown";
|
||||
import "../../../components/ha-spinner";
|
||||
import "../../../components/ha-switch";
|
||||
import "../../../components/item/ha-row-item";
|
||||
import "../../../components/progress/ha-progress-bar";
|
||||
import { apiContext, formattersContext } from "../../../data/context";
|
||||
import { UNAVAILABLE, UNKNOWN } from "../../../data/entity/entity";
|
||||
import type { UpdateEntity } from "../../../data/update";
|
||||
import type { BackupConfig } from "../../../data/backup";
|
||||
import { fetchBackupConfig } from "../../../data/backup";
|
||||
import {
|
||||
apiContext,
|
||||
configContext,
|
||||
formattersContext,
|
||||
internationalizationContext,
|
||||
statesContext,
|
||||
} from "../../../data/context";
|
||||
import { UNAVAILABLE, UNKNOWN } from "../../../data/entity/entity";
|
||||
import type { EntitySources } from "../../../data/entity/entity_sources";
|
||||
import { fetchEntitySourcesWithCache } from "../../../data/entity/entity_sources";
|
||||
import { getSupervisorUpdateConfig } from "../../../data/supervisor/update";
|
||||
import type { FrontendLocaleData } from "../../../data/translation";
|
||||
import type { UpdateEntity, UpdateType } from "../../../data/update";
|
||||
import {
|
||||
getUpdateType,
|
||||
latestVersionIsSkipped,
|
||||
updateButtonIsDisabled,
|
||||
UpdateEntityFeature,
|
||||
updateIsInstalling,
|
||||
updateReleaseNotes,
|
||||
} from "../../../data/update";
|
||||
import type { HomeAssistantApi, HomeAssistantFormatters } from "../../../types";
|
||||
import type {
|
||||
HomeAssistant,
|
||||
HomeAssistantApi,
|
||||
HomeAssistantConfig,
|
||||
HomeAssistantFormatters,
|
||||
HomeAssistantInternationalization,
|
||||
} from "../../../types";
|
||||
import { showAlertDialog } from "../../generic/show-dialog-box";
|
||||
import "../components/update/ha-more-info-update-backup";
|
||||
import type { HaMoreInfoUpdateBackup } from "../components/update/ha-more-info-update-backup";
|
||||
|
||||
@customElement("more-info-update")
|
||||
class MoreInfoUpdate extends LitElement {
|
||||
@@ -36,21 +61,182 @@ class MoreInfoUpdate extends LitElement {
|
||||
@consumeLocalize()
|
||||
private _localize!: LocalizeFunc;
|
||||
|
||||
@state()
|
||||
@consume({ context: internationalizationContext, subscribe: true })
|
||||
@transform<HomeAssistantInternationalization, FrontendLocaleData>({
|
||||
transformer: ({ locale }) => locale,
|
||||
})
|
||||
private _locale!: FrontendLocaleData;
|
||||
|
||||
@state()
|
||||
@consume({ context: formattersContext, subscribe: true })
|
||||
private _formatters!: HomeAssistantFormatters;
|
||||
|
||||
@state()
|
||||
@consume({ context: apiContext, subscribe: true })
|
||||
private _api!: HomeAssistantApi;
|
||||
|
||||
@state()
|
||||
@consume({ context: statesContext, subscribe: true })
|
||||
private _states!: HomeAssistant["states"];
|
||||
|
||||
@state()
|
||||
@consume({ context: configContext, subscribe: true })
|
||||
@transform<HomeAssistantConfig, HassConfig>({
|
||||
transformer: ({ config }) => config,
|
||||
})
|
||||
private _config!: HassConfig;
|
||||
|
||||
@state() private _releaseNotes?: string | null;
|
||||
|
||||
@state() private _error?: string;
|
||||
|
||||
@state() private _markdownLoading = true;
|
||||
|
||||
@query("ha-more-info-update-backup")
|
||||
private _backupElement?: HaMoreInfoUpdateBackup;
|
||||
@state() private _backupConfigLoading = true;
|
||||
|
||||
@state() private _backupConfig?: BackupConfig;
|
||||
|
||||
@state() private _createBackupLoading = true;
|
||||
|
||||
@state() private _createBackup = false;
|
||||
|
||||
@state() private _entitySources?: EntitySources;
|
||||
|
||||
private async _fetchBackupConfig() {
|
||||
try {
|
||||
const { config } = await fetchBackupConfig(this._api);
|
||||
this._backupConfig = config;
|
||||
} catch (err) {
|
||||
// ignore error, because user will get a manual backup option
|
||||
// eslint-disable-next-line no-console
|
||||
console.error(err);
|
||||
} finally {
|
||||
this._backupConfigLoading = false;
|
||||
}
|
||||
}
|
||||
|
||||
private async _fetchUpdateBackupConfig(type: UpdateType) {
|
||||
try {
|
||||
const config = await getSupervisorUpdateConfig(this._api);
|
||||
|
||||
// for home assistant and OS updates
|
||||
if (this._isHaOrOsUpdate(type)) {
|
||||
this._createBackup = config.core_backup_before_update;
|
||||
this._createBackupLoading = false;
|
||||
return;
|
||||
}
|
||||
|
||||
if (type === "addon") {
|
||||
this._createBackup = config.add_on_backup_before_update;
|
||||
}
|
||||
} catch (err) {
|
||||
// ignore error, because user can still set the config
|
||||
// eslint-disable-next-line no-console
|
||||
console.error(err);
|
||||
this._createBackup = false;
|
||||
} finally {
|
||||
this._createBackupLoading = false;
|
||||
}
|
||||
}
|
||||
|
||||
private async _fetchEntitySources() {
|
||||
this._entitySources = await fetchEntitySourcesWithCache({
|
||||
callWS: this._api.callWS,
|
||||
states: this._states,
|
||||
});
|
||||
}
|
||||
|
||||
private _isHaOrOsUpdate(type: UpdateType): boolean {
|
||||
return ["home_assistant", "home_assistant_os"].includes(type);
|
||||
}
|
||||
|
||||
private _computeCreateBackupTexts():
|
||||
{ title: string; description?: string } | undefined {
|
||||
if (
|
||||
!this.stateObj ||
|
||||
!supportsFeature(this.stateObj, UpdateEntityFeature.BACKUP)
|
||||
) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const updateType = this._entitySources
|
||||
? getUpdateType(this.stateObj, this._entitySources)
|
||||
: "generic";
|
||||
|
||||
if (this._isHaOrOsUpdate(updateType)) {
|
||||
if (this._backupConfigLoading) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const isBackupConfigValid =
|
||||
!!this._backupConfig &&
|
||||
!!this._backupConfig.automatic_backups_configured &&
|
||||
!!this._backupConfig.create_backup.password &&
|
||||
this._backupConfig.create_backup.agent_ids.length > 0;
|
||||
|
||||
if (!isBackupConfigValid) {
|
||||
return {
|
||||
title: this._localize(
|
||||
"ui.dialogs.more_info_control.update.create_backup.manual"
|
||||
),
|
||||
description: this._localize(
|
||||
"ui.dialogs.more_info_control.update.create_backup.manual_description"
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
const lastAutomaticBackupDate = this._backupConfig
|
||||
?.last_completed_automatic_backup
|
||||
? new Date(this._backupConfig?.last_completed_automatic_backup)
|
||||
: null;
|
||||
const now = new Date();
|
||||
|
||||
return {
|
||||
title: this._localize(
|
||||
"ui.dialogs.more_info_control.update.create_backup.automatic"
|
||||
),
|
||||
description: lastAutomaticBackupDate
|
||||
? this._localize(
|
||||
"ui.dialogs.more_info_control.update.create_backup.automatic_description_last",
|
||||
{
|
||||
relative_time: relativeTime(
|
||||
lastAutomaticBackupDate,
|
||||
this._locale,
|
||||
now,
|
||||
true
|
||||
),
|
||||
}
|
||||
)
|
||||
: this._localize(
|
||||
"ui.dialogs.more_info_control.update.create_backup.automatic_description_none"
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
// App backup
|
||||
if (updateType === "addon") {
|
||||
const version = this.stateObj.attributes.installed_version;
|
||||
return {
|
||||
title: this._localize(
|
||||
"ui.dialogs.more_info_control.update.create_backup.app"
|
||||
),
|
||||
description: version
|
||||
? this._localize(
|
||||
"ui.dialogs.more_info_control.update.create_backup.app_description",
|
||||
{ version: version }
|
||||
)
|
||||
: undefined,
|
||||
};
|
||||
}
|
||||
|
||||
// Fallback to generic UI
|
||||
return {
|
||||
title: this._localize(
|
||||
"ui.dialogs.more_info_control.update.create_backup.generic"
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
protected render() {
|
||||
if (
|
||||
@@ -62,6 +248,7 @@ class MoreInfoUpdate extends LitElement {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
const createBackupTexts = this._computeCreateBackupTexts();
|
||||
const releaseUrl = sanitizeHttpUrl(this.stateObj.attributes.release_url);
|
||||
|
||||
return html`
|
||||
@@ -160,9 +347,39 @@ class MoreInfoUpdate extends LitElement {
|
||||
}
|
||||
</div>
|
||||
<div class="footer">
|
||||
<ha-more-info-update-backup
|
||||
.stateObj=${this.stateObj}
|
||||
></ha-more-info-update-backup>
|
||||
${
|
||||
createBackupTexts || this._backupConfigLoading
|
||||
? html`
|
||||
<ha-row-item
|
||||
.headline=${createBackupTexts ? createBackupTexts.title : undefined}
|
||||
.supportingText=${createBackupTexts ? createBackupTexts.description : undefined}
|
||||
>
|
||||
${
|
||||
!createBackupTexts
|
||||
? html`<ha-fade-in slot="headline" .delay=${500}
|
||||
><wa-skeleton effect="sheen"></wa-skeleton
|
||||
></ha-fade-in>`
|
||||
: nothing
|
||||
}
|
||||
${
|
||||
this._createBackupLoading
|
||||
? html`<ha-fade-in
|
||||
class="skeleton-end"
|
||||
slot="end"
|
||||
.delay=${500}
|
||||
><wa-skeleton effect="sheen"></wa-skeleton
|
||||
></ha-fade-in>`
|
||||
: html`<ha-switch
|
||||
slot="end"
|
||||
.checked=${this._createBackup}
|
||||
@change=${this._createBackupChanged}
|
||||
.disabled=${updateIsInstalling(this.stateObj)}
|
||||
></ha-switch>`
|
||||
}
|
||||
</ha-row-item>
|
||||
`
|
||||
: nothing
|
||||
}
|
||||
<div class="actions">
|
||||
${
|
||||
this.stateObj.state === BINARY_STATE_OFF &&
|
||||
@@ -223,6 +440,36 @@ class MoreInfoUpdate extends LitElement {
|
||||
if (supportsFeature(this.stateObj!, UpdateEntityFeature.RELEASE_NOTES)) {
|
||||
this._fetchReleaseNotes();
|
||||
}
|
||||
if (supportsFeature(this.stateObj!, UpdateEntityFeature.BACKUP)) {
|
||||
this._fetchEntitySources()
|
||||
.then(() => {
|
||||
const type = getUpdateType(this.stateObj!, this._entitySources!);
|
||||
if (
|
||||
isComponentLoaded(this._config, "hassio") &&
|
||||
["addon", "home_assistant", "home_assistant_os"].includes(type)
|
||||
) {
|
||||
this._fetchUpdateBackupConfig(type);
|
||||
} else {
|
||||
this._createBackupLoading = false;
|
||||
}
|
||||
|
||||
if (this._isHaOrOsUpdate(type)) {
|
||||
this._fetchBackupConfig();
|
||||
} else {
|
||||
this._backupConfigLoading = false;
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
// ignore error, because the generic backup option remains available
|
||||
// eslint-disable-next-line no-console
|
||||
console.error(err);
|
||||
this._createBackupLoading = false;
|
||||
this._backupConfigLoading = false;
|
||||
});
|
||||
} else {
|
||||
this._createBackupLoading = false;
|
||||
this._backupConfigLoading = false;
|
||||
}
|
||||
}
|
||||
|
||||
private async _markdownLoaded() {
|
||||
@@ -242,12 +489,19 @@ class MoreInfoUpdate extends LitElement {
|
||||
}
|
||||
}
|
||||
|
||||
get _shouldCreateBackup(): boolean {
|
||||
if (!supportsFeature(this.stateObj!, UpdateEntityFeature.BACKUP)) {
|
||||
return false;
|
||||
}
|
||||
return this._createBackup;
|
||||
}
|
||||
|
||||
private _handleInstall(): void {
|
||||
const installData: Record<string, any> = {
|
||||
entity_id: this.stateObj!.entity_id,
|
||||
};
|
||||
|
||||
if (this._backupElement?.createBackup ?? false) {
|
||||
if (this._shouldCreateBackup) {
|
||||
installData.backup = true;
|
||||
}
|
||||
|
||||
@@ -261,6 +515,10 @@ class MoreInfoUpdate extends LitElement {
|
||||
this._api.callService("update", "install", installData);
|
||||
}
|
||||
|
||||
private _createBackupChanged(ev) {
|
||||
this._createBackup = ev.target.checked;
|
||||
}
|
||||
|
||||
private _handleSkip(): void {
|
||||
if (this.stateObj!.attributes.auto_update) {
|
||||
showAlertDialog(this, {
|
||||
@@ -331,6 +589,11 @@ class MoreInfoUpdate extends LitElement {
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
ha-row-item {
|
||||
width: 100%;
|
||||
--ha-row-item-padding-inline: var(--ha-space-6);
|
||||
}
|
||||
|
||||
.actions {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
@@ -364,6 +627,11 @@ class MoreInfoUpdate extends LitElement {
|
||||
box-sizing: border-box;
|
||||
padding-bottom: var(--ha-space-4);
|
||||
}
|
||||
.skeleton-end {
|
||||
width: 48px;
|
||||
height: 24px;
|
||||
display: block;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
|
||||
@@ -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>
|
||||
`
|
||||
|
||||
@@ -29,12 +29,17 @@ import type { HASSDomEvent } from "../../common/dom/fire_event";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import { mainWindow } from "../../common/dom/get_main_window";
|
||||
import { stopPropagation } from "../../common/dom/stop_propagation";
|
||||
import { computeAreaName } from "../../common/entity/compute_area_name";
|
||||
import { computeDeviceName } from "../../common/entity/compute_device_name";
|
||||
import { computeDomain } from "../../common/entity/compute_domain";
|
||||
import {
|
||||
computeEntityEntryNameList,
|
||||
computeEntityNameList,
|
||||
type EntityNameItem,
|
||||
} from "../../common/entity/compute_entity_name_display";
|
||||
computeEntityEntryName,
|
||||
computeEntityName,
|
||||
} from "../../common/entity/compute_entity_name";
|
||||
import {
|
||||
getEntityContext,
|
||||
getEntityEntryContext,
|
||||
} from "../../common/entity/context/get_entity_context";
|
||||
import { shouldHandleRequestSelectedEvent } from "../../common/mwc/handle-request-selected-event";
|
||||
import {
|
||||
getHistoryState,
|
||||
@@ -125,13 +130,6 @@ declare global {
|
||||
|
||||
const DEFAULT_VIEW: MoreInfoView = "info";
|
||||
|
||||
const BREADCRUMB_NAME: EntityNameItem[] = [
|
||||
{ type: "area" },
|
||||
{ type: "parent_device" },
|
||||
{ type: "device" },
|
||||
{ type: "entity" },
|
||||
];
|
||||
|
||||
@customElement("ha-more-info-dialog")
|
||||
export class MoreInfoDialog extends DirtyStateProviderMixin<
|
||||
EntitySettingsState | Helper | Record<string, string[]> | null,
|
||||
@@ -566,27 +564,44 @@ export class MoreInfoDialog extends DirtyStateProviderMixin<
|
||||
const showCloseIcon =
|
||||
isDefaultView && this._parentEntityIds.length === 0 && !this._childView;
|
||||
|
||||
const breadcrumb = (
|
||||
stateObj
|
||||
? computeEntityNameList(
|
||||
stateObj,
|
||||
BREADCRUMB_NAME,
|
||||
const context = stateObj
|
||||
? getEntityContext(
|
||||
stateObj,
|
||||
this.hass.entities,
|
||||
this.hass.devices,
|
||||
this.hass.areas,
|
||||
this.hass.floors
|
||||
)
|
||||
: this._entry
|
||||
? getEntityEntryContext(
|
||||
this._entry,
|
||||
this.hass.entities,
|
||||
this.hass.devices,
|
||||
this.hass.areas,
|
||||
this.hass.floors
|
||||
)
|
||||
: this._entry
|
||||
? computeEntityEntryNameList(
|
||||
this._entry,
|
||||
BREADCRUMB_NAME,
|
||||
this.hass.entities,
|
||||
this.hass.devices,
|
||||
this.hass.areas,
|
||||
this.hass.floors
|
||||
)
|
||||
: [entityId]
|
||||
).filter((v): v is string => Boolean(v));
|
||||
: undefined;
|
||||
|
||||
const entityName = stateObj
|
||||
? computeEntityName(stateObj, this.hass.entities, this.hass.devices)
|
||||
: this._entry
|
||||
? computeEntityEntryName(this._entry, this.hass.devices)
|
||||
: entityId;
|
||||
|
||||
const deviceName = context?.device
|
||||
? computeDeviceName(context.device)
|
||||
: undefined;
|
||||
const parentDeviceName = context?.parentDevice
|
||||
? computeDeviceName(context.parentDevice)
|
||||
: undefined;
|
||||
const areaName = context?.area ? computeAreaName(context.area) : undefined;
|
||||
|
||||
const breadcrumb = [
|
||||
areaName,
|
||||
parentDeviceName,
|
||||
deviceName,
|
||||
entityName,
|
||||
].filter((v): v is string => Boolean(v));
|
||||
const addToMenuItem = this.hass.localize(
|
||||
"ui.dialogs.more_info_control.add_to.item"
|
||||
);
|
||||
@@ -1016,32 +1031,6 @@ export class MoreInfoDialog extends DirtyStateProviderMixin<
|
||||
this._infoEditMode = false;
|
||||
this._detailsYamlMode = false;
|
||||
}
|
||||
|
||||
if (changedProps.has("_entityId")) {
|
||||
this._reportShownEntityToExternalApp(
|
||||
changedProps.get("_entityId") as string | null | undefined
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private _reportShownEntityToExternalApp(
|
||||
previousEntityId: string | null | undefined
|
||||
) {
|
||||
const external = this.hass.auth.external;
|
||||
if (!external) {
|
||||
return;
|
||||
}
|
||||
if (this._entityId) {
|
||||
external.fireMessage({
|
||||
type: "more_info/opened",
|
||||
payload: { entity_id: this._entityId },
|
||||
});
|
||||
} else if (previousEntityId) {
|
||||
external.fireMessage({
|
||||
type: "more_info/closed",
|
||||
payload: { entity_id: previousEntityId },
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private _entryUpdated(ev: CustomEvent<ExtEntityRegistryEntry>) {
|
||||
|
||||
@@ -24,6 +24,7 @@ export class CloudStepSignin extends LitElement {
|
||||
check-connection
|
||||
.hass=${this.hass}
|
||||
.email=${this.email}
|
||||
.localize=${this.hass.localize}
|
||||
@cloud-forgot-password=${this._forgotPassword}
|
||||
@cloud-logged-in=${this._loggedIn}
|
||||
></cloud-login>
|
||||
|
||||
@@ -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
|
||||
}
|
||||
${
|
||||
|
||||
@@ -184,20 +184,6 @@ interface EMOutgoingMessageAddEntityTo extends EMMessage {
|
||||
};
|
||||
}
|
||||
|
||||
interface EMOutgoingMessageMoreInfoOpened extends EMMessage {
|
||||
type: "more_info/opened";
|
||||
payload: {
|
||||
entity_id: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface EMOutgoingMessageMoreInfoClosed extends EMMessage {
|
||||
type: "more_info/closed";
|
||||
payload: {
|
||||
entity_id: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface EMOutgoingMessageFocusElement extends EMMessage {
|
||||
type: "focus_element";
|
||||
payload: {
|
||||
@@ -230,8 +216,6 @@ type EMOutgoingMessageWithoutAnswer =
|
||||
| EMOutgoingMessageHaptic
|
||||
| EMOutgoingMessageImportThreadCredentials
|
||||
| EMOutgoingMessageMatterCommission
|
||||
| EMOutgoingMessageMoreInfoOpened
|
||||
| EMOutgoingMessageMoreInfoClosed
|
||||
| EMOutgoingMessageSidebarShow
|
||||
| EMOutgoingMessageTagWrite
|
||||
| EMOutgoingMessageThemeUpdate
|
||||
|
||||
@@ -465,9 +465,6 @@ export const ENTITY_COMPONENT_ICONS: Record<string, ComponentIcons> = {
|
||||
on: "mdi:alert-circle",
|
||||
},
|
||||
},
|
||||
glass_break: {
|
||||
default: "mdi:glass-fragile",
|
||||
},
|
||||
heat: {
|
||||
default: "mdi:thermometer",
|
||||
state: {
|
||||
|
||||
@@ -5,7 +5,6 @@ import {
|
||||
invalidateThemeCache,
|
||||
} from "../common/dom/apply_themes_on_element";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import { computeEntityNameDisplayWithoutContext } from "../common/entity/compute_entity_name_display";
|
||||
import { computeFormatFunctions } from "../common/translations/entity-state";
|
||||
import { computeLocalize } from "../common/translations/localize";
|
||||
import {
|
||||
@@ -605,7 +604,10 @@ export const provideHass = (
|
||||
value: value !== null ? value : (stateObj.attributes[attribute] ?? ""),
|
||||
},
|
||||
],
|
||||
formatEntityName: computeEntityNameDisplayWithoutContext,
|
||||
formatEntityName: (stateObj, type) =>
|
||||
typeof type === "string"
|
||||
? type
|
||||
: (stateObj.attributes.friendly_name ?? stateObj.entity_id),
|
||||
...overrideData,
|
||||
};
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
|
||||
@@ -257,13 +257,6 @@ class PartialPanelResolver extends HassRouterPage {
|
||||
)
|
||||
) {
|
||||
await this.rebuild();
|
||||
// hass.panels can change again while rebuild() is in flight (e.g.
|
||||
// multiple integrations/resources updating panels around startup), so
|
||||
// the panel we were about to show may no longer exist. willUpdate will
|
||||
// re-run _updateRoutes for the newer panels, so just bail out here.
|
||||
if (!this.hass.panels[this._currentPage]) {
|
||||
return;
|
||||
}
|
||||
const component =
|
||||
COMPONENTS[this.hass.panels[this._currentPage].component_name];
|
||||
await promiseTimeout(
|
||||
|
||||
@@ -1,56 +0,0 @@
|
||||
import { ContextProvider } from "@lit/context";
|
||||
import type { LitElement, PropertyValues } from "lit";
|
||||
import { buildLiteInternationalization } from "../common/translations/lite-internationalization";
|
||||
import type { LocalizeFunc } from "../common/translations/localize";
|
||||
import {
|
||||
internationalizationContext,
|
||||
localeContext,
|
||||
localizeContext,
|
||||
} from "../data/context";
|
||||
import type { Constructor } from "../types";
|
||||
|
||||
interface LiteLocalizeElement {
|
||||
localize?: LocalizeFunc<any>;
|
||||
language?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Provides the i18n contexts from the "lite" localize state, so
|
||||
* context-consuming components work on pre-login roots that have no `hass`.
|
||||
*/
|
||||
export const provideLiteI18nMixin = <
|
||||
T extends Constructor<LitElement & LiteLocalizeElement>,
|
||||
>(
|
||||
superClass: T
|
||||
) =>
|
||||
class extends superClass {
|
||||
private __i18nProvider = new ContextProvider(this, {
|
||||
context: internationalizationContext,
|
||||
});
|
||||
|
||||
private __localizeProvider = new ContextProvider(this, {
|
||||
context: localizeContext,
|
||||
});
|
||||
|
||||
private __localeProvider = new ContextProvider(this, {
|
||||
context: localeContext,
|
||||
});
|
||||
|
||||
protected willUpdate(changedProperties: PropertyValues): void {
|
||||
super.willUpdate(changedProperties);
|
||||
|
||||
if (
|
||||
this.localize &&
|
||||
this.language &&
|
||||
(changedProperties.has("localize") || changedProperties.has("language"))
|
||||
) {
|
||||
const value = buildLiteInternationalization(
|
||||
this.language,
|
||||
this.localize
|
||||
);
|
||||
this.__i18nProvider.setValue(value);
|
||||
this.__localizeProvider.setValue(value.localize);
|
||||
this.__localeProvider.setValue(value.locale);
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -1,20 +1,21 @@
|
||||
import { LitElement, css, html } from "lit";
|
||||
import { customElement, state } from "lit/decorators";
|
||||
import { consumeLocalize } from "../../common/decorators/consume-context-entry";
|
||||
import type { LocalizeFunc } from "../../common/translations/localize";
|
||||
import { LitElement, css, html, nothing } from "lit";
|
||||
import { customElement } from "lit/decorators";
|
||||
import "../../components/ha-dialog";
|
||||
import { DialogMixin } from "../../dialogs/dialog-mixin";
|
||||
import type { AppDialogParams } from "./show-app-dialog";
|
||||
|
||||
@customElement("app-dialog")
|
||||
class DialogApp extends DialogMixin(LitElement) {
|
||||
@state()
|
||||
@consumeLocalize()
|
||||
private _localize!: LocalizeFunc;
|
||||
|
||||
class DialogApp extends DialogMixin<AppDialogParams>(LitElement) {
|
||||
protected render() {
|
||||
if (!this.params?.localize) {
|
||||
return nothing;
|
||||
}
|
||||
return html`<ha-dialog
|
||||
open
|
||||
header-title=${this._localize("ui.panel.page-onboarding.welcome.download_app") || "Click here to download the app"}
|
||||
header-title=${
|
||||
this.params.localize("ui.panel.page-onboarding.welcome.download_app") ||
|
||||
"Click here to download the app"
|
||||
}
|
||||
>
|
||||
<div>
|
||||
<div class="app-qr">
|
||||
@@ -26,13 +27,17 @@ class DialogApp extends DialogMixin(LitElement) {
|
||||
<img
|
||||
loading="lazy"
|
||||
src="/static/images/appstore.svg"
|
||||
alt=${this._localize("ui.panel.page-onboarding.welcome.appstore")}
|
||||
alt=${this.params.localize(
|
||||
"ui.panel.page-onboarding.welcome.appstore"
|
||||
)}
|
||||
class="icon"
|
||||
/>
|
||||
<img
|
||||
loading="lazy"
|
||||
src="/static/images/qr-appstore.svg"
|
||||
alt=${this._localize("ui.panel.page-onboarding.welcome.appstore")}
|
||||
alt=${this.params.localize(
|
||||
"ui.panel.page-onboarding.welcome.appstore"
|
||||
)}
|
||||
/>
|
||||
</a>
|
||||
<a
|
||||
@@ -43,7 +48,7 @@ class DialogApp extends DialogMixin(LitElement) {
|
||||
<img
|
||||
loading="lazy"
|
||||
src="/static/images/playstore.svg"
|
||||
alt=${this._localize(
|
||||
alt=${this.params.localize(
|
||||
"ui.panel.page-onboarding.welcome.playstore"
|
||||
)}
|
||||
class="icon"
|
||||
@@ -51,7 +56,7 @@ class DialogApp extends DialogMixin(LitElement) {
|
||||
<img
|
||||
loading="lazy"
|
||||
src="/static/images/qr-playstore.svg"
|
||||
alt=${this._localize(
|
||||
alt=${this.params.localize(
|
||||
"ui.panel.page-onboarding.welcome.playstore"
|
||||
)}
|
||||
/>
|
||||
|
||||
@@ -1,24 +1,22 @@
|
||||
import { mdiAccountGroup, mdiOpenInNew } from "@mdi/js";
|
||||
import { LitElement, css, html } from "lit";
|
||||
import { customElement, state } from "lit/decorators";
|
||||
import { consumeLocalize } from "../../common/decorators/consume-context-entry";
|
||||
import type { LocalizeFunc } from "../../common/translations/localize";
|
||||
import { LitElement, css, html, nothing } from "lit";
|
||||
import { customElement } from "lit/decorators";
|
||||
import "../../components/ha-dialog";
|
||||
import "../../components/ha-svg-icon";
|
||||
import "../../components/item/ha-list-item-button";
|
||||
import "../../components/list/ha-list-nav";
|
||||
import { DialogMixin } from "../../dialogs/dialog-mixin";
|
||||
import type { CommunityDialogParams } from "./show-community-dialog";
|
||||
|
||||
@customElement("community-dialog")
|
||||
class DialogCommunity extends DialogMixin(LitElement) {
|
||||
@state()
|
||||
@consumeLocalize()
|
||||
private _localize!: LocalizeFunc;
|
||||
|
||||
class DialogCommunity extends DialogMixin<CommunityDialogParams>(LitElement) {
|
||||
protected render() {
|
||||
if (!this.params?.localize) {
|
||||
return nothing;
|
||||
}
|
||||
return html`<ha-dialog
|
||||
open
|
||||
header-title=${this._localize(
|
||||
header-title=${this.params.localize(
|
||||
"ui.panel.page-onboarding.welcome.community"
|
||||
)}
|
||||
>
|
||||
@@ -34,7 +32,7 @@ class DialogCommunity extends DialogMixin(LitElement) {
|
||||
alt="Home Assistant Logo"
|
||||
/>
|
||||
<span slot="headline">
|
||||
${this._localize("ui.panel.page-onboarding.welcome.forums")}
|
||||
${this.params.localize("ui.panel.page-onboarding.welcome.forums")}
|
||||
</span>
|
||||
<ha-svg-icon slot="end" .path=${mdiOpenInNew}></ha-svg-icon>
|
||||
</ha-list-item-button>
|
||||
@@ -49,7 +47,7 @@ class DialogCommunity extends DialogMixin(LitElement) {
|
||||
alt="Open Home Foundation Logo"
|
||||
/>
|
||||
<span slot="headline">
|
||||
${this._localize(
|
||||
${this.params.localize(
|
||||
"ui.panel.page-onboarding.welcome.open_home_newsletter"
|
||||
)}
|
||||
</span>
|
||||
@@ -66,7 +64,7 @@ class DialogCommunity extends DialogMixin(LitElement) {
|
||||
alt="Discord Logo"
|
||||
/>
|
||||
<span slot="headline">
|
||||
${this._localize("ui.panel.page-onboarding.welcome.discord")}
|
||||
${this.params.localize("ui.panel.page-onboarding.welcome.discord")}
|
||||
</span>
|
||||
<ha-svg-icon slot="end" .path=${mdiOpenInNew}></ha-svg-icon>
|
||||
</ha-list-item-button>
|
||||
@@ -77,7 +75,9 @@ class DialogCommunity extends DialogMixin(LitElement) {
|
||||
>
|
||||
<ha-svg-icon .path=${mdiAccountGroup} slot="start"></ha-svg-icon>
|
||||
<span slot="headline">
|
||||
${this._localize("ui.panel.page-onboarding.welcome.social_media")}
|
||||
${this.params.localize(
|
||||
"ui.panel.page-onboarding.welcome.social_media"
|
||||
)}
|
||||
</span>
|
||||
<ha-svg-icon slot="end" .path=${mdiOpenInNew}></ha-svg-icon>
|
||||
</ha-list-item-button>
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user