mirror of
https://github.com/home-assistant/frontend.git
synced 2025-07-27 03:06:41 +00:00
20221206.0 (#14585)
This commit is contained in:
commit
dacef605c7
18
.gitignore
vendored
18
.gitignore
vendored
@ -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/
|
||||||
|
@ -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() {
|
||||||
|
@ -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;
|
||||||
|
@ -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
0
hassio/.gitignore
vendored
@ -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"
|
||||||
|
@ -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) =>
|
||||||
|
@ -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,
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
|
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";
|
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;
|
||||||
}
|
}
|
||||||
|
@ -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"
|
||||||
|
@ -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"] {
|
||||||
|
@ -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,
|
||||||
},
|
},
|
||||||
|
@ -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]
|
||||||
|
@ -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}
|
||||||
|
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 { 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,
|
||||||
|
@ -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>`}`
|
||||||
: ""}`;
|
: ""}`;
|
||||||
}
|
}
|
||||||
|
@ -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(
|
||||||
|
@ -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(
|
||||||
|
@ -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 {
|
||||||
|
@ -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<
|
||||||
|
@ -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"],
|
||||||
|
@ -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;
|
||||||
|
@ -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>
|
||||||
`}
|
`}
|
||||||
|
@ -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;
|
||||||
|
@ -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() {
|
||||||
|
@ -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>
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
|
@ -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),
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
@ -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":
|
||||||
|
@ -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 {
|
||||||
|
@ -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(
|
|
||||||
(entityId): [string, HassEntity] => [entityId, entities[entityId]]
|
|
||||||
),
|
|
||||||
{
|
|
||||||
title: computeStateName(groupEntity),
|
title: computeStateName(groupEntity),
|
||||||
show_header_toggle: groupEntity.attributes.control !== "hidden",
|
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;
|
||||||
};
|
};
|
||||||
|
@ -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) => [
|
|
||||||
entityId,
|
|
||||||
this.hass.states[entityId],
|
|
||||||
]),
|
|
||||||
{
|
|
||||||
title: params.cardTitle,
|
title: params.cardTitle,
|
||||||
}
|
});
|
||||||
);
|
|
||||||
if (!Object.isFrozen(this._cardConfig)) {
|
if (!Object.isFrozen(this._cardConfig)) {
|
||||||
this._cardConfig = deepFreeze(this._cardConfig);
|
this._cardConfig = deepFreeze(this._cardConfig);
|
||||||
}
|
}
|
||||||
|
@ -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()),
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -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
|
||||||
);
|
);
|
||||||
|
@ -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);
|
||||||
|
@ -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",
|
||||||
|
Loading…
x
Reference in New Issue
Block a user