Merge branch 'rc'

This commit is contained in:
Bram Kragten 2024-02-08 18:09:07 +01:00
commit b3766cbc62
42 changed files with 1326 additions and 92 deletions

View File

@ -28,7 +28,6 @@ class HcLaunchScreen extends LitElement {
:host { :host {
display: block; display: block;
height: 100vh; height: 100vh;
padding-top: 64px;
background-color: white; background-color: white;
font-size: 24px; font-size: 24px;
} }
@ -36,12 +35,13 @@ class HcLaunchScreen extends LitElement {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
text-align: center; text-align: center;
align-items: center;
height: 100%;
justify-content: space-evenly;
} }
img { img {
width: 717px; max-width: 80%;
height: 376px; object-fit: cover;
display: block;
margin: 0 auto;
} }
.status { .status {
padding-right: 54px; padding-right: 54px;

View File

@ -17,7 +17,7 @@ class HcLovelace extends LitElement {
@property({ attribute: false }) @property({ attribute: false })
public lovelaceConfig!: LovelaceConfig; public lovelaceConfig!: LovelaceConfig;
@property() public viewPath?: string | number; @property() public viewPath?: string | number | null;
@property() public urlPath: string | null = null; @property() public urlPath: string | null = null;
@ -93,6 +93,9 @@ class HcLovelace extends LitElement {
} }
private get _viewIndex() { private get _viewIndex() {
if (this.viewPath === null) {
return 0;
}
const selectedView = this.viewPath; const selectedView = this.viewPath;
const selectedViewInt = parseInt(selectedView as string, 10); const selectedViewInt = parseInt(selectedView as string, 10);
for (let i = 0; i < this.lovelaceConfig.views.length; i++) { for (let i = 0; i < this.lovelaceConfig.views.length; i++) {

View File

@ -51,10 +51,10 @@ export class HcMain extends HassElement {
@state() private _lovelacePath: string | number | null = null; @state() private _lovelacePath: string | number | null = null;
@state() private _error?: string;
@state() private _urlPath?: string | null; @state() private _urlPath?: string | null;
@state() private _error?: string;
private _hassUUID?: string; private _hassUUID?: string;
private _unsubLovelace?: UnsubscribeFunc; private _unsubLovelace?: UnsubscribeFunc;
@ -81,7 +81,7 @@ export class HcMain extends HassElement {
if ( if (
!this._lovelaceConfig || !this._lovelaceConfig ||
this._lovelacePath === null || this._urlPath === undefined ||
// Guard against part of HA not being loaded yet. // Guard against part of HA not being loaded yet.
!this.hass || !this.hass ||
!this.hass.states || !this.hass.states ||
@ -99,8 +99,8 @@ export class HcMain extends HassElement {
<hc-lovelace <hc-lovelace
.hass=${this.hass} .hass=${this.hass}
.lovelaceConfig=${this._lovelaceConfig} .lovelaceConfig=${this._lovelaceConfig}
.viewPath=${this._lovelacePath}
.urlPath=${this._urlPath} .urlPath=${this._urlPath}
.viewPath=${this._lovelacePath}
@config-refresh=${this._generateDefaultLovelaceConfig} @config-refresh=${this._generateDefaultLovelaceConfig}
></hc-lovelace> ></hc-lovelace>
`; `;
@ -226,9 +226,9 @@ export class HcMain extends HassElement {
this.initializeHass(auth, connection); this.initializeHass(auth, connection);
if (this._hassUUID !== msg.hassUUID) { if (this._hassUUID !== msg.hassUUID) {
this._hassUUID = msg.hassUUID; this._hassUUID = msg.hassUUID;
this._lovelacePath = null;
this._urlPath = undefined;
this._lovelaceConfig = undefined; this._lovelaceConfig = undefined;
this._urlPath = undefined;
this._lovelacePath = null;
if (this._unsubLovelace) { if (this._unsubLovelace) {
this._unsubLovelace(); this._unsubLovelace();
this._unsubLovelace = undefined; this._unsubLovelace = undefined;
@ -285,7 +285,7 @@ export class HcMain extends HassElement {
], ],
}; };
this._urlPath = "energy"; this._urlPath = "energy";
this._lovelacePath = 0; this._lovelacePath = null;
this._sendStatus(); this._sendStatus();
return; return;
} }

View File

@ -17,12 +17,14 @@ import { energyEntities } from "./stubs/entities";
import { mockEntityRegistry } from "./stubs/entity_registry"; import { mockEntityRegistry } from "./stubs/entity_registry";
import { mockEvents } from "./stubs/events"; import { mockEvents } from "./stubs/events";
import { mockFrontend } from "./stubs/frontend"; import { mockFrontend } from "./stubs/frontend";
import { mockIcons } from "./stubs/icons";
import { mockHistory } from "./stubs/history"; import { mockHistory } from "./stubs/history";
import { mockLovelace } from "./stubs/lovelace"; import { mockLovelace } from "./stubs/lovelace";
import { mockMediaPlayer } from "./stubs/media_player"; import { mockMediaPlayer } from "./stubs/media_player";
import { mockPersistentNotification } from "./stubs/persistent_notification"; import { mockPersistentNotification } from "./stubs/persistent_notification";
import { mockRecorder } from "./stubs/recorder"; import { mockRecorder } from "./stubs/recorder";
import { mockTodo } from "./stubs/todo"; import { mockTodo } from "./stubs/todo";
import { mockSensor } from "./stubs/sensor";
import { mockSystemLog } from "./stubs/system_log"; import { mockSystemLog } from "./stubs/system_log";
import { mockTemplate } from "./stubs/template"; import { mockTemplate } from "./stubs/template";
import { mockTranslations } from "./stubs/translations"; import { mockTranslations } from "./stubs/translations";
@ -50,11 +52,13 @@ export class HaDemo extends HomeAssistantAppEl {
mockHistory(hass); mockHistory(hass);
mockRecorder(hass); mockRecorder(hass);
mockTodo(hass); mockTodo(hass);
mockSensor(hass);
mockSystemLog(hass); mockSystemLog(hass);
mockTemplate(hass); mockTemplate(hass);
mockEvents(hass); mockEvents(hass);
mockMediaPlayer(hass); mockMediaPlayer(hass);
mockFrontend(hass); mockFrontend(hass);
mockIcons(hass);
mockEnergy(hass); mockEnergy(hass);
mockPersistentNotification(hass); mockPersistentNotification(hass);
mockConfigEntries(hass); mockConfigEntries(hass);

33
demo/src/stubs/icons.ts Normal file
View File

@ -0,0 +1,33 @@
import { IconCategory } from "../../../src/data/icons";
import { ENTITY_COMPONENT_ICONS } from "../../../src/fake_data/entity_component_icons";
import { MockHomeAssistant } from "../../../src/fake_data/provide_hass";
export const mockIcons = (hass: MockHomeAssistant) => {
hass.mockWS(
"frontend/get_icons",
async ({
category,
integration,
}: {
category: IconCategory;
integration?: string;
}) => {
if (integration) {
try {
const response = await fetch(
`https://raw.githubusercontent.com/home-assistant/core/dev/homeassistant/components/${integration}/icons.json`
).then((resp) => resp.json());
return { resources: { [integration]: response[category] || {} } };
} catch {
return { resources: {} };
}
}
if (category === "entity_component") {
return {
resources: ENTITY_COMPONENT_ICONS,
};
}
return { resources: {} };
}
);
};

58
demo/src/stubs/sensor.ts Normal file
View File

@ -0,0 +1,58 @@
import { MockHomeAssistant } from "../../../src/fake_data/provide_hass";
export const mockSensor = (hass: MockHomeAssistant) => {
hass.mockWS("sensor/numeric_device_classes", () => [
{
numeric_device_classes: [
"volume_storage",
"gas",
"data_size",
"irradiance",
"wind_speed",
"volatile_organic_compounds",
"volatile_organic_compounds_parts",
"voltage",
"frequency",
"precipitation_intensity",
"volume",
"precipitation",
"battery",
"nitrogen_dioxide",
"speed",
"signal_strength",
"pm1",
"nitrous_oxide",
"atmospheric_pressure",
"data_rate",
"temperature",
"power_factor",
"aqi",
"current",
"volume_flow_rate",
"humidity",
"duration",
"ozone",
"distance",
"pressure",
"pm25",
"weight",
"energy",
"carbon_monoxide",
"apparent_power",
"illuminance",
"energy_storage",
"moisture",
"power",
"water",
"carbon_dioxide",
"ph",
"reactive_power",
"monetary",
"nitrogen_monoxide",
"pm10",
"sound_pressure",
"sulphur_dioxide",
],
},
]);
};

View File

@ -21,4 +21,5 @@ export const mockTodo = (hass: MockHomeAssistant) => {
}, },
] as TodoItem[], ] as TodoItem[],
})); }));
hass.mockWS("todo/item/subscribe", (_msg, _hass) => () => {});
}; };

View File

@ -3,7 +3,6 @@
import { css, html, LitElement, nothing } from "lit"; import { css, html, LitElement, nothing } from "lit";
import { customElement, property } from "lit/decorators"; import { customElement, property } from "lit/decorators";
import "../../../../src/components/ha-card"; import "../../../../src/components/ha-card";
import "../../../../src/components/trace/hat-script-graph";
import "../../../../src/components/trace/hat-trace-timeline"; import "../../../../src/components/trace/hat-trace-timeline";
import { provideHass } from "../../../../src/fake_data/provide_hass"; import { provideHass } from "../../../../src/fake_data/provide_hass";
import { HomeAssistant } from "../../../../src/types"; import { HomeAssistant } from "../../../../src/types";

View File

@ -3,6 +3,7 @@ import { customElement, query } from "lit/decorators";
import { getEntity } from "../../../../src/fake_data/entity"; import { getEntity } from "../../../../src/fake_data/entity";
import { provideHass } from "../../../../src/fake_data/provide_hass"; import { provideHass } from "../../../../src/fake_data/provide_hass";
import "../../components/demo-cards"; import "../../components/demo-cards";
import { mockIcons } from "../../../../demo/src/stubs/icons";
const ENTITIES = [ const ENTITIES = [
getEntity("alarm_control_panel", "alarm", "disarmed", { getEntity("alarm_control_panel", "alarm", "disarmed", {
@ -84,6 +85,7 @@ class DemoAlarmPanelEntity extends LitElement {
hass.updateTranslations(null, "en"); hass.updateTranslations(null, "en");
hass.updateTranslations("lovelace", "en"); hass.updateTranslations("lovelace", "en");
hass.addEntities(ENTITIES); hass.addEntities(ENTITIES);
mockIcons(hass);
} }
} }

View File

@ -3,6 +3,7 @@ import { customElement, query } from "lit/decorators";
import { getEntity } from "../../../../src/fake_data/entity"; import { getEntity } from "../../../../src/fake_data/entity";
import { provideHass } from "../../../../src/fake_data/provide_hass"; import { provideHass } from "../../../../src/fake_data/provide_hass";
import "../../components/demo-cards"; import "../../components/demo-cards";
import { mockIcons } from "../../../../demo/src/stubs/icons";
const ENTITIES = [ const ENTITIES = [
getEntity("light", "bed_light", "on", { getEntity("light", "bed_light", "on", {
@ -146,6 +147,7 @@ class DemoArea extends LitElement {
entity_id: "binary_sensor.kitchen_door", entity_id: "binary_sensor.kitchen_door",
}, },
]); ]);
mockIcons(hass);
} }
} }

View File

@ -3,6 +3,7 @@ import { customElement, query } from "lit/decorators";
import { getEntity } from "../../../../src/fake_data/entity"; import { getEntity } from "../../../../src/fake_data/entity";
import { provideHass } from "../../../../src/fake_data/provide_hass"; import { provideHass } from "../../../../src/fake_data/provide_hass";
import "../../components/demo-cards"; import "../../components/demo-cards";
import { mockIcons } from "../../../../demo/src/stubs/icons";
const ENTITIES = [ const ENTITIES = [
getEntity("light", "controller_1", "on", { getEntity("light", "controller_1", "on", {
@ -66,6 +67,7 @@ class DemoConditional extends LitElement {
hass.updateTranslations(null, "en"); hass.updateTranslations(null, "en");
hass.updateTranslations("lovelace", "en"); hass.updateTranslations("lovelace", "en");
hass.addEntities(ENTITIES); hass.addEntities(ENTITIES);
mockIcons(hass);
} }
} }

View File

@ -3,6 +3,7 @@ import { customElement, query } from "lit/decorators";
import { getEntity } from "../../../../src/fake_data/entity"; import { getEntity } from "../../../../src/fake_data/entity";
import { provideHass } from "../../../../src/fake_data/provide_hass"; import { provideHass } from "../../../../src/fake_data/provide_hass";
import "../../components/demo-cards"; import "../../components/demo-cards";
import { mockIcons } from "../../../../demo/src/stubs/icons";
const ENTITIES = [ const ENTITIES = [
getEntity("light", "bed_light", "on", { getEntity("light", "bed_light", "on", {
@ -323,6 +324,7 @@ class DemoEntities extends LitElement {
hass.updateTranslations(null, "en"); hass.updateTranslations(null, "en");
hass.updateTranslations("lovelace", "en"); hass.updateTranslations("lovelace", "en");
hass.addEntities(ENTITIES); hass.addEntities(ENTITIES);
mockIcons(hass);
} }
} }

View File

@ -3,6 +3,7 @@ import { customElement, query } from "lit/decorators";
import { getEntity } from "../../../../src/fake_data/entity"; import { getEntity } from "../../../../src/fake_data/entity";
import { provideHass } from "../../../../src/fake_data/provide_hass"; import { provideHass } from "../../../../src/fake_data/provide_hass";
import "../../components/demo-cards"; import "../../components/demo-cards";
import { mockIcons } from "../../../../demo/src/stubs/icons";
const ENTITIES = [ const ENTITIES = [
getEntity("light", "bed_light", "on", { getEntity("light", "bed_light", "on", {
@ -82,6 +83,7 @@ class DemoButtonEntity extends LitElement {
hass.updateTranslations(null, "en"); hass.updateTranslations(null, "en");
hass.updateTranslations("lovelace", "en"); hass.updateTranslations("lovelace", "en");
hass.addEntities(ENTITIES); hass.addEntities(ENTITIES);
mockIcons(hass);
} }
} }

View File

@ -3,6 +3,7 @@ import { customElement, query } from "lit/decorators";
import { getEntity } from "../../../../src/fake_data/entity"; import { getEntity } from "../../../../src/fake_data/entity";
import { provideHass } from "../../../../src/fake_data/provide_hass"; import { provideHass } from "../../../../src/fake_data/provide_hass";
import "../../components/demo-cards"; import "../../components/demo-cards";
import { mockIcons } from "../../../../demo/src/stubs/icons";
const ENTITIES = [ const ENTITIES = [
getEntity("device_tracker", "demo_paulus", "work", { getEntity("device_tracker", "demo_paulus", "work", {
@ -123,6 +124,7 @@ class DemoEntityFilter extends LitElement {
hass.updateTranslations(null, "en"); hass.updateTranslations(null, "en");
hass.updateTranslations("lovelace", "en"); hass.updateTranslations("lovelace", "en");
hass.addEntities(ENTITIES); hass.addEntities(ENTITIES);
mockIcons(hass);
} }
} }

View File

@ -3,6 +3,7 @@ import { customElement, query } from "lit/decorators";
import { getEntity } from "../../../../src/fake_data/entity"; import { getEntity } from "../../../../src/fake_data/entity";
import { provideHass } from "../../../../src/fake_data/provide_hass"; import { provideHass } from "../../../../src/fake_data/provide_hass";
import "../../components/demo-cards"; import "../../components/demo-cards";
import { mockIcons } from "../../../../demo/src/stubs/icons";
const ENTITIES = [ const ENTITIES = [
getEntity("sensor", "brightness", "12", {}), getEntity("sensor", "brightness", "12", {}),
@ -128,6 +129,7 @@ class DemoGaugeEntity extends LitElement {
hass.updateTranslations(null, "en"); hass.updateTranslations(null, "en");
hass.updateTranslations("lovelace", "en"); hass.updateTranslations("lovelace", "en");
hass.addEntities(ENTITIES); hass.addEntities(ENTITIES);
mockIcons(hass);
} }
} }

View File

@ -3,6 +3,7 @@ import { customElement, query } from "lit/decorators";
import { getEntity } from "../../../../src/fake_data/entity"; import { getEntity } from "../../../../src/fake_data/entity";
import { provideHass } from "../../../../src/fake_data/provide_hass"; import { provideHass } from "../../../../src/fake_data/provide_hass";
import "../../components/demo-cards"; import "../../components/demo-cards";
import { mockIcons } from "../../../../demo/src/stubs/icons";
const ENTITIES = [ const ENTITIES = [
getEntity("device_tracker", "demo_paulus", "home", { getEntity("device_tracker", "demo_paulus", "home", {
@ -238,6 +239,7 @@ class DemoGlanceEntity extends LitElement {
hass.updateTranslations(null, "en"); hass.updateTranslations(null, "en");
hass.updateTranslations("lovelace", "en"); hass.updateTranslations("lovelace", "en");
hass.addEntities(ENTITIES); hass.addEntities(ENTITIES);
mockIcons(hass);
} }
} }

View File

@ -4,6 +4,7 @@ import { mockHistory } from "../../../../demo/src/stubs/history";
import { getEntity } from "../../../../src/fake_data/entity"; import { getEntity } from "../../../../src/fake_data/entity";
import { provideHass } from "../../../../src/fake_data/provide_hass"; import { provideHass } from "../../../../src/fake_data/provide_hass";
import "../../components/demo-cards"; import "../../components/demo-cards";
import { mockIcons } from "../../../../demo/src/stubs/icons";
const ENTITIES = [ const ENTITIES = [
getEntity("light", "kitchen_lights", "on", { getEntity("light", "kitchen_lights", "on", {
@ -214,6 +215,7 @@ class DemoStack extends LitElement {
hass.updateTranslations("lovelace", "en"); hass.updateTranslations("lovelace", "en");
hass.addEntities(ENTITIES); hass.addEntities(ENTITIES);
mockHistory(hass); mockHistory(hass);
mockIcons(hass);
} }
} }

View File

@ -3,6 +3,7 @@ import { customElement, query } from "lit/decorators";
import { getEntity } from "../../../../src/fake_data/entity"; import { getEntity } from "../../../../src/fake_data/entity";
import { provideHass } from "../../../../src/fake_data/provide_hass"; import { provideHass } from "../../../../src/fake_data/provide_hass";
import "../../components/demo-cards"; import "../../components/demo-cards";
import { mockIcons } from "../../../../demo/src/stubs/icons";
const ENTITIES = [ const ENTITIES = [
getEntity("light", "bed_light", "on", { getEntity("light", "bed_light", "on", {
@ -76,6 +77,7 @@ class DemoLightEntity extends LitElement {
hass.updateTranslations(null, "en"); hass.updateTranslations(null, "en");
hass.updateTranslations("lovelace", "en"); hass.updateTranslations("lovelace", "en");
hass.addEntities(ENTITIES); hass.addEntities(ENTITIES);
mockIcons(hass);
} }
} }

View File

@ -3,6 +3,7 @@ import { customElement, query } from "lit/decorators";
import { getEntity } from "../../../../src/fake_data/entity"; import { getEntity } from "../../../../src/fake_data/entity";
import { provideHass } from "../../../../src/fake_data/provide_hass"; import { provideHass } from "../../../../src/fake_data/provide_hass";
import "../../components/demo-cards"; import "../../components/demo-cards";
import { mockIcons } from "../../../../demo/src/stubs/icons";
const ENTITIES = [ const ENTITIES = [
getEntity("light", "bed_light", "on", { getEntity("light", "bed_light", "on", {
@ -138,6 +139,7 @@ class DemoPictureElements extends LitElement {
hass.updateTranslations(null, "en"); hass.updateTranslations(null, "en");
hass.updateTranslations("lovelace", "en"); hass.updateTranslations("lovelace", "en");
hass.addEntities(ENTITIES); hass.addEntities(ENTITIES);
mockIcons(hass);
} }
} }

View File

@ -3,6 +3,7 @@ import { customElement, query } from "lit/decorators";
import { getEntity } from "../../../../src/fake_data/entity"; import { getEntity } from "../../../../src/fake_data/entity";
import { provideHass } from "../../../../src/fake_data/provide_hass"; import { provideHass } from "../../../../src/fake_data/provide_hass";
import "../../components/demo-cards"; import "../../components/demo-cards";
import { mockIcons } from "../../../../demo/src/stubs/icons";
const ENTITIES = [ const ENTITIES = [
getEntity("light", "kitchen_lights", "on", { getEntity("light", "kitchen_lights", "on", {
@ -93,6 +94,7 @@ class DemoPictureEntity extends LitElement {
hass.updateTranslations(null, "en"); hass.updateTranslations(null, "en");
hass.updateTranslations("lovelace", "en"); hass.updateTranslations("lovelace", "en");
hass.addEntities(ENTITIES); hass.addEntities(ENTITIES);
mockIcons(hass);
} }
} }

View File

@ -3,6 +3,7 @@ import { customElement, query } from "lit/decorators";
import { getEntity } from "../../../../src/fake_data/entity"; import { getEntity } from "../../../../src/fake_data/entity";
import { provideHass } from "../../../../src/fake_data/provide_hass"; import { provideHass } from "../../../../src/fake_data/provide_hass";
import "../../components/demo-cards"; import "../../components/demo-cards";
import { mockIcons } from "../../../../demo/src/stubs/icons";
const ENTITIES = [ const ENTITIES = [
getEntity("switch", "decorative_lights", "on", { getEntity("switch", "decorative_lights", "on", {
@ -134,6 +135,7 @@ class DemoPictureGlance extends LitElement {
hass.updateTranslations(null, "en"); hass.updateTranslations(null, "en");
hass.updateTranslations("lovelace", "en"); hass.updateTranslations("lovelace", "en");
hass.addEntities(ENTITIES); hass.addEntities(ENTITIES);
mockIcons(hass);
} }
} }

View File

@ -3,6 +3,7 @@ import { customElement, query } from "lit/decorators";
import { provideHass } from "../../../../src/fake_data/provide_hass"; import { provideHass } from "../../../../src/fake_data/provide_hass";
import "../../components/demo-cards"; import "../../components/demo-cards";
import { createPlantEntities } from "../../data/plants"; import { createPlantEntities } from "../../data/plants";
import { mockIcons } from "../../../../demo/src/stubs/icons";
const CONFIGS = [ const CONFIGS = [
{ {
@ -43,6 +44,7 @@ export class DemoPlantEntity extends LitElement {
hass.updateTranslations(null, "en"); hass.updateTranslations(null, "en");
hass.updateTranslations("lovelace", "en"); hass.updateTranslations("lovelace", "en");
hass.addEntities(createPlantEntities()); hass.addEntities(createPlantEntities());
mockIcons(hass);
} }
} }

View File

@ -3,6 +3,7 @@ import { customElement, query } from "lit/decorators";
import { getEntity } from "../../../../src/fake_data/entity"; import { getEntity } from "../../../../src/fake_data/entity";
import { provideHass } from "../../../../src/fake_data/provide_hass"; import { provideHass } from "../../../../src/fake_data/provide_hass";
import "../../components/demo-cards"; import "../../components/demo-cards";
import { mockIcons } from "../../../../demo/src/stubs/icons";
const ENTITIES = [ const ENTITIES = [
getEntity("climate", "ecobee", "auto", { getEntity("climate", "ecobee", "auto", {
@ -116,6 +117,7 @@ class DemoThermostatEntity extends LitElement {
hass.updateTranslations(null, "en"); hass.updateTranslations(null, "en");
hass.updateTranslations("lovelace", "en"); hass.updateTranslations("lovelace", "en");
hass.addEntities(ENTITIES); hass.addEntities(ENTITIES);
mockIcons(hass);
} }
} }

View File

@ -6,6 +6,7 @@ import { VacuumEntityFeature } from "../../../../src/data/vacuum";
import { getEntity } from "../../../../src/fake_data/entity"; import { getEntity } from "../../../../src/fake_data/entity";
import { provideHass } from "../../../../src/fake_data/provide_hass"; import { provideHass } from "../../../../src/fake_data/provide_hass";
import "../../components/demo-cards"; import "../../components/demo-cards";
import { mockIcons } from "../../../../demo/src/stubs/icons";
const ENTITIES = [ const ENTITIES = [
getEntity("switch", "tv_outlet", "on", { getEntity("switch", "tv_outlet", "on", {
@ -184,6 +185,7 @@ class DemoTile extends LitElement {
hass.updateTranslations(null, "en"); hass.updateTranslations(null, "en");
hass.updateTranslations("lovelace", "en"); hass.updateTranslations("lovelace", "en");
hass.addEntities(ENTITIES); hass.addEntities(ENTITIES);
mockIcons(hass);
} }
} }

View File

@ -4,6 +4,7 @@ import { provideHass } from "../../../../src/fake_data/provide_hass";
import "../../components/demo-cards"; import "../../components/demo-cards";
import { getEntity } from "../../../../src/fake_data/entity"; import { getEntity } from "../../../../src/fake_data/entity";
import { mockTodo } from "../../../../demo/src/stubs/todo"; import { mockTodo } from "../../../../demo/src/stubs/todo";
import { mockIcons } from "../../../../demo/src/stubs/icons";
const ENTITIES = [ const ENTITIES = [
getEntity("todo", "shopping_list", "2", { getEntity("todo", "shopping_list", "2", {
@ -47,6 +48,7 @@ class DemoTodoListEntity extends LitElement {
hass.updateTranslations(null, "en"); hass.updateTranslations(null, "en");
hass.updateTranslations("lovelace", "en"); hass.updateTranslations("lovelace", "en");
hass.addEntities(ENTITIES); hass.addEntities(ENTITIES);
mockIcons(hass);
mockTodo(hass); mockTodo(hass);
} }

View File

@ -11,6 +11,7 @@ import "../../../../src/components/data-table/ha-data-table";
import type { DataTableColumnContainer } from "../../../../src/components/data-table/ha-data-table"; import type { DataTableColumnContainer } from "../../../../src/components/data-table/ha-data-table";
import "../../../../src/components/entity/state-badge"; import "../../../../src/components/entity/state-badge";
import { provideHass } from "../../../../src/fake_data/provide_hass"; import { provideHass } from "../../../../src/fake_data/provide_hass";
import { mockIcons } from "../../../../demo/src/stubs/icons";
import { HomeAssistant } from "../../../../src/types"; import { HomeAssistant } from "../../../../src/types";
const SENSOR_DEVICE_CLASSES = [ const SENSOR_DEVICE_CLASSES = [
@ -291,6 +292,7 @@ const ENTITIES: HassEntity[] = [
createEntity("water_heater.high_demand", "high_demand"), createEntity("water_heater.high_demand", "high_demand"),
createEntity("water_heater.heat_pump", "heat_pump"), createEntity("water_heater.heat_pump", "heat_pump"),
createEntity("water_heater.gas", "gas"), createEntity("water_heater.gas", "gas"),
createEntity("select.speed", "ridiculous_speed"),
]; ];
function createEntity( function createEntity(
@ -397,6 +399,16 @@ export class DemoEntityState extends LitElement {
protected firstUpdated(changedProps) { protected firstUpdated(changedProps) {
super.firstUpdated(changedProps); super.firstUpdated(changedProps);
const hass = provideHass(this); const hass = provideHass(this);
mockIcons(hass);
hass.updateHass({
entities: {
"select.speed": {
entity_id: "select.speed",
translation_key: "speed",
platform: "demo",
},
},
});
hass.updateTranslations(null, "en"); hass.updateTranslations(null, "en");
hass.updateTranslations("config", "en"); hass.updateTranslations("config", "en");
} }

View File

@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project] [project]
name = "home-assistant-frontend" name = "home-assistant-frontend"
version = "20240207.0" version = "20240207.1"
license = {text = "Apache-2.0"} license = {text = "Apache-2.0"}
description = "The Home Assistant frontend" description = "The Home Assistant frontend"
readme = "README.md" readme = "README.md"

View File

@ -2,6 +2,7 @@ import { LitElement, PropertyValues, css, html, nothing } from "lit";
import { customElement, property, query, state } from "lit/decorators"; import { customElement, property, query, state } from "lit/decorators";
import QRCode from "qrcode"; import QRCode from "qrcode";
import "./ha-alert"; import "./ha-alert";
import { rgb2hex } from "../common/color/convert-color";
@customElement("ha-qr-code") @customElement("ha-qr-code")
export class HaQrCode extends LitElement { export class HaQrCode extends LitElement {
@ -65,6 +66,26 @@ export class HaQrCode extends LitElement {
changedProperties.has("centerImage")) changedProperties.has("centerImage"))
) { ) {
const computedStyles = getComputedStyle(this); const computedStyles = getComputedStyle(this);
const textRgb = computedStyles.getPropertyValue(
"--rgb-primary-text-color"
);
const backgroundRgb = computedStyles.getPropertyValue(
"--rgb-card-background-color"
);
const textHex = rgb2hex(
textRgb.split(",").map((a) => parseInt(a, 10)) as [
number,
number,
number,
]
);
const backgroundHex = rgb2hex(
backgroundRgb.split(",").map((a) => parseInt(a, 10)) as [
number,
number,
number,
]
);
QRCode.toCanvas(canvas, this.data, { QRCode.toCanvas(canvas, this.data, {
errorCorrectionLevel: errorCorrectionLevel:
@ -74,8 +95,8 @@ export class HaQrCode extends LitElement {
margin: this.margin, margin: this.margin,
maskPattern: this.maskPattern, maskPattern: this.maskPattern,
color: { color: {
light: computedStyles.getPropertyValue("--card-background-color"), light: backgroundHex,
dark: computedStyles.getPropertyValue("--primary-text-color"), dark: textHex,
}, },
}).catch((err) => { }).catch((err) => {
this._error = err.message; this._error = err.message;

View File

@ -5,6 +5,7 @@ import {
html, html,
TemplateResult, TemplateResult,
svg, svg,
nothing,
} from "lit"; } from "lit";
import { customElement, property } from "lit/decorators"; import { customElement, property } from "lit/decorators";
import { NODE_SIZE, SPACING } from "./hat-graph-const"; import { NODE_SIZE, SPACING } from "./hat-graph-const";
@ -51,7 +52,7 @@ export class HatGraphNode extends LitElement {
: Math.ceil((NODE_SIZE + SPACING * 2) / 2)} ${width} ${height}" : Math.ceil((NODE_SIZE + SPACING * 2) / 2)} ${width} ${height}"
> >
${this.graphStart ${this.graphStart
? `` ? nothing
: svg` : svg`
<path <path
class="connector" class="connector"
@ -64,7 +65,6 @@ export class HatGraphNode extends LitElement {
`} `}
<g class="node"> <g class="node">
<circle cx="0" cy="0" r=${NODE_SIZE / 2} /> <circle cx="0" cy="0" r=${NODE_SIZE / 2} />
}
${this.badge ${this.badge
? svg` ? svg`
<g class="number"> <g class="number">
@ -81,9 +81,11 @@ export class HatGraphNode extends LitElement {
>${this.badge > 9 ? "9+" : this.badge}</text> >${this.badge > 9 ? "9+" : this.badge}</text>
</g> </g>
` `
: ""} : nothing}
<g style="pointer-events: none" transform="translate(${-12} ${-12})"> <g style="pointer-events: none" transform="translate(${-12} ${-12})">
${this.iconPath ? svg`<path class="icon" d=${this.iconPath}/>` : ""} ${this.iconPath
? svg`<path class="icon" d=${this.iconPath}/>`
: svg`<foreignObject><span class="icon"><slot name="icon"></slot></span></foreignObject>`}
</g> </g>
</g> </g>
</svg> </svg>
@ -152,6 +154,13 @@ export class HatGraphNode extends LitElement {
path.icon { path.icon {
fill: var(--icon-clr); fill: var(--icon-clr);
} }
foreignObject {
width: 24px;
height: 24px;
}
.icon {
color: var(--icon-clr);
}
`; `;
} }
} }

View File

@ -17,11 +17,10 @@ import {
mdiRoomService, mdiRoomService,
mdiShuffleDisabled, mdiShuffleDisabled,
} from "@mdi/js"; } from "@mdi/js";
import { LitElement, PropertyValues, css, html } from "lit"; import { LitElement, PropertyValues, css, html, nothing } from "lit";
import { customElement, property } from "lit/decorators"; import { customElement, property } from "lit/decorators";
import { ensureArray } from "../../common/array/ensure-array"; import { ensureArray } from "../../common/array/ensure-array";
import { fireEvent } from "../../common/dom/fire_event"; import { fireEvent } from "../../common/dom/fire_event";
import { ACTION_ICONS } from "../../data/action";
import { Condition, Trigger } from "../../data/automation"; import { Condition, Trigger } from "../../data/automation";
import { import {
Action, Action,
@ -41,11 +40,14 @@ import {
IfActionTraceStep, IfActionTraceStep,
TraceExtended, TraceExtended,
} from "../../data/trace"; } from "../../data/trace";
import { HomeAssistant } from "../../types";
import "../ha-icon-button"; import "../ha-icon-button";
import "../ha-service-icon";
import "./hat-graph-branch"; import "./hat-graph-branch";
import { BRANCH_HEIGHT, NODE_SIZE, SPACING } from "./hat-graph-const"; import { BRANCH_HEIGHT, NODE_SIZE, SPACING } from "./hat-graph-const";
import "./hat-graph-node"; import "./hat-graph-node";
import "./hat-graph-spacer"; import "./hat-graph-spacer";
import { ACTION_ICONS } from "../../data/action";
export interface NodeInfo { export interface NodeInfo {
path: string; path: string;
@ -64,6 +66,8 @@ export class HatScriptGraph extends LitElement {
@property({ attribute: false }) public selected?: string; @property({ attribute: false }) public selected?: string;
public hass!: HomeAssistant;
public renderedNodes: Record<string, NodeInfo> = {}; public renderedNodes: Record<string, NodeInfo> = {};
public trackedNodes: Record<string, NodeInfo> = {}; public trackedNodes: Record<string, NodeInfo> = {};
@ -415,13 +419,21 @@ export class HatScriptGraph extends LitElement {
return html` return html`
<hat-graph-node <hat-graph-node
.graphStart=${graphStart} .graphStart=${graphStart}
.iconPath=${mdiRoomService} .iconPath=${node.service ? undefined : mdiRoomService}
@focus=${this.selectNode(node, path)} @focus=${this.selectNode(node, path)}
?track=${path in this.trace.trace} ?track=${path in this.trace.trace}
?active=${this.selected === path} ?active=${this.selected === path}
.notEnabled=${disabled || node.enabled === false} .notEnabled=${disabled || node.enabled === false}
tabindex=${this.trace && path in this.trace.trace ? "0" : "-1"} tabindex=${this.trace && path in this.trace.trace ? "0" : "-1"}
></hat-graph-node> >
${node.service
? html`<ha-service-icon
slot="icon"
.hass=${this.hass}
.service=${node.service}
></ha-service-icon>`
: nothing}
</hat-graph-node>
`; `;
} }
@ -667,8 +679,6 @@ export class HatScriptGraph extends LitElement {
} }
.parent { .parent {
margin-left: 8px; margin-left: 8px;
margin-inline-start: 8px;
margin-inline-end: initial;
margin-top: 16px; margin-top: 16px;
} }
.error { .error {

View File

@ -9,6 +9,7 @@ import {
EntityRegistryEntry, EntityRegistryEntry,
} from "./entity_registry"; } from "./entity_registry";
import { isComponentLoaded } from "../common/config/is_component_loaded"; import { isComponentLoaded } from "../common/config/is_component_loaded";
import { atLeastVersion } from "../common/config/version";
const resources: { const resources: {
entity: Record<string, Promise<PlatformIcons>>; entity: Record<string, Promise<PlatformIcons>>;
@ -46,10 +47,10 @@ interface PlatformIcons {
}; };
} }
interface ComponentIcons { export interface ComponentIcons {
[device_class: string]: { [device_class: string]: {
state: Record<string, string>; state?: Record<string, string>;
state_attributes: Record< state_attributes?: Record<
string, string,
{ {
state: Record<string, string>; state: Record<string, string>;
@ -91,7 +92,10 @@ export const getPlatformIcons = async (
if (!force && integration in resources.entity) { if (!force && integration in resources.entity) {
return resources.entity[integration]; return resources.entity[integration];
} }
if (!isComponentLoaded(hass, integration)) { if (
!isComponentLoaded(hass, integration) ||
!atLeastVersion(hass.connection.haVersion, 2024, 2)
) {
return undefined; return undefined;
} }
const result = getHassIcons(hass, "entity", integration).then( const result = getHassIcons(hass, "entity", integration).then(
@ -106,6 +110,16 @@ export const getComponentIcons = async (
domain: string, domain: string,
force = false force = false
): Promise<ComponentIcons | undefined> => { ): Promise<ComponentIcons | undefined> => {
// For Cast, old instances can connect to it.
if (
__BACKWARDS_COMPAT__ &&
!atLeastVersion(hass.connection.haVersion, 2024, 2)
) {
return import("../fake_data/entity_component_icons")
.then((mod) => mod.ENTITY_COMPONENT_ICONS)
.then((res) => res[domain]);
}
if ( if (
!force && !force &&
resources.entity_component.resources && resources.entity_component.resources &&
@ -113,6 +127,7 @@ export const getComponentIcons = async (
) { ) {
return resources.entity_component.resources.then((res) => res[domain]); return resources.entity_component.resources.then((res) => res[domain]);
} }
if (!isComponentLoaded(hass, domain)) { if (!isComponentLoaded(hass, domain)) {
return undefined; return undefined;
} }

View File

@ -14,7 +14,53 @@ export const demoConfig: HassConfig = {
wind_speed: "m/s", wind_speed: "m/s",
accumulated_precipitation: "mm", accumulated_precipitation: "mm",
}, },
components: ["notify.html5", "history", "todo", "forecast_solar", "energy"], components: [
"notify.html5",
"history",
"forecast_solar",
"energy",
"person",
"number",
"select",
"tts",
"datetime",
"vacuum",
"wake_word",
"light",
"alarm_control_panel",
"text",
"lawn_mower",
"siren",
"input_boolean",
"lock",
"calendar",
"image",
"device_tracker",
"scene",
"script",
"todo",
"cover",
"switch",
"button",
"water_heater",
"binary_sensor",
"sensor",
"humidifier",
"valve",
"time",
"media_player",
"air_quality",
"camera",
"date",
"fan",
"automation",
"weather",
"climate",
"stt",
"update",
"event",
"demo",
],
time_zone: "America/Los_Angeles", time_zone: "America/Los_Angeles",
config_dir: "/config", config_dir: "/config",
version: "DEMO", version: "DEMO",

View File

@ -0,0 +1,962 @@
import { ComponentIcons } from "../data/icons";
export const ENTITY_COMPONENT_ICONS: Record<string, ComponentIcons> = {
person: {
_: {
default: "mdi:account",
state: {
not_home: "mdi:account-arrow-right",
},
},
},
number: {
_: {
default: "mdi:ray-vertex",
},
apparent_power: {
default: "mdi:flash",
},
aqi: {
default: "mdi:air-filter",
},
atmospheric_pressure: {
default: "mdi:thermometer-lines",
},
battery: {
default: "mdi:battery",
},
carbon_dioxide: {
default: "mdi:molecule-co2",
},
carbon_monoxide: {
default: "mdi:molecule-co",
},
current: {
default: "mdi:current-ac",
},
data_rate: {
default: "mdi:transmission-tower",
},
data_size: {
default: "mdi:database",
},
distance: {
default: "mdi:arrow-left-right",
},
duration: {
default: "mdi:progress-clock",
},
energy: {
default: "mdi:lightning-bolt",
},
energy_storage: {
default: "mdi:car-battery",
},
frequency: {
default: "mdi:sine-wave",
},
gas: {
default: "mdi:meter-gas",
},
humidity: {
default: "mdi:water-percent",
},
illuminance: {
default: "mdi:brightness-5",
},
irradiance: {
default: "mdi:sun-wireless",
},
moisture: {
default: "mdi:water-percent",
},
monetary: {
default: "mdi:cash",
},
nitrogen_dioxide: {
default: "mdi:molecule",
},
nitrogen_monoxide: {
default: "mdi:molecule",
},
nitrous_oxide: {
default: "mdi:molecule",
},
ozone: {
default: "mdi:molecule",
},
ph: {
default: "mdi:ph",
},
pm1: {
default: "mdi:molecule",
},
pm10: {
default: "mdi:molecule",
},
pm25: {
default: "mdi:molecule",
},
power: {
default: "mdi:flash",
},
power_factor: {
default: "mdi:angle-acute",
},
precipitation: {
default: "mdi:weather-rainy",
},
precipitation_intensity: {
default: "mdi:weather-pouring",
},
pressure: {
default: "mdi:gauge",
},
reactive_power: {
default: "mdi:flash",
},
signal_strength: {
default: "mdi:wifi",
},
sound_pressure: {
default: "mdi:ear-hearing",
},
speed: {
default: "mdi:speedometer",
},
sulfur_dioxide: {
default: "mdi:molecule",
},
temperature: {
default: "mdi:thermometer",
},
volatile_organic_compounds: {
default: "mdi:molecule",
},
volatile_organic_compounds_parts: {
default: "mdi:molecule",
},
voltage: {
default: "mdi:sine-wave",
},
volume: {
default: "mdi:car-coolant-level",
},
volume_storage: {
default: "mdi:storage-tank",
},
water: {
default: "mdi:water",
},
weight: {
default: "mdi:weight",
},
wind_speed: {
default: "mdi:weather-windy",
},
},
select: {
_: {
default: "mdi:format-list-bulleted",
},
},
tts: {
_: {
default: "mdi:speaker-message",
},
},
datetime: {
_: {
default: "mdi:calendar-clock",
},
},
vacuum: {
_: {
default: "mdi:robot-vacuum",
},
},
wake_word: {
_: {
default: "mdi:chat-sleep",
},
},
light: {
_: {
default: "mdi:lightbulb",
},
},
alarm_control_panel: {
_: {
default: "mdi:shield",
state: {
armed_away: "mdi:shield-lock",
armed_custom_bypass: "mdi:security",
armed_home: "mdi:shield-home",
armed_night: "mdi:shield-moon",
armed_vacation: "mdi:shield-airplane",
disarmed: "mdi:shield-off",
pending: "mdi:shield-outline",
triggered: "mdi:bell-ring",
},
},
},
text: {
_: {
default: "mdi:form-textbox",
},
},
lawn_mower: {
_: {
default: "mdi:robot-mower",
},
},
siren: {
_: {
default: "mdi:bullhorn",
},
},
input_boolean: {
_: {
default: "mdi:check-circle-outline",
state: {
off: "mdi:close-circle-outline",
},
},
},
lock: {
_: {
default: "mdi:lock",
state: {
jammed: "mdi:lock-alert",
locking: "mdi:lock-clock",
unlocked: "mdi:lock-open",
unlocking: "mdi:lock-clock",
},
},
},
calendar: {
_: {
default: "mdi:calendar",
state: {
on: "mdi:calendar-check",
off: "mdi:calendar-blank",
},
},
},
image: {
_: {
default: "mdi:image",
},
},
device_tracker: {
_: {
default: "mdi:account",
state: {
not_home: "mdi:account-arrow-right",
},
},
},
scene: {
_: {
default: "mdi:palette",
},
},
script: {
_: {
default: "mdi:script-text",
state: {
on: "mdi:script-text-play",
},
},
},
todo: {
_: {
default: "mdi:clipboard-list",
},
},
cover: {
_: {
default: "mdi:window-open",
state: {
closed: "mdi:window-closed",
closing: "mdi:arrow-down-box",
opening: "mdi:arrow-up-box",
},
},
blind: {
default: "mdi:blinds-horizontal",
state: {
closed: "mdi:blinds-horizontal-closed",
closing: "mdi:arrow-down-box",
opening: "mdi:arrow-up-box",
},
},
curtain: {
default: "mdi:curtains",
state: {
closed: "mdi:curtains-closed",
closing: "mdi:arrow-collapse-horizontal",
opening: "mdi:arrow-split-vertical",
},
},
damper: {
default: "mdi:circle",
state: {
closed: "mdi:circle-slice-8",
},
},
door: {
default: "mdi:door-open",
state: {
closed: "mdi:door-closed",
},
},
garage: {
default: "mdi:garage-open",
state: {
closed: "mdi:garage",
closing: "mdi:arrow-down-box",
opening: "mdi:arrow-up-box",
},
},
gate: {
default: "mdi:gate-open",
state: {
closed: "mdi:gate",
closing: "mdi:arrow-right",
opening: "mdi:arrow-right",
},
},
shade: {
default: "mdi:roller-shade",
state: {
closed: "mdi:roller-shade-closed",
closing: "mdi:arrow-down-box",
opening: "mdi:arrow-up-box",
},
},
shutter: {
default: "mdi:window-shutter-open",
state: {
closed: "mdi:window-shutter",
closing: "mdi:arrow-down-box",
opening: "mdi:arrow-up-box",
},
},
window: {
default: "mdi:window-open",
state: {
closed: "mdi:window-closed",
closing: "mdi:arrow-down-box",
opening: "mdi:arrow-up-box",
},
},
},
switch: {
_: {
default: "mdi:toggle-switch-variant",
},
switch: {
default: "mdi:toggle-switch-variant",
state: {
off: "mdi:toggle-switch-variant-off",
},
},
outlet: {
default: "mdi:power-plug",
state: {
off: "mdi:power-plug-off",
},
},
},
button: {
_: {
default: "mdi:button-pointer",
},
restart: {
default: "mdi:restart",
},
identify: {
default: "mdi:crosshairs-question",
},
update: {
default: "mdi:package-up",
},
},
water_heater: {
_: {
default: "mdi:water-boiler",
state: {
off: "mdi:water-boiler-off",
},
state_attributes: {
operation_mode: {
default: "mdi:circle-medium",
state: {
eco: "mdi:leaf",
electric: "mdi:lightning-bolt",
gas: "mdi:fire-circle",
heat_pump: "mdi:heat-wave",
high_demand: "mdi:finance",
off: "mdi:power",
performance: "mdi:rocket-launch",
},
},
},
},
},
binary_sensor: {
_: {
default: "mdi:radiobox-blank",
state: {
on: "mdi:checkbox-marked-circle",
},
},
battery: {
default: "mdi:battery",
state: {
on: "mdi:battery-outline",
},
},
battery_charging: {
default: "mdi:battery",
state: {
on: "mdi:battery-charging",
},
},
carbon_monoxide: {
default: "mdi:smoke-detector",
state: {
on: "mdi:smoke-detector-alert",
},
},
cold: {
default: "mdi:thermometer",
state: {
on: "mdi:snowflake",
},
},
connectivity: {
default: "mdi:close-network-outline",
state: {
on: "mdi:check-network-outline",
},
},
door: {
default: "mdi:door-closed",
state: {
on: "mdi:door-open",
},
},
garage_door: {
default: "mdi:garage",
state: {
on: "mdi:garage-open",
},
},
gas: {
default: "mdi:check-circle",
state: {
on: "mdi:alert-circle",
},
},
heat: {
default: "mdi:thermometer",
state: {
on: "mdi:fire",
},
},
light: {
default: "mdi:brightness-5",
state: {
on: "mdi:brightness-7",
},
},
lock: {
default: "mdi:lock",
state: {
on: "mdi:lock-open",
},
},
moisture: {
default: "mdi:water-off",
state: {
on: "mdi:water",
},
},
motion: {
default: "mdi:motion-sensor-off",
state: {
on: "mdi:motion-sensor",
},
},
moving: {
default: "mdi:arrow-right",
state: {
on: "mdi:octagon",
},
},
occupancy: {
default: "mdi:home-outline",
state: {
on: "mdi:home",
},
},
opening: {
default: "mdi:square",
state: {
on: "mdi:square-outline",
},
},
plug: {
default: "mdi:power-plug-off",
state: {
on: "mdi:power-plug",
},
},
power: {
default: "mdi:power-plug-off",
state: {
on: "mdi:power-plug",
},
},
presence: {
default: "mdi:home-outline",
state: {
on: "mdi:home",
},
},
problem: {
default: "mdi:check-circle",
state: {
on: "mdi:alert-circle",
},
},
running: {
default: "mdi:stop",
state: {
on: "mdi:play",
},
},
safety: {
default: "mdi:check-circle",
state: {
on: "mdi:alert-circle",
},
},
smoke: {
default: "mdi:smoke-detector-variant",
state: {
on: "mdi:smoke-detector-variant-alert",
},
},
sound: {
default: "mdi:music-note-off",
state: {
on: "mdi:music-note",
},
},
tamper: {
default: "mdi:check-circle",
state: {
on: "mdi:alert-circle",
},
},
update: {
default: "mdi:package",
state: {
on: "mdi:package-up",
},
},
vibration: {
default: "mdi:crop-portrait",
state: {
on: "mdi:vibrate",
},
},
window: {
default: "mdi:window-closed",
state: {
on: "mdi:window-open",
},
},
},
sensor: {
_: {
default: "mdi:eye",
},
apparent_power: {
default: "mdi:flash",
},
aqi: {
default: "mdi:air-filter",
},
atmospheric_pressure: {
default: "mdi:thermometer-lines",
},
carbon_dioxide: {
default: "mdi:molecule-co2",
},
carbon_monoxide: {
default: "mdi:molecule-co",
},
current: {
default: "mdi:current-ac",
},
data_rate: {
default: "mdi:transmission-tower",
},
data_size: {
default: "mdi:database",
},
date: {
default: "mdi:calendar",
},
distance: {
default: "mdi:arrow-left-right",
},
duration: {
default: "mdi:progress-clock",
},
energy: {
default: "mdi:lightning-bolt",
},
energy_storage: {
default: "mdi:car-battery",
},
enum: {
default: "mdi:eye",
},
frequency: {
default: "mdi:sine-wave",
},
gas: {
default: "mdi:meter-gas",
},
humidity: {
default: "mdi:water-percent",
},
illuminance: {
default: "mdi:brightness-5",
},
irradiance: {
default: "mdi:sun-wireless",
},
moisture: {
default: "mdi:water-percent",
},
monetary: {
default: "mdi:cash",
},
nitrogen_dioxide: {
default: "mdi:molecule",
},
nitrogen_monoxide: {
default: "mdi:molecule",
},
nitrous_oxide: {
default: "mdi:molecule",
},
ozone: {
default: "mdi:molecule",
},
ph: {
default: "mdi:ph",
},
pm1: {
default: "mdi:molecule",
},
pm10: {
default: "mdi:molecule",
},
pm25: {
default: "mdi:molecule",
},
power: {
default: "mdi:flash",
},
power_factor: {
default: "mdi:angle-acute",
},
precipitation: {
default: "mdi:weather-rainy",
},
precipitation_intensity: {
default: "mdi:weather-pouring",
},
pressure: {
default: "mdi:gauge",
},
reactive_power: {
default: "mdi:flash",
},
signal_strength: {
default: "mdi:wifi",
},
sound_pressure: {
default: "mdi:ear-hearing",
},
speed: {
default: "mdi:speedometer",
},
sulfur_dioxide: {
default: "mdi:molecule",
},
temperature: {
default: "mdi:thermometer",
},
timestamp: {
default: "mdi:clock",
},
volatile_organic_compounds: {
default: "mdi:molecule",
},
volatile_organic_compounds_parts: {
default: "mdi:molecule",
},
voltage: {
default: "mdi:sine-wave",
},
volume: {
default: "mdi:car-coolant-level",
},
volume_storage: {
default: "mdi:storage-tank",
},
water: {
default: "mdi:water",
},
weight: {
default: "mdi:weight",
},
wind_speed: {
default: "mdi:weather-windy",
},
},
humidifier: {
_: {
default: "mdi:air-humidifier",
state: {
off: "mdi:air-humidifier-off",
},
state_attributes: {
action: {
default: "mdi:circle-medium",
state: {
drying: "mdi:arrow-down-bold",
humidifying: "mdi:arrow-up-bold",
idle: "mdi:clock-outline",
off: "mdi:power",
},
},
mode: {
default: "mdi:circle-medium",
state: {
auto: "mdi:refresh-auto",
away: "mdi:account-arrow-right",
baby: "mdi:baby-carriage",
boost: "mdi:rocket-launch",
comfort: "mdi:sofa",
eco: "mdi:leaf",
home: "mdi:home",
normal: "mdi:water-percent",
sleep: "mdi:power-sleep",
},
},
},
},
},
valve: {
_: {
default: "mdi:pipe-valve",
},
gas: {
default: "mdi:meter-gas",
},
water: {
default: "mdi:pipe-valve",
},
},
time: {
_: {
default: "mdi:clock",
},
},
media_player: {
_: {
default: "mdi:cast",
state: {
off: "mdi:cast-off",
paused: "mdi:cast-connected",
playing: "mdi:cast-connected",
},
},
receiver: {
default: "mdi:audio-video",
state: {
off: "mdi:audio-video-off",
},
},
speaker: {
default: "mdi:speaker",
state: {
off: "mdi:speaker-off",
paused: "mdi:speaker-pause",
playing: "mdi:speaker-play",
},
},
tv: {
default: "mdi:television",
state: {
off: "mdi:television-off",
paused: "mdi:television-pause",
playing: "mdi:television-play",
},
},
},
air_quality: {
_: {
default: "mdi:air-filter",
},
},
camera: {
_: {
default: "mdi:video",
state: {
off: "mdi:video-off",
},
},
},
date: {
_: {
default: "mdi:calendar",
},
},
fan: {
_: {
default: "mdi:fan",
state: {
off: "mdi:fan-off",
},
state_attributes: {
direction: {
default: "mdi:rotate-right",
state: {
reverse: "mdi:rotate-left",
},
},
},
},
},
automation: {
_: {
default: "mdi:robot",
state: {
off: "mdi:robot-off",
unavailable: "mdi:robot-confused",
},
},
},
weather: {
_: {
default: "mdi:weather-partly-cloudy",
state: {
"clear-night": "mdi:weather-night",
cloudy: "mdi:weather-cloudy",
exceptional: "mdi:alert-circle-outline",
fog: "mdi:weather-fog",
hail: "mdi:weather-hail",
lightning: "mdi:weather-lightning",
"lightning-rainy": "mdi:weather-lightning-rainy",
pouring: "mdi:weather-pouring",
rainy: "mdi:weather-rainy",
snowy: "mdi:weather-snowy",
"snowy-rainy": "mdi:weather-snowy-rainy",
sunny: "mdi:weather-sunny",
windy: "mdi:weather-windy",
"windy-variant": "mdi:weather-windy-variant",
},
},
},
climate: {
_: {
default: "mdi:thermostat",
state_attributes: {
fan_mode: {
default: "mdi:circle-medium",
state: {
diffuse: "mdi:weather-windy",
focus: "mdi:target",
high: "mdi:speedometer",
low: "mdi:speedometer-slow",
medium: "mdi:speedometer-medium",
middle: "mdi:speedometer-medium",
off: "mdi:fan-off",
on: "mdi:fan",
},
},
hvac_action: {
default: "mdi:circle-medium",
state: {
cooling: "mdi:snowflake",
drying: "mdi:water-percent",
fan: "mdi:fan",
heating: "mdi:fire",
idle: "mdi:clock-outline",
off: "mdi:power",
preheating: "mdi:heat-wave",
},
},
preset_mode: {
default: "mdi:circle-medium",
state: {
activity: "mdi:motion-sensor",
away: "mdi:account-arrow-right",
boost: "mdi:rocket-launch",
comfort: "mdi:sofa",
eco: "mdi:leaf",
home: "mdi:home",
sleep: "mdi:bed",
},
},
swing_mode: {
default: "mdi:circle-medium",
state: {
both: "mdi:arrow-all",
horizontal: "mdi:arrow-left-right",
off: "mdi:arrow-oscillating-off",
on: "mdi:arrow-oscillating",
vertical: "mdi:arrow-up-down",
},
},
},
},
},
stt: {
_: {
default: "mdi:microphone-message",
},
},
update: {
_: {
default: "mdi:package-up",
state: {
off: "mdi:package",
},
},
},
event: {
_: {
default: "mdi:eye-check",
},
button: {
default: "mdi:gesture-tap-button",
},
doorbell: {
default: "mdi:doorbell",
},
motion: {
default: "mdi:motion-sensor",
},
},
};

View File

@ -232,6 +232,7 @@ export class HaAutomationTrace extends LitElement {
<div class="main"> <div class="main">
<div class="graph"> <div class="graph">
<hat-script-graph <hat-script-graph
.hass=${this.hass}
.trace=${this._trace} .trace=${this._trace}
.selected=${this._selected?.path} .selected=${this._selected?.path}
@graph-node-selected=${this._pickNode} @graph-node-selected=${this._pickNode}

View File

@ -31,11 +31,11 @@ import "../../../components/ha-area-picker";
import "../../../components/ha-icon"; import "../../../components/ha-icon";
import "../../../components/ha-icon-button-next"; import "../../../components/ha-icon-button-next";
import "../../../components/ha-icon-picker"; import "../../../components/ha-icon-picker";
import "../../../components/ha-state-icon";
import "../../../components/ha-list-item"; import "../../../components/ha-list-item";
import "../../../components/ha-radio"; import "../../../components/ha-radio";
import "../../../components/ha-select"; import "../../../components/ha-select";
import "../../../components/ha-settings-row"; import "../../../components/ha-settings-row";
import "../../../components/ha-state-icon";
import "../../../components/ha-switch"; import "../../../components/ha-switch";
import type { HaSwitch } from "../../../components/ha-switch"; import type { HaSwitch } from "../../../components/ha-switch";
import "../../../components/ha-textfield"; import "../../../components/ha-textfield";
@ -52,10 +52,6 @@ import {
createConfigFlow, createConfigFlow,
handleConfigFlowStep, handleConfigFlowStep,
} from "../../../data/config_flow"; } from "../../../data/config_flow";
import {
createOptionsFlow,
handleOptionsFlowStep,
} from "../../../data/options_flow";
import { DataEntryFlowStepCreateEntry } from "../../../data/data_entry_flow"; import { DataEntryFlowStepCreateEntry } from "../../../data/data_entry_flow";
import { import {
DeviceRegistryEntry, DeviceRegistryEntry,
@ -70,9 +66,13 @@ import {
subscribeEntityRegistry, subscribeEntityRegistry,
updateEntityRegistryEntry, updateEntityRegistryEntry,
} from "../../../data/entity_registry"; } from "../../../data/entity_registry";
import { entityIcon } from "../../../data/icons"; import { entityIcon, entryIcon } from "../../../data/icons";
import { domainToName } from "../../../data/integration"; import { domainToName } from "../../../data/integration";
import { getNumberDeviceClassConvertibleUnits } from "../../../data/number"; import { getNumberDeviceClassConvertibleUnits } from "../../../data/number";
import {
createOptionsFlow,
handleOptionsFlowStep,
} from "../../../data/options_flow";
import { import {
getSensorDeviceClassConvertibleUnits, getSensorDeviceClassConvertibleUnits,
getSensorNumericDeviceClasses, getSensorNumericDeviceClasses,
@ -392,7 +392,8 @@ export class EntityRegistrySettingsEditor extends LitElement {
)} )}
.placeholder=${this.entry.original_icon || .placeholder=${this.entry.original_icon ||
stateObj?.attributes.icon || stateObj?.attributes.icon ||
(stateObj && until(entityIcon(this.hass, stateObj)))} (stateObj && until(entityIcon(this.hass, stateObj))) ||
until(entryIcon(this.hass, this.entry))}
.disabled=${this.disabled} .disabled=${this.disabled}
> >
${!this._icon && !stateObj?.attributes.icon && stateObj ${!this._icon && !stateObj?.attributes.icon && stateObj

View File

@ -25,6 +25,7 @@ import { customElement, property, query, state } from "lit/decorators";
import { classMap } from "lit/directives/class-map"; import { classMap } from "lit/directives/class-map";
import { ifDefined } from "lit/directives/if-defined"; import { ifDefined } from "lit/directives/if-defined";
import { styleMap } from "lit/directives/style-map"; import { styleMap } from "lit/directives/style-map";
import { until } from "lit/directives/until";
import memoize from "memoize-one"; import memoize from "memoize-one";
import type { HASSDomEvent } from "../../../common/dom/fire_event"; import type { HASSDomEvent } from "../../../common/dom/fire_event";
import { computeDomain } from "../../../common/entity/compute_domain"; import { computeDomain } from "../../../common/entity/compute_domain";
@ -42,6 +43,7 @@ import type {
} from "../../../components/data-table/ha-data-table"; } from "../../../components/data-table/ha-data-table";
import "../../../components/ha-button-menu"; import "../../../components/ha-button-menu";
import "../../../components/ha-check-list-item"; import "../../../components/ha-check-list-item";
import "../../../components/ha-icon";
import "../../../components/ha-icon-button"; import "../../../components/ha-icon-button";
import "../../../components/ha-svg-icon"; import "../../../components/ha-svg-icon";
import { ConfigEntry, getConfigEntries } from "../../../data/config_entries"; import { ConfigEntry, getConfigEntries } from "../../../data/config_entries";
@ -53,6 +55,7 @@ import {
removeEntityRegistryEntry, removeEntityRegistryEntry,
updateEntityRegistryEntry, updateEntityRegistryEntry,
} from "../../../data/entity_registry"; } from "../../../data/entity_registry";
import { entryIcon } from "../../../data/icons";
import { domainToName } from "../../../data/integration"; import { domainToName } from "../../../data/integration";
import { import {
showAlertDialog, showAlertDialog,
@ -207,14 +210,23 @@ export class HaConfigEntities extends LitElement {
title: "", title: "",
label: localize("ui.panel.config.entities.picker.headers.state_icon"), label: localize("ui.panel.config.entities.picker.headers.state_icon"),
type: "icon", type: "icon",
template: (entry) => html` template: (entry) =>
<ha-state-icon entry.icon
title=${ifDefined(entry.entity?.state)} ? html`
slot="item-icon" <ha-state-icon
.hass=${this.hass} title=${ifDefined(entry.entity?.state)}
.stateObj=${entry.entity} slot="item-icon"
></ha-state-icon> .hass=${this.hass}
`, .stateObj=${entry.entity}
></ha-state-icon>
`
: html`
<ha-icon
icon=${until(
entryIcon(this.hass, entry as EntityRegistryEntry)
)}
></ha-icon>
`,
}, },
name: { name: {
main: true, main: true,

View File

@ -1,12 +1,12 @@
import "@material/mwc-button/mwc-button"; import "@material/mwc-button/mwc-button";
import { mdiCheckCircle, mdiCloseCircle } from "@mdi/js"; import { mdiAlertCircle, mdiCheckCircle, mdiCloseCircle } from "@mdi/js";
import { css, CSSResultGroup, html, LitElement, nothing } from "lit"; import { css, CSSResultGroup, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators"; import { customElement, property, state } from "lit/decorators";
import { fireEvent } from "../../../../../common/dom/fire_event"; import { fireEvent } from "../../../../../common/dom/fire_event";
import "../../../../../components/ha-circular-progress"; import "../../../../../components/ha-circular-progress";
import { createCloseHeading } from "../../../../../components/ha-dialog"; import { createCloseHeading } from "../../../../../components/ha-dialog";
import { pingMatterNode, MatterPingResult } from "../../../../../data/matter"; import { pingMatterNode, MatterPingResult } from "../../../../../data/matter";
import { haStyleDialog } from "../../../../../resources/styles"; import { haStyle, haStyleDialog } from "../../../../../resources/styles";
import { HomeAssistant } from "../../../../../types"; import { HomeAssistant } from "../../../../../types";
import { MatterPingNodeDialogParams } from "./show-dialog-matter-ping-node"; import { MatterPingNodeDialogParams } from "./show-dialog-matter-ping-node";
@ -40,33 +40,24 @@ class DialogMatterPingNode extends LitElement {
> >
${this._pingResult ${this._pingResult
? html` ? html`
<div class="flex-container"> <h2>
<ha-svg-icon ${this.hass.localize(
.path=${mdiCheckCircle} "ui.panel.config.matter.ping_node.ping_complete"
class="success" )}
></ha-svg-icon> </h2>
<div class="status"> <mwc-list>
<p> ${Object.entries(this._pingResult).map(
${this.hass.localize( ([ip, success]) =>
"ui.panel.config.matter.ping_node.ping_complete" html`<ha-list-item hasMeta noninteractive
)} >${ip}
</p> <ha-svg-icon
</div> slot="meta"
</div> .path=${success ? mdiCheckCircle : mdiAlertCircle}
<div> class=${success ? "success" : "failed"}
<mwc-list> ></ha-svg-icon>
${Object.entries(this._pingResult).map( </ha-list-item>`
([ip, success]) => )}
html`<ha-list-item hasMeta </mwc-list>
>${ip}
<ha-icon
slot="meta"
icon=${success ? "mdi:check" : "mdi:close"}
></ha-icon>
</ha-list-item>`
)}
</mwc-list>
</div>
<mwc-button slot="primaryAction" @click=${this.closeDialog}> <mwc-button slot="primaryAction" @click=${this.closeDialog}>
${this.hass.localize("ui.common.close")} ${this.hass.localize("ui.common.close")}
</mwc-button> </mwc-button>
@ -151,6 +142,7 @@ class DialogMatterPingNode extends LitElement {
static get styles(): CSSResultGroup { static get styles(): CSSResultGroup {
return [ return [
haStyle,
haStyleDialog, haStyleDialog,
css` css`
.success { .success {
@ -170,23 +162,22 @@ class DialogMatterPingNode extends LitElement {
margin-top: 16px; margin-top: 16px;
} }
.stage ha-svg-icon {
width: 16px;
height: 16px;
}
.stage { .stage {
padding: 8px; padding: 8px;
} }
ha-svg-icon { mwc-list {
width: 68px; --mdc-list-side-padding: 0;
height: 48px;
} }
.flex-container ha-circular-progress, .flex-container ha-circular-progress,
.flex-container ha-svg-icon { .flex-container ha-svg-icon {
margin-right: 20px; margin-right: 20px;
} }
.flex-container ha-svg-icon {
width: 68px;
height: 48px;
}
`, `,
]; ];
} }

View File

@ -217,6 +217,7 @@ export class HaScriptTrace extends LitElement {
<div class="main"> <div class="main">
<div class="graph"> <div class="graph">
<hat-script-graph <hat-script-graph
.hass=${this.hass}
.trace=${this._trace} .trace=${this._trace}
.selected=${this._selected?.path} .selected=${this._selected?.path}
@graph-node-selected=${this._pickNode} @graph-node-selected=${this._pickNode}

View File

@ -21,6 +21,7 @@ import "./hui-entity-picker-table";
import { CreateCardDialogParams } from "./show-create-card-dialog"; import { CreateCardDialogParams } from "./show-create-card-dialog";
import { showEditCardDialog } from "./show-edit-card-dialog"; import { showEditCardDialog } from "./show-edit-card-dialog";
import { showSuggestCardDialog } from "./show-suggest-card-dialog"; import { showSuggestCardDialog } from "./show-suggest-card-dialog";
import { computeCards } from "../../common/generate-lovelace-config";
declare global { declare global {
interface HASSDomEvents { interface HASSDomEvents {
@ -242,11 +243,17 @@ export class HuiCreateDialogCard
} }
private _suggestCards(): void { private _suggestCards(): void {
const cardConfig = computeCards(
this.hass.states,
this._selectedEntities,
{}
);
showSuggestCardDialog(this, { showSuggestCardDialog(this, {
lovelaceConfig: this._params!.lovelaceConfig, lovelaceConfig: this._params!.lovelaceConfig,
saveConfig: this._params!.saveConfig, saveConfig: this._params!.saveConfig,
path: this._params!.path as [number], path: this._params!.path as [number],
entities: this._selectedEntities, entities: this._selectedEntities,
cardConfig,
}); });
this.closeDialog(); this.closeDialog();

View File

@ -7,11 +7,11 @@ export interface SuggestCardDialogParams {
yaml?: boolean; yaml?: boolean;
saveConfig?: (config: LovelaceConfig) => void; saveConfig?: (config: LovelaceConfig) => void;
path?: [number]; path?: [number];
entities?: string[]; // Entities used to generate the card config. We pass this to create dialog when user chooses "Pick own" entities?: string[]; // We pass this to create dialog when user chooses "Pick own"
cardConfig?: LovelaceCardConfig[]; // We can pass a suggested config cardConfig: LovelaceCardConfig[]; // We can pass a suggested config
} }
const importsuggestCardDialog = () => import("./hui-dialog-suggest-card"); const importSuggestCardDialog = () => import("./hui-dialog-suggest-card");
export const showSuggestCardDialog = ( export const showSuggestCardDialog = (
element: HTMLElement, element: HTMLElement,
@ -19,7 +19,7 @@ export const showSuggestCardDialog = (
): void => { ): void => {
fireEvent(element, "show-dialog", { fireEvent(element, "show-dialog", {
dialogTag: "hui-dialog-suggest-card", dialogTag: "hui-dialog-suggest-card",
dialogImport: importsuggestCardDialog, dialogImport: importSuggestCardDialog,
dialogParams: suggestCardDialogParams, dialogParams: suggestCardDialogParams,
}); });
}; };

View File

@ -21,6 +21,7 @@ import "../card-editor/hui-entity-picker-table";
import { showSuggestCardDialog } from "../card-editor/show-suggest-card-dialog"; import { showSuggestCardDialog } from "../card-editor/show-suggest-card-dialog";
import { showSelectViewDialog } from "../select-view/show-select-view-dialog"; import { showSelectViewDialog } from "../select-view/show-select-view-dialog";
import { LovelaceConfig } from "../../../../data/lovelace/config/types"; import { LovelaceConfig } from "../../../../data/lovelace/config/types";
import { computeCards } from "../../common/generate-lovelace-config";
@customElement("hui-unused-entities") @customElement("hui-unused-entities")
export class HuiUnusedEntities extends LitElement { export class HuiUnusedEntities extends LitElement {
@ -126,12 +127,18 @@ export class HuiUnusedEntities extends LitElement {
} }
private _addToLovelaceView(): void { private _addToLovelaceView(): void {
const cardConfig = computeCards(
this.hass.states,
this._selectedEntities,
{}
);
if (this.lovelace.config.views.length === 1) { if (this.lovelace.config.views.length === 1) {
showSuggestCardDialog(this, { showSuggestCardDialog(this, {
lovelaceConfig: this.lovelace.config!, lovelaceConfig: this.lovelace.config!,
saveConfig: this.lovelace.saveConfig, saveConfig: this.lovelace.saveConfig,
path: [0], path: [0],
entities: this._selectedEntities, entities: this._selectedEntities,
cardConfig,
}); });
return; return;
} }
@ -144,6 +151,7 @@ export class HuiUnusedEntities extends LitElement {
saveConfig: this.lovelace.saveConfig, saveConfig: this.lovelace.saveConfig,
path: [viewIndex], path: [viewIndex],
entities: this._selectedEntities, entities: this._selectedEntities,
cardConfig,
}); });
}, },
}); });

View File

@ -40,14 +40,20 @@ class HuiInputSelectEntityRow extends LitElement implements LovelaceRow {
protected updated(changedProps: PropertyValues) { protected updated(changedProps: PropertyValues) {
super.updated(changedProps); super.updated(changedProps);
if (!this._config) {
return;
}
if (changedProps.has("hass")) { if (changedProps.has("hass")) {
const oldHass = changedProps.get("hass"); const oldHass = changedProps.get("hass");
const stateObj = this.hass?.states[this._config.entity] as
| InputSelectEntity
| undefined;
const oldStateObj = oldHass?.states[this._config.entity] as
| InputSelectEntity
| undefined;
if ( if (
this.hass && stateObj &&
oldHass && stateObj.attributes.options !== oldStateObj?.attributes.options
this._config?.entity &&
this.hass.states[this._config.entity].attributes.options !==
oldHass.states[this._config.entity].attributes.options
) { ) {
this._haSelect.layoutOptions(); this._haSelect.layoutOptions();
} }