20221206.0 (#14585)

This commit is contained in:
Bram Kragten 2022-12-06 18:35:05 +01:00 committed by GitHub
commit dacef605c7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
39 changed files with 637 additions and 252 deletions

18
.gitignore vendored
View File

@ -2,20 +2,20 @@
.reify-cache .reify-cache
# build # build
build build/
hass_frontend/* dist/
dist /hass_frontend/
translations /translations/
# yarn # yarn
.yarn/* .yarn/**
!.yarn/patches !.yarn/patches
!.yarn/releases !.yarn/releases
!.yarn/plugins !.yarn/plugins
!.yarn/sdks !.yarn/sdks
!.yarn/versions !.yarn/versions
.pnp.* .pnp.*
node_modules/* /node_modules/
yarn-error.log yarn-error.log
npm-debug.log npm-debug.log
@ -27,11 +27,11 @@ npm-debug.log
# venv stuff # venv stuff
pyvenv.cfg pyvenv.cfg
pip-selfcheck.json pip-selfcheck.json
venv/* /venv/
.venv .venv
# vscode # vscode
.vscode/* .vscode/**
!.vscode/extensions.json !.vscode/extensions.json
!.vscode/launch.json !.vscode/launch.json
!.vscode/tasks.json !.vscode/tasks.json
@ -46,4 +46,4 @@ src/cast/dev_const.ts
.tool-versions .tool-versions
# Home Assistant config # Home Assistant config
/config /config/

View File

@ -181,7 +181,7 @@ class HcCast extends LitElement {
private async _handlePickView(ev: Event) { private async _handlePickView(ev: Event) {
const path = (ev.currentTarget as any).getAttribute("data-path"); const path = (ev.currentTarget as any).getAttribute("data-path");
await ensureConnectedCastSession(this.castManager!, this.auth!); await ensureConnectedCastSession(this.castManager!, this.auth!);
castSendShowLovelaceView(this.castManager, path); castSendShowLovelaceView(this.castManager, path, this.auth.data.hassUrl);
} }
private async _handleLogout() { private async _handleLogout() {

View File

@ -33,7 +33,6 @@ import { castContext } from "../cast_context";
import "./hc-launch-screen"; import "./hc-launch-screen";
let resourcesLoaded = false; let resourcesLoaded = false;
@customElement("hc-main") @customElement("hc-main")
export class HcMain extends HassElement { export class HcMain extends HassElement {
@state() private _showDemo = false; @state() private _showDemo = false;
@ -46,6 +45,8 @@ export class HcMain extends HassElement {
@state() private _urlPath?: string | null; @state() private _urlPath?: string | null;
private _hassUUID?: string;
private _unsubLovelace?: UnsubscribeFunc; private _unsubLovelace?: UnsubscribeFunc;
public processIncomingMessage(msg: HassMessage) { public processIncomingMessage(msg: HassMessage) {
@ -125,6 +126,7 @@ export class HcMain extends HassElement {
if (this.hass) { if (this.hass) {
status.hassUrl = this.hass.auth.data.hassUrl; status.hassUrl = this.hass.auth.data.hassUrl;
status.hassUUID = this._hassUUID;
status.lovelacePath = this._lovelacePath; status.lovelacePath = this._lovelacePath;
status.urlPath = this._urlPath; status.urlPath = this._urlPath;
} }
@ -163,6 +165,18 @@ export class HcMain extends HassElement {
}; };
private async _handleGetStatusMessage(msg: GetStatusMessage) { private async _handleGetStatusMessage(msg: GetStatusMessage) {
if (
(this.hass && msg.hassUUID && msg.hassUUID !== this._hassUUID) ||
(this.hass && msg.hassUrl && msg.hassUrl !== this.hass.auth.data.hassUrl)
) {
this._error = "Not connected to the same Home Assistant instance.";
this._sendError(
ReceiverErrorCode.WRONG_INSTANCE,
this._error,
msg.senderId!
);
}
this._sendStatus(msg.senderId!); this._sendStatus(msg.senderId!);
} }
@ -179,6 +193,7 @@ export class HcMain extends HassElement {
expires_in: 0, expires_in: 0,
}), }),
}); });
this._hassUUID = msg.hassUUID;
} catch (err: any) { } catch (err: any) {
const errorMessage = this._getErrorMessage(err); const errorMessage = this._getErrorMessage(err);
this._error = errorMessage; this._error = errorMessage;
@ -209,9 +224,29 @@ export class HcMain extends HassElement {
if (!this.hass) { if (!this.hass) {
this._sendStatus(msg.senderId!); this._sendStatus(msg.senderId!);
this._error = "Cannot show Lovelace because we're not connected."; this._error = "Cannot show Lovelace because we're not connected.";
this._sendError(ReceiverErrorCode.NOT_CONNECTED, this._error); this._sendError(
ReceiverErrorCode.NOT_CONNECTED,
this._error,
msg.senderId!
);
return; return;
} }
if (
(msg.hassUUID && msg.hassUUID !== this._hassUUID) ||
(msg.hassUrl && msg.hassUrl !== this.hass.auth.data.hassUrl)
) {
this._sendStatus(msg.senderId!);
this._error =
"Cannot show Lovelace because we're not connected to the same Home Assistant instance.";
this._sendError(
ReceiverErrorCode.WRONG_INSTANCE,
this._error,
msg.senderId!
);
return;
}
this._error = undefined; this._error = undefined;
if (msg.urlPath === "lovelace") { if (msg.urlPath === "lovelace") {
msg.urlPath = null; msg.urlPath = null;

View File

@ -4,14 +4,12 @@ import {
} from "home-assistant-js-websocket"; } from "home-assistant-js-websocket";
import { css, html, LitElement, TemplateResult } from "lit"; import { css, html, LitElement, TemplateResult } from "lit";
import { customElement, property } from "lit/decorators"; import { customElement, property } from "lit/decorators";
import { styleMap } from "lit/directives/style-map";
import memoizeOne from "memoize-one"; import memoizeOne from "memoize-one";
import { computeDomain } from "../../../../src/common/entity/compute_domain"; import { computeDomain } from "../../../../src/common/entity/compute_domain";
import { computeStateDisplay } from "../../../../src/common/entity/compute_state_display"; import { computeStateDisplay } from "../../../../src/common/entity/compute_state_display";
import { stateColorCss } from "../../../../src/common/entity/state_color";
import { stateIconPath } from "../../../../src/common/entity/state_icon_path";
import "../../../../src/components/data-table/ha-data-table"; 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/ha-chip"; import "../../../../src/components/ha-chip";
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";
@ -105,6 +103,12 @@ const ENTITIES: HassEntity[] = [
createEntity("alarm_control_panel.arming", "arming"), createEntity("alarm_control_panel.arming", "arming"),
createEntity("alarm_control_panel.disarming", "disarming"), createEntity("alarm_control_panel.disarming", "disarming"),
createEntity("alarm_control_panel.triggered", "triggered"), createEntity("alarm_control_panel.triggered", "triggered"),
// Alert
createEntity("alert.off", "off"),
createEntity("alert.on", "on"),
// Automation
createEntity("automation.off", "off"),
createEntity("automation.on", "on"),
// Binary Sensor // Binary Sensor
...BINARY_SENSOR_DEVICE_CLASSES.map((dc) => ...BINARY_SENSOR_DEVICE_CLASSES.map((dc) =>
createEntity(`binary_sensor.${dc}`, "on", dc) createEntity(`binary_sensor.${dc}`, "on", dc)
@ -113,8 +117,11 @@ const ENTITIES: HassEntity[] = [
createEntity("button.restart", "unknown", "restart"), createEntity("button.restart", "unknown", "restart"),
createEntity("button.update", "unknown", "update"), createEntity("button.update", "unknown", "update"),
// Calendar // Calendar
createEntity("calendar.on", "on"),
createEntity("calendar.off", "off"), createEntity("calendar.off", "off"),
createEntity("calendar.on", "on"),
// Camera
createEntity("camera.off", "off"),
createEntity("camera.on", "on"),
// Climate // Climate
createEntity("climate.off", "off"), createEntity("climate.off", "off"),
createEntity("climate.heat", "heat"), createEntity("climate.heat", "heat"),
@ -124,10 +131,10 @@ const ENTITIES: HassEntity[] = [
createEntity("climate.dry", "dry"), createEntity("climate.dry", "dry"),
createEntity("climate.fan_only", "fan_only"), createEntity("climate.fan_only", "fan_only"),
// Cover // Cover
createEntity("cover.opening", "opening"),
createEntity("cover.open", "open"),
createEntity("cover.closing", "closing"), createEntity("cover.closing", "closing"),
createEntity("cover.closed", "closed"), createEntity("cover.closed", "closed"),
createEntity("cover.opening", "opening"),
createEntity("cover.open", "open"),
createEntity("cover.awning", "open", "awning"), createEntity("cover.awning", "open", "awning"),
createEntity("cover.blind", "open", "blind"), createEntity("cover.blind", "open", "blind"),
createEntity("cover.curtain", "open", "curtain"), createEntity("cover.curtain", "open", "curtain"),
@ -139,24 +146,27 @@ const ENTITIES: HassEntity[] = [
createEntity("cover.shutter", "open", "shutter"), createEntity("cover.shutter", "open", "shutter"),
createEntity("cover.window", "open", "window"), createEntity("cover.window", "open", "window"),
// Device tracker/person // Device tracker/person
createEntity("device_tracker.home", "home"),
createEntity("device_tracker.not_home", "not_home"), createEntity("device_tracker.not_home", "not_home"),
createEntity("device_tracker.home", "home"),
createEntity("device_tracker.work", "work"), createEntity("device_tracker.work", "work"),
createEntity("person.home", "home"), createEntity("person.home", "home"),
createEntity("person.not_home", "not_home"), createEntity("person.not_home", "not_home"),
createEntity("person.work", "work"), createEntity("person.work", "work"),
// Fan // Fan
createEntity("fan.on", "on"),
createEntity("fan.off", "off"), createEntity("fan.off", "off"),
createEntity("fan.on", "on"),
// Humidifier // Humidifier
createEntity("humidifier.on", "on"),
createEntity("humidifier.off", "off"), createEntity("humidifier.off", "off"),
createEntity("humidifier.on", "on"),
// Helpers
createEntity("input_boolean.off", "off"),
createEntity("input_boolean.on", "on"),
// Light // Light
createEntity("light.on", "on"),
createEntity("light.off", "off"), createEntity("light.off", "off"),
createEntity("light.on", "on"),
// Locks // Locks
createEntity("lock.locked", "locked"),
createEntity("lock.unlocked", "unlocked"), createEntity("lock.unlocked", "unlocked"),
createEntity("lock.locked", "locked"),
createEntity("lock.locking", "locking"), createEntity("lock.locking", "locking"),
createEntity("lock.unlocking", "unlocking"), createEntity("lock.unlocking", "unlocking"),
createEntity("lock.jammed", "jammed"), createEntity("lock.jammed", "jammed"),
@ -180,6 +190,12 @@ const ENTITIES: HassEntity[] = [
createEntity("media_player.speaker_playing", "playing", "speaker"), createEntity("media_player.speaker_playing", "playing", "speaker"),
createEntity("media_player.speaker_paused", "paused", "speaker"), createEntity("media_player.speaker_paused", "paused", "speaker"),
createEntity("media_player.speaker_standby", "standby", "speaker"), createEntity("media_player.speaker_standby", "standby", "speaker"),
// Remote
createEntity("remote.off", "off"),
createEntity("remote.on", "on"),
// Script
createEntity("script.off", "off"),
createEntity("script.on", "on"),
// Sensor // Sensor
...SENSOR_DEVICE_CLASSES.map((dc) => createEntity(`sensor.${dc}`, "10", dc)), ...SENSOR_DEVICE_CLASSES.map((dc) => createEntity(`sensor.${dc}`, "10", dc)),
// Battery sensor // Battery sensor
@ -196,9 +212,12 @@ const ENTITIES: HassEntity[] = [
createEntity("switch.outlet_on", "on", "outlet"), createEntity("switch.outlet_on", "on", "outlet"),
createEntity("switch.switch_off", "off", "switch"), createEntity("switch.switch_off", "off", "switch"),
createEntity("switch.switch_on", "on", "switch"), createEntity("switch.switch_on", "on", "switch"),
// Timer
createEntity("timer.off", "off"),
createEntity("timer.on", "on"),
// Vacuum // Vacuum
createEntity("vacuum.cleaning", "cleaning"),
createEntity("vacuum.docked", "docked"), createEntity("vacuum.docked", "docked"),
createEntity("vacuum.cleaning", "cleaning"),
createEntity("vacuum.paused", "paused"), createEntity("vacuum.paused", "paused"),
createEntity("vacuum.idle", "idle"), createEntity("vacuum.idle", "idle"),
createEntity("vacuum.returning", "returning"), createEntity("vacuum.returning", "returning"),
@ -280,18 +299,12 @@ export class DemoEntityState extends LitElement {
const columns: DataTableColumnContainer<EntityRowData> = { const columns: DataTableColumnContainer<EntityRowData> = {
icon: { icon: {
title: "Icon", title: "Icon",
template: (_, entry) => { template: (_, entry) => html`
const cssColor = stateColorCss(entry.stateObj); <state-badge
return html` .stateObj=${entry.stateObj}
<ha-svg-icon .stateColor=${true}
style=${styleMap({ ></state-badge>
color: `rgb(${cssColor})`, `,
})}
.path=${stateIconPath(entry.stateObj)}
>
</ha-svg-icon>
`;
},
}, },
entity_id: { entity_id: {
title: "Entity id", title: "Entity id",

0
hassio/.gitignore vendored
View File

View File

@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project] [project]
name = "home-assistant-frontend" name = "home-assistant-frontend"
version = "20221205.0" version = "20221206.0"
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

@ -8,6 +8,8 @@ import { BaseCastMessage } from "./types";
export interface GetStatusMessage extends BaseCastMessage { export interface GetStatusMessage extends BaseCastMessage {
type: "get_status"; type: "get_status";
hassUrl?: string;
hassUUID?: string;
} }
export interface ConnectMessage extends BaseCastMessage { export interface ConnectMessage extends BaseCastMessage {
@ -15,12 +17,15 @@ export interface ConnectMessage extends BaseCastMessage {
refreshToken: string; refreshToken: string;
clientId: string | null; clientId: string | null;
hassUrl: string; hassUrl: string;
hassUUID?: string;
} }
export interface ShowLovelaceViewMessage extends BaseCastMessage { export interface ShowLovelaceViewMessage extends BaseCastMessage {
type: "show_lovelace_view"; type: "show_lovelace_view";
viewPath: string | number | null; viewPath: string | number | null;
urlPath: string | null; urlPath: string | null;
hassUrl: string;
hassUUID?: string;
} }
export interface ShowDemoMessage extends BaseCastMessage { export interface ShowDemoMessage extends BaseCastMessage {
@ -43,6 +48,7 @@ export const castSendAuth = (cast: CastManager, auth: Auth) =>
export const castSendShowLovelaceView = ( export const castSendShowLovelaceView = (
cast: CastManager, cast: CastManager,
hassUrl: string,
viewPath: ShowLovelaceViewMessage["viewPath"], viewPath: ShowLovelaceViewMessage["viewPath"],
urlPath?: string | null urlPath?: string | null
) => ) =>
@ -50,6 +56,7 @@ export const castSendShowLovelaceView = (
type: "show_lovelace_view", type: "show_lovelace_view",
viewPath, viewPath,
urlPath: urlPath || null, urlPath: urlPath || null,
hassUrl: CAST_DEV ? CAST_DEV_HASS_URL : hassUrl,
}); });
export const castSendShowDemo = (cast: CastManager) => export const castSendShowDemo = (cast: CastManager) =>

View File

@ -7,6 +7,7 @@ export interface ReceiverStatusMessage extends BaseCastMessage {
connected: boolean; connected: boolean;
showDemo: boolean; showDemo: boolean;
hassUrl?: string; hassUrl?: string;
hassUUID?: string;
lovelacePath?: string | number | null; lovelacePath?: string | number | null;
urlPath?: string | null; urlPath?: string | null;
} }
@ -23,6 +24,7 @@ export const enum ReceiverErrorCode {
CONNECTION_LOST = 3, CONNECTION_LOST = 3,
HASS_URL_MISSING = 4, HASS_URL_MISSING = 4,
NO_HTTPS = 5, NO_HTTPS = 5,
WRONG_INSTANCE = 20,
NOT_CONNECTED = 21, NOT_CONNECTED = 21,
FETCH_CONFIG_FAILED = 22, FETCH_CONFIG_FAILED = 22,
} }

View File

@ -188,6 +188,14 @@ export const DOMAINS_WITH_CARD = [
"water_heater", "water_heater",
]; ];
export const SENSOR_ENTITIES = [
"sensor",
"binary_sensor",
"camera",
"device_tracker",
"weather",
];
/** Domains that render an input element instead of a text value when displayed in a row. /** Domains that render an input element instead of a text value when displayed in a row.
* Those rows should then not show a cursor pointer when hovered (which would normally * Those rows should then not show a cursor pointer when hovered (which would normally
* be the default) unless the element itself enforces it (e.g. a button). Also those elements * be the default) unless the element itself enforces it (e.g. a button). Also those elements

View File

@ -0,0 +1,10 @@
import { HassEntity } from "home-assistant-js-websocket";
export const personColor = (stateObj: HassEntity): string | undefined => {
switch (stateObj.state) {
case "home":
return "person-home";
default:
return "person-zone";
}
};

View File

@ -33,6 +33,10 @@ export function stateActive(stateObj: HassEntity, state?: string): boolean {
return compareState === "problem"; return compareState === "problem";
case "group": case "group":
return ["on", "home", "open", "locked", "problem"].includes(compareState); return ["on", "home", "open", "locked", "problem"].includes(compareState);
case "timer":
return compareState === "active";
case "camera":
return compareState === "streaming";
default: default:
return true; return true;
} }

View File

@ -5,6 +5,7 @@ import { alarmControlPanelColor } from "./color/alarm_control_panel_color";
import { binarySensorColor } from "./color/binary_sensor_color"; import { binarySensorColor } from "./color/binary_sensor_color";
import { climateColor } from "./color/climate_color"; import { climateColor } from "./color/climate_color";
import { lockColor } from "./color/lock_color"; import { lockColor } from "./color/lock_color";
import { personColor } from "./color/person_color";
import { sensorColor } from "./color/sensor_color"; import { sensorColor } from "./color/sensor_color";
import { computeDomain } from "./compute_domain"; import { computeDomain } from "./compute_domain";
import { stateActive } from "./state_active"; import { stateActive } from "./state_active";
@ -28,6 +29,9 @@ export const stateColor = (stateObj: HassEntity, state?: string) => {
const domain = computeDomain(stateObj.entity_id); const domain = computeDomain(stateObj.entity_id);
switch (domain) { switch (domain) {
case "automation":
return "automation";
case "alarm_control_panel": case "alarm_control_panel":
return alarmControlPanelColor(compareState); return alarmControlPanelColor(compareState);
@ -55,6 +59,10 @@ export const stateColor = (stateObj: HassEntity, state?: string) => {
case "media_player": case "media_player":
return "media-player"; return "media-player";
case "person":
case "device_tracker":
return personColor(stateObj);
case "sensor": case "sensor":
return sensorColor(stateObj); return sensorColor(stateObj);
@ -68,8 +76,20 @@ export const stateColor = (stateObj: HassEntity, state?: string) => {
return compareState === "above_horizon" ? "sun-day" : "sun-night"; return compareState === "above_horizon" ? "sun-day" : "sun-night";
case "switch": case "switch":
case "input_boolean":
return "switch"; return "switch";
case "alert":
return "alert";
case "calendar":
case "camera":
case "remote":
case "script":
case "timer":
case "group":
return "active";
case "update": case "update":
return updateIsInstalling(stateObj as UpdateEntity) return updateIsInstalling(stateObj as UpdateEntity)
? "update-installing" ? "update-installing"

View File

@ -1,29 +1,6 @@
import { css } from "lit"; import { css } from "lit";
export const iconColorCSS = css` export const iconColorCSS = css`
ha-state-icon[data-active][data-domain="alert"],
ha-state-icon[data-active][data-domain="automation"],
ha-state-icon[data-active][data-domain="binary_sensor"],
ha-state-icon[data-active][data-domain="calendar"],
ha-state-icon[data-active][data-domain="camera"],
ha-state-icon[data-active][data-domain="cover"],
ha-state-icon[data-active][data-domain="device_tracker"],
ha-state-icon[data-active][data-domain="fan"],
ha-state-icon[data-active][data-domain="humidifier"],
ha-state-icon[data-active][data-domain="light"],
ha-state-icon[data-active][data-domain="input_boolean"],
ha-state-icon[data-active][data-domain="lock"],
ha-state-icon[data-active][data-domain="media_player"],
ha-state-icon[data-active][data-domain="remote"],
ha-state-icon[data-active][data-domain="script"],
ha-state-icon[data-active][data-domain="sun"],
ha-state-icon[data-active][data-domain="switch"],
ha-state-icon[data-active][data-domain="timer"],
ha-state-icon[data-active][data-domain="vacuum"],
ha-state-icon[data-active][data-domain="group"] {
color: var(--paper-item-icon-active-color, #fdd835);
}
ha-state-icon[data-active][data-domain="alarm_control_panel"][data-state="pending"], ha-state-icon[data-active][data-domain="alarm_control_panel"][data-state="pending"],
ha-state-icon[data-active][data-domain="alarm_control_panel"][data-state="arming"], ha-state-icon[data-active][data-domain="alarm_control_panel"][data-state="arming"],
ha-state-icon[data-active][data-domain="alarm_control_panel"][data-state="triggered"] { ha-state-icon[data-active][data-domain="alarm_control_panel"][data-state="triggered"] {

View File

@ -26,7 +26,7 @@ class StateHistoryChartLine extends LitElement {
@property() public identifier?: string; @property() public identifier?: string;
@property({ type: Boolean }) public isSingleDevice = false; @property({ type: Boolean }) public showNames = true;
@property({ attribute: false }) public endTime!: Date; @property({ attribute: false }) public endTime!: Date;
@ -101,7 +101,7 @@ class StateHistoryChartLine extends LitElement {
propagate: true, propagate: true,
}, },
legend: { legend: {
display: !this.isSingleDevice, display: this.showNames,
labels: { labels: {
usePointStyle: true, usePointStyle: true,
}, },

View File

@ -1,73 +1,14 @@
import type { ChartData, ChartDataset, ChartOptions } from "chart.js"; import type { ChartData, ChartDataset, ChartOptions } from "chart.js";
import { HassEntity } from "home-assistant-js-websocket";
import { css, CSSResultGroup, html, LitElement, PropertyValues } from "lit"; import { css, CSSResultGroup, html, LitElement, PropertyValues } from "lit";
import { customElement, property, state } from "lit/decorators"; import { customElement, property, state } from "lit/decorators";
import { getGraphColorByIndex } from "../../common/color/colors";
import { rgb2hex } from "../../common/color/convert-color";
import { formatDateTimeWithSeconds } from "../../common/datetime/format_date_time"; import { formatDateTimeWithSeconds } from "../../common/datetime/format_date_time";
import { stateActive } from "../../common/entity/state_active";
import { stateColor } from "../../common/entity/state_color";
import { numberFormatToLocale } from "../../common/number/format_number"; import { numberFormatToLocale } from "../../common/number/format_number";
import { computeRTL } from "../../common/util/compute_rtl"; import { computeRTL } from "../../common/util/compute_rtl";
import { TimelineEntity } from "../../data/history"; import { TimelineEntity } from "../../data/history";
import { HomeAssistant } from "../../types"; import { HomeAssistant } from "../../types";
import { MIN_TIME_BETWEEN_UPDATES } from "./ha-chart-base"; import { MIN_TIME_BETWEEN_UPDATES } from "./ha-chart-base";
import type { TimeLineData } from "./timeline-chart/const"; import type { TimeLineData } from "./timeline-chart/const";
import { computeTimelineColor } from "./timeline-chart/timeline-color";
const stateColorTokenMap: Map<string, string> = new Map();
const stateColorMap: Map<string, string> = new Map();
let colorIndex = 0;
export const getStateColorToken = (
stateString: string,
entityState?: HassEntity
) => {
if (!entityState || !stateActive(entityState, stateString)) {
return `disabled`;
}
const color = stateColor(entityState, stateString);
if (color) {
return `state-${color}`;
}
return undefined;
};
const getColor = (
stateString: string,
computedStyles: CSSStyleDeclaration,
entityState?: HassEntity
) => {
const stateColorToken = getStateColorToken(stateString, entityState);
if (stateColorToken) {
if (stateColorTokenMap.has(stateColorToken)) {
return stateColorTokenMap.get(stateColorToken);
}
const value = computedStyles.getPropertyValue(
`--rgb-${stateColorToken}-color`
);
if (value) {
const parsedValue = value.split(",").map((v) => Number(v)) as [
number,
number,
number
];
const hexValue = rgb2hex(parsedValue);
stateColorTokenMap.set(stateColorToken, hexValue);
return hexValue;
}
}
if (stateColorMap.has(stateString)) {
return stateColorMap.get(stateString);
}
const color = getGraphColorByIndex(colorIndex, computedStyles);
colorIndex++;
stateColorMap.set(stateString, color);
return color;
};
@customElement("state-history-chart-timeline") @customElement("state-history-chart-timeline")
export class StateHistoryChartTimeline extends LitElement { export class StateHistoryChartTimeline extends LitElement {
@ -83,7 +24,7 @@ export class StateHistoryChartTimeline extends LitElement {
@property() public identifier?: string; @property() public identifier?: string;
@property({ type: Boolean }) public isSingleDevice = false; @property({ type: Boolean }) public showNames = true;
@property({ type: Boolean }) public chunked = false; @property({ type: Boolean }) public chunked = false;
@ -175,8 +116,7 @@ export class StateHistoryChartTimeline extends LitElement {
drawTicks: false, drawTicks: false,
}, },
ticks: { ticks: {
display: display: this.chunked || this.showNames,
this.chunked || !this.isSingleDevice || this.data.length !== 1,
}, },
afterSetDimensions: (y) => { afterSetDimensions: (y) => {
y.maxWidth = y.chart.width * 0.18; y.maxWidth = y.chart.width * 0.18;
@ -271,7 +211,7 @@ export class StateHistoryChartTimeline extends LitElement {
start: prevLastChanged, start: prevLastChanged,
end: newLastChanged, end: newLastChanged,
label: locState, label: locState,
color: getColor( color: computeTimelineColor(
prevState, prevState,
computedStyles, computedStyles,
this.hass.states[stateInfo.entity_id] this.hass.states[stateInfo.entity_id]
@ -289,7 +229,7 @@ export class StateHistoryChartTimeline extends LitElement {
start: prevLastChanged, start: prevLastChanged,
end: endTime, end: endTime,
label: locState, label: locState,
color: getColor( color: computeTimelineColor(
prevState, prevState,
computedStyles, computedStyles,
this.hass.states[stateInfo.entity_id] this.hass.states[stateInfo.entity_id]

View File

@ -48,7 +48,7 @@ class StateHistoryCharts extends LitElement {
@property({ type: Boolean, attribute: "up-to-now" }) public upToNow = false; @property({ type: Boolean, attribute: "up-to-now" }) public upToNow = false;
@property({ type: Boolean, attribute: "no-single" }) public noSingle = false; @property({ type: Boolean }) public showNames = true;
@property({ type: Boolean }) public isLoadingData = false; @property({ type: Boolean }) public isLoadingData = false;
@ -128,8 +128,7 @@ class StateHistoryCharts extends LitElement {
.unit=${item.unit} .unit=${item.unit}
.data=${item.data} .data=${item.data}
.identifier=${item.identifier} .identifier=${item.identifier}
.isSingleDevice=${!this.noSingle && .showNames=${this.showNames}
this.historyData.line?.length === 1}
.endTime=${this._computedEndTime} .endTime=${this._computedEndTime}
.names=${this.names} .names=${this.names}
></state-history-chart-line> ></state-history-chart-line>
@ -141,8 +140,7 @@ class StateHistoryCharts extends LitElement {
.data=${item} .data=${item}
.startTime=${this._computedStartTime} .startTime=${this._computedStartTime}
.endTime=${this._computedEndTime} .endTime=${this._computedEndTime}
.isSingleDevice=${!this.noSingle && .showNames=${this.showNames}
this.historyData.timeline?.length === 1}
.names=${this.names} .names=${this.names}
.narrow=${this.narrow} .narrow=${this.narrow}
.chunked=${this.virtualize} .chunked=${this.virtualize}

View File

@ -0,0 +1,99 @@
import { HassEntity } from "home-assistant-js-websocket";
import { getGraphColorByIndex } from "../../../common/color/colors";
import { lab2hex, rgb2hex, rgb2lab } from "../../../common/color/convert-color";
import { labBrighten } from "../../../common/color/lab";
import { computeDomain } from "../../../common/entity/compute_domain";
import { stateActive } from "../../../common/entity/state_active";
import { stateColor } from "../../../common/entity/state_color";
const DOMAIN_STATE_SHADES: Record<string, Record<string, number>> = {
media_player: {
paused: 0.5,
idle: 1,
},
vacuum: {
returning: 0.5,
},
};
const cssColorMap: Map<string, [number, number, number]> = new Map();
function cssToRgb(
cssVariable: string,
computedStyles: CSSStyleDeclaration
): [number, number, number] | undefined {
if (!cssVariable.startsWith("--rgb")) {
return undefined;
}
if (cssColorMap.has(cssVariable)) {
return cssColorMap.get(cssVariable)!;
}
const value = computedStyles.getPropertyValue(cssVariable);
if (!value) return undefined;
const rgb = value.split(",").map((v) => Number(v)) as [
number,
number,
number
];
cssColorMap.set(cssVariable, rgb);
return rgb;
}
function computeTimelineStateColor(
state: string,
computedStyles: CSSStyleDeclaration,
stateObj?: HassEntity
): string | undefined {
if (!stateObj || !stateActive(stateObj, state)) {
const rgb = cssToRgb("--rgb-disabled-color", computedStyles);
if (!rgb) return undefined;
return rgb2hex(rgb);
}
const color = stateColor(stateObj, state);
if (!color) return undefined;
const domain = computeDomain(stateObj.entity_id);
const rgb = cssToRgb(`--rgb-state-${color}-color`, computedStyles);
if (!rgb) return undefined;
const shade = DOMAIN_STATE_SHADES[domain]?.[state] as number | number;
if (!shade) {
return rgb2hex(rgb);
}
return lab2hex(labBrighten(rgb2lab(rgb), shade));
}
let colorIndex = 0;
const stateColorMap: Map<string, string> = new Map();
function computeTimeLineGenericColor(
state: string,
computedStyles: CSSStyleDeclaration
): string {
if (stateColorMap.has(state)) {
return stateColorMap.get(state)!;
}
const color = getGraphColorByIndex(colorIndex, computedStyles);
colorIndex++;
stateColorMap.set(state, color);
return color;
}
export function computeTimelineColor(
state: string,
computedStyles: CSSStyleDeclaration,
stateObj?: HassEntity
): string {
return (
computeTimelineStateColor(state, computedStyles, stateObj) ||
computeTimeLineGenericColor(state, computedStyles)
);
}

View File

@ -2,6 +2,7 @@ import { getColorByIndex } from "../common/color/colors";
import { computeDomain } from "../common/entity/compute_domain"; import { computeDomain } from "../common/entity/compute_domain";
import { computeStateName } from "../common/entity/compute_state_name"; import { computeStateName } from "../common/entity/compute_state_name";
import type { HomeAssistant } from "../types"; import type { HomeAssistant } from "../types";
import { UNAVAILABLE_STATES } from "./entity";
export interface Calendar { export interface Calendar {
entity_id: string; entity_id: string;
@ -56,13 +57,14 @@ export const fetchCalendarEvents = async (
start: Date, start: Date,
end: Date, end: Date,
calendars: Calendar[] calendars: Calendar[]
): Promise<CalendarEvent[]> => { ): Promise<{ events: CalendarEvent[]; errors: string[] }> => {
const params = encodeURI( const params = encodeURI(
`?start=${start.toISOString()}&end=${end.toISOString()}` `?start=${start.toISOString()}&end=${end.toISOString()}`
); );
const calEvents: CalendarEvent[] = []; const calEvents: CalendarEvent[] = [];
const promises: Promise<any>[] = []; const errors: string[] = [];
const promises: Promise<CalendarEvent[]>[] = [];
calendars.forEach((cal) => { calendars.forEach((cal) => {
promises.push( promises.push(
@ -73,9 +75,15 @@ export const fetchCalendarEvents = async (
); );
}); });
const results = await Promise.all(promises); for (const [idx, promise] of promises.entries()) {
let result: CalendarEvent[];
results.forEach((result, idx) => { try {
// eslint-disable-next-line no-await-in-loop
result = await promise;
} catch (err) {
errors.push(calendars[idx].entity_id);
continue;
}
const cal = calendars[idx]; const cal = calendars[idx];
result.forEach((ev) => { result.forEach((ev) => {
const eventStart = getCalendarDate(ev.start); const eventStart = getCalendarDate(ev.start);
@ -104,9 +112,9 @@ export const fetchCalendarEvents = async (
calEvents.push(event); calEvents.push(event);
}); });
}); }
return calEvents; return { events: calEvents, errors };
}; };
const getCalendarDate = (dateObj: any): string | undefined => { const getCalendarDate = (dateObj: any): string | undefined => {
@ -127,7 +135,11 @@ const getCalendarDate = (dateObj: any): string | undefined => {
export const getCalendars = (hass: HomeAssistant): Calendar[] => export const getCalendars = (hass: HomeAssistant): Calendar[] =>
Object.keys(hass.states) Object.keys(hass.states)
.filter((eid) => computeDomain(eid) === "calendar") .filter(
(eid) =>
computeDomain(eid) === "calendar" &&
!UNAVAILABLE_STATES.includes(hass.states[eid].state)
)
.sort() .sort()
.map((eid, idx) => ({ .map((eid, idx) => ({
entity_id: eid, entity_id: eid,

View File

@ -67,12 +67,14 @@ export class MoreInfoHistory extends LitElement {
.statTypes=${statTypes} .statTypes=${statTypes}
.names=${this._statNames} .names=${this._statNames}
hideLegend hideLegend
.showNames=${false}
></statistics-chart>` ></statistics-chart>`
: html`<state-history-charts : html`<state-history-charts
up-to-now up-to-now
.hass=${this.hass} .hass=${this.hass}
.historyData=${this._stateHistory} .historyData=${this._stateHistory}
.isLoadingData=${!this._stateHistory} .isLoadingData=${!this._stateHistory}
.showNames=${false}
></state-history-charts>`}` ></state-history-charts>`}`
: ""}`; : ""}`;
} }

View File

@ -1,7 +1,13 @@
import "@material/mwc-button"; import "@material/mwc-button";
import { mdiClose } from "@mdi/js"; import { mdiClose } from "@mdi/js";
import { ComboBoxLitRenderer } from "@vaadin/combo-box/lit"; import { ComboBoxLitRenderer } from "@vaadin/combo-box/lit";
import { addDays, addHours, startOfHour } from "date-fns/esm"; import {
addDays,
addHours,
addMilliseconds,
differenceInMilliseconds,
startOfHour,
} from "date-fns/esm";
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { customElement, property, state } from "lit/decorators"; import { customElement, property, state } from "lit/decorators";
import memoizeOne from "memoize-one"; import memoizeOne from "memoize-one";
@ -35,6 +41,8 @@ class DialogCalendarEventEditor extends LitElement {
@state() private _error?: string; @state() private _error?: string;
@state() private _info?: string;
@state() private _params?: CalendarEventDetailDialogParams; @state() private _params?: CalendarEventDetailDialogParams;
@state() private _calendars: Calendar[] = []; @state() private _calendars: Calendar[] = [];
@ -57,6 +65,7 @@ class DialogCalendarEventEditor extends LitElement {
public showDialog(params: CalendarEventEditDialogParams): void { public showDialog(params: CalendarEventEditDialogParams): void {
this._error = undefined; this._error = undefined;
this._info = undefined;
this._params = params; this._params = params;
this._calendars = params.calendars; this._calendars = params.calendars;
this._calendarId = params.calendarId || this._calendars[0].entity_id; this._calendarId = params.calendarId || this._calendars[0].entity_id;
@ -120,6 +129,14 @@ class DialogCalendarEventEditor extends LitElement {
${this._error ${this._error
? html`<ha-alert alert-type="error">${this._error}</ha-alert>` ? html`<ha-alert alert-type="error">${this._error}</ha-alert>`
: ""} : ""}
${this._info
? html`<ha-alert
alert-type="info"
dismissable
@alert-dismissed-clicked=${this._clearInfo}
>${this._info}</ha-alert
>`
: ""}
<ha-textfield <ha-textfield
class="summary" class="summary"
@ -260,6 +277,10 @@ class DialogCalendarEventEditor extends LitElement {
} }
); );
private _clearInfo() {
this._info = undefined;
}
private _handleSummaryChanged(ev) { private _handleSummaryChanged(ev) {
this._summary = ev.target.value; this._summary = ev.target.value;
} }
@ -277,9 +298,30 @@ class DialogCalendarEventEditor extends LitElement {
} }
private _startDateChanged(ev: CustomEvent) { private _startDateChanged(ev: CustomEvent) {
// Store previous event duration
const duration = differenceInMilliseconds(this._dtend!, this._dtstart!);
this._dtstart = new Date( this._dtstart = new Date(
ev.detail.value + "T" + this._dtstart!.toISOString().split("T")[1] ev.detail.value + "T" + this._dtstart!.toISOString().split("T")[1]
); );
// Prevent that the end time can be before the start time. Try to keep the
// duration the same.
if (this._dtend! <= this._dtstart!) {
const newEnd = addMilliseconds(this._dtstart, duration);
// en-CA locale used for date format YYYY-MM-DD
// en-GB locale used for 24h time format HH:MM:SS
this._dtend = new Date(
`${newEnd.toLocaleDateString("en-CA", {
timeZone: this.hass.config.time_zone,
})}T${newEnd.toLocaleTimeString("en-GB", {
timeZone: this.hass.config.time_zone,
})}`
);
this._info = this.hass.localize(
"ui.components.calendar.event.end_auto_adjusted"
);
}
} }
private _endDateChanged(ev: CustomEvent) { private _endDateChanged(ev: CustomEvent) {
@ -289,9 +331,28 @@ class DialogCalendarEventEditor extends LitElement {
} }
private _startTimeChanged(ev: CustomEvent) { private _startTimeChanged(ev: CustomEvent) {
// Store previous event duration
const duration = differenceInMilliseconds(this._dtend!, this._dtstart!);
this._dtstart = new Date( this._dtstart = new Date(
this._dtstart!.toISOString().split("T")[0] + "T" + ev.detail.value this._dtstart!.toISOString().split("T")[0] + "T" + ev.detail.value
); );
// Prevent that the end time can be before the start time. Try to keep the
// duration the same.
if (this._dtend! <= this._dtstart!) {
const newEnd = addMilliseconds(new Date(this._dtstart), duration);
this._dtend = new Date(
`${newEnd.toLocaleDateString("en-CA", {
timeZone: this.hass.config.time_zone,
})}T${newEnd.toLocaleTimeString("en-GB", {
timeZone: this.hass.config.time_zone,
})}`
);
this._info = this.hass.localize(
"ui.components.calendar.event.end_auto_adjusted"
);
}
} }
private _endTimeChanged(ev: CustomEvent) { private _endTimeChanged(ev: CustomEvent) {
@ -337,6 +398,13 @@ class DialogCalendarEventEditor extends LitElement {
return; return;
} }
if (this._dtend! <= this._dtstart!) {
this._error = this.hass.localize(
"ui.components.calendar.event.invalid_duration"
);
return;
}
this._submitting = true; this._submitting = true;
try { try {
await createCalendarEvent( await createCalendarEvent(

View File

@ -97,6 +97,8 @@ export class HAFullCalendar extends LitElement {
@property() public initialView: FullCalendarView = "dayGridMonth"; @property() public initialView: FullCalendarView = "dayGridMonth";
@property({ attribute: false }) public error?: string = undefined;
private calendar?: Calendar; private calendar?: Calendar;
private _viewButtons?: ToggleButton[]; private _viewButtons?: ToggleButton[];
@ -116,6 +118,14 @@ export class HAFullCalendar extends LitElement {
return html` return html`
${this.calendar ${this.calendar
? html` ? html`
${this.error
? html`<ha-alert
alert-type="error"
dismissable
@alert-dismissed-clicked=${this._clearError}
>${this.error}</ha-alert
>`
: ""}
<div class="header"> <div class="header">
${!this.narrow ${!this.narrow
? html` ? html`
@ -380,6 +390,10 @@ export class HAFullCalendar extends LitElement {
); );
}); });
private _clearError() {
this.error = undefined;
}
static get styles(): CSSResultGroup { static get styles(): CSSResultGroup {
return [ return [
haStyle, haStyle,
@ -449,6 +463,11 @@ export class HAFullCalendar extends LitElement {
z-index: 1; z-index: 1;
} }
ha-alert {
display: block;
margin: 4px 0;
}
#calendar { #calendar {
flex-grow: 1; flex-grow: 1;
background-color: var( background-color: var(

View File

@ -15,6 +15,7 @@ import { customElement, property, state } from "lit/decorators";
import { styleMap } from "lit/directives/style-map"; import { styleMap } from "lit/directives/style-map";
import { LocalStorage } from "../../common/decorators/local-storage"; import { LocalStorage } from "../../common/decorators/local-storage";
import { HASSDomEvent } from "../../common/dom/fire_event"; import { HASSDomEvent } from "../../common/dom/fire_event";
import { computeStateName } from "../../common/entity/compute_state_name";
import "../../components/ha-card"; import "../../components/ha-card";
import "../../components/ha-icon-button"; import "../../components/ha-icon-button";
import "../../components/ha-menu-button"; import "../../components/ha-menu-button";
@ -40,6 +41,8 @@ class PanelCalendar extends LitElement {
@state() private _events: CalendarEvent[] = []; @state() private _events: CalendarEvent[] = [];
@state() private _error?: string = undefined;
@LocalStorage("deSelectedCalendars", true) @LocalStorage("deSelectedCalendars", true)
private _deSelectedCalendars: string[] = []; private _deSelectedCalendars: string[] = [];
@ -101,6 +104,7 @@ class PanelCalendar extends LitElement {
.calendars=${this._calendars} .calendars=${this._calendars}
.narrow=${this.narrow} .narrow=${this.narrow}
.hass=${this.hass} .hass=${this.hass}
.error=${this._error}
@view-changed=${this._handleViewChanged} @view-changed=${this._handleViewChanged}
></ha-full-calendar> ></ha-full-calendar>
</div> </div>
@ -118,9 +122,9 @@ class PanelCalendar extends LitElement {
start: Date, start: Date,
end: Date, end: Date,
calendars: Calendar[] calendars: Calendar[]
): Promise<CalendarEvent[]> { ): Promise<{ events: CalendarEvent[]; errors: string[] }> {
if (!calendars.length) { if (!calendars.length) {
return []; return { events: [], errors: [] };
} }
return fetchCalendarEvents(this.hass, start, end, calendars); return fetchCalendarEvents(this.hass, start, end, calendars);
@ -135,8 +139,9 @@ class PanelCalendar extends LitElement {
const checked = ev.target.checked; const checked = ev.target.checked;
if (checked) { if (checked) {
const events = await this._fetchEvents(this._start!, this._end!, [cal]); const result = await this._fetchEvents(this._start!, this._end!, [cal]);
this._events = [...this._events, ...events]; this._events = [...this._events, ...result.events];
this._handleErrors(result.errors);
this._deSelectedCalendars = this._deSelectedCalendars.filter( this._deSelectedCalendars = this._deSelectedCalendars.filter(
(deCal) => deCal !== cal.entity_id (deCal) => deCal !== cal.entity_id
); );
@ -161,19 +166,40 @@ class PanelCalendar extends LitElement {
): Promise<void> { ): Promise<void> {
this._start = ev.detail.start; this._start = ev.detail.start;
this._end = ev.detail.end; this._end = ev.detail.end;
this._events = await this._fetchEvents( const result = await this._fetchEvents(
this._start, this._start,
this._end, this._end,
this._selectedCalendars this._selectedCalendars
); );
this._events = result.events;
this._handleErrors(result.errors);
} }
private async _handleRefresh(): Promise<void> { private async _handleRefresh(): Promise<void> {
this._events = await this._fetchEvents( const result = await this._fetchEvents(
this._start!, this._start!,
this._end!, this._end!,
this._selectedCalendars this._selectedCalendars
); );
this._events = result.events;
this._handleErrors(result.errors);
}
private _handleErrors(error_entity_ids: string[]) {
this._error = undefined;
if (error_entity_ids.length > 0) {
const nameList = error_entity_ids
.map((error_entity_id) =>
this.hass!.states[error_entity_id]
? computeStateName(this.hass!.states[error_entity_id])
: error_entity_id
)
.join(", ");
this._error = `${this.hass!.localize(
"ui.components.calendar.event_retrieval_error"
)} ${nameList}`;
}
} }
static get styles(): CSSResultGroup { static get styles(): CSSResultGroup {

View File

@ -75,6 +75,7 @@ import {
showDeviceRegistryDetailDialog, showDeviceRegistryDetailDialog,
} from "./device-registry-detail/show-dialog-device-registry-detail"; } from "./device-registry-detail/show-dialog-device-registry-detail";
import "../../../layouts/hass-subpage"; import "../../../layouts/hass-subpage";
import { SENSOR_ENTITIES } from "../../../common/const";
export interface EntityRegistryStateEntry extends EntityRegistryEntry { export interface EntityRegistryStateEntry extends EntityRegistryEntry {
stateName?: string | null; stateName?: string | null;
@ -172,13 +173,7 @@ export class HaConfigDevicePage extends LitElement {
const result = groupBy(entities, (entry) => const result = groupBy(entities, (entry) =>
entry.entity_category entry.entity_category
? entry.entity_category ? entry.entity_category
: [ : SENSOR_ENTITIES.includes(computeDomain(entry.entity_id))
"sensor",
"binary_sensor",
"camera",
"device_tracker",
"weather",
].includes(computeDomain(entry.entity_id))
? "sensor" ? "sensor"
: "control" : "control"
) as Record< ) as Record<

View File

@ -117,7 +117,7 @@ const OVERRIDE_NUMBER_UNITS = {
const OVERRIDE_SENSOR_UNITS = { const OVERRIDE_SENSOR_UNITS = {
distance: ["cm", "ft", "in", "km", "m", "mi", "mm", "yd"], distance: ["cm", "ft", "in", "km", "m", "mi", "mm", "yd"],
gas: ["ft³", "m³"], gas: ["ft³", "m³"],
precipitation: ["in", "mm"], precipitation: ["cm", "in", "mm"],
precipitation_intensity: ["in/d", "in/h", "mm/d", "mm/h"], precipitation_intensity: ["in/d", "in/h", "mm/d", "mm/h"],
pressure: ["hPa", "Pa", "kPa", "bar", "cbar", "mbar", "mmHg", "inHg", "psi"], pressure: ["hPa", "Pa", "kPa", "bar", "cbar", "mbar", "mmHg", "inHg", "psi"],
speed: ["ft/s", "in/d", "in/h", "km/h", "kn", "m/s", "mm/d", "mm/h", "mph"], speed: ["ft/s", "in/d", "in/h", "km/h", "kn", "m/s", "mm/d", "mm/h", "mph"],

View File

@ -6,7 +6,7 @@ import interactionPlugin from "@fullcalendar/interaction";
import timeGridPlugin from "@fullcalendar/timegrid"; import timeGridPlugin from "@fullcalendar/timegrid";
// @ts-ignore // @ts-ignore
import timegridStyle from "@fullcalendar/timegrid/main.css"; import timegridStyle from "@fullcalendar/timegrid/main.css";
import { isSameDay } from "date-fns"; import { addDays, isSameDay, isSameWeek, nextDay } from "date-fns";
import { import {
css, css,
CSSResultGroup, CSSResultGroup,
@ -17,15 +17,17 @@ import {
unsafeCSS, unsafeCSS,
} from "lit"; } from "lit";
import { customElement, property, state } from "lit/decorators"; import { customElement, property, state } from "lit/decorators";
import { firstWeekdayIndex } from "../../../../common/datetime/first_weekday";
import { formatTime24h } from "../../../../common/datetime/format_time"; import { formatTime24h } from "../../../../common/datetime/format_time";
import { useAmPm } from "../../../../common/datetime/use_am_pm"; import { useAmPm } from "../../../../common/datetime/use_am_pm";
import { firstWeekdayIndex } from "../../../../common/datetime/first_weekday";
import { fireEvent } from "../../../../common/dom/fire_event"; import { fireEvent } from "../../../../common/dom/fire_event";
import { debounce } from "../../../../common/util/debounce";
import "../../../../components/ha-icon-picker"; import "../../../../components/ha-icon-picker";
import "../../../../components/ha-textfield"; import "../../../../components/ha-textfield";
import { Schedule, ScheduleDay, weekdays } from "../../../../data/schedule"; import { Schedule, ScheduleDay, weekdays } from "../../../../data/schedule";
import { haStyle } from "../../../../resources/styles"; import { haStyle } from "../../../../resources/styles";
import { HomeAssistant } from "../../../../types"; import { HomeAssistant } from "../../../../types";
import { installResizeObserver } from "../../../lovelace/common/install-resize-observer";
const defaultFullCalendarConfig: CalendarOptions = { const defaultFullCalendarConfig: CalendarOptions = {
plugins: [timeGridPlugin, interactionPlugin], plugins: [timeGridPlugin, interactionPlugin],
@ -71,6 +73,8 @@ class HaScheduleForm extends LitElement {
private _item?: Schedule; private _item?: Schedule;
private _resizeObserver?: ResizeObserver;
set item(item: Schedule) { set item(item: Schedule) {
this._item = item; this._item = item;
if (item) { if (item) {
@ -104,6 +108,40 @@ class HaScheduleForm extends LitElement {
); );
} }
public connectedCallback(): void {
super.connectedCallback();
this.updateComplete.then(() => this._attachObserver());
}
public disconnectedCallback(): void {
if (this._resizeObserver) {
this._resizeObserver.disconnect();
}
}
private _measureForm() {
const form = this.shadowRoot!.querySelector(".form");
if (!form) {
return;
}
this.calendar?.updateSize();
}
private async _attachObserver(): Promise<void> {
if (!this._resizeObserver) {
await installResizeObserver();
this._resizeObserver = new ResizeObserver(
debounce(() => this._measureForm(), 250, false)
);
}
const form = this.shadowRoot!.querySelector(".form");
if (!form) {
return;
}
this._resizeObserver.observe(form);
}
protected render(): TemplateResult { protected render(): TemplateResult {
if (!this.hass) { if (!this.hass) {
return html``; return html``;
@ -207,11 +245,6 @@ class HaScheduleForm extends LitElement {
private get _events() { private get _events() {
const events: any[] = []; const events: any[] = [];
const currentDay = new Date().getDay();
const baseDay =
currentDay === 0 && firstWeekdayIndex(this.hass.locale) === 1
? 7
: currentDay;
for (const [i, day] of weekdays.entries()) { for (const [i, day] of weekdays.entries()) {
if (!this[`_${day}`].length) { if (!this[`_${day}`].length) {
@ -219,14 +252,15 @@ class HaScheduleForm extends LitElement {
} }
this[`_${day}`].forEach((item: ScheduleDay, index: number) => { this[`_${day}`].forEach((item: ScheduleDay, index: number) => {
// Add 7 to 0 because we start the calendar on Monday, except when the locale says otherwise (firstWeekdayIndex() != 1) let date = nextDay(new Date(), i as Day);
const distance = if (
i - !isSameWeek(date, new Date(), {
baseDay + weekStartsOn: firstWeekdayIndex(this.hass.locale),
(i === 0 && firstWeekdayIndex(this.hass.locale) === 1 ? 7 : 0); })
) {
const start = new Date(); date = addDays(date, -7);
start.setDate(start.getDate() + distance); }
const start = new Date(date);
const start_tokens = item.from.split(":"); const start_tokens = item.from.split(":");
start.setHours( start.setHours(
parseInt(start_tokens[0]), parseInt(start_tokens[0]),
@ -235,8 +269,7 @@ class HaScheduleForm extends LitElement {
0 0
); );
const end = new Date(); const end = new Date(date);
end.setDate(end.getDate() + distance);
const end_tokens = item.to.split(":"); const end_tokens = item.to.split(":");
end.setHours(parseInt(end_tokens[0]), parseInt(end_tokens[1]), 0, 0); end.setHours(parseInt(end_tokens[0]), parseInt(end_tokens[1]), 0, 0);
@ -397,12 +430,45 @@ class HaScheduleForm extends LitElement {
--fc-border-color: var(--divider-color); --fc-border-color: var(--divider-color);
--fc-event-border-color: var(--divider-color); --fc-event-border-color: var(--divider-color);
} }
.fc-scroller {
overflow-x: visible !important;
}
.fc-v-event .fc-event-time { .fc-v-event .fc-event-time {
white-space: inherit; white-space: inherit;
} }
.fc-theme-standard .fc-scrollgrid {
border: 1px solid var(--divider-color);
border-radius: var(--mdc-shape-small, 4px);
}
.fc-scrollgrid-section-header td {
border: none;
}
:host([narrow]) .fc-scrollgrid-sync-table {
overflow: hidden;
}
table.fc-scrollgrid-sync-table
tbody
tr:first-child
.fc-daygrid-day-top {
padding-top: 0;
}
.fc-scroller::-webkit-scrollbar {
width: 0.4rem;
height: 0.4rem;
}
.fc-scroller::-webkit-scrollbar-thumb {
-webkit-border-radius: 4px;
border-radius: 4px;
background: var(--scrollbar-thumb-color);
}
.fc-scroller {
overflow-y: auto;
scrollbar-color: var(--scrollbar-thumb-color) transparent;
scrollbar-width: thin;
}
.fc-timegrid-event-short .fc-event-time:after {
content: ""; /* prevent trailing dash in half hour events since we do not have event titles */
}
a { a {
color: inherit !important; color: inherit !important;

View File

@ -163,7 +163,6 @@ class HaPanelHistory extends SubscribeMixin(LitElement) {
.hass=${this.hass} .hass=${this.hass}
.historyData=${this._stateHistory} .historyData=${this._stateHistory}
.endTime=${this._endDate} .endTime=${this._endDate}
no-single
> >
</state-history-charts> </state-history-charts>
`} `}

View File

@ -1,5 +1,6 @@
import "@lit-labs/virtualizer"; import "@lit-labs/virtualizer";
import { VisibilityChangedEvent } from "@lit-labs/virtualizer/Virtualizer"; import { VisibilityChangedEvent } from "@lit-labs/virtualizer/Virtualizer";
import type { HassEntity } from "home-assistant-js-websocket";
import { import {
css, css,
CSSResultGroup, CSSResultGroup,
@ -8,37 +9,35 @@ import {
PropertyValues, PropertyValues,
TemplateResult, TemplateResult,
} from "lit"; } from "lit";
import type { HassEntity } from "home-assistant-js-websocket";
import { customElement, eventOptions, property } from "lit/decorators"; import { customElement, eventOptions, property } from "lit/decorators";
import { classMap } from "lit/directives/class-map"; import { classMap } from "lit/directives/class-map";
import { styleMap } from "lit/directives/style-map"; import { styleMap } from "lit/directives/style-map";
import { isComponentLoaded } from "../../common/config/is_component_loaded";
import { formatDate } from "../../common/datetime/format_date"; import { formatDate } from "../../common/datetime/format_date";
import { formatTimeWithSeconds } from "../../common/datetime/format_time"; import { formatTimeWithSeconds } from "../../common/datetime/format_time";
import { restoreScroll } from "../../common/decorators/restore-scroll"; import { restoreScroll } from "../../common/decorators/restore-scroll";
import { fireEvent } from "../../common/dom/fire_event"; import { fireEvent } from "../../common/dom/fire_event";
import { computeDomain } from "../../common/entity/compute_domain"; import { computeDomain } from "../../common/entity/compute_domain";
import { isComponentLoaded } from "../../common/config/is_component_loaded"; import { navigate } from "../../common/navigate";
import { stateActive } from "../../common/entity/state_active"; import { computeTimelineColor } from "../../components/chart/timeline-chart/timeline-color";
import { stateColorCss } from "../../common/entity/state_color";
import "../../components/entity/state-badge"; import "../../components/entity/state-badge";
import "../../components/ha-circular-progress"; import "../../components/ha-circular-progress";
import "../../components/ha-icon-next";
import "../../components/ha-relative-time"; import "../../components/ha-relative-time";
import { import {
createHistoricState, createHistoricState,
localizeTriggerSource,
localizeStateMessage, localizeStateMessage,
localizeTriggerSource,
LogbookEntry, LogbookEntry,
} from "../../data/logbook"; } from "../../data/logbook";
import { TraceContexts } from "../../data/trace"; import { TraceContexts } from "../../data/trace";
import { import {
buttonLinkStyle,
haStyle, haStyle,
haStyleScrollbar, haStyleScrollbar,
buttonLinkStyle,
} from "../../resources/styles"; } from "../../resources/styles";
import { HomeAssistant } from "../../types"; import { HomeAssistant } from "../../types";
import { brandsUrl } from "../../util/brands-url"; import { brandsUrl } from "../../util/brands-url";
import "../../components/ha-icon-next";
import { navigate } from "../../common/navigate";
declare global { declare global {
interface HASSDomEvents { interface HASSDomEvents {
@ -264,14 +263,15 @@ class HaLogbookRenderer extends LitElement {
const stateObj = this.hass.states[item.entity_id!] as const stateObj = this.hass.states[item.entity_id!] as
| HassEntity | HassEntity
| undefined; | undefined;
const computedStyles = getComputedStyle(this);
const color = const color =
stateObj && stateActive(stateObj, item.state) item.state !== undefined
? stateColorCss(stateObj, item.state) ? computeTimelineColor(item.state, computedStyles, stateObj)
: undefined; : undefined;
const style = { const style = {
"--indicator-color": color, backgroundColor: color,
}; };
return html` <div class="indicator" style=${styleMap(style)}></div> `; return html` <div class="indicator" style=${styleMap(style)}></div> `;
@ -577,9 +577,7 @@ class HaLogbookRenderer extends LitElement {
} }
.indicator { .indicator {
background-color: rgb( background-color: rgb(var(--rgb-disabled-color));
var(--indicator-color, var(--rgb-disabled-color))
);
height: 8px; height: 8px;
width: 8px; width: 8px;
border-radius: 4px; border-radius: 4px;

View File

@ -10,6 +10,7 @@ import { customElement, property, state, query } from "lit/decorators";
import { getColorByIndex } from "../../../common/color/colors"; import { getColorByIndex } from "../../../common/color/colors";
import { applyThemesOnElement } from "../../../common/dom/apply_themes_on_element"; import { applyThemesOnElement } from "../../../common/dom/apply_themes_on_element";
import { HASSDomEvent } from "../../../common/dom/fire_event"; import { HASSDomEvent } from "../../../common/dom/fire_event";
import { computeStateName } from "../../../common/entity/compute_state_name";
import { debounce } from "../../../common/util/debounce"; import { debounce } from "../../../common/util/debounce";
import "../../../components/ha-card"; import "../../../components/ha-card";
import { import {
@ -69,6 +70,8 @@ export class HuiCalendarCard extends LitElement implements LovelaceCard {
@state() private _veryNarrow = false; @state() private _veryNarrow = false;
@state() private _error?: string = undefined;
@query("ha-full-calendar", true) private _calendar?: HAFullCalendar; @query("ha-full-calendar", true) private _calendar?: HAFullCalendar;
private _startDate?: Date; private _startDate?: Date;
@ -131,6 +134,7 @@ export class HuiCalendarCard extends LitElement implements LovelaceCard {
.hass=${this.hass} .hass=${this.hass}
.views=${views} .views=${views}
.initialView=${this._config.initial_view!} .initialView=${this._config.initial_view!}
.error=${this._error}
@view-changed=${this._handleViewChanged} @view-changed=${this._handleViewChanged}
></ha-full-calendar> ></ha-full-calendar>
</ha-card> </ha-card>
@ -169,12 +173,28 @@ export class HuiCalendarCard extends LitElement implements LovelaceCard {
return; return;
} }
this._events = await fetchCalendarEvents( this._error = undefined;
const result = await fetchCalendarEvents(
this.hass!, this.hass!,
this._startDate, this._startDate,
this._endDate, this._endDate,
this._calendars this._calendars
); );
this._events = result.events;
if (result.errors.length > 0) {
const nameList = result.errors
.map((error_entity_id) =>
this.hass!.states[error_entity_id]
? computeStateName(this.hass!.states[error_entity_id])
: error_entity_id
)
.join(", ");
this._error = `${this.hass!.localize(
"ui.components.calendar.event_retrieval_error"
)} ${nameList}`;
}
} }
private _measureCard() { private _measureCard() {

View File

@ -144,7 +144,7 @@ export class HuiHistoryGraphCard extends LitElement implements LovelaceCard {
.historyData=${this._stateHistory} .historyData=${this._stateHistory}
.names=${this._names} .names=${this._names}
up-to-now up-to-now
no-single .showNames=${this._config.show_names}
></state-history-charts> ></state-history-charts>
</div> </div>
</ha-card> </ha-card>

View File

@ -128,6 +128,18 @@ export class HuiTileCard extends LitElement implements LovelaceCard {
} }
private _computeStateColor = memoize((entity: HassEntity, color?: string) => { private _computeStateColor = memoize((entity: HassEntity, color?: string) => {
if (UNAVAILABLE_STATES.includes(entity.state)) {
return undefined;
}
// Use default color for person/device_tracker because color is on the badge
if (
computeDomain(entity.entity_id) === "person" ||
computeDomain(entity.entity_id) === "device_tracker"
) {
return "var(--rgb-state-default-color)";
}
if (!stateActive(entity)) { if (!stateActive(entity)) {
return undefined; return undefined;
} }

View File

@ -1,6 +1,5 @@
import { mdiHelp, mdiHome, mdiHomeExportOutline } from "@mdi/js"; import { mdiHome, mdiHomeExportOutline } from "@mdi/js";
import { HassEntity } from "home-assistant-js-websocket"; import { HassEntity } from "home-assistant-js-websocket";
import { UNAVAILABLE_STATES } from "../../../../../data/entity";
import { HomeAssistant } from "../../../../../types"; import { HomeAssistant } from "../../../../../types";
import { ComputeBadgeFunction } from "./tile-badge"; import { ComputeBadgeFunction } from "./tile-badge";
@ -17,20 +16,18 @@ function getZone(entity: HassEntity, hass: HomeAssistant) {
function personBadgeIcon(entity: HassEntity) { function personBadgeIcon(entity: HassEntity) {
const state = entity.state; const state = entity.state;
if (UNAVAILABLE_STATES.includes(state)) {
return mdiHelp;
}
return state === "not_home" ? mdiHomeExportOutline : mdiHome; return state === "not_home" ? mdiHomeExportOutline : mdiHome;
} }
function personBadgeColor(entity: HassEntity, inZone?: boolean) { function personBadgeColor(entity: HassEntity) {
if (inZone) { switch (entity.state) {
return "var(--rgb-state-person-zone-color)"; case "home":
return "var(--rgb-badge-person-home-color)";
case "not_home":
return "var(--rgb-badge-person-not-home-color)";
default:
return "var(--rgb-badge-person-zone-color)";
} }
const state = entity.state;
return state === "not_home"
? "var(--rgb-state-person-not-home-color)"
: "var(--rgb-state-person-home-color)";
} }
export const computePersonBadge: ComputeBadgeFunction = (stateObj, hass) => { export const computePersonBadge: ComputeBadgeFunction = (stateObj, hass) => {
@ -39,6 +36,6 @@ export const computePersonBadge: ComputeBadgeFunction = (stateObj, hass) => {
return { return {
iconPath: personBadgeIcon(stateObj), iconPath: personBadgeIcon(stateObj),
icon: zone?.attributes.icon, icon: zone?.attributes.icon,
color: personBadgeColor(stateObj, Boolean(zone)), color: personBadgeColor(stateObj),
}; };
}; };

View File

@ -1,5 +1,7 @@
import { mdiExclamationThick } from "@mdi/js";
import { HassEntity } from "home-assistant-js-websocket"; import { HassEntity } from "home-assistant-js-websocket";
import { computeDomain } from "../../../../../common/entity/compute_domain"; import { computeDomain } from "../../../../../common/entity/compute_domain";
import { UNAVAILABLE, UNKNOWN } from "../../../../../data/entity";
import { HomeAssistant } from "../../../../../types"; import { HomeAssistant } from "../../../../../types";
import { computeClimateBadge } from "./tile-badge-climate"; import { computeClimateBadge } from "./tile-badge-climate";
import { computePersonBadge } from "./tile-badge-person"; import { computePersonBadge } from "./tile-badge-person";
@ -16,6 +18,15 @@ export type ComputeBadgeFunction = (
) => TileBadge | undefined; ) => TileBadge | undefined;
export const computeTileBadge: ComputeBadgeFunction = (stateObj, hass) => { export const computeTileBadge: ComputeBadgeFunction = (stateObj, hass) => {
if (stateObj.state === UNKNOWN) {
return undefined;
}
if (stateObj.state === UNAVAILABLE) {
return {
color: "var(--rgb-orange-color)",
iconPath: mdiExclamationThick,
};
}
const domain = computeDomain(stateObj.entity_id); const domain = computeDomain(stateObj.entity_id);
switch (domain) { switch (domain) {
case "person": case "person":

View File

@ -300,6 +300,7 @@ export interface HistoryGraphCardConfig extends LovelaceCardConfig {
hours_to_show?: number; hours_to_show?: number;
refresh_interval?: number; refresh_interval?: number;
title?: string; title?: string;
show_names?: boolean;
} }
export interface StatisticsGraphCardConfig extends LovelaceCardConfig { export interface StatisticsGraphCardConfig extends LovelaceCardConfig {

View File

@ -1,4 +1,5 @@
import { HassEntities, HassEntity } from "home-assistant-js-websocket"; import { HassEntities, HassEntity } from "home-assistant-js-websocket";
import { SENSOR_ENTITIES } from "../../../common/const";
import { computeDomain } from "../../../common/entity/compute_domain"; import { computeDomain } from "../../../common/entity/compute_domain";
import { computeStateDomain } from "../../../common/entity/compute_state_domain"; import { computeStateDomain } from "../../../common/entity/compute_state_domain";
import { computeStateName } from "../../../common/entity/compute_state_name"; import { computeStateName } from "../../../common/entity/compute_state_name";
@ -23,7 +24,7 @@ import {
PictureEntityCardConfig, PictureEntityCardConfig,
ThermostatCardConfig, ThermostatCardConfig,
} from "../cards/types"; } from "../cards/types";
import { LovelaceRowConfig } from "../entity-rows/types"; import { EntityConfig } from "../entity-rows/types";
import { ButtonsHeaderFooterConfig } from "../header-footer/types"; import { ButtonsHeaderFooterConfig } from "../header-footer/types";
const HIDE_DOMAIN = new Set([ const HIDE_DOMAIN = new Set([
@ -96,14 +97,15 @@ const splitByAreaDevice = (
}; };
export const computeCards = ( export const computeCards = (
states: Array<[string, HassEntity?]>, states: HassEntities,
entityIds: string[],
entityCardOptions: Partial<EntitiesCardConfig>, entityCardOptions: Partial<EntitiesCardConfig>,
renderFooterEntities = true renderFooterEntities = true
): LovelaceCardConfig[] => { ): LovelaceCardConfig[] => {
const cards: LovelaceCardConfig[] = []; const cards: LovelaceCardConfig[] = [];
// For entity card // For entity card
const entities: Array<string | LovelaceRowConfig> = []; const entitiesConf: Array<string | EntityConfig> = [];
const titlePrefix = entityCardOptions.title const titlePrefix = entityCardOptions.title
? entityCardOptions.title.toLowerCase() ? entityCardOptions.title.toLowerCase()
@ -111,7 +113,8 @@ export const computeCards = (
const footerEntities: ButtonsHeaderFooterConfig["entities"] = []; const footerEntities: ButtonsHeaderFooterConfig["entities"] = [];
for (const [entityId, stateObj] of states) { for (const entityId of entityIds) {
const stateObj = states[entityId];
const domain = computeDomain(entityId); const domain = computeDomain(entityId);
if (domain === "alarm_control_panel") { if (domain === "alarm_control_panel") {
@ -200,20 +203,49 @@ export const computeCards = (
} }
: entityId; : entityId;
entities.push(entityConf); entitiesConf.push(entityConf);
} }
} }
entitiesConf.sort((a, b) => {
const entityIdA = typeof a === "string" ? a : a.entity;
const entityIdB = typeof b === "string" ? b : b.entity;
const categoryA = SENSOR_ENTITIES.includes(computeDomain(entityIdA))
? "sensor"
: "control";
const categoryB = SENSOR_ENTITIES.includes(computeDomain(entityIdB))
? "sensor"
: "control";
if (categoryA !== categoryB) {
return categoryA === "sensor" ? 1 : -1;
}
return stringCompare(
typeof a === "string"
? states[a]
? computeStateName(states[a])
: ""
: a.name || "",
typeof b === "string"
? states[b]
? computeStateName(states[b])
: ""
: b.name || ""
);
});
// If we ended up with footer entities but no normal entities, // If we ended up with footer entities but no normal entities,
// render the footer entities as normal entities. // render the footer entities as normal entities.
if (entities.length === 0 && footerEntities.length > 0) { if (entitiesConf.length === 0 && footerEntities.length > 0) {
return computeCards(states, entityCardOptions, false); return computeCards(states, entityIds, entityCardOptions, false);
} }
if (entities.length > 0 || footerEntities.length > 0) { if (entitiesConf.length > 0 || footerEntities.length > 0) {
const card: EntitiesCardConfig = { const card: EntitiesCardConfig = {
type: "entities", type: "entities",
entities, entities: entitiesConf,
...entityCardOptions, ...entityCardOptions,
}; };
if (footerEntities.length > 0) { if (footerEntities.length > 0) {
@ -354,15 +386,10 @@ export const generateViewConfig = (
for (const groupEntity of splitted.groups) { for (const groupEntity of splitted.groups) {
cards.push( cards.push(
...computeCards( ...computeCards(entities, groupEntity.attributes.entity_id, {
groupEntity.attributes.entity_id.map( title: computeStateName(groupEntity),
(entityId): [string, HassEntity] => [entityId, entities[entityId]] show_header_toggle: groupEntity.attributes.control !== "hidden",
), })
{
title: computeStateName(groupEntity),
show_header_toggle: groupEntity.attributes.control !== "hidden",
}
)
); );
} }
@ -398,17 +425,13 @@ export const generateViewConfig = (
.forEach((domain) => { .forEach((domain) => {
cards.push( cards.push(
...computeCards( ...computeCards(
ungroupedEntitites[domain] entities,
.sort((a, b) => ungroupedEntitites[domain].sort((a, b) =>
stringCompare( stringCompare(
computeStateName(entities[a]), computeStateName(entities[a]),
computeStateName(entities[b]) computeStateName(entities[b])
)
) )
.map((entityId): [string, HassEntity] => [ ),
entityId,
entities[entityId],
]),
{ {
title: domainTranslations[domain], title: domainTranslations[domain],
} }
@ -466,28 +489,35 @@ export const generateDefaultViewConfig = (
groupOrders groupOrders
); );
const splittedCards: LovelaceCardConfig[] = []; const areaCards: LovelaceCardConfig[] = [];
for (const [areaId, areaEntities] of Object.entries( for (const [areaId, areaEntities] of Object.entries(
splittedByAreaDevice.areasWithEntities splittedByAreaDevice.areasWithEntities
)) { )) {
const area = areaEntries[areaId]; const area = areaEntries[areaId];
splittedCards.push( areaCards.push(
...computeCards( ...computeCards(
areaEntities.map((entity) => [entity.entity_id, entity]), entities,
areaEntities.map((entity) => entity.entity_id),
{ {
title: area.name, title: area.name,
} }
) )
); );
} }
areaCards.sort((a, b) => stringCompare(a.title || "", b.title || ""));
const deviceCards: LovelaceCardConfig[] = [];
for (const [deviceId, deviceEntities] of Object.entries( for (const [deviceId, deviceEntities] of Object.entries(
splittedByAreaDevice.devicesWithEntities splittedByAreaDevice.devicesWithEntities
)) { )) {
const device = deviceEntries[deviceId]; const device = deviceEntries[deviceId];
splittedCards.push( deviceCards.push(
...computeCards( ...computeCards(
deviceEntities.map((entity) => [entity.entity_id, entity]), entities,
deviceEntities.map((entity) => entity.entity_id),
{ {
title: title:
device.name_by_user || device.name_by_user ||
@ -503,6 +533,11 @@ export const generateDefaultViewConfig = (
) )
); );
} }
deviceCards.sort((a, b) => stringCompare(a.title || "", b.title || ""));
let energyCard: LovelaceCardConfig | undefined;
if (energyPrefs) { if (energyPrefs) {
// Distribution card requires the grid to be configured // Distribution card requires the grid to be configured
const grid = energyPrefs.energy_sources.find( const grid = energyPrefs.energy_sources.find(
@ -510,17 +545,21 @@ export const generateDefaultViewConfig = (
) as GridSourceTypeEnergyPreference | undefined; ) as GridSourceTypeEnergyPreference | undefined;
if (grid && grid.flow_from.length > 0) { if (grid && grid.flow_from.length > 0) {
splittedCards.push({ energyCard = {
title: localize( title: localize(
"ui.panel.lovelace.cards.energy.energy_distribution.title_today" "ui.panel.lovelace.cards.energy.energy_distribution.title_today"
), ),
type: "energy-distribution", type: "energy-distribution",
link_dashboard: true, link_dashboard: true,
}); };
} }
} }
config.cards!.unshift(...splittedCards); config.cards!.unshift(
...areaCards,
...(energyCard ? [energyCard] : []),
...deviceCards
);
return config; return config;
}; };

View File

@ -30,15 +30,9 @@ export class HuiDialogSuggestCard extends LitElement {
this._params = params; this._params = params;
this._cardConfig = this._cardConfig =
params.cardConfig || params.cardConfig ||
computeCards( computeCards(this.hass.states, params.entities, {
params.entities.map((entityId) => [ title: params.cardTitle,
entityId, });
this.hass.states[entityId],
]),
{
title: params.cardTitle,
}
);
if (!Object.isFrozen(this._cardConfig)) { if (!Object.isFrozen(this._cardConfig)) {
this._cardConfig = deepFreeze(this._cardConfig); this._cardConfig = deepFreeze(this._cardConfig);
} }

View File

@ -4,6 +4,7 @@ import { customElement, property, state } from "lit/decorators";
import { import {
array, array,
assert, assert,
boolean,
number, number,
object, object,
optional, optional,
@ -28,6 +29,7 @@ const cardConfigStruct = assign(
title: optional(string()), title: optional(string()),
hours_to_show: optional(number()), hours_to_show: optional(number()),
refresh_interval: optional(number()), refresh_interval: optional(number()),
show_names: optional(boolean()),
}) })
); );

View File

@ -122,6 +122,7 @@ class HuiCastRow extends LitElement implements LovelaceRow {
await ensureConnectedCastSession(this._castManager!, this.hass.auth); await ensureConnectedCastSession(this._castManager!, this.hass.auth);
castSendShowLovelaceView( castSendShowLovelaceView(
this._castManager!, this._castManager!,
this.hass.auth.data.hassUrl,
this._config!.view, this._config!.view,
this._config!.dashboard this._config!.dashboard
); );

View File

@ -137,10 +137,13 @@ documentContainer.innerHTML = `<custom-style>
/* rgb state color */ /* rgb state color */
--rgb-state-default-color: 68, 115, 158; --rgb-state-default-color: 68, 115, 158;
--rgb-state-active-color: var(--rgb-primary-color);
--rgb-state-alarm-armed-color: var(--rgb-red-color); --rgb-state-alarm-armed-color: var(--rgb-red-color);
--rgb-state-alarm-pending-color: var(--rgb-orange-color); --rgb-state-alarm-pending-color: var(--rgb-orange-color);
--rgb-state-alarm-arming-color: var(--rgb-orange-color); --rgb-state-alarm-arming-color: var(--rgb-orange-color);
--rgb-state-alarm-triggered-color: var(--rgb-red-color); --rgb-state-alarm-triggered-color: var(--rgb-red-color);
--rgb-state-alert-color: var(--rgb-red-color);
--rgb-state-automation-color: var(--rgb-amber-color);
--rgb-state-binary-sensor-color: var(--rgb-primary-color); --rgb-state-binary-sensor-color: var(--rgb-primary-color);
--rgb-state-binary-sensor-alerting-color: var(--rgb-red-color); --rgb-state-binary-sensor-alerting-color: var(--rgb-red-color);
--rgb-state-cover-color: var(--rgb-purple-color); --rgb-state-cover-color: var(--rgb-purple-color);
@ -152,7 +155,6 @@ documentContainer.innerHTML = `<custom-style>
--rgb-state-lock-pending-color: var(--rgb-orange-color); --rgb-state-lock-pending-color: var(--rgb-orange-color);
--rgb-state-media-player-color: var(--rgb-indigo-color); --rgb-state-media-player-color: var(--rgb-indigo-color);
--rgb-state-person-home-color: var(--rgb-green-color); --rgb-state-person-home-color: var(--rgb-green-color);
--rgb-state-person-not-home-color: var(--rgb-red-color);
--rgb-state-person-zone-color: var(--rgb-blue-color); --rgb-state-person-zone-color: var(--rgb-blue-color);
--rgb-state-sensor-battery-high-color: var(--rgb-green-color); --rgb-state-sensor-battery-high-color: var(--rgb-green-color);
--rgb-state-sensor-battery-low-color: var(--rgb-red-color); --rgb-state-sensor-battery-low-color: var(--rgb-red-color);
@ -173,6 +175,11 @@ documentContainer.innerHTML = `<custom-style>
--rgb-state-climate-heat-cool-color: var(--rgb-state-default-color); --rgb-state-climate-heat-cool-color: var(--rgb-state-default-color);
--rgb-state-climate-idle-color: var(--rgb-disabled-color); --rgb-state-climate-idle-color: var(--rgb-disabled-color);
/* rgb state badge color */
--rgb-badge-person-home-color: var(--rgb-state-person-home-color);
--rgb-badge-person-zone-color: var(--rgb-state-person-zone-color);
--rgb-badge-person-not-home-color: var(--rgb-red-color);
/* input components */ /* input components */
--input-idle-line-color: rgba(0, 0, 0, 0.42); --input-idle-line-color: rgba(0, 0, 0, 0.42);
--input-hover-line-color: rgba(0, 0, 0, 0.87); --input-hover-line-color: rgba(0, 0, 0, 0.87);

View File

@ -634,6 +634,7 @@
"label": "Calendar", "label": "Calendar",
"my_calendars": "My Calendars", "my_calendars": "My Calendars",
"today": "Today", "today": "Today",
"event_retrieval_error": "Could not retrieve events for calendars: ",
"event": { "event": {
"add": "Add Event", "add": "Add Event",
"delete": "Delete Event", "delete": "Delete Event",
@ -642,7 +643,9 @@
"all_day": "All Day", "all_day": "All Day",
"start": "Start", "start": "Start",
"end": "End", "end": "End",
"not_all_required_fields": "Not all required fields are filled in.", "invalid_duration": "The duration of the event is not valid. Please check start and end date.",
"not_all_required_fields": "Not all required fields are filled in",
"end_auto_adjusted": "Event end was adjusted to prevent negative duration",
"confirm_delete": { "confirm_delete": {
"delete": "Delete Event", "delete": "Delete Event",
"delete_this": "Delete Only This Event", "delete_this": "Delete Only This Event",