mirror of
https://github.com/home-assistant/frontend.git
synced 2026-08-16 10:02:41 +01:00
* Show target-state entity icon in scene editor review mode Render the entity badge in review mode (previously live-only) across both the device-grouped and standalone entity lists. In review mode the badge uses the scene's stored target state instead of the entity's live state, so the icon previews what the scene will set once applied. * Strip stale entity_picture from the synthesized review-mode state state-badge hides the icon and renders a background image whenever a state carries an entity_picture (see its willUpdate). A scene snapshots that URL with an access token that is stale by the time review mode renders, so an image-backed entity - for example a robot vacuum's "Map data" camera entity on its device - showed a blank badge in review mode instead of an icon. Drop entity_picture / entity_picture_local from the synthesized state so the entity's own icon resolves. This is not a pre-existing defect: it handles a case that rendering the badge in review mode (previous commit) introduces. * Handle null and scalar scene entity values in review mode badges An entity left without a value in the YAML editor parses as null, which crashed the review-mode render. The scene config API also returns raw scenes.yaml content without validation, so hand-edited scenes deliver boolean states as-is (YAML 1.1 parses unquoted on/off as booleans); these previously rendered as if the entity had no state at all. Booleans map to on/off to match how the backend applies them when a scene is activated (_convert_states in the homeassistant scene platform). The backend rejects null and numeric states at save, but review mode renders before save, so the frontend has to tolerate them. * Extract and memoize the scene target-state synthesis The editor re-renders on every hass change, and building a fresh state object per row each time defeated Lit dirty-checking: every state-badge re-ran willUpdate and every ha-state-icon restarted its async icon resolution. Memoizing the synthesized objects per config keeps the references stable so unchanged badges skip all of that. Moving the synthesis to src/data/scene.ts makes it unit-testable; the null, boolean, numeric, string, and picture-stripping cases are now covered by tests. * Sanitize brightness and rgb_color in the synthesized review-mode state state-badge does arithmetic on brightness and joins rgb_color, assuming backend-shaped values. Hand-typed YAML can hold both as strings: a string rgb_color threw a TypeError that left the badge blank, and a string brightness computed a brightness(36049%) filter that washed the icon out to invisible. Coerce numeric-string brightness and drop malformed values so the badge always renders the target state. * Borrow the live device_class for review-mode badge icons Icon resolution keys on device_class, which string-shorthand and hand-written minimal scene entries do not carry, so a garage cover fell back to the generic window icon and sensors to the domain default. Only this identity attribute is borrowed from the live state - merging stateful attributes like rgb_color would mis-color an off target. * Reject unusable scene targets and trim the badge-state synthesis Review-mode badges now render only when the scene holds a usable target state. Entries with no state to show - null values, dicts without a state key, arrays, non-scalar states - yield no badge instead of falling back to the live state, which was indistinguishable from a real target and, for dicts without a state, crashed state-badge via stateColorCss on lights. rgb_color and brightness are dropped from inactive targets: a live entity never carries them while off, and state-badge applies them without checking activity, so a scene turning a light off rendered an active-looking colored icon. Entity pictures are stripped only for DOMAINS_WITH_DYNAMIC_PICTURE, matching createHistoricState in the logbook; stable pictures on other domains are kept. The brightness/rgb_color type coercion and the live device_class borrowing are removed: they defended against hand-typed shapes that state-badge already warns about, and the borrowing made the memoized synthesis depend on hass state outside its memoize key. * Apply suggestion from @MindFreeze --------- Co-authored-by: Przemysław Szypowicz <2733699+pszypowicz@users.noreply.github.com> Co-authored-by: Petar Petrov <MindFreeze@users.noreply.github.com>
117 lines
3.7 KiB
TypeScript
117 lines
3.7 KiB
TypeScript
import { describe, expect, it } from "vitest";
|
|
|
|
import { sceneEntityStateObj } from "../../src/data/scene";
|
|
|
|
describe("sceneEntityStateObj", () => {
|
|
it("builds a state object from string shorthand", () => {
|
|
expect(sceneEntityStateObj("light.kitchen", "on")).toEqual({
|
|
entity_id: "light.kitchen",
|
|
state: "on",
|
|
attributes: {},
|
|
});
|
|
});
|
|
|
|
it("builds a state object from the dict form", () => {
|
|
expect(
|
|
sceneEntityStateObj("light.kitchen", {
|
|
state: "on",
|
|
brightness: 180,
|
|
rgb_color: [255, 64, 112],
|
|
})
|
|
).toEqual({
|
|
entity_id: "light.kitchen",
|
|
state: "on",
|
|
attributes: { brightness: 180, rgb_color: [255, 64, 112] },
|
|
});
|
|
});
|
|
|
|
it("strips media-derived entity pictures but keeps other attributes", () => {
|
|
const sceneEntity = {
|
|
state: "playing",
|
|
entity_picture: "/api/media_player_proxy/x?token=stale",
|
|
entity_picture_local: "/local/x.jpg",
|
|
friendly_name: "Player",
|
|
};
|
|
expect(sceneEntityStateObj("media_player.x", sceneEntity)).toEqual({
|
|
entity_id: "media_player.x",
|
|
state: "playing",
|
|
attributes: { friendly_name: "Player" },
|
|
});
|
|
// The input scene config must not be mutated.
|
|
expect(sceneEntity.entity_picture).toBe(
|
|
"/api/media_player_proxy/x?token=stale"
|
|
);
|
|
});
|
|
|
|
it("keeps a stable entity picture on other domains", () => {
|
|
expect(
|
|
sceneEntityStateObj("vacuum.robot", {
|
|
state: "cleaning",
|
|
entity_picture: "/local/robot.png",
|
|
})?.attributes.entity_picture
|
|
).toBe("/local/robot.png");
|
|
});
|
|
|
|
it("returns undefined for null and undefined values", () => {
|
|
// An entity left without a value in the YAML editor parses as null.
|
|
expect(sceneEntityStateObj("light.kitchen", null)).toBeUndefined();
|
|
expect(sceneEntityStateObj("light.kitchen", undefined)).toBeUndefined();
|
|
});
|
|
|
|
it("returns undefined for an array value", () => {
|
|
expect(sceneEntityStateObj("light.kitchen", [255, 100])).toBeUndefined();
|
|
});
|
|
|
|
it("returns undefined for a numeric value", () => {
|
|
// The backend only accepts string and boolean states, so a number never
|
|
// becomes a target the scene can apply.
|
|
expect(sceneEntityStateObj("input_number.x", 23)).toBeUndefined();
|
|
expect(
|
|
sceneEntityStateObj("input_number.x", { state: 23 })
|
|
).toBeUndefined();
|
|
});
|
|
|
|
it("returns undefined when the dict holds no usable state", () => {
|
|
expect(
|
|
sceneEntityStateObj("light.kitchen", { brightness: 100 })
|
|
).toBeUndefined();
|
|
expect(
|
|
sceneEntityStateObj("light.kitchen", { state: null })
|
|
).toBeUndefined();
|
|
expect(
|
|
sceneEntityStateObj("light.kitchen", { state: { brightness: 100 } })
|
|
).toBeUndefined();
|
|
});
|
|
|
|
it("normalizes boolean shorthand to on/off like the backend does", () => {
|
|
// YAML 1.1 parses unquoted on/off in scenes.yaml as booleans.
|
|
expect(sceneEntityStateObj("light.kitchen", true)?.state).toBe("on");
|
|
expect(sceneEntityStateObj("light.kitchen", false)?.state).toBe("off");
|
|
});
|
|
|
|
it("normalizes a boolean state in the dict form", () => {
|
|
expect(sceneEntityStateObj("light.kitchen", { state: true })).toEqual({
|
|
entity_id: "light.kitchen",
|
|
state: "on",
|
|
attributes: {},
|
|
});
|
|
});
|
|
|
|
it("drops color attributes for an inactive target", () => {
|
|
// A live entity never carries these while off, and state-badge applies
|
|
// them without checking activity.
|
|
expect(
|
|
sceneEntityStateObj("light.kitchen", {
|
|
state: "off",
|
|
brightness: 180,
|
|
rgb_color: [255, 0, 0],
|
|
friendly_name: "Kitchen",
|
|
})
|
|
).toEqual({
|
|
entity_id: "light.kitchen",
|
|
state: "off",
|
|
attributes: { friendly_name: "Kitchen" },
|
|
});
|
|
});
|
|
});
|