mirror of
https://github.com/home-assistant/frontend.git
synced 2026-10-01 01:12:54 +00:00
Compare commits
53
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6b060d3082 | ||
|
|
2f8bdf4260 | ||
|
|
ee1e27772f | ||
|
|
d7f8d25a1d | ||
|
|
33615bc5dc | ||
|
|
87105ea76a | ||
|
|
c1ea783993 | ||
|
|
380e9b5a81 | ||
|
|
de21f7ef43 | ||
|
|
fa3c12050c | ||
|
|
bee8497724 | ||
|
|
2de21fad27 | ||
|
|
72fcf19091 | ||
|
|
efa322cc6b | ||
|
|
18f79dfc91 | ||
|
|
5861c7487c | ||
|
|
9dbfa821d3 | ||
|
|
1821e8fd67 | ||
|
|
c8a3aa1d9a | ||
|
|
6c606a4968 | ||
|
|
f59a4b4996 | ||
|
|
4c08607dfe | ||
|
|
f082816d9c | ||
|
|
b503862853 | ||
|
|
1afa83d244 | ||
|
|
7b93dde3c6 | ||
|
|
f8cb27b7ab | ||
|
|
62f1509be4 | ||
|
|
efcf93191f | ||
|
|
577ededaf4 | ||
|
|
1cd16ad301 | ||
|
|
2c2f733a82 | ||
|
|
22df9c704c | ||
|
|
d73341b2f8 | ||
|
|
907d15e7c1 | ||
|
|
662ac1dc57 | ||
|
|
39e7a05258 | ||
|
|
5cb7de5ac6 | ||
|
|
0c7d3c8483 | ||
|
|
c92e5aa134 | ||
|
|
8f73718a35 | ||
|
|
4bf2acdb43 | ||
|
|
f622e5eeb9 | ||
|
|
595068597f | ||
|
|
c708f35ab6 | ||
|
|
205874c800 | ||
|
|
47ddcbe2e5 | ||
|
|
c43b8bfbe0 | ||
|
|
e349b00b15 | ||
|
|
0bb8bdc98a | ||
|
|
bbc5385843 | ||
|
|
43dc5651d3 | ||
|
|
18dcf7abc0 |
@@ -29,6 +29,7 @@ import { mockLabelRegistry } from "./stubs/label_registry";
|
||||
import { mockIcons } from "./stubs/icons";
|
||||
import { mockHistory } from "./stubs/history";
|
||||
import { mockLovelace } from "./stubs/lovelace";
|
||||
import { zoneRegistryEntries } from "./stubs/map";
|
||||
import { mockMediaPlayer } from "./stubs/media_player";
|
||||
import { mockPersistentNotification } from "./stubs/persistent_notification";
|
||||
import { mockRecorder } from "./stubs/recorder";
|
||||
@@ -181,6 +182,7 @@ export class HaDemo extends HomeAssistantAppEl {
|
||||
modified_at: 0,
|
||||
},
|
||||
...connectivityEntityRegistryEntries,
|
||||
...zoneRegistryEntries,
|
||||
]);
|
||||
|
||||
hass.addEntities(energyEntities());
|
||||
|
||||
@@ -1,53 +1,3 @@
|
||||
export const mapEntities = () =>
|
||||
Object.values({
|
||||
"zone.home": {
|
||||
entity_id: "zone.home",
|
||||
state: "zoning",
|
||||
attributes: {
|
||||
hidden: true,
|
||||
latitude: 52.3631339,
|
||||
longitude: 4.8903147,
|
||||
radius: 200,
|
||||
friendly_name: "Home",
|
||||
icon: "hademo:home",
|
||||
},
|
||||
},
|
||||
"zone.uva": {
|
||||
entity_id: "zone.buckhead",
|
||||
state: "zoning",
|
||||
attributes: {
|
||||
hidden: true,
|
||||
radius: 400,
|
||||
friendly_name: "UvA",
|
||||
icon: "hademo:school",
|
||||
latitude: 52.3558182,
|
||||
longitude: 4.9535376,
|
||||
},
|
||||
},
|
||||
"person.arsaboo": {
|
||||
entity_id: "person.arsaboo",
|
||||
state: "not_home",
|
||||
attributes: {
|
||||
radius: 50,
|
||||
friendly_name: "Arsaboo",
|
||||
latitude: 52.3579946,
|
||||
longitude: 4.8664597,
|
||||
entity_picture: "/assets/arsaboo/images/arsaboo.jpg",
|
||||
},
|
||||
},
|
||||
"person.melody": {
|
||||
entity_id: "person.melody",
|
||||
state: "not_home",
|
||||
attributes: {
|
||||
radius: 50,
|
||||
friendly_name: "Melody",
|
||||
latitude: 52.3408927,
|
||||
longitude: 4.8711073,
|
||||
entity_picture: "/assets/arsaboo/images/melody.jpg",
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export const energyEntities = () =>
|
||||
Object.values({
|
||||
"sensor.grid_fossil_fuel_percentage": {
|
||||
|
||||
@@ -9,7 +9,7 @@ import {
|
||||
import "../custom-cards/cast-demo-row";
|
||||
import "../custom-cards/ha-demo-card";
|
||||
import "../custom-cards/ha-demo-next-card";
|
||||
import { mapEntities } from "./entities";
|
||||
import { mockMap } from "./map";
|
||||
|
||||
export const mockLovelace = (
|
||||
hass: MockHomeAssistant,
|
||||
@@ -17,7 +17,7 @@ export const mockLovelace = (
|
||||
) => {
|
||||
hass.mockWS("lovelace/config", ({ url_path }) => {
|
||||
if (url_path === "map") {
|
||||
hass.addEntities(mapEntities());
|
||||
mockMap(hass);
|
||||
return {
|
||||
strategy: {
|
||||
type: "map",
|
||||
|
||||
@@ -0,0 +1,243 @@
|
||||
import type { EntityRegistryEntry } from "../../../src/data/entity/entity_registry";
|
||||
import type { EntityHistoryState } from "../../../src/data/history";
|
||||
import type { Zone } from "../../../src/data/zone";
|
||||
import { demoConfig } from "../../../src/fake_data/demo_config";
|
||||
import type { EntityInput } from "../../../src/fake_data/entities/types";
|
||||
import type { MockHomeAssistant } from "../../../src/fake_data/provide_hass";
|
||||
|
||||
const HOUR = 3600 * 1000;
|
||||
|
||||
type ZoneData = Omit<Zone, "id">;
|
||||
|
||||
const HOME: ZoneData = {
|
||||
name: "Home",
|
||||
icon: "mdi:home",
|
||||
latitude: demoConfig.latitude,
|
||||
longitude: demoConfig.longitude,
|
||||
radius: 100,
|
||||
passive: false,
|
||||
};
|
||||
|
||||
export const zones: Zone[] = [
|
||||
{
|
||||
id: "gym",
|
||||
name: "Gym",
|
||||
icon: "mdi:dumbbell",
|
||||
latitude: 52.358,
|
||||
longitude: 4.8686,
|
||||
radius: 150,
|
||||
passive: false,
|
||||
},
|
||||
{
|
||||
id: "school",
|
||||
name: "School",
|
||||
icon: "mdi:school",
|
||||
latitude: 52.3558182,
|
||||
longitude: 4.9535376,
|
||||
radius: 250,
|
||||
passive: false,
|
||||
},
|
||||
{
|
||||
id: "supermarket",
|
||||
name: "Supermarket",
|
||||
icon: "mdi:cart",
|
||||
latitude: 52.3793,
|
||||
longitude: 4.8994,
|
||||
radius: 100,
|
||||
passive: false,
|
||||
},
|
||||
{
|
||||
id: "work",
|
||||
name: "Work",
|
||||
icon: "mdi:briefcase",
|
||||
latitude: 52.3909184,
|
||||
longitude: 4.8530821,
|
||||
radius: 200,
|
||||
passive: false,
|
||||
},
|
||||
];
|
||||
|
||||
interface DemoPerson {
|
||||
id: string;
|
||||
name: string;
|
||||
picture: string;
|
||||
latitude: number;
|
||||
longitude: number;
|
||||
// Hours ago and the state entered then, oldest first. The first entry is
|
||||
// the state before the 24 hour activity window, the last one the current
|
||||
// state.
|
||||
timeline: [number, string][];
|
||||
}
|
||||
|
||||
const PEOPLE: DemoPerson[] = [
|
||||
{
|
||||
id: "arsaboo",
|
||||
name: "Arsaboo",
|
||||
picture: "/assets/arsaboo/images/arsaboo.jpg",
|
||||
latitude: 52.3732,
|
||||
longitude: 4.8904,
|
||||
timeline: [
|
||||
[30, "home"],
|
||||
[10, "not_home"],
|
||||
[9.5, "Work"],
|
||||
[5, "not_home"],
|
||||
[4.6, "Gym"],
|
||||
[3.4, "not_home"],
|
||||
[3.1, "Supermarket"],
|
||||
[2.8, "not_home"],
|
||||
[2.4, "home"],
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "melody",
|
||||
name: "Melody",
|
||||
picture: "/assets/arsaboo/images/melody.jpg",
|
||||
latitude: 52.373,
|
||||
longitude: 4.8901,
|
||||
timeline: [
|
||||
[30, "home"],
|
||||
[9, "not_home"],
|
||||
[8.6, "School"],
|
||||
[4, "not_home"],
|
||||
[3.6, "home"],
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "oscar",
|
||||
name: "Oscar",
|
||||
picture: "/assets/kernehed/oscar.jpg",
|
||||
latitude: 52.391,
|
||||
longitude: 4.8532,
|
||||
timeline: [
|
||||
[30, "home"],
|
||||
[9, "not_home"],
|
||||
[8.7, "Work"],
|
||||
[4.5, "not_home"],
|
||||
[4.2, "Supermarket"],
|
||||
[3.8, "not_home"],
|
||||
[3.5, "Work"],
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "bella",
|
||||
name: "Bella",
|
||||
picture: "/assets/kernehed/bella.jpg",
|
||||
latitude: 52.365,
|
||||
longitude: 4.8795,
|
||||
timeline: [
|
||||
[30, "home"],
|
||||
[8, "not_home"],
|
||||
[7.6, "Work"],
|
||||
[1.2, "not_home"],
|
||||
[0.9, "Gym"],
|
||||
[0.3, "not_home"],
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
const CAR: EntityInput = {
|
||||
entity_id: "device_tracker.car",
|
||||
state: "not_home",
|
||||
attributes: {
|
||||
friendly_name: "Car",
|
||||
icon: "mdi:car",
|
||||
source_type: "gps",
|
||||
latitude: 52.3805,
|
||||
longitude: 4.8718,
|
||||
gps_accuracy: 10,
|
||||
},
|
||||
};
|
||||
|
||||
const lastMove = (person: DemoPerson) =>
|
||||
person.timeline[person.timeline.length - 1];
|
||||
|
||||
const zoneEntity = (entityId: string, zone: ZoneData): EntityInput => {
|
||||
// A person's state is "home" for the home zone, the zone name otherwise
|
||||
const personState = entityId === "zone.home" ? "home" : zone.name;
|
||||
const persons = PEOPLE.filter(
|
||||
(person) => lastMove(person)[1] === personState
|
||||
).map((person) => `person.${person.id}`);
|
||||
return {
|
||||
entity_id: entityId,
|
||||
state: String(persons.length),
|
||||
attributes: {
|
||||
latitude: zone.latitude,
|
||||
longitude: zone.longitude,
|
||||
radius: zone.radius,
|
||||
passive: zone.passive,
|
||||
persons,
|
||||
editable: true,
|
||||
icon: zone.icon,
|
||||
friendly_name: zone.name,
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
export const zoneEntities = (): EntityInput[] => [
|
||||
zoneEntity("zone.home", HOME),
|
||||
...zones.map((zone) => zoneEntity(`zone.${zone.id}`, zone)),
|
||||
];
|
||||
|
||||
// UI zones are in the entity registry, so the zone editor does not also list
|
||||
// them as YAML zones
|
||||
export const zoneRegistryEntries: EntityRegistryEntry[] = zones.map((zone) => ({
|
||||
config_entry_id: null,
|
||||
config_subentry_id: null,
|
||||
device_id: null,
|
||||
area_id: null,
|
||||
disabled_by: null,
|
||||
entity_id: `zone.${zone.id}`,
|
||||
id: `zone.${zone.id}`,
|
||||
name: null,
|
||||
icon: null,
|
||||
labels: [],
|
||||
categories: {},
|
||||
platform: "zone",
|
||||
hidden_by: null,
|
||||
entity_category: null,
|
||||
has_entity_name: false,
|
||||
unique_id: zone.id,
|
||||
options: null,
|
||||
created_at: 0,
|
||||
modified_at: 0,
|
||||
}));
|
||||
|
||||
const personEntity = (person: DemoPerson, now: number): EntityInput => {
|
||||
const [hoursAgo, state] = lastMove(person);
|
||||
return {
|
||||
entity_id: `person.${person.id}`,
|
||||
state,
|
||||
last_changed: new Date(now - hoursAgo * HOUR).toISOString(),
|
||||
attributes: {
|
||||
friendly_name: person.name,
|
||||
entity_picture: person.picture,
|
||||
latitude: person.latitude,
|
||||
longitude: person.longitude,
|
||||
gps_accuracy: 20,
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
const personHistory = (person: DemoPerson, now: number): EntityHistoryState[] =>
|
||||
person.timeline.map(([hoursAgo, state]) => {
|
||||
const time = (now - hoursAgo * HOUR) / 1000;
|
||||
return { s: state, a: {}, lu: time, lc: time };
|
||||
});
|
||||
|
||||
export const mockMap = (hass: MockHomeAssistant) => {
|
||||
const now = Date.now();
|
||||
hass.addEntities([
|
||||
...zoneEntities(),
|
||||
...PEOPLE.map((person) => personEntity(person, now)),
|
||||
CAR,
|
||||
]);
|
||||
hass.mockWS(
|
||||
"history/history_during_period",
|
||||
({ entity_ids }: { entity_ids?: string[] }) =>
|
||||
Object.fromEntries(
|
||||
PEOPLE.filter(
|
||||
(person) => !entity_ids || entity_ids.includes(`person.${person.id}`)
|
||||
).map((person) => [`person.${person.id}`, personHistory(person, now)])
|
||||
)
|
||||
);
|
||||
};
|
||||
+2
-22
@@ -1,27 +1,7 @@
|
||||
import type { Zone } from "../../../src/data/zone";
|
||||
import type { MockHomeAssistant } from "../../../src/fake_data/provide_hass";
|
||||
|
||||
const zones: Zone[] = [
|
||||
{
|
||||
id: "home",
|
||||
name: "Home",
|
||||
icon: "mdi:home",
|
||||
latitude: 52.3731339,
|
||||
longitude: 4.8903147,
|
||||
radius: 100,
|
||||
passive: false,
|
||||
},
|
||||
{
|
||||
id: "work",
|
||||
name: "Work",
|
||||
icon: "mdi:briefcase",
|
||||
latitude: 52.3909184,
|
||||
longitude: 4.8530821,
|
||||
radius: 200,
|
||||
passive: false,
|
||||
},
|
||||
];
|
||||
import { zoneEntities, zones } from "./map";
|
||||
|
||||
export const mockZone = (hass: MockHomeAssistant) => {
|
||||
hass.mockWS("zone/list", () => zones);
|
||||
hass.addEntities(zoneEntities());
|
||||
};
|
||||
|
||||
+1
-1
@@ -227,7 +227,7 @@
|
||||
"webpackbar": "7.0.0",
|
||||
"workbox-build": "7.4.1"
|
||||
},
|
||||
"packageManager": "pnpm@11.28.0",
|
||||
"packageManager": "pnpm@12.7.0",
|
||||
"volta": {
|
||||
"node": "24.21.0"
|
||||
}
|
||||
|
||||
Generated
+158
-1
@@ -1,3 +1,161 @@
|
||||
---
|
||||
lockfileVersion: '9.0'
|
||||
|
||||
importers:
|
||||
|
||||
.:
|
||||
configDependencies: {}
|
||||
packageManagerDependencies:
|
||||
pnpm:
|
||||
specifier: 12.7.0
|
||||
version: 12.7.0
|
||||
|
||||
packages:
|
||||
|
||||
'@pnpm/[email protected]':
|
||||
resolution: {integrity: sha512-gJTCsUbazAEbIMF9l2t+z3YWHCI0AiThtBsxU6FrxXK0tZsBRZZWpleLs0uY8Dy6V0RcPEusn3UyuAyFn3dkeA==}
|
||||
cpu: [arm64]
|
||||
os: [android]
|
||||
|
||||
'@pnpm/[email protected]':
|
||||
resolution: {integrity: sha512-7187pEwqAwbrmXesKiCXfmQUARVacBooJDTWqL+9olZh7sZwnECq5pB/KzuYZbnmdHZUkBWRyvChzKojmtSE7Q==}
|
||||
cpu: [x64]
|
||||
os: [android]
|
||||
|
||||
'@pnpm/[email protected]':
|
||||
resolution: {integrity: sha512-ppzJ2ln60/Oq0BJpy5bHqwZb0q1Trg/+xb7myxjaIgZfaybwnH3OuFX5w8PPQdRrjYwljgygYLC/7LMtZc/iuQ==}
|
||||
cpu: [arm64]
|
||||
os: [darwin]
|
||||
|
||||
'@pnpm/[email protected]':
|
||||
resolution: {integrity: sha512-+qxS/A5O4rFD7T+qqWnWjESbKVAzKsh9LubDHt9xZ34AbwVk3ZFcAW/7vX5R1jqq+uGgziMPtcVJKc5CICVHCQ==}
|
||||
cpu: [x64]
|
||||
os: [darwin]
|
||||
|
||||
'@pnpm/[email protected]':
|
||||
resolution: {integrity: sha512-uv0x93hoecec7IlT/FCwQe2siStGspG7G3EZQtIHH2n3ClDzbsQcEiW1QbMoAyZoLVrR7Pm9ovTndUCRcZ5y1g==}
|
||||
cpu: [x64]
|
||||
os: [freebsd]
|
||||
|
||||
'@pnpm/[email protected]':
|
||||
resolution: {integrity: sha512-XZlRiVL/l8mOhPyaocElpaHq6rKHDvkP/oyo1hHgUePX+XzBtQ0KLEgkBLZbk1/f5yGWimQTPmBNvzaYzQLdOg==}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@pnpm/[email protected]':
|
||||
resolution: {integrity: sha512-vwPQU+Bt3qXMhzjcmMa25W3yP8OyF9yFxHoojr10QJm9lRUmz/SwtM2oHbHKZKkyid/ngmMARXsChFyuRQig5w==}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@pnpm/[email protected]':
|
||||
resolution: {integrity: sha512-qzE3TEXYLw0gR2ytclbsGeQ7cA981GeTHMAn86cw9+Jn8nQNa8LZ5ZzCxKRXtGvkEN/FDc0oyGdtILT4JHSStg==}
|
||||
cpu: [ppc64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@pnpm/[email protected]':
|
||||
resolution: {integrity: sha512-bTw09mf/v+AuftWePL7pO3WZR0p52A9flr8zpGlcvxKUAKRTE2BDOVeZsWk/8f01ZaO2/0ECtEds6CKfec591w==}
|
||||
cpu: [riscv64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@pnpm/[email protected]':
|
||||
resolution: {integrity: sha512-frVk6Ilh2sKVs+CINnJCp3+kiGRi/cE3DARmFftljlrGX09livS4UU37uc7EsAxI4Kbt0WZjeTVT2q6XbVJCrA==}
|
||||
cpu: [s390x]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@pnpm/[email protected]':
|
||||
resolution: {integrity: sha512-AhoNY7xkhUb0GLqjrtxmxKZsoNM1Jea/VH/ypxt9nyBA5gXJGXo6uM+2Pode3vrWf1rSiVGicokkyEZLkDKf+A==}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@pnpm/[email protected]':
|
||||
resolution: {integrity: sha512-gGW7NJFmr33IJ6KZu+1w90KBtFxMVf/+AUG8aKJpy4v6dRnUd5v84PcH2ejUuxp/fm3zM0IY6h9LwcYPu9dxdg==}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@pnpm/[email protected]':
|
||||
resolution: {integrity: sha512-cI+aZwzOCQc7hQwCA1BydiM3FpxVHdfdMzI3jOG7MQ0d8NHvk5MrdKTTKiKJH2L5VdwWoWHFXbnvhCEyN/fVsg==}
|
||||
cpu: [arm64]
|
||||
os: [win32]
|
||||
|
||||
'@pnpm/[email protected]':
|
||||
resolution: {integrity: sha512-+eZ6gFsDbdyuw7GO7amdRh+GpjrQOIe9qjjOmeznWeEsfEeD8GNSrRXtbWe/zpPcxheexAnO7UCeavkhHLVFWg==}
|
||||
cpu: [x64]
|
||||
os: [win32]
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-nFZHfjYAaNbp3KapLvtORrPcWlL86/PYTXi5c2wN7ViaVhYhpS9oIZp6OfrMY83AecMibvnJ1fArefdOaagHtg==}
|
||||
engines: {node: '>=18.*'}
|
||||
hasBin: true
|
||||
|
||||
snapshots:
|
||||
|
||||
'@pnpm/[email protected]':
|
||||
optional: true
|
||||
|
||||
'@pnpm/[email protected]':
|
||||
optional: true
|
||||
|
||||
'@pnpm/[email protected]':
|
||||
optional: true
|
||||
|
||||
'@pnpm/[email protected]':
|
||||
optional: true
|
||||
|
||||
'@pnpm/[email protected]':
|
||||
optional: true
|
||||
|
||||
'@pnpm/[email protected]':
|
||||
optional: true
|
||||
|
||||
'@pnpm/[email protected]':
|
||||
optional: true
|
||||
|
||||
'@pnpm/[email protected]':
|
||||
optional: true
|
||||
|
||||
'@pnpm/[email protected]':
|
||||
optional: true
|
||||
|
||||
'@pnpm/[email protected]':
|
||||
optional: true
|
||||
|
||||
'@pnpm/[email protected]':
|
||||
optional: true
|
||||
|
||||
'@pnpm/[email protected]':
|
||||
optional: true
|
||||
|
||||
'@pnpm/[email protected]':
|
||||
optional: true
|
||||
|
||||
'@pnpm/[email protected]':
|
||||
optional: true
|
||||
|
||||
[email protected]:
|
||||
optionalDependencies:
|
||||
'@pnpm/exe.android-arm64': 12.7.0
|
||||
'@pnpm/exe.android-x64': 12.7.0
|
||||
'@pnpm/exe.darwin-arm64': 12.7.0
|
||||
'@pnpm/exe.darwin-x64': 12.7.0
|
||||
'@pnpm/exe.freebsd-x64': 12.7.0
|
||||
'@pnpm/exe.linux-arm64': 12.7.0
|
||||
'@pnpm/exe.linux-arm64-musl': 12.7.0
|
||||
'@pnpm/exe.linux-ppc64': 12.7.0
|
||||
'@pnpm/exe.linux-riscv64': 12.7.0
|
||||
'@pnpm/exe.linux-s390x': 12.7.0
|
||||
'@pnpm/exe.linux-x64': 12.7.0
|
||||
'@pnpm/exe.linux-x64-musl': 12.7.0
|
||||
'@pnpm/exe.win32-arm64': 12.7.0
|
||||
'@pnpm/exe.win32-x64': 12.7.0
|
||||
|
||||
---
|
||||
lockfileVersion: '9.0'
|
||||
|
||||
settings:
|
||||
@@ -5514,7 +5672,6 @@ packages:
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-POQXvpdL69+CluYsillJ7SUhKvytYjW9vG/GKpnf+xP8UWgYEM/RaMzHHofbALDiKbbP1W8UEYmgGl39WkPZsg==}
|
||||
engines: {'0': node >= 0.2.0}
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-p/nXbhSEcu3pZRdkW1OfJhpsVtW1gd4Wa1fnQc9YLiTfAjn0312eMKimbdIQzuZl9aa9xUGaRlP9T/CJE/ditQ==}
|
||||
|
||||
+1
-1
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "home-assistant-frontend"
|
||||
version = "20260826.0"
|
||||
version = "20260930.0"
|
||||
license = "Apache-2.0"
|
||||
license-files = ["LICENSE*"]
|
||||
description = "The Home Assistant frontend"
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
import { css, type ReactiveControllerHost } from "lit";
|
||||
import { ref } from "lit/directives/ref";
|
||||
|
||||
export const scrollFadeStyles = css`
|
||||
.scroll-fade-start,
|
||||
.scroll-fade-end {
|
||||
--scroll-fade-direction: to right;
|
||||
--scroll-fade-start: 0px;
|
||||
--scroll-fade-end: 0px;
|
||||
mask-image: linear-gradient(
|
||||
var(--scroll-fade-direction),
|
||||
transparent 0%,
|
||||
black var(--scroll-fade-start),
|
||||
black calc(100% - var(--scroll-fade-end)),
|
||||
transparent 100%
|
||||
);
|
||||
}
|
||||
.scroll-fade-start:dir(rtl),
|
||||
.scroll-fade-end:dir(rtl) {
|
||||
--scroll-fade-direction: to left;
|
||||
}
|
||||
.scroll-fade-start {
|
||||
--scroll-fade-start: var(--ha-space-4);
|
||||
}
|
||||
.scroll-fade-end {
|
||||
--scroll-fade-end: var(--ha-space-4);
|
||||
}
|
||||
`;
|
||||
|
||||
export class ScrollFadeController {
|
||||
public start = false;
|
||||
|
||||
public end = false;
|
||||
|
||||
private _host: ReactiveControllerHost;
|
||||
|
||||
private _element?: Element;
|
||||
|
||||
private _resizeObserver = new ResizeObserver(() => this._update());
|
||||
|
||||
constructor(host: ReactiveControllerHost) {
|
||||
this._host = host;
|
||||
}
|
||||
|
||||
public target() {
|
||||
return ref(this._setElement);
|
||||
}
|
||||
|
||||
private _setElement = (element?: Element) => {
|
||||
if (element === this._element) {
|
||||
return;
|
||||
}
|
||||
this._element?.removeEventListener("scroll", this._update);
|
||||
this._resizeObserver.disconnect();
|
||||
this._element = element;
|
||||
if (element) {
|
||||
element.addEventListener("scroll", this._update, { passive: true });
|
||||
this._resizeObserver.observe(element);
|
||||
for (const child of element.children) {
|
||||
this._resizeObserver.observe(child);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
private _update = () => {
|
||||
if (!this._element) {
|
||||
return;
|
||||
}
|
||||
const rtl = this._element.matches(":dir(rtl)");
|
||||
const scrolled = rtl ? -this._element.scrollLeft : this._element.scrollLeft;
|
||||
const maxScroll = this._element.scrollWidth - this._element.clientWidth;
|
||||
const start = scrolled > 1;
|
||||
const end = maxScroll - scrolled > 1;
|
||||
if (start === this.start && end === this.end) {
|
||||
return;
|
||||
}
|
||||
this.start = start;
|
||||
this.end = end;
|
||||
this._host.requestUpdate();
|
||||
};
|
||||
}
|
||||
@@ -8,46 +8,46 @@
|
||||
|
||||
/** Drawn on the light palette */
|
||||
export const HA_MAP_COLORS = {
|
||||
background: "#f4efe6",
|
||||
land: "#f4efe6",
|
||||
water: "#bcd9e8",
|
||||
labelWater: "#5b86a3",
|
||||
natureWood: "#cfe1bf",
|
||||
natureGrass: "#e2ecda",
|
||||
naturePark: "#cfe1bf",
|
||||
natureLeisure: "#e2ecda",
|
||||
natureAgriculture: "#e8eee2",
|
||||
natureWetland: "#e5ebe0",
|
||||
natureSand: "#f1eadf",
|
||||
natureRock: "#f1eadf",
|
||||
glacier: "#f9f6f1",
|
||||
areaResidential: "#efe8dc",
|
||||
areaCommercial: "#f4e1db",
|
||||
areaIndustrial: "#ede5ce",
|
||||
areaWaste: "#ece3d5",
|
||||
areaBurial: "#efe8dc",
|
||||
siteParking: "#ede6d8",
|
||||
siteSports: "#cfe1bf26",
|
||||
building: "#e7ddca",
|
||||
buildingBg: "#dcccb2",
|
||||
background: "#f5efe5",
|
||||
land: "#f5efe5",
|
||||
water: "#b8daec",
|
||||
labelWater: "#5487aa",
|
||||
natureWood: "#cfe4bc",
|
||||
natureGrass: "#e2eed8",
|
||||
naturePark: "#cfe4bc",
|
||||
natureLeisure: "#e2eed8",
|
||||
natureAgriculture: "#e8efe1",
|
||||
natureWetland: "#e5ecdf",
|
||||
natureSand: "#f3eadd",
|
||||
natureRock: "#f3eadd",
|
||||
glacier: "#faf6f0",
|
||||
areaResidential: "#f1e9da",
|
||||
areaCommercial: "#f7e0d8",
|
||||
areaIndustrial: "#f0e7cb",
|
||||
areaWaste: "#eee3d3",
|
||||
areaBurial: "#f1e9da",
|
||||
siteParking: "#efe7d6",
|
||||
siteSports: "#cfe4bc26",
|
||||
building: "#eadec7",
|
||||
buildingBg: "#e0cdae",
|
||||
roadStreet: "#fcfaf8",
|
||||
roadStreetBg: "#e6dac7",
|
||||
roadTrunk: "#fbe6c2",
|
||||
roadTrunkBg: "#e5cea5",
|
||||
roadMotorway: "#ebd4ab",
|
||||
roadMotorwayBg: "#deba78",
|
||||
transitRail: "#e9dfce",
|
||||
transitSubway: "#eae1d1",
|
||||
transitCycle: "#f2ede3",
|
||||
transitFoot: "#f2ede3",
|
||||
boundary: "#d6c3a4",
|
||||
boundaryDisputed: "#e2d6c0",
|
||||
label: "#4a463d",
|
||||
roadStreetBg: "#e9dbc4",
|
||||
roadTrunk: "#ffe7be",
|
||||
roadTrunkBg: "#ebd09f",
|
||||
roadMotorway: "#f1d6a5",
|
||||
roadMotorwayBg: "#e8bd6e",
|
||||
transitRail: "#ece0cb",
|
||||
transitSubway: "#ece2cf",
|
||||
transitCycle: "#f3eee2",
|
||||
transitFoot: "#f3eee2",
|
||||
boundary: "#dbc49f",
|
||||
boundaryDisputed: "#e5d7bd",
|
||||
label: "#4b473c",
|
||||
labelHalo: "#faf8f5cc",
|
||||
labelSymbol: "#8d8676",
|
||||
labelPoi: "#8d867666",
|
||||
labelShield: "#faf8f4",
|
||||
labelHousenumber: "#8d86764d",
|
||||
labelSymbol: "#8f8774",
|
||||
labelPoi: "#8f877466",
|
||||
labelShield: "#fbf8f3",
|
||||
labelHousenumber: "#8f87744d",
|
||||
};
|
||||
|
||||
/** Drawn on the dark palette, in full: a light value left standing here would
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { StructError } from "superstruct";
|
||||
import type { LocalizeFunc } from "../translations/localize";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
|
||||
export const handleStructError = (
|
||||
localize: LocalizeFunc,
|
||||
hass: HomeAssistant,
|
||||
err: Error
|
||||
): { warnings: string[]; errors?: string[] } => {
|
||||
if (!(err instanceof StructError)) {
|
||||
@@ -13,13 +13,13 @@ export const handleStructError = (
|
||||
for (const failure of err.failures()) {
|
||||
if (failure.value === undefined) {
|
||||
errors.push(
|
||||
localize("ui.errors.config.key_missing", {
|
||||
hass.localize("ui.errors.config.key_missing", {
|
||||
key: failure.path.join("."),
|
||||
})
|
||||
);
|
||||
} else if (failure.type === "never") {
|
||||
warnings.push(
|
||||
localize("ui.errors.config.key_not_expected", {
|
||||
hass.localize("ui.errors.config.key_not_expected", {
|
||||
key: failure.path.join("."),
|
||||
})
|
||||
);
|
||||
@@ -27,7 +27,7 @@ export const handleStructError = (
|
||||
continue;
|
||||
} else if (failure.type === "enums") {
|
||||
warnings.push(
|
||||
localize("ui.errors.config.key_wrong_type", {
|
||||
hass.localize("ui.errors.config.key_wrong_type", {
|
||||
key: failure.path.join("."),
|
||||
type_correct: failure.message.replace("Expected ", "").split(", ")[0],
|
||||
type_wrong: JSON.stringify(failure.value),
|
||||
@@ -35,7 +35,7 @@ export const handleStructError = (
|
||||
);
|
||||
} else {
|
||||
warnings.push(
|
||||
localize("ui.errors.config.key_wrong_type", {
|
||||
hass.localize("ui.errors.config.key_wrong_type", {
|
||||
key: failure.path.join("."),
|
||||
type_correct: failure.refinement || failure.type,
|
||||
type_wrong: JSON.stringify(failure.value),
|
||||
|
||||
@@ -604,7 +604,7 @@ export class HaMap extends ReactiveElement {
|
||||
private get _darkMode() {
|
||||
return (
|
||||
this.themeMode === "dark" ||
|
||||
(this.themeMode === "auto" && Boolean(this._ui?.themes.darkMode))
|
||||
(this.themeMode === "auto" && Boolean(this._ui?.themes?.darkMode))
|
||||
);
|
||||
}
|
||||
|
||||
@@ -623,7 +623,7 @@ export class HaMap extends ReactiveElement {
|
||||
// mode the page is in. A card forced to the other mode draws the other
|
||||
// palette, so those values would be the wrong half of the theme; the
|
||||
// style's own colors are the better answer there.
|
||||
const pageDark = Boolean(this._ui?.themes.darkMode);
|
||||
const pageDark = Boolean(this._ui?.themes?.darkMode);
|
||||
const colors =
|
||||
this._darkMode === pageDark ? readMapThemeColors(this) : undefined;
|
||||
if (!deepEqual(colors, this._themeColors)) {
|
||||
|
||||
@@ -697,7 +697,7 @@ export interface TriggerSidebarConfig extends BaseSidebarConfig {
|
||||
cut: () => void;
|
||||
copy: () => void;
|
||||
insertAfter: (value: Trigger | Trigger[]) => boolean;
|
||||
toggleYamlMode: (yamlMode?: boolean) => void;
|
||||
toggleYamlMode: () => void;
|
||||
config: Trigger;
|
||||
description?: TriggerDescription;
|
||||
yamlMode: boolean;
|
||||
@@ -715,7 +715,7 @@ export interface ConditionSidebarConfig extends BaseSidebarConfig {
|
||||
cut: () => void;
|
||||
copy: () => void;
|
||||
insertAfter: (value: Condition | Condition[]) => boolean;
|
||||
toggleYamlMode: (yamlMode?: boolean) => void;
|
||||
toggleYamlMode: () => void;
|
||||
config: Condition;
|
||||
description?: ConditionDescription;
|
||||
yamlMode: boolean;
|
||||
@@ -734,7 +734,7 @@ export interface ActionSidebarConfig extends BaseSidebarConfig {
|
||||
copy: () => void;
|
||||
insertAfter: (value: Action | Action[]) => boolean;
|
||||
run: () => void;
|
||||
toggleYamlMode: (yamlMode?: boolean) => void;
|
||||
toggleYamlMode: () => void;
|
||||
config: {
|
||||
action: Action;
|
||||
};
|
||||
|
||||
@@ -56,8 +56,8 @@ export class MockBaseEntity {
|
||||
this.areaId = input.area_id;
|
||||
this.deviceId = input.device_id;
|
||||
this.platform = input.platform;
|
||||
this.lastChanged = randomTime();
|
||||
this.lastUpdated = randomTime();
|
||||
this.lastChanged = input.last_changed ?? randomTime();
|
||||
this.lastUpdated = input.last_changed ?? randomTime();
|
||||
|
||||
const attributes: EntityAttributes = input.attributes || {};
|
||||
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
import { MockBaseEntity } from "./base-entity";
|
||||
import type { EntityAttributes } from "./types";
|
||||
|
||||
const LOCATION_ATTRIBUTES = [
|
||||
"latitude",
|
||||
"longitude",
|
||||
"gps_accuracy",
|
||||
"radius",
|
||||
"passive",
|
||||
"persons",
|
||||
"editable",
|
||||
"source",
|
||||
"source_type",
|
||||
"in_zones",
|
||||
];
|
||||
|
||||
export class MockLocationEntity extends MockBaseEntity {
|
||||
protected _getStateAttributes(): EntityAttributes {
|
||||
const stateAttrs: EntityAttributes = {};
|
||||
for (const key of LOCATION_ATTRIBUTES) {
|
||||
if (key in this.attributes) {
|
||||
stateAttrs[key] = this.attributes[key];
|
||||
}
|
||||
}
|
||||
return stateAttrs;
|
||||
}
|
||||
}
|
||||
@@ -11,6 +11,7 @@ import { MockLawnMowerEntity } from "./lawn-mower-entity";
|
||||
import { MockInputSelectEntity } from "./input-select-entity";
|
||||
import { MockInputTextEntity } from "./input-text-entity";
|
||||
import { MockLightEntity } from "./light-entity";
|
||||
import { MockLocationEntity } from "./location-entity";
|
||||
import { MockLockEntity } from "./lock-entity";
|
||||
import { MockMediaPlayerEntity } from "./media-player-entity";
|
||||
import { MockToggleEntity } from "./toggle-entity";
|
||||
@@ -25,6 +26,7 @@ const TYPES: Record<string, EntityConstructor> = {
|
||||
alarm_control_panel: MockAlarmControlPanelEntity,
|
||||
climate: MockClimateEntity,
|
||||
cover: MockCoverEntity,
|
||||
device_tracker: MockLocationEntity,
|
||||
fan: MockFanEntity,
|
||||
group: MockGroupEntity,
|
||||
humidifier: MockHumidifierEntity,
|
||||
@@ -36,10 +38,12 @@ const TYPES: Record<string, EntityConstructor> = {
|
||||
light: MockLightEntity,
|
||||
lock: MockLockEntity,
|
||||
media_player: MockMediaPlayerEntity,
|
||||
person: MockLocationEntity,
|
||||
switch: MockToggleEntity,
|
||||
vacuum: MockVacuumEntity,
|
||||
valve: MockValveEntity,
|
||||
water_heater: MockWaterHeaterEntity,
|
||||
zone: MockLocationEntity,
|
||||
};
|
||||
|
||||
export const getEntity = (input: EntityInput): MockBaseEntity => {
|
||||
|
||||
@@ -17,6 +17,8 @@ export type EntityInput = Pick<
|
||||
device_id?: string;
|
||||
/** Integration that provides the entity, defaults to "demo" */
|
||||
platform?: string;
|
||||
/** When the state last changed, a random recent time by default */
|
||||
last_changed?: string;
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -95,7 +95,6 @@ import { showEditorToast } from "../editor-toast";
|
||||
import "../ha-automation-editor-warning";
|
||||
import "../ha-automation-row-options";
|
||||
import { overflowStyles, rowStyles } from "../styles";
|
||||
import { getUiSupportWarnings } from "../ui-support";
|
||||
import { getDeviceTarget } from "../target/get_device_target";
|
||||
import { getEntityTarget } from "../target/get_entity_target";
|
||||
import "../target/ha-automation-row-targets";
|
||||
@@ -239,13 +238,6 @@ export default class HaAutomationActionRow extends LitElement {
|
||||
return;
|
||||
}
|
||||
const type = getAutomationActionType(this.action);
|
||||
if (this._warnings && this._yamlMode && type) {
|
||||
this._warnings = getUiSupportWarnings(
|
||||
this.hass.localize,
|
||||
`ha-automation-action-${type}`,
|
||||
this.action
|
||||
);
|
||||
}
|
||||
this._uiModeAvailable =
|
||||
type !== undefined && !YAML_ONLY_ACTION_TYPES.has(type as any);
|
||||
if (!this._uiModeAvailable && !this._yamlMode) {
|
||||
@@ -440,9 +432,7 @@ export default class HaAutomationActionRow extends LitElement {
|
||||
class="event-chip"
|
||||
aria-live="polite"
|
||||
>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.automation.editor.actions.disabled"
|
||||
)}
|
||||
${this.hass.localize("ui.panel.config.automation.editor.actions.disabled")}
|
||||
</ha-automation-row-event-chip>
|
||||
|
||||
<ha-automation-row-event-chip
|
||||
@@ -1137,7 +1127,7 @@ export default class HaAutomationActionRow extends LitElement {
|
||||
};
|
||||
|
||||
private _handleUiModeNotAvailable(ev: CustomEvent) {
|
||||
this._warnings = handleStructError(this.hass.localize, ev.detail).warnings;
|
||||
this._warnings = handleStructError(this.hass, ev.detail).warnings;
|
||||
if (!this._yamlMode) {
|
||||
this._yamlMode = true;
|
||||
}
|
||||
@@ -1185,10 +1175,8 @@ export default class HaAutomationActionRow extends LitElement {
|
||||
this._renameAction();
|
||||
},
|
||||
editNote: this._editNoteAction,
|
||||
toggleYamlMode: (yamlMode?: boolean) => {
|
||||
if (yamlMode === undefined || yamlMode !== this._yamlMode) {
|
||||
this._toggleYamlMode();
|
||||
}
|
||||
toggleYamlMode: () => {
|
||||
this._toggleYamlMode();
|
||||
this.openSidebar();
|
||||
},
|
||||
disable: this._onDisable,
|
||||
|
||||
@@ -24,7 +24,6 @@ import {
|
||||
import { conditionDescriptionsContext } from "../../../../../data/context";
|
||||
import { domainToName } from "../../../../../data/integration";
|
||||
import type { HomeAssistant, ValueChangedEvent } from "../../../../../types";
|
||||
import { checkElementUiSupport } from "../../ui-support";
|
||||
import "../../condition/ha-automation-condition-editor";
|
||||
import type HaAutomationConditionEditor from "../../condition/ha-automation-condition-editor";
|
||||
import "../../condition/types/ha-automation-condition-and";
|
||||
@@ -65,19 +64,6 @@ export class HaConditionAction extends LitElement implements ActionElement {
|
||||
return { condition: "state" };
|
||||
}
|
||||
|
||||
public static checkUiSupport(
|
||||
localize: LocalizeFunc,
|
||||
action: Condition
|
||||
): Error | undefined {
|
||||
return typeof action.condition === "string"
|
||||
? checkElementUiSupport(
|
||||
localize,
|
||||
`ha-automation-condition-${action.condition}`,
|
||||
action
|
||||
)
|
||||
: undefined;
|
||||
}
|
||||
|
||||
protected render() {
|
||||
const buildingBlock = CONDITION_BUILDING_BLOCKS.includes(
|
||||
this.action.condition
|
||||
|
||||
@@ -6,7 +6,6 @@ import { hasTemplate } from "../../../../../common/string/has-template";
|
||||
import type { HaDurationData } from "../../../../../components/ha-duration-input";
|
||||
import "../../../../../components/ha-duration-input";
|
||||
import type { DelayAction } from "../../../../../data/script";
|
||||
import type { LocalizeFunc } from "../../../../../common/translations/localize";
|
||||
import type { HomeAssistant } from "../../../../../types";
|
||||
import type { ActionElement } from "../ha-automation-action-row";
|
||||
import { createDurationData } from "../../../../../common/datetime/create_duration_data";
|
||||
@@ -25,26 +24,17 @@ export class HaDelayAction extends LitElement implements ActionElement {
|
||||
return { delay: "" };
|
||||
}
|
||||
|
||||
public static checkUiSupport(
|
||||
localize: LocalizeFunc,
|
||||
action: DelayAction
|
||||
): Error | undefined {
|
||||
// Check for templates in action. If found, revert to YAML mode.
|
||||
if (hasTemplate(action)) {
|
||||
return Error(localize("ui.errors.config.no_template_editor_support"));
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
public willUpdate(changedProperties: PropertyValues<this>) {
|
||||
if (!changedProperties.has("action")) {
|
||||
return;
|
||||
}
|
||||
const err = this.action
|
||||
? HaDelayAction.checkUiSupport(this.hass.localize, this.action)
|
||||
: undefined;
|
||||
if (err) {
|
||||
fireEvent(this, "ui-mode-not-available", err);
|
||||
// Check for templates in action. If found, revert to YAML mode.
|
||||
if (this.action && hasTemplate(this.action)) {
|
||||
fireEvent(
|
||||
this,
|
||||
"ui-mode-not-available",
|
||||
Error(this.hass.localize("ui.errors.config.no_template_editor_support"))
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -10,7 +10,6 @@ import "../../../../../components/input/ha-input";
|
||||
import type { HaInput } from "../../../../../components/input/ha-input";
|
||||
import type { ServiceAction } from "../../../../../data/script";
|
||||
import { serviceActionStruct } from "../../../../../data/script";
|
||||
import type { LocalizeFunc } from "../../../../../common/translations/localize";
|
||||
import type { HomeAssistant } from "../../../../../types";
|
||||
import type { ActionElement } from "../ha-automation-action-row";
|
||||
|
||||
@@ -32,35 +31,30 @@ export class HaServiceAction extends LitElement implements ActionElement {
|
||||
return { action: "", data: {} };
|
||||
}
|
||||
|
||||
public static checkUiSupport(
|
||||
localize: LocalizeFunc,
|
||||
action: ServiceAction
|
||||
): Error | undefined {
|
||||
try {
|
||||
assert(action, serviceActionStruct);
|
||||
} catch (err: any) {
|
||||
return err;
|
||||
}
|
||||
if (
|
||||
action &&
|
||||
Object.entries(action).some(
|
||||
([key, val]) => !["data", "target"].includes(key) && hasTemplate(val)
|
||||
)
|
||||
) {
|
||||
return Error(localize("ui.errors.config.no_template_editor_support"));
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
protected willUpdate(changedProperties: PropertyValues<this>) {
|
||||
if (!changedProperties.has("action")) {
|
||||
return;
|
||||
}
|
||||
const err = HaServiceAction.checkUiSupport(this.hass.localize, this.action);
|
||||
if (err) {
|
||||
try {
|
||||
assert(this.action, serviceActionStruct);
|
||||
} catch (err: any) {
|
||||
fireEvent(this, "ui-mode-not-available", err);
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
this.action &&
|
||||
Object.entries(this.action).some(
|
||||
([key, val]) => !["data", "target"].includes(key) && hasTemplate(val)
|
||||
)
|
||||
) {
|
||||
fireEvent(
|
||||
this,
|
||||
"ui-mode-not-available",
|
||||
Error(this.hass.localize("ui.errors.config.no_template_editor_support"))
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (this.action.entity_id) {
|
||||
this._action = {
|
||||
...this.action,
|
||||
|
||||
@@ -72,7 +72,6 @@ import { isMac } from "../../../../util/is_mac";
|
||||
import { showEditorToast } from "../editor-toast";
|
||||
import "../ha-automation-editor-warning";
|
||||
import { overflowStyles, rowStyles } from "../styles";
|
||||
import { getUiSupportWarnings, isConfigObject } from "../ui-support";
|
||||
import "./ha-automation-condition-editor";
|
||||
import type HaAutomationConditionEditor from "./ha-automation-condition-editor";
|
||||
import "./types/ha-automation-condition-and";
|
||||
@@ -231,9 +230,7 @@ export default class HaAutomationConditionRow extends LitElement {
|
||||
class="event-chip"
|
||||
aria-live="polite"
|
||||
>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.automation.editor.actions.disabled"
|
||||
)}
|
||||
${this.hass.localize("ui.panel.config.automation.editor.actions.disabled")}
|
||||
</ha-automation-row-event-chip>
|
||||
|
||||
<ha-automation-row-event-chip
|
||||
@@ -543,19 +540,6 @@ export default class HaAutomationConditionRow extends LitElement {
|
||||
if (changedProperties.has("yamlMode")) {
|
||||
this._warnings = undefined;
|
||||
}
|
||||
if (
|
||||
changedProperties.has("condition") &&
|
||||
this._warnings &&
|
||||
this._yamlMode &&
|
||||
isConfigObject(this.condition) &&
|
||||
typeof this.condition.condition === "string"
|
||||
) {
|
||||
this._warnings = getUiSupportWarnings(
|
||||
this.hass.localize,
|
||||
`ha-automation-condition-${this.condition.condition}`,
|
||||
this.condition
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
protected updated(changedProperties: PropertyValues<this>) {
|
||||
@@ -876,7 +860,7 @@ export default class HaAutomationConditionRow extends LitElement {
|
||||
}
|
||||
|
||||
private _handleUiModeNotAvailable(ev: CustomEvent) {
|
||||
this._warnings = handleStructError(this.hass.localize, ev.detail).warnings;
|
||||
this._warnings = handleStructError(this.hass, ev.detail).warnings;
|
||||
if (!this._yamlMode) {
|
||||
this._yamlMode = true;
|
||||
}
|
||||
@@ -922,10 +906,8 @@ export default class HaAutomationConditionRow extends LitElement {
|
||||
this._renameCondition();
|
||||
},
|
||||
editNote: this._editNoteCondition,
|
||||
toggleYamlMode: (yamlMode?: boolean) => {
|
||||
if (yamlMode === undefined || yamlMode !== this._yamlMode) {
|
||||
this._toggleYamlMode();
|
||||
}
|
||||
toggleYamlMode: () => {
|
||||
this._toggleYamlMode();
|
||||
this.openSidebar();
|
||||
},
|
||||
disable: this._onDisable,
|
||||
|
||||
+4
-18
@@ -51,26 +51,12 @@ export default class HaNumericStateCondition extends LitElement {
|
||||
};
|
||||
}
|
||||
|
||||
public static checkUiSupport(
|
||||
_localize: LocalizeFunc,
|
||||
condition: NumericStateCondition
|
||||
): Error | undefined {
|
||||
try {
|
||||
assert(condition, numericStateConditionStruct);
|
||||
} catch (err: any) {
|
||||
return err;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
public shouldUpdate(changedProperties: PropertyValues<this>) {
|
||||
if (changedProperties.has("condition")) {
|
||||
const err = HaNumericStateCondition.checkUiSupport(
|
||||
this.hass.localize,
|
||||
this.condition
|
||||
);
|
||||
if (err) {
|
||||
fireEvent(this, "ui-mode-not-available", err);
|
||||
try {
|
||||
assert(this.condition, numericStateConditionStruct);
|
||||
} catch (e: any) {
|
||||
fireEvent(this, "ui-mode-not-available", e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,7 +20,6 @@ import "../../../../../components/ha-form/ha-form";
|
||||
import type { SchemaUnion } from "../../../../../components/ha-form/types";
|
||||
import type { StateCondition } from "../../../../../data/automation";
|
||||
import { STATE_CONDITION_HIDDEN_ATTRIBUTES } from "../../../../../data/entity/entity_attributes";
|
||||
import type { LocalizeFunc } from "../../../../../common/translations/localize";
|
||||
import type { HomeAssistant } from "../../../../../types";
|
||||
import { forDictStruct } from "../../structs";
|
||||
import type { ConditionElement } from "../ha-automation-condition-row";
|
||||
@@ -81,26 +80,12 @@ export class HaStateCondition extends LitElement implements ConditionElement {
|
||||
return { condition: "state", entity_id: "", state: [] };
|
||||
}
|
||||
|
||||
public static checkUiSupport(
|
||||
_localize: LocalizeFunc,
|
||||
condition: StateCondition
|
||||
): Error | undefined {
|
||||
try {
|
||||
assert(condition, stateConditionStruct);
|
||||
} catch (err: any) {
|
||||
return err;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
public shouldUpdate(changedProperties: PropertyValues<this>) {
|
||||
if (changedProperties.has("condition")) {
|
||||
const err = HaStateCondition.checkUiSupport(
|
||||
this.hass.localize,
|
||||
this.condition
|
||||
);
|
||||
if (err) {
|
||||
fireEvent(this, "ui-mode-not-available", err);
|
||||
try {
|
||||
assert(this.condition, stateConditionStruct);
|
||||
} catch (e: any) {
|
||||
fireEvent(this, "ui-mode-not-available", e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,6 @@ import { customElement, property, query, state } from "lit/decorators";
|
||||
import { tinykeys } from "tinykeys";
|
||||
import { consume } from "../../../common/decorators/consume";
|
||||
import { fireEvent } from "../../../common/dom/fire_event";
|
||||
import type { HASSDomEvent } from "../../../common/dom/fire_event";
|
||||
import { computeRTL } from "../../../common/util/compute_rtl";
|
||||
import "../../../components/ha-resizable-bottom-sheet";
|
||||
import type { HaResizableBottomSheet } from "../../../components/ha-resizable-bottom-sheet";
|
||||
@@ -254,10 +253,8 @@ export default class HaAutomationSidebar extends LitElement {
|
||||
this.config?.close(true);
|
||||
}
|
||||
|
||||
private _toggleYamlMode = (
|
||||
ev: HASSDomEvent<HASSDomEvents["toggle-yaml-mode"]>
|
||||
) => {
|
||||
(this.config as ActionSidebarConfig)?.toggleYamlMode(ev.detail?.yamlMode);
|
||||
private _toggleYamlMode = () => {
|
||||
(this.config as ActionSidebarConfig)?.toggleYamlMode();
|
||||
};
|
||||
|
||||
private _handleMouseDown = (ev: MouseEvent | TouchEvent) => {
|
||||
@@ -449,7 +446,7 @@ declare global {
|
||||
}
|
||||
|
||||
interface HASSDomEvents {
|
||||
"toggle-yaml-mode": { yamlMode: boolean } | undefined;
|
||||
"toggle-yaml-mode": undefined;
|
||||
"yaml-changed": {
|
||||
value: unknown;
|
||||
};
|
||||
|
||||
@@ -32,7 +32,6 @@ import type { ActionSidebarConfig } from "../../../../data/automation";
|
||||
import type { DomainManifestLookup } from "../../../../data/integration";
|
||||
import { domainToName } from "../../../../data/integration";
|
||||
import type {
|
||||
Action,
|
||||
NonConditionAction,
|
||||
RepeatAction,
|
||||
ServiceAction,
|
||||
@@ -43,7 +42,6 @@ import type HaAutomationConditionEditor from "../action/ha-automation-action-edi
|
||||
import { getRepeatType } from "../action/types/ha-automation-action-repeat";
|
||||
import "../ha-automation-note";
|
||||
import { overflowStyles, sidebarEditorStyles } from "../styles";
|
||||
import { getUiSupportWarnings, isConfigObject } from "../ui-support";
|
||||
import "./ha-automation-sidebar-card";
|
||||
import "../action/ha-automation-action-editor";
|
||||
import { renderCtrlOrCmd } from "../../../../common/keyboard/ctrl-or-cmd";
|
||||
@@ -79,7 +77,6 @@ export default class HaAutomationSidebarAction extends LitElement {
|
||||
this.yamlMode = this.config.yamlMode;
|
||||
if (this.yamlMode) {
|
||||
this.editor?.yamlEditor?.setValue(this.config.config.action);
|
||||
this._warnings = this._checkUiSupport(this.config.config.action);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -396,7 +393,7 @@ export default class HaAutomationSidebarAction extends LitElement {
|
||||
}
|
||||
|
||||
private _handleUiModeNotAvailable(ev: CustomEvent) {
|
||||
this._warnings = handleStructError(this.hass.localize, ev.detail).warnings;
|
||||
this._warnings = handleStructError(this.hass, ev.detail).warnings;
|
||||
if (!this.yamlMode) {
|
||||
this.yamlMode = true;
|
||||
}
|
||||
@@ -423,26 +420,10 @@ export default class HaAutomationSidebarAction extends LitElement {
|
||||
ev.stopPropagation();
|
||||
|
||||
this.config?.save?.(ev.detail.value);
|
||||
this._warnings = this._checkUiSupport(ev.detail.value);
|
||||
}
|
||||
|
||||
private _checkUiSupport(action: Action): string[] | undefined {
|
||||
if (!this.config.uiSupported || !isConfigObject(action)) {
|
||||
return undefined;
|
||||
}
|
||||
const type = getAutomationActionType(action);
|
||||
if (!type) {
|
||||
return undefined;
|
||||
}
|
||||
return getUiSupportWarnings(
|
||||
this.hass.localize,
|
||||
`ha-automation-action-${type}`,
|
||||
action
|
||||
);
|
||||
}
|
||||
|
||||
private _toggleYamlMode = () => {
|
||||
fireEvent(this, "toggle-yaml-mode", { yamlMode: !this.yamlMode });
|
||||
fireEvent(this, "toggle-yaml-mode");
|
||||
};
|
||||
|
||||
private _handleDropdownSelect(ev: HaDropdownSelectEvent) {
|
||||
|
||||
@@ -22,7 +22,6 @@ import { handleStructError } from "../../../../common/structs/handle-errors";
|
||||
import type { HaDropdownSelectEvent } from "../../../../components/ha-dropdown";
|
||||
import "../../../../components/ha-dropdown-item";
|
||||
import type {
|
||||
Condition,
|
||||
ConditionSidebarConfig,
|
||||
LegacyCondition,
|
||||
} from "../../../../data/automation";
|
||||
@@ -37,7 +36,6 @@ import "../condition/ha-automation-condition-editor";
|
||||
import type HaAutomationConditionEditor from "../condition/ha-automation-condition-editor";
|
||||
import "../ha-automation-note";
|
||||
import { overflowStyles, sidebarEditorStyles } from "../styles";
|
||||
import { getUiSupportWarnings, isConfigObject } from "../ui-support";
|
||||
import "./ha-automation-sidebar-card";
|
||||
import { renderCtrlOrCmd } from "../../../../common/keyboard/ctrl-or-cmd";
|
||||
|
||||
@@ -74,7 +72,6 @@ export default class HaAutomationSidebarCondition extends LitElement {
|
||||
this.yamlMode = this.config.yamlMode;
|
||||
if (this.yamlMode) {
|
||||
this.editor?.yamlEditor?.setValue(this.config.config);
|
||||
this._warnings = this._checkUiSupport(this.config.config);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -364,7 +361,7 @@ export default class HaAutomationSidebarCondition extends LitElement {
|
||||
}
|
||||
|
||||
private _handleUiModeNotAvailable(ev: CustomEvent) {
|
||||
this._warnings = handleStructError(this.hass.localize, ev.detail).warnings;
|
||||
this._warnings = handleStructError(this.hass, ev.detail).warnings;
|
||||
if (!this.yamlMode) {
|
||||
this.yamlMode = true;
|
||||
}
|
||||
@@ -389,26 +386,10 @@ export default class HaAutomationSidebarCondition extends LitElement {
|
||||
ev.stopPropagation();
|
||||
|
||||
this.config?.save?.(ev.detail.value);
|
||||
this._warnings = this._checkUiSupport(ev.detail.value);
|
||||
}
|
||||
|
||||
private _checkUiSupport(condition: Condition): string[] | undefined {
|
||||
if (
|
||||
!this.config.uiSupported ||
|
||||
!isConfigObject(condition) ||
|
||||
typeof condition.condition !== "string"
|
||||
) {
|
||||
return undefined;
|
||||
}
|
||||
return getUiSupportWarnings(
|
||||
this.hass.localize,
|
||||
`ha-automation-condition-${condition.condition}`,
|
||||
condition
|
||||
);
|
||||
}
|
||||
|
||||
private _toggleYamlMode = () => {
|
||||
fireEvent(this, "toggle-yaml-mode", { yamlMode: !this.yamlMode });
|
||||
fireEvent(this, "toggle-yaml-mode");
|
||||
};
|
||||
|
||||
private _handleDropdownSelect(ev: HaDropdownSelectEvent) {
|
||||
|
||||
@@ -36,7 +36,6 @@ import "../ha-automation-note";
|
||||
import { overflowStyles, sidebarEditorStyles } from "../styles";
|
||||
import "../trigger/ha-automation-trigger-editor";
|
||||
import type HaAutomationTriggerEditor from "../trigger/ha-automation-trigger-editor";
|
||||
import { getUiSupportWarnings, isConfigObject } from "../ui-support";
|
||||
import "./ha-automation-sidebar-card";
|
||||
import { renderCtrlOrCmd } from "../../../../common/keyboard/ctrl-or-cmd";
|
||||
|
||||
@@ -69,7 +68,6 @@ export default class HaAutomationSidebarTrigger extends LitElement {
|
||||
this.yamlMode = this.config.yamlMode;
|
||||
if (this.yamlMode) {
|
||||
this.editor?.yamlEditor?.setValue(this.config.config);
|
||||
this._warnings = this._checkUiSupport(this.config.config);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -323,7 +321,7 @@ export default class HaAutomationSidebarTrigger extends LitElement {
|
||||
}
|
||||
|
||||
private _handleUiModeNotAvailable(ev: CustomEvent) {
|
||||
this._warnings = handleStructError(this.hass.localize, ev.detail).warnings;
|
||||
this._warnings = handleStructError(this.hass, ev.detail).warnings;
|
||||
if (!this.yamlMode) {
|
||||
this.yamlMode = true;
|
||||
}
|
||||
@@ -348,26 +346,10 @@ export default class HaAutomationSidebarTrigger extends LitElement {
|
||||
ev.stopPropagation();
|
||||
|
||||
this.config?.save?.(ev.detail.value);
|
||||
this._warnings = this._checkUiSupport(ev.detail.value);
|
||||
}
|
||||
|
||||
private _checkUiSupport(trigger: Trigger): string[] | undefined {
|
||||
if (
|
||||
!this.config.uiSupported ||
|
||||
!isConfigObject(trigger) ||
|
||||
isTriggerList(trigger)
|
||||
) {
|
||||
return undefined;
|
||||
}
|
||||
return getUiSupportWarnings(
|
||||
this.hass.localize,
|
||||
`ha-automation-trigger-${trigger.trigger}`,
|
||||
trigger
|
||||
);
|
||||
}
|
||||
|
||||
private _toggleYamlMode = () => {
|
||||
fireEvent(this, "toggle-yaml-mode", { yamlMode: !this.yamlMode });
|
||||
fireEvent(this, "toggle-yaml-mode");
|
||||
};
|
||||
|
||||
private _handleDropdownSelect(ev: HaDropdownSelectEvent) {
|
||||
|
||||
@@ -75,7 +75,6 @@ import "../ha-automation-editor-warning";
|
||||
import "../ha-automation-row-options";
|
||||
import "../ha-automation-row-threshold";
|
||||
import { overflowStyles, rowStyles } from "../styles";
|
||||
import { getUiSupportWarnings, isConfigObject } from "../ui-support";
|
||||
import { getDeviceTarget } from "../target/get_device_target";
|
||||
import { getEntityTarget } from "../target/get_entity_target";
|
||||
import "../target/ha-automation-row-targets";
|
||||
@@ -358,9 +357,7 @@ export default class HaAutomationTriggerRow extends LitElement {
|
||||
class="event-chip"
|
||||
aria-live="polite"
|
||||
>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.automation.editor.actions.disabled"
|
||||
)}
|
||||
${this.hass.localize("ui.panel.config.automation.editor.actions.disabled")}
|
||||
</ha-automation-row-event-chip>
|
||||
|
||||
<ha-automation-row-event-chip
|
||||
@@ -669,19 +666,6 @@ export default class HaAutomationTriggerRow extends LitElement {
|
||||
if (changedProperties.has("yamlMode")) {
|
||||
this._warnings = undefined;
|
||||
}
|
||||
if (
|
||||
changedProperties.has("trigger") &&
|
||||
this._warnings &&
|
||||
this._yamlMode &&
|
||||
isConfigObject(this.trigger) &&
|
||||
!isTriggerList(this.trigger)
|
||||
) {
|
||||
this._warnings = getUiSupportWarnings(
|
||||
this.hass.localize,
|
||||
`ha-automation-trigger-${this.trigger.trigger}`,
|
||||
this.trigger
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
protected override updated(changedProps: PropertyValues<this>): void {
|
||||
@@ -762,7 +746,7 @@ export default class HaAutomationTriggerRow extends LitElement {
|
||||
}, 5000);
|
||||
|
||||
private _handleUiModeNotAvailable(ev: CustomEvent) {
|
||||
this._warnings = handleStructError(this.hass.localize, ev.detail).warnings;
|
||||
this._warnings = handleStructError(this.hass, ev.detail).warnings;
|
||||
if (!this._yamlMode) {
|
||||
this._yamlMode = true;
|
||||
}
|
||||
@@ -806,10 +790,8 @@ export default class HaAutomationTriggerRow extends LitElement {
|
||||
this._renameTrigger();
|
||||
},
|
||||
editNote: this._editNoteTrigger,
|
||||
toggleYamlMode: (yamlMode?: boolean) => {
|
||||
if (yamlMode === undefined || yamlMode !== this._yamlMode) {
|
||||
this._toggleYamlMode();
|
||||
}
|
||||
toggleYamlMode: () => {
|
||||
this._toggleYamlMode();
|
||||
this.openSidebar();
|
||||
},
|
||||
disable: this._onDisable,
|
||||
|
||||
@@ -54,34 +54,6 @@ export class HaStateTrigger extends LitElement implements TriggerElement {
|
||||
return { trigger: "state", entity_id: [] };
|
||||
}
|
||||
|
||||
public static checkUiSupport(
|
||||
localize: LocalizeFunc,
|
||||
trigger: StateTrigger
|
||||
): Error | undefined {
|
||||
// Check for templates in trigger. If found, revert to YAML mode.
|
||||
// Exclude "for" since the UI now supports templates there via choose.
|
||||
const { for: forValue, ...triggerWithoutFor } = trigger;
|
||||
if (hasTemplate(triggerWithoutFor)) {
|
||||
return Error(localize("ui.errors.config.no_template_editor_support"));
|
||||
}
|
||||
try {
|
||||
// The editor drops `milliseconds: 0` before rendering, so accept it here.
|
||||
if (
|
||||
forValue &&
|
||||
typeof forValue === "object" &&
|
||||
forValue.milliseconds === 0
|
||||
) {
|
||||
const { milliseconds: _milliseconds, ...forWithoutMs } = forValue;
|
||||
assert({ ...trigger, for: forWithoutMs }, stateTriggerStruct);
|
||||
} else {
|
||||
assert(trigger, stateTriggerStruct);
|
||||
}
|
||||
} catch (err: any) {
|
||||
return err;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
private _schema = memoizeOne(
|
||||
(
|
||||
localize: LocalizeFunc,
|
||||
@@ -227,9 +199,21 @@ export class HaStateTrigger extends LitElement implements TriggerElement {
|
||||
) {
|
||||
delete this.trigger.for.milliseconds;
|
||||
}
|
||||
const err = HaStateTrigger.checkUiSupport(this.hass.localize, this.trigger);
|
||||
if (err) {
|
||||
fireEvent(this, "ui-mode-not-available", err);
|
||||
// Check for templates in trigger. If found, revert to YAML mode.
|
||||
// Exclude "for" since the UI now supports templates there via choose.
|
||||
const { for: _for, ...triggerWithoutFor } = this.trigger;
|
||||
if (triggerWithoutFor && hasTemplate(triggerWithoutFor)) {
|
||||
fireEvent(
|
||||
this,
|
||||
"ui-mode-not-available",
|
||||
Error(this.hass.localize("ui.errors.config.no_template_editor_support"))
|
||||
);
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
assert(this.trigger, stateTriggerStruct);
|
||||
} catch (e: any) {
|
||||
fireEvent(this, "ui-mode-not-available", e);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
|
||||
@@ -2,29 +2,15 @@ import type { PropertyValues } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import {
|
||||
array,
|
||||
assert,
|
||||
assign,
|
||||
literal,
|
||||
object,
|
||||
optional,
|
||||
record,
|
||||
string,
|
||||
union,
|
||||
unknown,
|
||||
} from "superstruct";
|
||||
import { firstWeekdayIndex } from "../../../../../common/datetime/first_weekday";
|
||||
import { fireEvent } from "../../../../../common/dom/fire_event";
|
||||
import { computeDomain } from "../../../../../common/entity/compute_domain";
|
||||
import { hasTemplate } from "../../../../../common/string/has-template";
|
||||
import type { LocalizeFunc } from "../../../../../common/translations/localize";
|
||||
import "../../../../../components/ha-form/ha-form";
|
||||
import type { SchemaUnion } from "../../../../../components/ha-form/types";
|
||||
import type { TimeTrigger } from "../../../../../data/automation";
|
||||
import type { FrontendLocaleData } from "../../../../../data/translation";
|
||||
import type { HomeAssistant } from "../../../../../types";
|
||||
import { baseTriggerStruct } from "../../structs";
|
||||
import type { TriggerElement } from "../ha-automation-trigger-row";
|
||||
|
||||
const MODE_TIME = "time";
|
||||
@@ -32,20 +18,6 @@ const MODE_ENTITY = "entity";
|
||||
const VALID_DOMAINS = ["sensor", "input_datetime"];
|
||||
const DAYS = ["sun", "mon", "tue", "wed", "thu", "fri", "sat"] as const;
|
||||
|
||||
const timeTriggerStruct = assign(
|
||||
baseTriggerStruct,
|
||||
object({
|
||||
alias: optional(string()),
|
||||
trigger: literal("time"),
|
||||
variables: optional(record(string(), unknown())),
|
||||
at: union([
|
||||
string(),
|
||||
object({ entity_id: string(), offset: optional(string()) }),
|
||||
]),
|
||||
weekday: optional(union([string(), array(string())])),
|
||||
})
|
||||
);
|
||||
|
||||
@customElement("ha-automation-trigger-time")
|
||||
export class HaTimeTrigger extends LitElement implements TriggerElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
@@ -61,25 +33,6 @@ export class HaTimeTrigger extends LitElement implements TriggerElement {
|
||||
return { trigger: "time", at: "" };
|
||||
}
|
||||
|
||||
public static checkUiSupport(
|
||||
localize: LocalizeFunc,
|
||||
trigger: TimeTrigger
|
||||
): Error | undefined {
|
||||
// We don't support multiple times atm.
|
||||
if (Array.isArray(trigger.at)) {
|
||||
return Error(localize("ui.errors.config.editor_not_supported"));
|
||||
}
|
||||
if (hasTemplate(trigger.at) || hasTemplate(trigger.weekday)) {
|
||||
return Error(localize("ui.errors.config.no_template_editor_support"));
|
||||
}
|
||||
try {
|
||||
assert(trigger, timeTriggerStruct);
|
||||
} catch (err: any) {
|
||||
return err;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
private _schema = memoizeOne(
|
||||
(
|
||||
localize: LocalizeFunc,
|
||||
@@ -143,16 +96,18 @@ export class HaTimeTrigger extends LitElement implements TriggerElement {
|
||||
}
|
||||
);
|
||||
|
||||
public shouldUpdate(changedProperties: PropertyValues<this>) {
|
||||
public willUpdate(changedProperties: PropertyValues<this>) {
|
||||
if (!changedProperties.has("trigger")) {
|
||||
return true;
|
||||
return;
|
||||
}
|
||||
const err = HaTimeTrigger.checkUiSupport(this.hass.localize, this.trigger);
|
||||
if (err) {
|
||||
fireEvent(this, "ui-mode-not-available", err);
|
||||
return false;
|
||||
// We don't support multiple times atm.
|
||||
if (this.trigger && Array.isArray(this.trigger.at)) {
|
||||
fireEvent(
|
||||
this,
|
||||
"ui-mode-not-available",
|
||||
Error(this.hass.localize("ui.errors.config.editor_not_supported"))
|
||||
);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private _data = memoizeOne(
|
||||
|
||||
@@ -1,39 +0,0 @@
|
||||
import { handleStructError } from "../../../common/structs/handle-errors";
|
||||
import type { LocalizeFunc } from "../../../common/translations/localize";
|
||||
|
||||
/**
|
||||
* Static check on editor elements, returns an error when the config can't be
|
||||
* edited in the visual editor. Must not depend on element state.
|
||||
*/
|
||||
export type CheckUiSupport<T = any> = (
|
||||
localize: LocalizeFunc,
|
||||
config: T
|
||||
) => Error | undefined;
|
||||
|
||||
/** YAML can parse to scalars like `null`, which are never valid configs */
|
||||
export const isConfigObject = (
|
||||
value: unknown
|
||||
): value is Record<string, unknown> =>
|
||||
typeof value === "object" && value !== null && !Array.isArray(value);
|
||||
|
||||
interface ElementWithUiSupportCheck extends CustomElementConstructor {
|
||||
checkUiSupport?: CheckUiSupport;
|
||||
}
|
||||
|
||||
export const checkElementUiSupport = (
|
||||
localize: LocalizeFunc,
|
||||
elementName: string,
|
||||
config: unknown
|
||||
): Error | undefined =>
|
||||
(
|
||||
customElements.get(elementName) as ElementWithUiSupportCheck | undefined
|
||||
)?.checkUiSupport?.(localize, config);
|
||||
|
||||
export const getUiSupportWarnings = (
|
||||
localize: LocalizeFunc,
|
||||
elementName: string,
|
||||
config: unknown
|
||||
): string[] | undefined => {
|
||||
const err = checkElementUiSupport(localize, elementName, config);
|
||||
return err ? handleStructError(localize, err).warnings : undefined;
|
||||
};
|
||||
@@ -219,27 +219,6 @@ export class HuiViewBadges extends LitElement {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* Use before and after because padding doesn't work well with scrolling */
|
||||
.badges::before,
|
||||
.badges::after {
|
||||
content: "";
|
||||
position: relative;
|
||||
display: block;
|
||||
min-width: var(--badge-padding, 0px);
|
||||
height: 16px;
|
||||
background-color: transparent;
|
||||
}
|
||||
.badges::before {
|
||||
margin-left: -8px;
|
||||
margin-inline-start: -8px;
|
||||
margin-inline-end: 0;
|
||||
}
|
||||
.badges::after {
|
||||
margin-right: -8px;
|
||||
margin-inline-end: -8px;
|
||||
margin-inline-start: 0;
|
||||
}
|
||||
|
||||
.badges > * {
|
||||
min-width: fit-content;
|
||||
}
|
||||
|
||||
@@ -1,9 +1,13 @@
|
||||
import type { PropertyValues } from "lit";
|
||||
import { LitElement, css, html, nothing } from "lit";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { classMap } from "lit/directives/class-map";
|
||||
import { ifDefined } from "lit/directives/if-defined";
|
||||
import { DragScrollController } from "../../../common/controllers/drag-scroll-controller";
|
||||
import {
|
||||
ScrollFadeController,
|
||||
scrollFadeStyles,
|
||||
} from "../../../common/controllers/scroll-fade-controller";
|
||||
import "../../../components/ha-card";
|
||||
import "../../../components/ha-icon";
|
||||
import "../../../components/ha-icon-next";
|
||||
@@ -52,20 +56,14 @@ export class HuiHeadingCard extends LitElement implements LovelaceCard {
|
||||
|
||||
@property({ type: Boolean }) public preview = false;
|
||||
|
||||
@query(".badges") private _badges?: HTMLDivElement;
|
||||
|
||||
@state() private _config?: HeadingCardConfig;
|
||||
|
||||
@state() private _badgesOverflowing = false;
|
||||
|
||||
private _dragScrollController = new DragScrollController(this, {
|
||||
selector: ".badges",
|
||||
enabled: false,
|
||||
});
|
||||
|
||||
private _resizeObserver?: ResizeObserver;
|
||||
|
||||
private _observedBadges?: HTMLDivElement;
|
||||
private _badgesScrollFade = new ScrollFadeController(this);
|
||||
|
||||
public setConfig(config: HeadingCardConfig): void {
|
||||
this._config = {
|
||||
@@ -84,30 +82,6 @@ export class HuiHeadingCard extends LitElement implements LovelaceCard {
|
||||
this._dragScrollController.enabled = !this.preview;
|
||||
}
|
||||
|
||||
protected updated(): void {
|
||||
if (!this._resizeObserver) {
|
||||
this._resizeObserver = new ResizeObserver(() => {
|
||||
this._measureBadgesOverflow();
|
||||
});
|
||||
}
|
||||
|
||||
if (this._observedBadges !== this._badges) {
|
||||
this._resizeObserver.disconnect();
|
||||
this._observedBadges = this._badges;
|
||||
|
||||
if (this._observedBadges) {
|
||||
this._resizeObserver.observe(this._observedBadges);
|
||||
} else {
|
||||
this._badgesOverflowing = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public disconnectedCallback(): void {
|
||||
super.disconnectedCallback();
|
||||
this._resizeObserver?.disconnect();
|
||||
}
|
||||
|
||||
public getCardSize(): number {
|
||||
return 1;
|
||||
}
|
||||
@@ -124,26 +98,6 @@ export class HuiHeadingCard extends LitElement implements LovelaceCard {
|
||||
handleAction(this, this.hass!, this._config!, ev.detail.action!);
|
||||
}
|
||||
|
||||
private _measureBadgesOverflow() {
|
||||
if (!this._badges) {
|
||||
this._badgesOverflowing = false;
|
||||
return;
|
||||
}
|
||||
|
||||
// `.overflowing` pseudo-elements inflate `scrollWidth`, subtract to keep the check symmetric.
|
||||
const padding = this._badgesOverflowing
|
||||
? parseFloat(
|
||||
getComputedStyle(this._badges).getPropertyValue("--ha-space-4")
|
||||
) || 0
|
||||
: 0;
|
||||
const overflowing =
|
||||
this._badges.scrollWidth - padding > this._badges!.clientWidth + 1;
|
||||
|
||||
if (overflowing !== this._badgesOverflowing) {
|
||||
this._badgesOverflowing = overflowing;
|
||||
}
|
||||
}
|
||||
|
||||
protected render() {
|
||||
if (!this._config || !this.hass) {
|
||||
return nothing;
|
||||
@@ -187,9 +141,11 @@ export class HuiHeadingCard extends LitElement implements LovelaceCard {
|
||||
class=${classMap({
|
||||
badges: true,
|
||||
draggable: !this.preview,
|
||||
overflowing: this._badgesOverflowing,
|
||||
"scroll-fade-start": this._badgesScrollFade.start,
|
||||
"scroll-fade-end": this._badgesScrollFade.end,
|
||||
dragging: badgeDragging,
|
||||
})}
|
||||
${this._badgesScrollFade.target()}
|
||||
>
|
||||
<div class="badges-row">
|
||||
${badges.map(
|
||||
@@ -213,6 +169,8 @@ export class HuiHeadingCard extends LitElement implements LovelaceCard {
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
${scrollFadeStyles}
|
||||
|
||||
ha-card {
|
||||
background: none;
|
||||
backdrop-filter: none;
|
||||
@@ -314,16 +272,7 @@ export class HuiHeadingCard extends LitElement implements LovelaceCard {
|
||||
scrollbar-color: var(--scrollbar-thumb-color) transparent;
|
||||
scrollbar-width: none;
|
||||
}
|
||||
.badges.overflowing {
|
||||
mask-image: linear-gradient(
|
||||
90deg,
|
||||
transparent 0%,
|
||||
black var(--ha-space-4),
|
||||
black calc(100% - var(--ha-space-4)),
|
||||
transparent 100%
|
||||
);
|
||||
}
|
||||
.badges.draggable.overflowing {
|
||||
.badges.draggable:is(.scroll-fade-start, .scroll-fade-end) {
|
||||
cursor: grab;
|
||||
}
|
||||
.badges-row {
|
||||
@@ -335,36 +284,9 @@ export class HuiHeadingCard extends LitElement implements LovelaceCard {
|
||||
gap: var(--ha-space-2);
|
||||
margin: 0;
|
||||
}
|
||||
/* Use before and after because padding doesn't work well with scrolling */
|
||||
.badges-row::before,
|
||||
.badges-row::after {
|
||||
content: "";
|
||||
position: relative;
|
||||
display: block;
|
||||
min-width: var(--badge-padding, 0);
|
||||
height: var(--ha-space-4);
|
||||
background-color: transparent;
|
||||
}
|
||||
.badges-row::before {
|
||||
margin-inline-start: calc(var(--ha-space-2) * -1);
|
||||
margin-inline-end: 0;
|
||||
}
|
||||
.badges-row::after {
|
||||
margin-inline-end: calc(var(--ha-space-2) * -1);
|
||||
margin-inline-start: 0;
|
||||
}
|
||||
.badges-row > * {
|
||||
min-width: fit-content;
|
||||
}
|
||||
.badges.overflowing .badges-row {
|
||||
--badge-padding: var(--ha-space-4);
|
||||
}
|
||||
.badges.overflowing .badges-row > *:first-child {
|
||||
margin-inline-start: calc(var(--ha-space-2) * -1);
|
||||
}
|
||||
.badges.overflowing .badges-row > *:last-child {
|
||||
margin-inline-end: calc(var(--ha-space-2) * -1);
|
||||
}
|
||||
.badges.dragging {
|
||||
cursor: grabbing;
|
||||
pointer-events: none;
|
||||
|
||||
@@ -354,7 +354,7 @@ export class HaCardConditionEditor extends LitElement {
|
||||
this._uiWarnings = [];
|
||||
} catch (err) {
|
||||
this._uiWarnings = handleStructError(
|
||||
this.hass.localize,
|
||||
this.hass,
|
||||
err as Error
|
||||
).warnings;
|
||||
this._uiAvailable = false;
|
||||
@@ -762,10 +762,7 @@ export class HaCardConditionEditor extends LitElement {
|
||||
// Automation editors emit this when UI mode can't handle the config.
|
||||
private _handleUiModeNotAvailable(ev: CustomEvent) {
|
||||
ev.stopPropagation();
|
||||
this._uiWarnings = handleStructError(
|
||||
this.hass.localize,
|
||||
ev.detail
|
||||
).warnings;
|
||||
this._uiWarnings = handleStructError(this.hass, ev.detail).warnings;
|
||||
this._uiAvailable = false;
|
||||
if (!this._yamlMode) {
|
||||
this._yamlMode = true;
|
||||
|
||||
@@ -421,7 +421,7 @@ export abstract class HuiElementEditor<
|
||||
try {
|
||||
this._configElement.setConfig(this.value);
|
||||
} catch (err: any) {
|
||||
const msgs = handleStructError(this.hass.localize, err);
|
||||
const msgs = handleStructError(this.hass, err);
|
||||
throw new GUISupportError(
|
||||
"Config is not supported",
|
||||
msgs.warnings,
|
||||
|
||||
@@ -4,6 +4,10 @@ import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { classMap } from "lit/directives/class-map";
|
||||
import { DragScrollController } from "../../../common/controllers/drag-scroll-controller";
|
||||
import {
|
||||
ScrollFadeController,
|
||||
scrollFadeStyles,
|
||||
} from "../../../common/controllers/scroll-fade-controller";
|
||||
import "../../../components/ha-ripple";
|
||||
import "../../../components/ha-sortable";
|
||||
import "../../../components/ha-svg-icon";
|
||||
@@ -56,6 +60,8 @@ export class HuiViewHeader extends LitElement {
|
||||
enabled: false,
|
||||
});
|
||||
|
||||
private _badgesScrollFade = new ScrollFadeController(this);
|
||||
|
||||
connectedCallback(): void {
|
||||
super.connectedCallback();
|
||||
this.addEventListener(
|
||||
@@ -201,9 +207,7 @@ export class HuiViewHeader extends LitElement {
|
||||
this.config?.badges_position ?? DEFAULT_VIEW_HEADER_BADGES_POSITION;
|
||||
const badgesWrap =
|
||||
this.config?.badges_wrap ?? DEFAULT_VIEW_HEADER_BADGES_WRAP;
|
||||
const badgeDragging = this._dragScrollController.scrolling
|
||||
? "dragging"
|
||||
: "";
|
||||
const badgesScrollable = !editMode && badgesWrap === "scroll";
|
||||
|
||||
const hasHeading = card !== undefined;
|
||||
const hasBadges = this.badges.length > 0;
|
||||
@@ -272,7 +276,17 @@ export class HuiViewHeader extends LitElement {
|
||||
this.lovelace && (editMode || this.badges.length > 0)
|
||||
? html`
|
||||
<div
|
||||
class="badges ${badgesPosition} ${badgesWrap} ${badgeDragging}"
|
||||
class=${classMap({
|
||||
badges: true,
|
||||
[badgesPosition]: true,
|
||||
[badgesWrap]: true,
|
||||
dragging: this._dragScrollController.scrolling,
|
||||
"scroll-fade-start":
|
||||
badgesScrollable && this._badgesScrollFade.start,
|
||||
"scroll-fade-end":
|
||||
badgesScrollable && this._badgesScrollFade.end,
|
||||
})}
|
||||
${this._badgesScrollFade.target()}
|
||||
>
|
||||
<hui-view-badges
|
||||
.badges=${this.badges}
|
||||
@@ -290,6 +304,8 @@ export class HuiViewHeader extends LitElement {
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
${scrollFadeStyles}
|
||||
|
||||
:host([hidden]) {
|
||||
display: none !important;
|
||||
}
|
||||
@@ -370,13 +386,6 @@ export class HuiViewHeader extends LitElement {
|
||||
max-width: 100%;
|
||||
scrollbar-color: var(--scrollbar-thumb-color) transparent;
|
||||
scrollbar-width: none;
|
||||
mask-image: linear-gradient(
|
||||
90deg,
|
||||
transparent 0%,
|
||||
black 16px,
|
||||
black calc(100% - 16px),
|
||||
transparent 100%
|
||||
);
|
||||
}
|
||||
|
||||
hui-view-badges {
|
||||
@@ -413,12 +422,7 @@ export class HuiViewHeader extends LitElement {
|
||||
|
||||
.container:not(.edit-mode) .layout.badges-scroll hui-view-badges {
|
||||
--badges-wrap: nowrap;
|
||||
--badges-aligmnent: flex-start;
|
||||
--badge-padding: 16px;
|
||||
}
|
||||
|
||||
.container:not(.edit-mode) .layout.center.badges-scroll hui-view-badges {
|
||||
--badges-aligmnent: space-around;
|
||||
width: max-content;
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
@@ -435,7 +439,7 @@ export class HuiViewHeader extends LitElement {
|
||||
hui-view-badges {
|
||||
--badges-wrap: wrap;
|
||||
--badges-aligmnent: flex-end;
|
||||
--badge-padding: 0;
|
||||
width: 100%;
|
||||
}
|
||||
.layout.responsive.has-heading hui-view-badges {
|
||||
--badges-aligmnent: flex-end;
|
||||
|
||||
@@ -0,0 +1,106 @@
|
||||
import type { ReactiveControllerHost } from "@lit/reactive-element/reactive-controller";
|
||||
import { html, render } from "lit";
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { ScrollFadeController } from "../../../src/common/controllers/scroll-fade-controller";
|
||||
|
||||
const SCROLLER_WIDTH = 100;
|
||||
const CONTENT_WIDTH = 300;
|
||||
const MAX_SCROLL = CONTENT_WIDTH - SCROLLER_WIDTH;
|
||||
|
||||
const observe = vi.fn();
|
||||
|
||||
let resizeCallback: () => void;
|
||||
|
||||
class MockResizeObserver {
|
||||
constructor(callback: () => void) {
|
||||
resizeCallback = callback;
|
||||
}
|
||||
|
||||
observe = observe;
|
||||
|
||||
unobserve = vi.fn();
|
||||
|
||||
disconnect = vi.fn();
|
||||
}
|
||||
|
||||
const createHost = (): ReactiveControllerHost => ({
|
||||
addController: vi.fn(),
|
||||
removeController: vi.fn(),
|
||||
requestUpdate: vi.fn(),
|
||||
updateComplete: Promise.resolve(true),
|
||||
});
|
||||
|
||||
describe("ScrollFadeController", () => {
|
||||
let host: ReactiveControllerHost;
|
||||
let controller: ScrollFadeController;
|
||||
let container: HTMLDivElement;
|
||||
let scroller: HTMLDivElement;
|
||||
|
||||
const setLayout = (scrollLeft: number, contentWidth: number) => {
|
||||
Object.defineProperties(scroller, {
|
||||
scrollLeft: { value: scrollLeft, configurable: true },
|
||||
scrollWidth: { value: contentWidth, configurable: true },
|
||||
clientWidth: { value: SCROLLER_WIDTH, configurable: true },
|
||||
});
|
||||
};
|
||||
|
||||
const scrollTo = (scrollLeft: number, contentWidth = CONTENT_WIDTH) => {
|
||||
setLayout(scrollLeft, contentWidth);
|
||||
scroller.dispatchEvent(new Event("scroll"));
|
||||
};
|
||||
|
||||
const fades = () => ({ start: controller.start, end: controller.end });
|
||||
|
||||
beforeEach(() => {
|
||||
vi.stubGlobal("ResizeObserver", MockResizeObserver);
|
||||
observe.mockClear();
|
||||
host = createHost();
|
||||
controller = new ScrollFadeController(host);
|
||||
container = document.createElement("div");
|
||||
document.body.appendChild(container);
|
||||
render(html`<div ${controller.target()}><div></div></div>`, container);
|
||||
scroller = container.firstElementChild as HTMLDivElement;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
container.remove();
|
||||
vi.unstubAllGlobals();
|
||||
});
|
||||
|
||||
it("does not fade when the content fits", () => {
|
||||
scrollTo(0, SCROLLER_WIDTH);
|
||||
expect(fades()).toEqual({ start: false, end: false });
|
||||
});
|
||||
|
||||
it("fades only the edges that hide content", () => {
|
||||
scrollTo(0);
|
||||
expect(fades()).toEqual({ start: false, end: true });
|
||||
scrollTo(MAX_SCROLL / 2);
|
||||
expect(fades()).toEqual({ start: true, end: true });
|
||||
scrollTo(MAX_SCROLL);
|
||||
expect(fades()).toEqual({ start: true, end: false });
|
||||
});
|
||||
|
||||
it("handles the negative scrollLeft of right-to-left layouts", () => {
|
||||
scroller.dir = "rtl";
|
||||
scrollTo(-MAX_SCROLL / 2);
|
||||
expect(fades()).toEqual({ start: true, end: true });
|
||||
scrollTo(-MAX_SCROLL);
|
||||
expect(fades()).toEqual({ start: true, end: false });
|
||||
});
|
||||
|
||||
it("watches the content, which can grow without resizing the scroller", () => {
|
||||
expect(observe).toHaveBeenCalledWith(scroller.firstElementChild);
|
||||
scrollTo(MAX_SCROLL);
|
||||
expect(fades()).toEqual({ start: true, end: false });
|
||||
setLayout(MAX_SCROLL, CONTENT_WIDTH + SCROLLER_WIDTH);
|
||||
resizeCallback();
|
||||
expect(fades()).toEqual({ start: true, end: true });
|
||||
});
|
||||
|
||||
it("only requests an update when a fade changes", () => {
|
||||
scrollTo(MAX_SCROLL / 4);
|
||||
scrollTo(MAX_SCROLL / 2);
|
||||
expect(host.requestUpdate).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
@@ -226,6 +226,20 @@ describe("ha-map engine selection", () => {
|
||||
expect(entityHandles(el)).toHaveLength(2);
|
||||
});
|
||||
|
||||
it("sets up while themes are not loaded yet, as during onboarding", async () => {
|
||||
const el = document.createElement("ha-map");
|
||||
el.themeMode = "auto";
|
||||
(el as any)._ui = { themes: null };
|
||||
(el as any)._states = STATES;
|
||||
(el as any)._config = {
|
||||
config: { latitude: 52.3731339, longitude: 4.8903147 },
|
||||
};
|
||||
document.body.appendChild(el);
|
||||
await vi.waitUntil(() => isLoaded(el));
|
||||
|
||||
expect(fakeEngine.instances).toHaveLength(1);
|
||||
});
|
||||
|
||||
it("runs on Leaflet when WebGL2 is not available", async () => {
|
||||
webgl2.supported = false;
|
||||
const el = await createMap();
|
||||
|
||||
Reference in New Issue
Block a user