mirror of
https://github.com/home-assistant/frontend.git
synced 2025-07-24 09:46:36 +00:00
20221206.0 (#14585)
This commit is contained in:
commit
dacef605c7
18
.gitignore
vendored
18
.gitignore
vendored
@ -2,20 +2,20 @@
|
||||
.reify-cache
|
||||
|
||||
# build
|
||||
build
|
||||
hass_frontend/*
|
||||
dist
|
||||
translations
|
||||
build/
|
||||
dist/
|
||||
/hass_frontend/
|
||||
/translations/
|
||||
|
||||
# yarn
|
||||
.yarn/*
|
||||
.yarn/**
|
||||
!.yarn/patches
|
||||
!.yarn/releases
|
||||
!.yarn/plugins
|
||||
!.yarn/sdks
|
||||
!.yarn/versions
|
||||
.pnp.*
|
||||
node_modules/*
|
||||
/node_modules/
|
||||
yarn-error.log
|
||||
npm-debug.log
|
||||
|
||||
@ -27,11 +27,11 @@ npm-debug.log
|
||||
# venv stuff
|
||||
pyvenv.cfg
|
||||
pip-selfcheck.json
|
||||
venv/*
|
||||
/venv/
|
||||
.venv
|
||||
|
||||
# vscode
|
||||
.vscode/*
|
||||
.vscode/**
|
||||
!.vscode/extensions.json
|
||||
!.vscode/launch.json
|
||||
!.vscode/tasks.json
|
||||
@ -46,4 +46,4 @@ src/cast/dev_const.ts
|
||||
.tool-versions
|
||||
|
||||
# Home Assistant config
|
||||
/config
|
||||
/config/
|
||||
|
@ -181,7 +181,7 @@ class HcCast extends LitElement {
|
||||
private async _handlePickView(ev: Event) {
|
||||
const path = (ev.currentTarget as any).getAttribute("data-path");
|
||||
await ensureConnectedCastSession(this.castManager!, this.auth!);
|
||||
castSendShowLovelaceView(this.castManager, path);
|
||||
castSendShowLovelaceView(this.castManager, path, this.auth.data.hassUrl);
|
||||
}
|
||||
|
||||
private async _handleLogout() {
|
||||
|
@ -33,7 +33,6 @@ import { castContext } from "../cast_context";
|
||||
import "./hc-launch-screen";
|
||||
|
||||
let resourcesLoaded = false;
|
||||
|
||||
@customElement("hc-main")
|
||||
export class HcMain extends HassElement {
|
||||
@state() private _showDemo = false;
|
||||
@ -46,6 +45,8 @@ export class HcMain extends HassElement {
|
||||
|
||||
@state() private _urlPath?: string | null;
|
||||
|
||||
private _hassUUID?: string;
|
||||
|
||||
private _unsubLovelace?: UnsubscribeFunc;
|
||||
|
||||
public processIncomingMessage(msg: HassMessage) {
|
||||
@ -125,6 +126,7 @@ export class HcMain extends HassElement {
|
||||
|
||||
if (this.hass) {
|
||||
status.hassUrl = this.hass.auth.data.hassUrl;
|
||||
status.hassUUID = this._hassUUID;
|
||||
status.lovelacePath = this._lovelacePath;
|
||||
status.urlPath = this._urlPath;
|
||||
}
|
||||
@ -163,6 +165,18 @@ export class HcMain extends HassElement {
|
||||
};
|
||||
|
||||
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!);
|
||||
}
|
||||
|
||||
@ -179,6 +193,7 @@ export class HcMain extends HassElement {
|
||||
expires_in: 0,
|
||||
}),
|
||||
});
|
||||
this._hassUUID = msg.hassUUID;
|
||||
} catch (err: any) {
|
||||
const errorMessage = this._getErrorMessage(err);
|
||||
this._error = errorMessage;
|
||||
@ -209,9 +224,29 @@ export class HcMain extends HassElement {
|
||||
if (!this.hass) {
|
||||
this._sendStatus(msg.senderId!);
|
||||
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;
|
||||
}
|
||||
|
||||
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;
|
||||
if (msg.urlPath === "lovelace") {
|
||||
msg.urlPath = null;
|
||||
|
@ -4,14 +4,12 @@ import {
|
||||
} from "home-assistant-js-websocket";
|
||||
import { css, html, LitElement, TemplateResult } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { styleMap } from "lit/directives/style-map";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { computeDomain } from "../../../../src/common/entity/compute_domain";
|
||||
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 type { DataTableColumnContainer } from "../../../../src/components/data-table/ha-data-table";
|
||||
import "../../../../src/components/entity/state-badge";
|
||||
import "../../../../src/components/ha-chip";
|
||||
import { provideHass } from "../../../../src/fake_data/provide_hass";
|
||||
import { HomeAssistant } from "../../../../src/types";
|
||||
@ -105,6 +103,12 @@ const ENTITIES: HassEntity[] = [
|
||||
createEntity("alarm_control_panel.arming", "arming"),
|
||||
createEntity("alarm_control_panel.disarming", "disarming"),
|
||||
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_DEVICE_CLASSES.map((dc) =>
|
||||
createEntity(`binary_sensor.${dc}`, "on", dc)
|
||||
@ -113,8 +117,11 @@ const ENTITIES: HassEntity[] = [
|
||||
createEntity("button.restart", "unknown", "restart"),
|
||||
createEntity("button.update", "unknown", "update"),
|
||||
// Calendar
|
||||
createEntity("calendar.on", "on"),
|
||||
createEntity("calendar.off", "off"),
|
||||
createEntity("calendar.on", "on"),
|
||||
// Camera
|
||||
createEntity("camera.off", "off"),
|
||||
createEntity("camera.on", "on"),
|
||||
// Climate
|
||||
createEntity("climate.off", "off"),
|
||||
createEntity("climate.heat", "heat"),
|
||||
@ -124,10 +131,10 @@ const ENTITIES: HassEntity[] = [
|
||||
createEntity("climate.dry", "dry"),
|
||||
createEntity("climate.fan_only", "fan_only"),
|
||||
// Cover
|
||||
createEntity("cover.opening", "opening"),
|
||||
createEntity("cover.open", "open"),
|
||||
createEntity("cover.closing", "closing"),
|
||||
createEntity("cover.closed", "closed"),
|
||||
createEntity("cover.opening", "opening"),
|
||||
createEntity("cover.open", "open"),
|
||||
createEntity("cover.awning", "open", "awning"),
|
||||
createEntity("cover.blind", "open", "blind"),
|
||||
createEntity("cover.curtain", "open", "curtain"),
|
||||
@ -139,24 +146,27 @@ const ENTITIES: HassEntity[] = [
|
||||
createEntity("cover.shutter", "open", "shutter"),
|
||||
createEntity("cover.window", "open", "window"),
|
||||
// Device tracker/person
|
||||
createEntity("device_tracker.home", "home"),
|
||||
createEntity("device_tracker.not_home", "not_home"),
|
||||
createEntity("device_tracker.home", "home"),
|
||||
createEntity("device_tracker.work", "work"),
|
||||
createEntity("person.home", "home"),
|
||||
createEntity("person.not_home", "not_home"),
|
||||
createEntity("person.work", "work"),
|
||||
// Fan
|
||||
createEntity("fan.on", "on"),
|
||||
createEntity("fan.off", "off"),
|
||||
createEntity("fan.on", "on"),
|
||||
// Humidifier
|
||||
createEntity("humidifier.on", "on"),
|
||||
createEntity("humidifier.off", "off"),
|
||||
createEntity("humidifier.on", "on"),
|
||||
// Helpers
|
||||
createEntity("input_boolean.off", "off"),
|
||||
createEntity("input_boolean.on", "on"),
|
||||
// Light
|
||||
createEntity("light.on", "on"),
|
||||
createEntity("light.off", "off"),
|
||||
createEntity("light.on", "on"),
|
||||
// Locks
|
||||
createEntity("lock.locked", "locked"),
|
||||
createEntity("lock.unlocked", "unlocked"),
|
||||
createEntity("lock.locked", "locked"),
|
||||
createEntity("lock.locking", "locking"),
|
||||
createEntity("lock.unlocking", "unlocking"),
|
||||
createEntity("lock.jammed", "jammed"),
|
||||
@ -180,6 +190,12 @@ const ENTITIES: HassEntity[] = [
|
||||
createEntity("media_player.speaker_playing", "playing", "speaker"),
|
||||
createEntity("media_player.speaker_paused", "paused", "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_DEVICE_CLASSES.map((dc) => createEntity(`sensor.${dc}`, "10", dc)),
|
||||
// Battery sensor
|
||||
@ -196,9 +212,12 @@ const ENTITIES: HassEntity[] = [
|
||||
createEntity("switch.outlet_on", "on", "outlet"),
|
||||
createEntity("switch.switch_off", "off", "switch"),
|
||||
createEntity("switch.switch_on", "on", "switch"),
|
||||
// Timer
|
||||
createEntity("timer.off", "off"),
|
||||
createEntity("timer.on", "on"),
|
||||
// Vacuum
|
||||
createEntity("vacuum.cleaning", "cleaning"),
|
||||
createEntity("vacuum.docked", "docked"),
|
||||
createEntity("vacuum.cleaning", "cleaning"),
|
||||
createEntity("vacuum.paused", "paused"),
|
||||
createEntity("vacuum.idle", "idle"),
|
||||
createEntity("vacuum.returning", "returning"),
|
||||
@ -280,18 +299,12 @@ export class DemoEntityState extends LitElement {
|
||||
const columns: DataTableColumnContainer<EntityRowData> = {
|
||||
icon: {
|
||||
title: "Icon",
|
||||
template: (_, entry) => {
|
||||
const cssColor = stateColorCss(entry.stateObj);
|
||||
return html`
|
||||
<ha-svg-icon
|
||||
style=${styleMap({
|
||||
color: `rgb(${cssColor})`,
|
||||
})}
|
||||
.path=${stateIconPath(entry.stateObj)}
|
||||
>
|
||||
</ha-svg-icon>
|
||||
`;
|
||||
},
|
||||
template: (_, entry) => html`
|
||||
<state-badge
|
||||
.stateObj=${entry.stateObj}
|
||||
.stateColor=${true}
|
||||
></state-badge>
|
||||
`,
|
||||
},
|
||||
entity_id: {
|
||||
title: "Entity id",
|
||||
|
0
hassio/.gitignore
vendored
0
hassio/.gitignore
vendored
@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "home-assistant-frontend"
|
||||
version = "20221205.0"
|
||||
version = "20221206.0"
|
||||
license = {text = "Apache-2.0"}
|
||||
description = "The Home Assistant frontend"
|
||||
readme = "README.md"
|
||||
|
@ -8,6 +8,8 @@ import { BaseCastMessage } from "./types";
|
||||
|
||||
export interface GetStatusMessage extends BaseCastMessage {
|
||||
type: "get_status";
|
||||
hassUrl?: string;
|
||||
hassUUID?: string;
|
||||
}
|
||||
|
||||
export interface ConnectMessage extends BaseCastMessage {
|
||||
@ -15,12 +17,15 @@ export interface ConnectMessage extends BaseCastMessage {
|
||||
refreshToken: string;
|
||||
clientId: string | null;
|
||||
hassUrl: string;
|
||||
hassUUID?: string;
|
||||
}
|
||||
|
||||
export interface ShowLovelaceViewMessage extends BaseCastMessage {
|
||||
type: "show_lovelace_view";
|
||||
viewPath: string | number | null;
|
||||
urlPath: string | null;
|
||||
hassUrl: string;
|
||||
hassUUID?: string;
|
||||
}
|
||||
|
||||
export interface ShowDemoMessage extends BaseCastMessage {
|
||||
@ -43,6 +48,7 @@ export const castSendAuth = (cast: CastManager, auth: Auth) =>
|
||||
|
||||
export const castSendShowLovelaceView = (
|
||||
cast: CastManager,
|
||||
hassUrl: string,
|
||||
viewPath: ShowLovelaceViewMessage["viewPath"],
|
||||
urlPath?: string | null
|
||||
) =>
|
||||
@ -50,6 +56,7 @@ export const castSendShowLovelaceView = (
|
||||
type: "show_lovelace_view",
|
||||
viewPath,
|
||||
urlPath: urlPath || null,
|
||||
hassUrl: CAST_DEV ? CAST_DEV_HASS_URL : hassUrl,
|
||||
});
|
||||
|
||||
export const castSendShowDemo = (cast: CastManager) =>
|
||||
|
@ -7,6 +7,7 @@ export interface ReceiverStatusMessage extends BaseCastMessage {
|
||||
connected: boolean;
|
||||
showDemo: boolean;
|
||||
hassUrl?: string;
|
||||
hassUUID?: string;
|
||||
lovelacePath?: string | number | null;
|
||||
urlPath?: string | null;
|
||||
}
|
||||
@ -23,6 +24,7 @@ export const enum ReceiverErrorCode {
|
||||
CONNECTION_LOST = 3,
|
||||
HASS_URL_MISSING = 4,
|
||||
NO_HTTPS = 5,
|
||||
WRONG_INSTANCE = 20,
|
||||
NOT_CONNECTED = 21,
|
||||
FETCH_CONFIG_FAILED = 22,
|
||||
}
|
||||
|
@ -188,6 +188,14 @@ export const DOMAINS_WITH_CARD = [
|
||||
"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.
|
||||
* 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
|
||||
|
10
src/common/entity/color/person_color.ts
Normal file
10
src/common/entity/color/person_color.ts
Normal 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";
|
||||
}
|
||||
};
|
@ -33,6 +33,10 @@ export function stateActive(stateObj: HassEntity, state?: string): boolean {
|
||||
return compareState === "problem";
|
||||
case "group":
|
||||
return ["on", "home", "open", "locked", "problem"].includes(compareState);
|
||||
case "timer":
|
||||
return compareState === "active";
|
||||
case "camera":
|
||||
return compareState === "streaming";
|
||||
default:
|
||||
return true;
|
||||
}
|
||||
|
@ -5,6 +5,7 @@ import { alarmControlPanelColor } from "./color/alarm_control_panel_color";
|
||||
import { binarySensorColor } from "./color/binary_sensor_color";
|
||||
import { climateColor } from "./color/climate_color";
|
||||
import { lockColor } from "./color/lock_color";
|
||||
import { personColor } from "./color/person_color";
|
||||
import { sensorColor } from "./color/sensor_color";
|
||||
import { computeDomain } from "./compute_domain";
|
||||
import { stateActive } from "./state_active";
|
||||
@ -28,6 +29,9 @@ export const stateColor = (stateObj: HassEntity, state?: string) => {
|
||||
const domain = computeDomain(stateObj.entity_id);
|
||||
|
||||
switch (domain) {
|
||||
case "automation":
|
||||
return "automation";
|
||||
|
||||
case "alarm_control_panel":
|
||||
return alarmControlPanelColor(compareState);
|
||||
|
||||
@ -55,6 +59,10 @@ export const stateColor = (stateObj: HassEntity, state?: string) => {
|
||||
case "media_player":
|
||||
return "media-player";
|
||||
|
||||
case "person":
|
||||
case "device_tracker":
|
||||
return personColor(stateObj);
|
||||
|
||||
case "sensor":
|
||||
return sensorColor(stateObj);
|
||||
|
||||
@ -68,8 +76,20 @@ export const stateColor = (stateObj: HassEntity, state?: string) => {
|
||||
return compareState === "above_horizon" ? "sun-day" : "sun-night";
|
||||
|
||||
case "switch":
|
||||
case "input_boolean":
|
||||
return "switch";
|
||||
|
||||
case "alert":
|
||||
return "alert";
|
||||
|
||||
case "calendar":
|
||||
case "camera":
|
||||
case "remote":
|
||||
case "script":
|
||||
case "timer":
|
||||
case "group":
|
||||
return "active";
|
||||
|
||||
case "update":
|
||||
return updateIsInstalling(stateObj as UpdateEntity)
|
||||
? "update-installing"
|
||||
|
@ -1,29 +1,6 @@
|
||||
import { css } from "lit";
|
||||
|
||||
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="arming"],
|
||||
ha-state-icon[data-active][data-domain="alarm_control_panel"][data-state="triggered"] {
|
||||
|
@ -26,7 +26,7 @@ class StateHistoryChartLine extends LitElement {
|
||||
|
||||
@property() public identifier?: string;
|
||||
|
||||
@property({ type: Boolean }) public isSingleDevice = false;
|
||||
@property({ type: Boolean }) public showNames = true;
|
||||
|
||||
@property({ attribute: false }) public endTime!: Date;
|
||||
|
||||
@ -101,7 +101,7 @@ class StateHistoryChartLine extends LitElement {
|
||||
propagate: true,
|
||||
},
|
||||
legend: {
|
||||
display: !this.isSingleDevice,
|
||||
display: this.showNames,
|
||||
labels: {
|
||||
usePointStyle: true,
|
||||
},
|
||||
|
@ -1,73 +1,14 @@
|
||||
import type { ChartData, ChartDataset, ChartOptions } from "chart.js";
|
||||
import { HassEntity } from "home-assistant-js-websocket";
|
||||
import { css, CSSResultGroup, html, LitElement, PropertyValues } from "lit";
|
||||
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 { stateActive } from "../../common/entity/state_active";
|
||||
import { stateColor } from "../../common/entity/state_color";
|
||||
import { numberFormatToLocale } from "../../common/number/format_number";
|
||||
import { computeRTL } from "../../common/util/compute_rtl";
|
||||
import { TimelineEntity } from "../../data/history";
|
||||
import { HomeAssistant } from "../../types";
|
||||
import { MIN_TIME_BETWEEN_UPDATES } from "./ha-chart-base";
|
||||
import type { TimeLineData } from "./timeline-chart/const";
|
||||
|
||||
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;
|
||||
};
|
||||
import { computeTimelineColor } from "./timeline-chart/timeline-color";
|
||||
|
||||
@customElement("state-history-chart-timeline")
|
||||
export class StateHistoryChartTimeline extends LitElement {
|
||||
@ -83,7 +24,7 @@ export class StateHistoryChartTimeline extends LitElement {
|
||||
|
||||
@property() public identifier?: string;
|
||||
|
||||
@property({ type: Boolean }) public isSingleDevice = false;
|
||||
@property({ type: Boolean }) public showNames = true;
|
||||
|
||||
@property({ type: Boolean }) public chunked = false;
|
||||
|
||||
@ -175,8 +116,7 @@ export class StateHistoryChartTimeline extends LitElement {
|
||||
drawTicks: false,
|
||||
},
|
||||
ticks: {
|
||||
display:
|
||||
this.chunked || !this.isSingleDevice || this.data.length !== 1,
|
||||
display: this.chunked || this.showNames,
|
||||
},
|
||||
afterSetDimensions: (y) => {
|
||||
y.maxWidth = y.chart.width * 0.18;
|
||||
@ -271,7 +211,7 @@ export class StateHistoryChartTimeline extends LitElement {
|
||||
start: prevLastChanged,
|
||||
end: newLastChanged,
|
||||
label: locState,
|
||||
color: getColor(
|
||||
color: computeTimelineColor(
|
||||
prevState,
|
||||
computedStyles,
|
||||
this.hass.states[stateInfo.entity_id]
|
||||
@ -289,7 +229,7 @@ export class StateHistoryChartTimeline extends LitElement {
|
||||
start: prevLastChanged,
|
||||
end: endTime,
|
||||
label: locState,
|
||||
color: getColor(
|
||||
color: computeTimelineColor(
|
||||
prevState,
|
||||
computedStyles,
|
||||
this.hass.states[stateInfo.entity_id]
|
||||
|
@ -48,7 +48,7 @@ class StateHistoryCharts extends LitElement {
|
||||
|
||||
@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;
|
||||
|
||||
@ -128,8 +128,7 @@ class StateHistoryCharts extends LitElement {
|
||||
.unit=${item.unit}
|
||||
.data=${item.data}
|
||||
.identifier=${item.identifier}
|
||||
.isSingleDevice=${!this.noSingle &&
|
||||
this.historyData.line?.length === 1}
|
||||
.showNames=${this.showNames}
|
||||
.endTime=${this._computedEndTime}
|
||||
.names=${this.names}
|
||||
></state-history-chart-line>
|
||||
@ -141,8 +140,7 @@ class StateHistoryCharts extends LitElement {
|
||||
.data=${item}
|
||||
.startTime=${this._computedStartTime}
|
||||
.endTime=${this._computedEndTime}
|
||||
.isSingleDevice=${!this.noSingle &&
|
||||
this.historyData.timeline?.length === 1}
|
||||
.showNames=${this.showNames}
|
||||
.names=${this.names}
|
||||
.narrow=${this.narrow}
|
||||
.chunked=${this.virtualize}
|
||||
|
99
src/components/chart/timeline-chart/timeline-color.ts
Normal file
99
src/components/chart/timeline-chart/timeline-color.ts
Normal 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)
|
||||
);
|
||||
}
|
@ -2,6 +2,7 @@ import { getColorByIndex } from "../common/color/colors";
|
||||
import { computeDomain } from "../common/entity/compute_domain";
|
||||
import { computeStateName } from "../common/entity/compute_state_name";
|
||||
import type { HomeAssistant } from "../types";
|
||||
import { UNAVAILABLE_STATES } from "./entity";
|
||||
|
||||
export interface Calendar {
|
||||
entity_id: string;
|
||||
@ -56,13 +57,14 @@ export const fetchCalendarEvents = async (
|
||||
start: Date,
|
||||
end: Date,
|
||||
calendars: Calendar[]
|
||||
): Promise<CalendarEvent[]> => {
|
||||
): Promise<{ events: CalendarEvent[]; errors: string[] }> => {
|
||||
const params = encodeURI(
|
||||
`?start=${start.toISOString()}&end=${end.toISOString()}`
|
||||
);
|
||||
|
||||
const calEvents: CalendarEvent[] = [];
|
||||
const promises: Promise<any>[] = [];
|
||||
const errors: string[] = [];
|
||||
const promises: Promise<CalendarEvent[]>[] = [];
|
||||
|
||||
calendars.forEach((cal) => {
|
||||
promises.push(
|
||||
@ -73,9 +75,15 @@ export const fetchCalendarEvents = async (
|
||||
);
|
||||
});
|
||||
|
||||
const results = await Promise.all(promises);
|
||||
|
||||
results.forEach((result, idx) => {
|
||||
for (const [idx, promise] of promises.entries()) {
|
||||
let result: CalendarEvent[];
|
||||
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];
|
||||
result.forEach((ev) => {
|
||||
const eventStart = getCalendarDate(ev.start);
|
||||
@ -104,9 +112,9 @@ export const fetchCalendarEvents = async (
|
||||
|
||||
calEvents.push(event);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
return calEvents;
|
||||
return { events: calEvents, errors };
|
||||
};
|
||||
|
||||
const getCalendarDate = (dateObj: any): string | undefined => {
|
||||
@ -127,7 +135,11 @@ const getCalendarDate = (dateObj: any): string | undefined => {
|
||||
|
||||
export const getCalendars = (hass: HomeAssistant): Calendar[] =>
|
||||
Object.keys(hass.states)
|
||||
.filter((eid) => computeDomain(eid) === "calendar")
|
||||
.filter(
|
||||
(eid) =>
|
||||
computeDomain(eid) === "calendar" &&
|
||||
!UNAVAILABLE_STATES.includes(hass.states[eid].state)
|
||||
)
|
||||
.sort()
|
||||
.map((eid, idx) => ({
|
||||
entity_id: eid,
|
||||
|
@ -67,12 +67,14 @@ export class MoreInfoHistory extends LitElement {
|
||||
.statTypes=${statTypes}
|
||||
.names=${this._statNames}
|
||||
hideLegend
|
||||
.showNames=${false}
|
||||
></statistics-chart>`
|
||||
: html`<state-history-charts
|
||||
up-to-now
|
||||
.hass=${this.hass}
|
||||
.historyData=${this._stateHistory}
|
||||
.isLoadingData=${!this._stateHistory}
|
||||
.showNames=${false}
|
||||
></state-history-charts>`}`
|
||||
: ""}`;
|
||||
}
|
||||
|
@ -1,7 +1,13 @@
|
||||
import "@material/mwc-button";
|
||||
import { mdiClose } from "@mdi/js";
|
||||
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 { customElement, property, state } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
@ -35,6 +41,8 @@ class DialogCalendarEventEditor extends LitElement {
|
||||
|
||||
@state() private _error?: string;
|
||||
|
||||
@state() private _info?: string;
|
||||
|
||||
@state() private _params?: CalendarEventDetailDialogParams;
|
||||
|
||||
@state() private _calendars: Calendar[] = [];
|
||||
@ -57,6 +65,7 @@ class DialogCalendarEventEditor extends LitElement {
|
||||
|
||||
public showDialog(params: CalendarEventEditDialogParams): void {
|
||||
this._error = undefined;
|
||||
this._info = undefined;
|
||||
this._params = params;
|
||||
this._calendars = params.calendars;
|
||||
this._calendarId = params.calendarId || this._calendars[0].entity_id;
|
||||
@ -120,6 +129,14 @@ class DialogCalendarEventEditor extends LitElement {
|
||||
${this._error
|
||||
? 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
|
||||
class="summary"
|
||||
@ -260,6 +277,10 @@ class DialogCalendarEventEditor extends LitElement {
|
||||
}
|
||||
);
|
||||
|
||||
private _clearInfo() {
|
||||
this._info = undefined;
|
||||
}
|
||||
|
||||
private _handleSummaryChanged(ev) {
|
||||
this._summary = ev.target.value;
|
||||
}
|
||||
@ -277,9 +298,30 @@ class DialogCalendarEventEditor extends LitElement {
|
||||
}
|
||||
|
||||
private _startDateChanged(ev: CustomEvent) {
|
||||
// Store previous event duration
|
||||
const duration = differenceInMilliseconds(this._dtend!, this._dtstart!);
|
||||
|
||||
this._dtstart = new Date(
|
||||
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) {
|
||||
@ -289,9 +331,28 @@ class DialogCalendarEventEditor extends LitElement {
|
||||
}
|
||||
|
||||
private _startTimeChanged(ev: CustomEvent) {
|
||||
// Store previous event duration
|
||||
const duration = differenceInMilliseconds(this._dtend!, this._dtstart!);
|
||||
|
||||
this._dtstart = new Date(
|
||||
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) {
|
||||
@ -337,6 +398,13 @@ class DialogCalendarEventEditor extends LitElement {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this._dtend! <= this._dtstart!) {
|
||||
this._error = this.hass.localize(
|
||||
"ui.components.calendar.event.invalid_duration"
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
this._submitting = true;
|
||||
try {
|
||||
await createCalendarEvent(
|
||||
|
@ -97,6 +97,8 @@ export class HAFullCalendar extends LitElement {
|
||||
|
||||
@property() public initialView: FullCalendarView = "dayGridMonth";
|
||||
|
||||
@property({ attribute: false }) public error?: string = undefined;
|
||||
|
||||
private calendar?: Calendar;
|
||||
|
||||
private _viewButtons?: ToggleButton[];
|
||||
@ -116,6 +118,14 @@ export class HAFullCalendar extends LitElement {
|
||||
return html`
|
||||
${this.calendar
|
||||
? html`
|
||||
${this.error
|
||||
? html`<ha-alert
|
||||
alert-type="error"
|
||||
dismissable
|
||||
@alert-dismissed-clicked=${this._clearError}
|
||||
>${this.error}</ha-alert
|
||||
>`
|
||||
: ""}
|
||||
<div class="header">
|
||||
${!this.narrow
|
||||
? html`
|
||||
@ -380,6 +390,10 @@ export class HAFullCalendar extends LitElement {
|
||||
);
|
||||
});
|
||||
|
||||
private _clearError() {
|
||||
this.error = undefined;
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return [
|
||||
haStyle,
|
||||
@ -449,6 +463,11 @@ export class HAFullCalendar extends LitElement {
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
ha-alert {
|
||||
display: block;
|
||||
margin: 4px 0;
|
||||
}
|
||||
|
||||
#calendar {
|
||||
flex-grow: 1;
|
||||
background-color: var(
|
||||
|
@ -15,6 +15,7 @@ import { customElement, property, state } from "lit/decorators";
|
||||
import { styleMap } from "lit/directives/style-map";
|
||||
import { LocalStorage } from "../../common/decorators/local-storage";
|
||||
import { HASSDomEvent } from "../../common/dom/fire_event";
|
||||
import { computeStateName } from "../../common/entity/compute_state_name";
|
||||
import "../../components/ha-card";
|
||||
import "../../components/ha-icon-button";
|
||||
import "../../components/ha-menu-button";
|
||||
@ -40,6 +41,8 @@ class PanelCalendar extends LitElement {
|
||||
|
||||
@state() private _events: CalendarEvent[] = [];
|
||||
|
||||
@state() private _error?: string = undefined;
|
||||
|
||||
@LocalStorage("deSelectedCalendars", true)
|
||||
private _deSelectedCalendars: string[] = [];
|
||||
|
||||
@ -101,6 +104,7 @@ class PanelCalendar extends LitElement {
|
||||
.calendars=${this._calendars}
|
||||
.narrow=${this.narrow}
|
||||
.hass=${this.hass}
|
||||
.error=${this._error}
|
||||
@view-changed=${this._handleViewChanged}
|
||||
></ha-full-calendar>
|
||||
</div>
|
||||
@ -118,9 +122,9 @@ class PanelCalendar extends LitElement {
|
||||
start: Date,
|
||||
end: Date,
|
||||
calendars: Calendar[]
|
||||
): Promise<CalendarEvent[]> {
|
||||
): Promise<{ events: CalendarEvent[]; errors: string[] }> {
|
||||
if (!calendars.length) {
|
||||
return [];
|
||||
return { events: [], errors: [] };
|
||||
}
|
||||
|
||||
return fetchCalendarEvents(this.hass, start, end, calendars);
|
||||
@ -135,8 +139,9 @@ class PanelCalendar extends LitElement {
|
||||
const checked = ev.target.checked;
|
||||
|
||||
if (checked) {
|
||||
const events = await this._fetchEvents(this._start!, this._end!, [cal]);
|
||||
this._events = [...this._events, ...events];
|
||||
const result = await this._fetchEvents(this._start!, this._end!, [cal]);
|
||||
this._events = [...this._events, ...result.events];
|
||||
this._handleErrors(result.errors);
|
||||
this._deSelectedCalendars = this._deSelectedCalendars.filter(
|
||||
(deCal) => deCal !== cal.entity_id
|
||||
);
|
||||
@ -161,19 +166,40 @@ class PanelCalendar extends LitElement {
|
||||
): Promise<void> {
|
||||
this._start = ev.detail.start;
|
||||
this._end = ev.detail.end;
|
||||
this._events = await this._fetchEvents(
|
||||
const result = await this._fetchEvents(
|
||||
this._start,
|
||||
this._end,
|
||||
this._selectedCalendars
|
||||
);
|
||||
this._events = result.events;
|
||||
this._handleErrors(result.errors);
|
||||
}
|
||||
|
||||
private async _handleRefresh(): Promise<void> {
|
||||
this._events = await this._fetchEvents(
|
||||
const result = await this._fetchEvents(
|
||||
this._start!,
|
||||
this._end!,
|
||||
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 {
|
||||
|
@ -75,6 +75,7 @@ import {
|
||||
showDeviceRegistryDetailDialog,
|
||||
} from "./device-registry-detail/show-dialog-device-registry-detail";
|
||||
import "../../../layouts/hass-subpage";
|
||||
import { SENSOR_ENTITIES } from "../../../common/const";
|
||||
|
||||
export interface EntityRegistryStateEntry extends EntityRegistryEntry {
|
||||
stateName?: string | null;
|
||||
@ -172,13 +173,7 @@ export class HaConfigDevicePage extends LitElement {
|
||||
const result = groupBy(entities, (entry) =>
|
||||
entry.entity_category
|
||||
? entry.entity_category
|
||||
: [
|
||||
"sensor",
|
||||
"binary_sensor",
|
||||
"camera",
|
||||
"device_tracker",
|
||||
"weather",
|
||||
].includes(computeDomain(entry.entity_id))
|
||||
: SENSOR_ENTITIES.includes(computeDomain(entry.entity_id))
|
||||
? "sensor"
|
||||
: "control"
|
||||
) as Record<
|
||||
|
@ -117,7 +117,7 @@ const OVERRIDE_NUMBER_UNITS = {
|
||||
const OVERRIDE_SENSOR_UNITS = {
|
||||
distance: ["cm", "ft", "in", "km", "m", "mi", "mm", "yd"],
|
||||
gas: ["ft³", "m³"],
|
||||
precipitation: ["in", "mm"],
|
||||
precipitation: ["cm", "in", "mm"],
|
||||
precipitation_intensity: ["in/d", "in/h", "mm/d", "mm/h"],
|
||||
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"],
|
||||
|
@ -6,7 +6,7 @@ import interactionPlugin from "@fullcalendar/interaction";
|
||||
import timeGridPlugin from "@fullcalendar/timegrid";
|
||||
// @ts-ignore
|
||||
import timegridStyle from "@fullcalendar/timegrid/main.css";
|
||||
import { isSameDay } from "date-fns";
|
||||
import { addDays, isSameDay, isSameWeek, nextDay } from "date-fns";
|
||||
import {
|
||||
css,
|
||||
CSSResultGroup,
|
||||
@ -17,15 +17,17 @@ import {
|
||||
unsafeCSS,
|
||||
} from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { firstWeekdayIndex } from "../../../../common/datetime/first_weekday";
|
||||
import { formatTime24h } from "../../../../common/datetime/format_time";
|
||||
import { useAmPm } from "../../../../common/datetime/use_am_pm";
|
||||
import { firstWeekdayIndex } from "../../../../common/datetime/first_weekday";
|
||||
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||
import { debounce } from "../../../../common/util/debounce";
|
||||
import "../../../../components/ha-icon-picker";
|
||||
import "../../../../components/ha-textfield";
|
||||
import { Schedule, ScheduleDay, weekdays } from "../../../../data/schedule";
|
||||
import { haStyle } from "../../../../resources/styles";
|
||||
import { HomeAssistant } from "../../../../types";
|
||||
import { installResizeObserver } from "../../../lovelace/common/install-resize-observer";
|
||||
|
||||
const defaultFullCalendarConfig: CalendarOptions = {
|
||||
plugins: [timeGridPlugin, interactionPlugin],
|
||||
@ -71,6 +73,8 @@ class HaScheduleForm extends LitElement {
|
||||
|
||||
private _item?: Schedule;
|
||||
|
||||
private _resizeObserver?: ResizeObserver;
|
||||
|
||||
set item(item: Schedule) {
|
||||
this._item = 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 {
|
||||
if (!this.hass) {
|
||||
return html``;
|
||||
@ -207,11 +245,6 @@ class HaScheduleForm extends LitElement {
|
||||
|
||||
private get _events() {
|
||||
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()) {
|
||||
if (!this[`_${day}`].length) {
|
||||
@ -219,14 +252,15 @@ class HaScheduleForm extends LitElement {
|
||||
}
|
||||
|
||||
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)
|
||||
const distance =
|
||||
i -
|
||||
baseDay +
|
||||
(i === 0 && firstWeekdayIndex(this.hass.locale) === 1 ? 7 : 0);
|
||||
|
||||
const start = new Date();
|
||||
start.setDate(start.getDate() + distance);
|
||||
let date = nextDay(new Date(), i as Day);
|
||||
if (
|
||||
!isSameWeek(date, new Date(), {
|
||||
weekStartsOn: firstWeekdayIndex(this.hass.locale),
|
||||
})
|
||||
) {
|
||||
date = addDays(date, -7);
|
||||
}
|
||||
const start = new Date(date);
|
||||
const start_tokens = item.from.split(":");
|
||||
start.setHours(
|
||||
parseInt(start_tokens[0]),
|
||||
@ -235,8 +269,7 @@ class HaScheduleForm extends LitElement {
|
||||
0
|
||||
);
|
||||
|
||||
const end = new Date();
|
||||
end.setDate(end.getDate() + distance);
|
||||
const end = new Date(date);
|
||||
const end_tokens = item.to.split(":");
|
||||
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-event-border-color: var(--divider-color);
|
||||
}
|
||||
.fc-scroller {
|
||||
overflow-x: visible !important;
|
||||
}
|
||||
|
||||
.fc-v-event .fc-event-time {
|
||||
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 {
|
||||
color: inherit !important;
|
||||
|
@ -163,7 +163,6 @@ class HaPanelHistory extends SubscribeMixin(LitElement) {
|
||||
.hass=${this.hass}
|
||||
.historyData=${this._stateHistory}
|
||||
.endTime=${this._endDate}
|
||||
no-single
|
||||
>
|
||||
</state-history-charts>
|
||||
`}
|
||||
|
@ -1,5 +1,6 @@
|
||||
import "@lit-labs/virtualizer";
|
||||
import { VisibilityChangedEvent } from "@lit-labs/virtualizer/Virtualizer";
|
||||
import type { HassEntity } from "home-assistant-js-websocket";
|
||||
import {
|
||||
css,
|
||||
CSSResultGroup,
|
||||
@ -8,37 +9,35 @@ import {
|
||||
PropertyValues,
|
||||
TemplateResult,
|
||||
} from "lit";
|
||||
import type { HassEntity } from "home-assistant-js-websocket";
|
||||
import { customElement, eventOptions, property } from "lit/decorators";
|
||||
import { classMap } from "lit/directives/class-map";
|
||||
import { styleMap } from "lit/directives/style-map";
|
||||
import { isComponentLoaded } from "../../common/config/is_component_loaded";
|
||||
import { formatDate } from "../../common/datetime/format_date";
|
||||
import { formatTimeWithSeconds } from "../../common/datetime/format_time";
|
||||
import { restoreScroll } from "../../common/decorators/restore-scroll";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import { computeDomain } from "../../common/entity/compute_domain";
|
||||
import { isComponentLoaded } from "../../common/config/is_component_loaded";
|
||||
import { stateActive } from "../../common/entity/state_active";
|
||||
import { stateColorCss } from "../../common/entity/state_color";
|
||||
import { navigate } from "../../common/navigate";
|
||||
import { computeTimelineColor } from "../../components/chart/timeline-chart/timeline-color";
|
||||
import "../../components/entity/state-badge";
|
||||
import "../../components/ha-circular-progress";
|
||||
import "../../components/ha-icon-next";
|
||||
import "../../components/ha-relative-time";
|
||||
import {
|
||||
createHistoricState,
|
||||
localizeTriggerSource,
|
||||
localizeStateMessage,
|
||||
localizeTriggerSource,
|
||||
LogbookEntry,
|
||||
} from "../../data/logbook";
|
||||
import { TraceContexts } from "../../data/trace";
|
||||
import {
|
||||
buttonLinkStyle,
|
||||
haStyle,
|
||||
haStyleScrollbar,
|
||||
buttonLinkStyle,
|
||||
} from "../../resources/styles";
|
||||
import { HomeAssistant } from "../../types";
|
||||
import { brandsUrl } from "../../util/brands-url";
|
||||
import "../../components/ha-icon-next";
|
||||
import { navigate } from "../../common/navigate";
|
||||
|
||||
declare global {
|
||||
interface HASSDomEvents {
|
||||
@ -264,14 +263,15 @@ class HaLogbookRenderer extends LitElement {
|
||||
const stateObj = this.hass.states[item.entity_id!] as
|
||||
| HassEntity
|
||||
| undefined;
|
||||
const computedStyles = getComputedStyle(this);
|
||||
|
||||
const color =
|
||||
stateObj && stateActive(stateObj, item.state)
|
||||
? stateColorCss(stateObj, item.state)
|
||||
item.state !== undefined
|
||||
? computeTimelineColor(item.state, computedStyles, stateObj)
|
||||
: undefined;
|
||||
|
||||
const style = {
|
||||
"--indicator-color": color,
|
||||
backgroundColor: color,
|
||||
};
|
||||
|
||||
return html` <div class="indicator" style=${styleMap(style)}></div> `;
|
||||
@ -577,9 +577,7 @@ class HaLogbookRenderer extends LitElement {
|
||||
}
|
||||
|
||||
.indicator {
|
||||
background-color: rgb(
|
||||
var(--indicator-color, var(--rgb-disabled-color))
|
||||
);
|
||||
background-color: rgb(var(--rgb-disabled-color));
|
||||
height: 8px;
|
||||
width: 8px;
|
||||
border-radius: 4px;
|
||||
|
@ -10,6 +10,7 @@ import { customElement, property, state, query } from "lit/decorators";
|
||||
import { getColorByIndex } from "../../../common/color/colors";
|
||||
import { applyThemesOnElement } from "../../../common/dom/apply_themes_on_element";
|
||||
import { HASSDomEvent } from "../../../common/dom/fire_event";
|
||||
import { computeStateName } from "../../../common/entity/compute_state_name";
|
||||
import { debounce } from "../../../common/util/debounce";
|
||||
import "../../../components/ha-card";
|
||||
import {
|
||||
@ -69,6 +70,8 @@ export class HuiCalendarCard extends LitElement implements LovelaceCard {
|
||||
|
||||
@state() private _veryNarrow = false;
|
||||
|
||||
@state() private _error?: string = undefined;
|
||||
|
||||
@query("ha-full-calendar", true) private _calendar?: HAFullCalendar;
|
||||
|
||||
private _startDate?: Date;
|
||||
@ -131,6 +134,7 @@ export class HuiCalendarCard extends LitElement implements LovelaceCard {
|
||||
.hass=${this.hass}
|
||||
.views=${views}
|
||||
.initialView=${this._config.initial_view!}
|
||||
.error=${this._error}
|
||||
@view-changed=${this._handleViewChanged}
|
||||
></ha-full-calendar>
|
||||
</ha-card>
|
||||
@ -169,12 +173,28 @@ export class HuiCalendarCard extends LitElement implements LovelaceCard {
|
||||
return;
|
||||
}
|
||||
|
||||
this._events = await fetchCalendarEvents(
|
||||
this._error = undefined;
|
||||
const result = await fetchCalendarEvents(
|
||||
this.hass!,
|
||||
this._startDate,
|
||||
this._endDate,
|
||||
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() {
|
||||
|
@ -144,7 +144,7 @@ export class HuiHistoryGraphCard extends LitElement implements LovelaceCard {
|
||||
.historyData=${this._stateHistory}
|
||||
.names=${this._names}
|
||||
up-to-now
|
||||
no-single
|
||||
.showNames=${this._config.show_names}
|
||||
></state-history-charts>
|
||||
</div>
|
||||
</ha-card>
|
||||
|
@ -128,6 +128,18 @@ export class HuiTileCard extends LitElement implements LovelaceCard {
|
||||
}
|
||||
|
||||
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)) {
|
||||
return undefined;
|
||||
}
|
||||
|
@ -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 { UNAVAILABLE_STATES } from "../../../../../data/entity";
|
||||
import { HomeAssistant } from "../../../../../types";
|
||||
import { ComputeBadgeFunction } from "./tile-badge";
|
||||
|
||||
@ -17,20 +16,18 @@ function getZone(entity: HassEntity, hass: HomeAssistant) {
|
||||
|
||||
function personBadgeIcon(entity: HassEntity) {
|
||||
const state = entity.state;
|
||||
if (UNAVAILABLE_STATES.includes(state)) {
|
||||
return mdiHelp;
|
||||
}
|
||||
return state === "not_home" ? mdiHomeExportOutline : mdiHome;
|
||||
}
|
||||
|
||||
function personBadgeColor(entity: HassEntity, inZone?: boolean) {
|
||||
if (inZone) {
|
||||
return "var(--rgb-state-person-zone-color)";
|
||||
function personBadgeColor(entity: HassEntity) {
|
||||
switch (entity.state) {
|
||||
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) => {
|
||||
@ -39,6 +36,6 @@ export const computePersonBadge: ComputeBadgeFunction = (stateObj, hass) => {
|
||||
return {
|
||||
iconPath: personBadgeIcon(stateObj),
|
||||
icon: zone?.attributes.icon,
|
||||
color: personBadgeColor(stateObj, Boolean(zone)),
|
||||
color: personBadgeColor(stateObj),
|
||||
};
|
||||
};
|
||||
|
@ -1,5 +1,7 @@
|
||||
import { mdiExclamationThick } from "@mdi/js";
|
||||
import { HassEntity } from "home-assistant-js-websocket";
|
||||
import { computeDomain } from "../../../../../common/entity/compute_domain";
|
||||
import { UNAVAILABLE, UNKNOWN } from "../../../../../data/entity";
|
||||
import { HomeAssistant } from "../../../../../types";
|
||||
import { computeClimateBadge } from "./tile-badge-climate";
|
||||
import { computePersonBadge } from "./tile-badge-person";
|
||||
@ -16,6 +18,15 @@ export type ComputeBadgeFunction = (
|
||||
) => TileBadge | undefined;
|
||||
|
||||
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);
|
||||
switch (domain) {
|
||||
case "person":
|
||||
|
@ -300,6 +300,7 @@ export interface HistoryGraphCardConfig extends LovelaceCardConfig {
|
||||
hours_to_show?: number;
|
||||
refresh_interval?: number;
|
||||
title?: string;
|
||||
show_names?: boolean;
|
||||
}
|
||||
|
||||
export interface StatisticsGraphCardConfig extends LovelaceCardConfig {
|
||||
|
@ -1,4 +1,5 @@
|
||||
import { HassEntities, HassEntity } from "home-assistant-js-websocket";
|
||||
import { SENSOR_ENTITIES } from "../../../common/const";
|
||||
import { computeDomain } from "../../../common/entity/compute_domain";
|
||||
import { computeStateDomain } from "../../../common/entity/compute_state_domain";
|
||||
import { computeStateName } from "../../../common/entity/compute_state_name";
|
||||
@ -23,7 +24,7 @@ import {
|
||||
PictureEntityCardConfig,
|
||||
ThermostatCardConfig,
|
||||
} from "../cards/types";
|
||||
import { LovelaceRowConfig } from "../entity-rows/types";
|
||||
import { EntityConfig } from "../entity-rows/types";
|
||||
import { ButtonsHeaderFooterConfig } from "../header-footer/types";
|
||||
|
||||
const HIDE_DOMAIN = new Set([
|
||||
@ -96,14 +97,15 @@ const splitByAreaDevice = (
|
||||
};
|
||||
|
||||
export const computeCards = (
|
||||
states: Array<[string, HassEntity?]>,
|
||||
states: HassEntities,
|
||||
entityIds: string[],
|
||||
entityCardOptions: Partial<EntitiesCardConfig>,
|
||||
renderFooterEntities = true
|
||||
): LovelaceCardConfig[] => {
|
||||
const cards: LovelaceCardConfig[] = [];
|
||||
|
||||
// For entity card
|
||||
const entities: Array<string | LovelaceRowConfig> = [];
|
||||
const entitiesConf: Array<string | EntityConfig> = [];
|
||||
|
||||
const titlePrefix = entityCardOptions.title
|
||||
? entityCardOptions.title.toLowerCase()
|
||||
@ -111,7 +113,8 @@ export const computeCards = (
|
||||
|
||||
const footerEntities: ButtonsHeaderFooterConfig["entities"] = [];
|
||||
|
||||
for (const [entityId, stateObj] of states) {
|
||||
for (const entityId of entityIds) {
|
||||
const stateObj = states[entityId];
|
||||
const domain = computeDomain(entityId);
|
||||
|
||||
if (domain === "alarm_control_panel") {
|
||||
@ -200,20 +203,49 @@ export const computeCards = (
|
||||
}
|
||||
: 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,
|
||||
// render the footer entities as normal entities.
|
||||
if (entities.length === 0 && footerEntities.length > 0) {
|
||||
return computeCards(states, entityCardOptions, false);
|
||||
if (entitiesConf.length === 0 && footerEntities.length > 0) {
|
||||
return computeCards(states, entityIds, entityCardOptions, false);
|
||||
}
|
||||
|
||||
if (entities.length > 0 || footerEntities.length > 0) {
|
||||
if (entitiesConf.length > 0 || footerEntities.length > 0) {
|
||||
const card: EntitiesCardConfig = {
|
||||
type: "entities",
|
||||
entities,
|
||||
entities: entitiesConf,
|
||||
...entityCardOptions,
|
||||
};
|
||||
if (footerEntities.length > 0) {
|
||||
@ -354,15 +386,10 @@ export const generateViewConfig = (
|
||||
|
||||
for (const groupEntity of splitted.groups) {
|
||||
cards.push(
|
||||
...computeCards(
|
||||
groupEntity.attributes.entity_id.map(
|
||||
(entityId): [string, HassEntity] => [entityId, entities[entityId]]
|
||||
),
|
||||
{
|
||||
title: computeStateName(groupEntity),
|
||||
show_header_toggle: groupEntity.attributes.control !== "hidden",
|
||||
}
|
||||
)
|
||||
...computeCards(entities, groupEntity.attributes.entity_id, {
|
||||
title: computeStateName(groupEntity),
|
||||
show_header_toggle: groupEntity.attributes.control !== "hidden",
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
@ -398,17 +425,13 @@ export const generateViewConfig = (
|
||||
.forEach((domain) => {
|
||||
cards.push(
|
||||
...computeCards(
|
||||
ungroupedEntitites[domain]
|
||||
.sort((a, b) =>
|
||||
stringCompare(
|
||||
computeStateName(entities[a]),
|
||||
computeStateName(entities[b])
|
||||
)
|
||||
entities,
|
||||
ungroupedEntitites[domain].sort((a, b) =>
|
||||
stringCompare(
|
||||
computeStateName(entities[a]),
|
||||
computeStateName(entities[b])
|
||||
)
|
||||
.map((entityId): [string, HassEntity] => [
|
||||
entityId,
|
||||
entities[entityId],
|
||||
]),
|
||||
),
|
||||
{
|
||||
title: domainTranslations[domain],
|
||||
}
|
||||
@ -466,28 +489,35 @@ export const generateDefaultViewConfig = (
|
||||
groupOrders
|
||||
);
|
||||
|
||||
const splittedCards: LovelaceCardConfig[] = [];
|
||||
const areaCards: LovelaceCardConfig[] = [];
|
||||
|
||||
for (const [areaId, areaEntities] of Object.entries(
|
||||
splittedByAreaDevice.areasWithEntities
|
||||
)) {
|
||||
const area = areaEntries[areaId];
|
||||
splittedCards.push(
|
||||
areaCards.push(
|
||||
...computeCards(
|
||||
areaEntities.map((entity) => [entity.entity_id, entity]),
|
||||
entities,
|
||||
areaEntities.map((entity) => entity.entity_id),
|
||||
{
|
||||
title: area.name,
|
||||
}
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
areaCards.sort((a, b) => stringCompare(a.title || "", b.title || ""));
|
||||
|
||||
const deviceCards: LovelaceCardConfig[] = [];
|
||||
|
||||
for (const [deviceId, deviceEntities] of Object.entries(
|
||||
splittedByAreaDevice.devicesWithEntities
|
||||
)) {
|
||||
const device = deviceEntries[deviceId];
|
||||
splittedCards.push(
|
||||
deviceCards.push(
|
||||
...computeCards(
|
||||
deviceEntities.map((entity) => [entity.entity_id, entity]),
|
||||
entities,
|
||||
deviceEntities.map((entity) => entity.entity_id),
|
||||
{
|
||||
title:
|
||||
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) {
|
||||
// Distribution card requires the grid to be configured
|
||||
const grid = energyPrefs.energy_sources.find(
|
||||
@ -510,17 +545,21 @@ export const generateDefaultViewConfig = (
|
||||
) as GridSourceTypeEnergyPreference | undefined;
|
||||
|
||||
if (grid && grid.flow_from.length > 0) {
|
||||
splittedCards.push({
|
||||
energyCard = {
|
||||
title: localize(
|
||||
"ui.panel.lovelace.cards.energy.energy_distribution.title_today"
|
||||
),
|
||||
type: "energy-distribution",
|
||||
link_dashboard: true,
|
||||
});
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
config.cards!.unshift(...splittedCards);
|
||||
config.cards!.unshift(
|
||||
...areaCards,
|
||||
...(energyCard ? [energyCard] : []),
|
||||
...deviceCards
|
||||
);
|
||||
|
||||
return config;
|
||||
};
|
||||
|
@ -30,15 +30,9 @@ export class HuiDialogSuggestCard extends LitElement {
|
||||
this._params = params;
|
||||
this._cardConfig =
|
||||
params.cardConfig ||
|
||||
computeCards(
|
||||
params.entities.map((entityId) => [
|
||||
entityId,
|
||||
this.hass.states[entityId],
|
||||
]),
|
||||
{
|
||||
title: params.cardTitle,
|
||||
}
|
||||
);
|
||||
computeCards(this.hass.states, params.entities, {
|
||||
title: params.cardTitle,
|
||||
});
|
||||
if (!Object.isFrozen(this._cardConfig)) {
|
||||
this._cardConfig = deepFreeze(this._cardConfig);
|
||||
}
|
||||
|
@ -4,6 +4,7 @@ import { customElement, property, state } from "lit/decorators";
|
||||
import {
|
||||
array,
|
||||
assert,
|
||||
boolean,
|
||||
number,
|
||||
object,
|
||||
optional,
|
||||
@ -28,6 +29,7 @@ const cardConfigStruct = assign(
|
||||
title: optional(string()),
|
||||
hours_to_show: optional(number()),
|
||||
refresh_interval: optional(number()),
|
||||
show_names: optional(boolean()),
|
||||
})
|
||||
);
|
||||
|
||||
|
@ -122,6 +122,7 @@ class HuiCastRow extends LitElement implements LovelaceRow {
|
||||
await ensureConnectedCastSession(this._castManager!, this.hass.auth);
|
||||
castSendShowLovelaceView(
|
||||
this._castManager!,
|
||||
this.hass.auth.data.hassUrl,
|
||||
this._config!.view,
|
||||
this._config!.dashboard
|
||||
);
|
||||
|
@ -137,10 +137,13 @@ documentContainer.innerHTML = `<custom-style>
|
||||
|
||||
/* rgb state color */
|
||||
--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-pending-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-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-alerting-color: var(--rgb-red-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-media-player-color: var(--rgb-indigo-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-sensor-battery-high-color: var(--rgb-green-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-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-idle-line-color: rgba(0, 0, 0, 0.42);
|
||||
--input-hover-line-color: rgba(0, 0, 0, 0.87);
|
||||
|
@ -634,6 +634,7 @@
|
||||
"label": "Calendar",
|
||||
"my_calendars": "My Calendars",
|
||||
"today": "Today",
|
||||
"event_retrieval_error": "Could not retrieve events for calendars: ",
|
||||
"event": {
|
||||
"add": "Add Event",
|
||||
"delete": "Delete Event",
|
||||
@ -642,7 +643,9 @@
|
||||
"all_day": "All Day",
|
||||
"start": "Start",
|
||||
"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": {
|
||||
"delete": "Delete Event",
|
||||
"delete_this": "Delete Only This Event",
|
||||
|
Loading…
x
Reference in New Issue
Block a user