Compare commits

..

35 Commits

Author SHA1 Message Date
Paul Bottein a86bab98ad Fix cursor jump when dragging slider 2024-10-14 10:48:34 +02:00
Paul Bottein 8939dd2213 Use 12 columns grid 2024-10-14 10:29:14 +02:00
Paul Bottein fde1bb7d6a Allow to resize card in the grid with more precision 2024-10-11 12:04:14 +02:00
Wendelin 07e5aa30c6 Add hide completed option to hui-todo-list-card (#22323) 2024-10-11 08:58:40 +02:00
Paul Bottein 3f0ec03a14 Improve zigbee remove device dialog (#22276)
* Improve zigbee remove device dialog

* Fix translations
2024-10-11 06:40:15 +02:00
Alex Jurkiewicz 1bb871b9ac fix(script/bootstrap): Improve missing Yarn error (#22308) 2024-10-10 18:27:22 +03:00
Paul Bottein 0e8783fb01 Use default font for heading card (#22322) 2024-10-10 15:19:14 +00:00
Bram Kragten 1d88c4465b Bumped version to 20241010.0 2024-10-10 17:14:04 +02:00
Wendelin af2d575bf0 Fix ha-selector-action drag and drop (#22273)
* Fix ha-selector-action with removing memoize-one

* Fix array-move to update parent reference.

* Fix array-move if item is no array
2024-10-10 16:53:35 +02:00
Stefan Agner 92165d776a Fix command selection for OTBRs without dataset (#22318)
Typically, the Home Assistant OTBR integration makes sure that we
either setup or read the current dataset. However, in some cases,
e.g. when reinstalling the add-on, deleting the dataset, and starting
the add-on while keeping the OTBR config entry, the dataset is not
available and a new one is not being created (since the config entry
is not recreated).

Just support this particular case as well.

Fixes: #22306
2024-10-10 16:50:44 +02:00
renovate[bot] a8bbd8ab90 Update dependency @codemirror/commands to v6.7.0 (#22316)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-10 10:21:29 +00:00
renovate[bot] 43ac9dbea7 Update vaadinWebComponents monorepo to v24.4.11 (#22315)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-10 10:20:33 +00:00
renovate[bot] bba9eca4e9 Update dependency eslint-plugin-wc to v2.2.0 (#22310)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-10 10:04:10 +02:00
renovate[bot] 40f65b1980 Update dependency del to v8 (#22311)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-10 10:03:29 +02:00
karwosts 23a33b10a1 Allow override entity_id in more-info action (#22147) 2024-10-09 14:14:03 +02:00
Simon Lamon 67a93013c7 Revert "Fix drag and drop when using action and trigger selector" (#22296)
Revert "Fix drag and drop when using action and trigger selector (#22291)"

This reverts commit 99035cea8f.
2024-10-09 10:22:40 +00:00
Bram Kragten 1f838d7529 Update statistics issues from dev tools (#22286)
update statistics issues from dev tools
2024-10-09 09:29:30 +02:00
TJ Horner ffc0435144 Fix erroneous addition of service: key in ha-automation-action-play_media element (#22294)
fix: no longer erroneously set 'service' in ha-automation-action-play_media
2024-10-08 21:07:38 +00:00
David F. Mulcahey 5877d69c87 Fix ZHA group dashboard display on mobile (#22279) 2024-10-08 21:20:07 +02:00
Paul Bottein 99035cea8f Fix drag and drop when using action and trigger selector (#22291)
* Fix drag and drop when using action selector

* Fix drag and drop when using trigger selector
2024-10-08 21:04:45 +02:00
__JosephAbbey 1b441a7eec Add support for relative start and end time displays in state-display (#22249)
* Add support for relative start and end time displays in state-display

* Add support for sun attributes as well

* Improve state-display code for relative-time

---------

Co-authored-by: Wendelin <w@pe8.at>
2024-10-08 10:04:16 +02:00
Paul Bottein ad49e9f7b0 Add minimal size for badges and cards in edit mode (#22271) 2024-10-07 17:33:47 +02:00
Petar Petrov e32b15ede2 Hide service dropdown for predefined actions in automations (#22275)
Hide service dropdown for predefined actions
2024-10-07 15:49:44 +02:00
Wendelin a35b4376ea Fix unused entities view (#22274)
Fix compute-unused-entities when using sections
2024-10-07 15:28:14 +02:00
Simon Lamon 619f9f76ee Fixup service/action when entity is picked in activate scene (#22259)
Fixup service/action when entity is picked
2024-10-07 09:23:48 +02:00
dependabot[bot] f771bc10db Bump actions/cache from 4.0.2 to 4.1.0 (#22270)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-10-07 08:44:00 +02:00
renovate[bot] b8889a1183 Update dependency eslint-plugin-import to v2.31.0 (#22260)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-06 10:47:08 +00:00
renovate[bot] eb6b45eaed Update babel monorepo to v7.25.7 (#22250)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-05 20:11:42 +02:00
renovate[bot] 31a748ed93 Update dependency date-fns-tz to v3.2.0 (#22209)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-04 21:38:25 +02:00
Bram Kragten 0110bdd24a Fix and update step flow create (#22223)
* Fix and update step flow create

* cleanup
2024-10-04 14:13:21 +02:00
Bram Kragten 365b712976 Add temporary logging to webrtc player (#22213)
* add temporary logging to webrtc player

* Update ha-web-rtc-player.ts

* Update ha-web-rtc-player.ts

* Update ha-web-rtc-player.ts
2024-10-03 21:01:21 +02:00
Paul Bottein 7d97dbe15b Fix potential undefined select element in color picker (#22212) 2024-10-03 11:31:36 +02:00
Paul Bottein 8bc0ea5a0b Update heading entity schema to allow empty entity id (#22211) 2024-10-03 11:15:13 +02:00
Adam Kapos 44948a3474 Disable backdrop filter on Heading Card (#22204) 2024-10-03 09:45:29 +02:00
Robert Resch bc51b53b4a Use camera ws endpoint to get WebRTC config (#22009) 2024-10-03 09:19:49 +02:00
27 changed files with 1135 additions and 1017 deletions
+1 -1
View File
@@ -37,7 +37,7 @@ jobs:
- name: Build resources
run: ./node_modules/.bin/gulp gen-icons-json build-translations build-locale-data gather-gallery-pages
- name: Setup lint cache
uses: actions/cache@v4.0.2
uses: actions/cache@v4.1.0
with:
path: |
node_modules/.cache/prettier
+13 -13
View File
@@ -25,10 +25,10 @@
"license": "Apache-2.0",
"type": "module",
"dependencies": {
"@babel/runtime": "7.25.6",
"@babel/runtime": "7.25.7",
"@braintree/sanitize-url": "7.1.0",
"@codemirror/autocomplete": "6.18.1",
"@codemirror/commands": "6.6.2",
"@codemirror/commands": "6.7.0",
"@codemirror/language": "6.10.3",
"@codemirror/legacy-modes": "6.4.1",
"@codemirror/search": "6.5.6",
@@ -89,8 +89,8 @@
"@polymer/polymer": "3.5.1",
"@replit/codemirror-indentation-markers": "6.5.3",
"@thomasloven/round-slider": "0.6.0",
"@vaadin/combo-box": "24.4.10",
"@vaadin/vaadin-themable-mixin": "24.4.10",
"@vaadin/combo-box": "24.4.11",
"@vaadin/vaadin-themable-mixin": "24.4.11",
"@vibrant/color": "3.2.1-alpha.1",
"@vibrant/core": "3.2.1-alpha.1",
"@vibrant/quantizer-mmcq": "3.2.1-alpha.1",
@@ -104,7 +104,7 @@
"core-js": "3.38.1",
"cropperjs": "1.6.2",
"date-fns": "4.1.0",
"date-fns-tz": "3.1.3",
"date-fns-tz": "3.2.0",
"deep-clone-simple": "1.1.1",
"deep-freeze": "0.0.1",
"dialog-polyfill": "0.5.6",
@@ -151,12 +151,12 @@
"xss": "1.0.15"
},
"devDependencies": {
"@babel/core": "7.25.2",
"@babel/core": "7.25.7",
"@babel/helper-define-polyfill-provider": "0.6.2",
"@babel/plugin-proposal-decorators": "7.24.7",
"@babel/plugin-transform-runtime": "7.25.4",
"@babel/preset-env": "7.25.4",
"@babel/preset-typescript": "7.24.7",
"@babel/plugin-proposal-decorators": "7.25.7",
"@babel/plugin-transform-runtime": "7.25.7",
"@babel/preset-env": "7.25.7",
"@babel/preset-typescript": "7.25.7",
"@bundle-stats/plugin-webpack-filter": "4.15.1",
"@koa/cors": "5.0.0",
"@lokalise/node-api": "12.7.0",
@@ -195,17 +195,17 @@
"babel-plugin-template-html-minifier": "4.1.0",
"browserslist-useragent-regexp": "4.1.3",
"chai": "5.1.1",
"del": "7.1.0",
"del": "8.0.0",
"eslint": "8.57.1",
"eslint-config-airbnb-base": "15.0.0",
"eslint-config-airbnb-typescript": "18.0.0",
"eslint-config-prettier": "9.1.0",
"eslint-import-resolver-webpack": "0.13.9",
"eslint-plugin-import": "2.30.0",
"eslint-plugin-import": "2.31.0",
"eslint-plugin-lit": "1.15.0",
"eslint-plugin-lit-a11y": "4.1.4",
"eslint-plugin-unused-imports": "4.1.4",
"eslint-plugin-wc": "2.1.1",
"eslint-plugin-wc": "2.2.0",
"fancy-log": "2.0.0",
"fs-extra": "11.2.0",
"glob": "11.0.0",
+1 -1
View File
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "home-assistant-frontend"
version = "20241002.3"
version = "20241010.0"
license = {text = "Apache-2.0"}
description = "The Home Assistant frontend"
readme = "README.md"
+5 -1
View File
@@ -18,5 +18,9 @@ if [[ -n "$DEVCONTAINER" ]]; then
fi
fi
if ! command -v yarn &> /dev/null; then
echo "Error: yarn not found. Please install it following the official instructions: https://yarnpkg.com/getting-started/install" >&2
exit 1
fi
# Install node modules
yarn install
yarn install
+2 -2
View File
@@ -68,7 +68,7 @@ export class HaGridSizeEditor extends LitElement {
.min=${columnMin}
.max=${columnMax}
.range=${this.columns}
.value=${fullWidth ? this.columns : columnValue}
.value=${fullWidth ? this.columns : this.value?.columns}
@value-changed=${this._valueChanged}
@slider-moved=${this._sliderMoved}
.disabled=${disabledColumns}
@@ -83,7 +83,7 @@ export class HaGridSizeEditor extends LitElement {
.max=${rowMax}
.range=${this.rows}
vertical
.value=${rowValue}
.value=${autoHeight ? rowMin : this.value?.rows}
@value-changed=${this._valueChanged}
@slider-moved=${this._sliderMoved}
.disabled=${disabledRows}
+53 -34
View File
@@ -1,3 +1,4 @@
/* eslint-disable no-console */
import {
css,
CSSResultGroup,
@@ -6,11 +7,14 @@ import {
PropertyValues,
TemplateResult,
} from "lit";
import { customElement, property, state, query } from "lit/decorators";
import { isComponentLoaded } from "../common/config/is_component_loaded";
import { customElement, property, query, state } from "lit/decorators";
import { ifDefined } from "lit/directives/if-defined";
import { fireEvent } from "../common/dom/fire_event";
import { handleWebRtcOffer, WebRtcAnswer } from "../data/camera";
import { fetchWebRtcSettings } from "../data/rtsp_to_webrtc";
import {
fetchWebRtcClientConfiguration,
handleWebRtcOffer,
WebRtcAnswer,
} from "../data/camera";
import type { HomeAssistant } from "../types";
import "./ha-alert";
@@ -37,12 +41,11 @@ class HaWebRtcPlayer extends LitElement {
@property({ type: Boolean, attribute: "playsinline" })
public playsInline = false;
@property() public posterUrl!: string;
@property({ attribute: "poster-url" }) public posterUrl?: string;
@state() private _error?: string;
// don't cache this, as we remove it on disconnects
@query("#remote-stream") private _videoEl!: HTMLVideoElement;
@query("#remote-stream", true) private _videoEl!: HTMLVideoElement;
private _peerConnection?: RTCPeerConnection;
@@ -59,7 +62,7 @@ class HaWebRtcPlayer extends LitElement {
.muted=${this.muted}
?playsinline=${this.playsInline}
?controls=${this.controls}
.poster=${this.posterUrl}
poster=${ifDefined(this.posterUrl)}
@loadeddata=${this._loadedData}
></video>
`;
@@ -81,20 +84,30 @@ class HaWebRtcPlayer extends LitElement {
if (!changedProperties.has("entityid")) {
return;
}
if (!this._videoEl) {
return;
}
this._startWebRtc();
}
private async _startWebRtc(): Promise<void> {
console.time("WebRTC");
this._error = undefined;
const configuration = await this._fetchPeerConfiguration();
const peerConnection = new RTCPeerConnection(configuration);
// Some cameras (such as nest) require a data channel to establish a stream
// however, not used by any integrations.
peerConnection.createDataChannel("dataSendChannel");
console.timeLog("WebRTC", "start clientConfig");
const clientConfig = await fetchWebRtcClientConfiguration(
this.hass,
this.entityid
);
console.timeLog("WebRTC", "end clientConfig", clientConfig);
const peerConnection = new RTCPeerConnection(clientConfig.configuration);
if (clientConfig.dataChannel) {
// Some cameras (such as nest) require a data channel to establish a stream
// however, not used by any integrations.
peerConnection.createDataChannel(clientConfig.dataChannel);
}
peerConnection.addTransceiver("audio", { direction: "recvonly" });
peerConnection.addTransceiver("video", { direction: "recvonly" });
@@ -102,30 +115,48 @@ class HaWebRtcPlayer extends LitElement {
offerToReceiveAudio: true,
offerToReceiveVideo: true,
};
console.timeLog("WebRTC", "start createOffer", offerOptions);
const offer: RTCSessionDescriptionInit =
await peerConnection.createOffer(offerOptions);
console.timeLog("WebRTC", "end createOffer", offer);
console.timeLog("WebRTC", "start setLocalDescription");
await peerConnection.setLocalDescription(offer);
console.timeLog("WebRTC", "end setLocalDescription");
console.timeLog("WebRTC", "start iceResolver");
let candidates = ""; // Build an Offer SDP string with ice candidates
const iceResolver = new Promise<void>((resolve) => {
peerConnection.addEventListener("icecandidate", async (event) => {
peerConnection.addEventListener("icecandidate", (event) => {
if (!event.candidate?.candidate) {
resolve(); // Gathering complete
return;
}
console.timeLog("WebRTC", "iceResolver candidate", event.candidate);
candidates += `a=${event.candidate.candidate}\r\n`;
});
});
await iceResolver;
console.timeLog("WebRTC", "end iceResolver", candidates);
const offer_sdp = offer.sdp! + candidates;
let webRtcAnswer: WebRtcAnswer;
try {
console.timeLog("WebRTC", "start WebRTCOffer", offer_sdp);
webRtcAnswer = await handleWebRtcOffer(
this.hass,
this.entityid,
offer_sdp
);
console.timeLog("WebRTC", "end webRtcOffer", webRtcAnswer);
} catch (err: any) {
this._error = "Failed to start WebRTC stream: " + err.message;
peerConnection.close();
@@ -135,6 +166,7 @@ class HaWebRtcPlayer extends LitElement {
// Setup callbacks to render remote stream once media tracks are discovered.
const remoteStream = new MediaStream();
peerConnection.addEventListener("track", (event) => {
console.timeLog("WebRTC", "track", event);
remoteStream.addTrack(event.track);
this._videoEl.srcObject = remoteStream;
});
@@ -146,7 +178,9 @@ class HaWebRtcPlayer extends LitElement {
sdp: webRtcAnswer.answer,
});
try {
console.timeLog("WebRTC", "start setRemoteDescription", remoteDesc);
await peerConnection.setRemoteDescription(remoteDesc);
console.timeLog("WebRTC", "end setRemoteDescription");
} catch (err: any) {
this._error = "Failed to connect WebRTC stream: " + err.message;
peerConnection.close();
@@ -155,23 +189,6 @@ class HaWebRtcPlayer extends LitElement {
this._peerConnection = peerConnection;
}
private async _fetchPeerConfiguration(): Promise<RTCConfiguration> {
if (!isComponentLoaded(this.hass!, "rtsp_to_webrtc")) {
return {};
}
const settings = await fetchWebRtcSettings(this.hass!);
if (!settings || !settings.stun_server) {
return {};
}
return {
iceServers: [
{
urls: [`stun:${settings.stun_server!}`],
},
],
};
}
private _cleanUp() {
if (this._remoteStream) {
this._remoteStream.getTracks().forEach((track) => {
@@ -190,6 +207,8 @@ class HaWebRtcPlayer extends LitElement {
}
private _loadedData() {
console.timeLog("WebRTC", "loadedData");
console.timeEnd("WebRTC");
// @ts-ignore
fireEvent(this, "load");
}
+14
View File
@@ -133,3 +133,17 @@ export const isCameraMediaSource = (mediaContentId: string) =>
export const getEntityIdFromCameraMediaSource = (mediaContentId: string) =>
mediaContentId.substring(CAMERA_MEDIA_SOURCE_PREFIX.length);
export interface WebRTCClientConfiguration {
configuration: RTCConfiguration;
dataChannel?: string;
}
export const fetchWebRtcClientConfiguration = async (
hass: HomeAssistant,
entityId: string
) =>
hass.callWS<WebRTCClientConfiguration>({
type: "camera/webrtc/get_client_config",
entity_id: entityId,
});
+1
View File
@@ -28,6 +28,7 @@ export interface UrlActionConfig extends BaseActionConfig {
export interface MoreInfoActionConfig extends BaseActionConfig {
action: "more-info";
entity_id?: string;
}
export interface AssistActionConfig extends BaseActionConfig {
+6 -1
View File
@@ -1,11 +1,16 @@
import type { Condition } from "../../../panels/lovelace/common/validate-condition";
import type { LovelaceLayoutOptions } from "../../../panels/lovelace/types";
import type {
LovelaceGridOptions,
LovelaceLayoutOptions,
} from "../../../panels/lovelace/types";
export interface LovelaceCardConfig {
index?: number;
view_index?: number;
view_layout?: any;
/** @deprecated Use `grid_options` instead */
layout_options?: LovelaceLayoutOptions;
grid_options?: LovelaceGridOptions;
type: string;
[key: string]: any;
visibility?: Condition[];
-10
View File
@@ -1,10 +0,0 @@
import { HomeAssistant } from "../types";
export interface WebRtcSettings {
stun_server?: string;
}
export const fetchWebRtcSettings = async (hass: HomeAssistant) =>
hass.callWS<WebRtcSettings>({
type: "rtsp_to_webrtc/get_settings",
});
@@ -117,6 +117,7 @@ export class HaServiceAction extends LitElement implements ActionElement {
.value=${this._action}
.disabled=${this.disabled}
.showAdvanced=${this.hass.userData?.showAdvanced}
.hidePicker=${!!this._action.metadata}
@value-changed=${this._actionChanged}
></ha-service-control>
${domain && service && this.hass.services[domain]?.[service]?.response
@@ -83,9 +83,15 @@ export const getZHADeviceActions = async (
classes: "warning",
action: async () => {
const confirmed = await showConfirmationDialog(el, {
text: hass.localize(
"ui.dialogs.zha_device_info.confirmations.remove"
title: hass.localize(
"ui.dialogs.zha_device_info.confirmations.remove_title"
),
text: hass.localize(
"ui.dialogs.zha_device_info.confirmations.remove_text"
),
confirmText: hass.localize("ui.common.remove"),
dismissText: hass.localize("ui.common.cancel"),
destructive: true,
});
if (!confirmed) {
+38 -13
View File
@@ -5,6 +5,7 @@ import { MediaQueriesListener } from "../../../common/dom/media_query";
import "../../../components/ha-svg-icon";
import { LovelaceCardConfig } from "../../../data/lovelace/config/card";
import type { HomeAssistant } from "../../../types";
import { migrateLayoutToGridOptions } from "../common/compute-card-grid-size";
import { computeCardSize } from "../common/compute-card-size";
import {
attachConditionMediaQueriesListeners,
@@ -12,7 +13,7 @@ import {
} from "../common/validate-condition";
import { createCardElement } from "../create-element/create-card-element";
import { createErrorCardConfig } from "../create-element/create-element-base";
import type { LovelaceCard, LovelaceLayoutOptions } from "../types";
import type { LovelaceCard, LovelaceGridOptions } from "../types";
declare global {
interface HASSDomEvents {
@@ -67,20 +68,44 @@ export class HuiCard extends ReactiveElement {
return 1;
}
public getLayoutOptions(): LovelaceLayoutOptions {
const configOptions = this.config?.layout_options ?? {};
if (this._element) {
const cardOptions = this._element.getLayoutOptions?.() ?? {};
return {
...cardOptions,
...configOptions,
};
}
return configOptions;
public getGridOptions(): LovelaceGridOptions {
const elementOptions = this.getElementGridOptions();
const configOptions = this.getConfigGridOptions();
return {
...elementOptions,
...configOptions,
};
}
public getElementLayoutOptions(): LovelaceLayoutOptions {
return this._element?.getLayoutOptions?.() ?? {};
// options provided by the element
public getElementGridOptions(): LovelaceGridOptions {
if (!this._element) return {};
if (this._element.getGridOptions) {
return this._element.getGridOptions();
}
if (this._element.getLayoutOptions) {
// eslint-disable-next-line no-console
console.warn(
`This card (${this.config?.type}) is using "getLayoutOptions" and it is deprecated, contact the developer to suggest to use "getGridOptions" instead`
);
const config = migrateLayoutToGridOptions(
this._element.getLayoutOptions()
);
return config;
}
return {};
}
// options provided by the config
public getConfigGridOptions(): LovelaceGridOptions {
if (this.config?.grid_options) {
return this.config.grid_options;
}
if (this.config?.layout_options) {
return migrateLayoutToGridOptions(this.config.layout_options);
}
return {};
}
private _updateElement(config: LovelaceCardConfig) {
@@ -187,7 +187,6 @@ export class HuiHeadingCard extends LitElement implements LovelaceCard {
}
.content p {
margin: 0;
font-family: Roboto;
font-style: normal;
white-space: nowrap;
overflow: hidden;
@@ -275,7 +275,7 @@ export class HuiTodoListCard extends LitElement implements LovelaceCard {
"ui.panel.lovelace.cards.todo-list.no_unchecked_items"
)}
</p>`}
${checkedItems.length
${!this._config.hide_completed && checkedItems.length
? html`
<div role="separator">
<div class="divider"></div>
+1
View File
@@ -453,6 +453,7 @@ export interface TodoListCardConfig extends LovelaceCardConfig {
title?: string;
theme?: string;
entity?: string;
hide_completed?: boolean;
}
export interface StackCardConfig extends LovelaceCardConfig {
@@ -1,8 +1,34 @@
import { conditionalClamp } from "../../../common/number/clamp";
import { LovelaceLayoutOptions } from "../types";
import { LovelaceGridOptions, LovelaceLayoutOptions } from "../types";
const GRID_COLUMN_MULTIPLIER = 3;
const multiplyBy = <T extends number | string | undefined>(
value: T,
multiplier: number
): T => (typeof value === "number" ? ((value * multiplier) as T) : value);
export const migrateLayoutToGridOptions = (
options: LovelaceLayoutOptions
): LovelaceGridOptions => {
const gridOptions: LovelaceGridOptions = {
columns: multiplyBy(options.grid_columns, GRID_COLUMN_MULTIPLIER),
max_columns: multiplyBy(options.grid_max_columns, GRID_COLUMN_MULTIPLIER),
min_columns: multiplyBy(options.grid_min_columns, GRID_COLUMN_MULTIPLIER),
rows: options.grid_rows,
max_rows: options.grid_max_rows,
min_rows: options.grid_min_rows,
};
for (const [key, value] of Object.entries(gridOptions)) {
if (value === undefined) {
delete gridOptions[key];
}
}
return gridOptions;
};
export const DEFAULT_GRID_SIZE = {
columns: 4,
columns: 12,
rows: "auto",
} as CardGridSize;
@@ -12,14 +38,14 @@ export type CardGridSize = {
};
export const computeCardGridSize = (
options: LovelaceLayoutOptions
options: LovelaceGridOptions
): CardGridSize => {
const rows = options.grid_rows ?? DEFAULT_GRID_SIZE.rows;
const columns = options.grid_columns ?? DEFAULT_GRID_SIZE.columns;
const minRows = options.grid_min_rows;
const maxRows = options.grid_max_rows;
const minColumns = options.grid_min_columns;
const maxColumns = options.grid_max_columns;
const rows = options.rows ?? DEFAULT_GRID_SIZE.rows;
const columns = options.columns ?? DEFAULT_GRID_SIZE.columns;
const minRows = options.min_rows;
const maxRows = options.max_rows;
const minColumns = options.min_columns;
const maxColumns = options.max_columns;
const clampedRows =
typeof rows === "string" ? rows : conditionalClamp(rows, minRows, maxRows);
+7 -6
View File
@@ -94,12 +94,13 @@ export const handleAction = async (
switch (actionConfig.action) {
case "more-info": {
if (config.entity || config.camera_image || config.image_entity) {
fireEvent(node, "hass-more-info", {
entityId: (config.entity ||
config.camera_image ||
config.image_entity)!,
});
const entityId =
actionConfig.entity_id ||
config.entity ||
config.camera_image ||
config.image_entity;
if (entityId) {
fireEvent(node, "hass-more-info", { entityId });
} else {
showToast(node, {
message: hass.localize(
@@ -1,7 +1,7 @@
import type { ActionDetail } from "@material/mwc-list";
import { mdiCheck, mdiDotsVertical } from "@mdi/js";
import { css, html, LitElement, nothing, PropertyValues } from "lit";
import { customElement, property, query, state } from "lit/decorators";
import { customElement, property, state } from "lit/decorators";
import { styleMap } from "lit/directives/style-map";
import memoizeOne from "memoize-one";
import { fireEvent } from "../../../../common/dom/fire_event";
@@ -17,7 +17,6 @@ import "../../../../components/ha-slider";
import "../../../../components/ha-svg-icon";
import "../../../../components/ha-switch";
import "../../../../components/ha-yaml-editor";
import type { HaYamlEditor } from "../../../../components/ha-yaml-editor";
import { LovelaceCardConfig } from "../../../../data/lovelace/config/card";
import { LovelaceSectionConfig } from "../../../../data/lovelace/config/section";
import { haStyle } from "../../../../resources/styles";
@@ -26,8 +25,9 @@ import { HuiCard } from "../../cards/hui-card";
import {
CardGridSize,
computeCardGridSize,
migrateLayoutToGridOptions,
} from "../../common/compute-card-grid-size";
import { LovelaceLayoutOptions } from "../../types";
import { LovelaceGridOptions } from "../../types";
@customElement("hui-card-layout-editor")
export class HuiCardLayoutEditor extends LitElement {
@@ -37,21 +37,16 @@ export class HuiCardLayoutEditor extends LitElement {
@property({ attribute: false }) public sectionConfig!: LovelaceSectionConfig;
@state() _defaultLayoutOptions?: LovelaceLayoutOptions;
@state() _defaultGridOptions?: LovelaceGridOptions;
@state() public _yamlMode = false;
@state() public _uiAvailable = true;
@query("ha-yaml-editor") private _yamlEditor?: HaYamlEditor;
private _cardElement?: HuiCard;
private _mergedOptions = memoizeOne(
(
options?: LovelaceLayoutOptions,
defaultOptions?: LovelaceLayoutOptions
) => ({
(options?: LovelaceGridOptions, defaultOptions?: LovelaceGridOptions) => ({
...defaultOptions,
...options,
})
@@ -60,19 +55,30 @@ export class HuiCardLayoutEditor extends LitElement {
private _computeCardGridSize = memoizeOne(computeCardGridSize);
private _isDefault = memoizeOne(
(options?: LovelaceLayoutOptions) =>
options?.grid_columns === undefined && options?.grid_rows === undefined
(options?: LovelaceGridOptions) =>
options?.columns === undefined && options?.rows === undefined
);
private _configGridOptions = (config: LovelaceCardConfig) => {
if (config.grid_options) {
return config.grid_options;
}
if (config.layout_options) {
return migrateLayoutToGridOptions(config.layout_options);
}
return {};
};
render() {
const configGridOptions = this._configGridOptions(this.config);
const options = this._mergedOptions(
this.config.layout_options,
this._defaultLayoutOptions
configGridOptions,
this._defaultGridOptions
);
const value = this._computeCardGridSize(options);
const totalColumns = (this.sectionConfig.column_span ?? 1) * 4;
const totalColumns = (this.sectionConfig.column_span ?? 1) * 12;
return html`
<div class="header">
@@ -130,24 +136,24 @@ export class HuiCardLayoutEditor extends LitElement {
? html`
<ha-yaml-editor
.hass=${this.hass}
.defaultValue=${this.config.layout_options}
.defaultValue=${configGridOptions}
@value-changed=${this._valueChanged}
></ha-yaml-editor>
`
: html`
<ha-grid-size-picker
style=${styleMap({
"max-width": `${totalColumns * 45 + 50}px`,
"max-width": `${totalColumns * 20 + 50}px`,
})}
.columns=${totalColumns}
.hass=${this.hass}
.value=${value}
.isDefault=${this._isDefault(this.config.layout_options)}
.isDefault=${this._isDefault(configGridOptions)}
@value-changed=${this._gridSizeChanged}
.rowMin=${options.grid_min_rows}
.rowMax=${options.grid_max_rows}
.columnMin=${options.grid_min_columns}
.columnMax=${options.grid_max_columns}
.rowMin=${options.min_rows}
.rowMax=${options.max_rows}
.columnMin=${options.min_columns}
.columnMax=${options.max_columns}
></ha-grid-size-picker>
<ha-settings-row>
<span slot="heading" data-for="full-width">
@@ -167,6 +173,19 @@ export class HuiCardLayoutEditor extends LitElement {
>
</ha-switch>
</ha-settings-row>
<ha-settings-row>
<span slot="heading" data-for="full-width">
${this.hass.localize(
"ui.panel.lovelace.editor.edit_card.layout.precision_mode"
)}
</span>
<span slot="description" data-for="full-width">
${this.hass.localize(
"ui.panel.lovelace.editor.edit_card.layout.precision_mode_helper"
)}
</span>
<ha-switch name="full-precision_mode"> </ha-switch>
</ha-settings-row>
`}
`;
}
@@ -180,11 +199,10 @@ export class HuiCardLayoutEditor extends LitElement {
this._cardElement.config = this.config;
this._cardElement.addEventListener("card-updated", (ev: Event) => {
ev.stopPropagation();
this._defaultLayoutOptions =
this._cardElement?.getElementLayoutOptions();
this._defaultGridOptions = this._cardElement?.getElementGridOptions();
});
this._cardElement.load();
this._defaultLayoutOptions = this._cardElement.getElementLayoutOptions();
this._defaultGridOptions = this._cardElement.getElementGridOptions();
} catch (err) {
// eslint-disable-next-line no-console
console.error(err);
@@ -211,53 +229,49 @@ export class HuiCardLayoutEditor extends LitElement {
case 1:
this._yamlMode = true;
break;
case 2:
this._reset();
break;
}
}
private async _reset() {
const newConfig = { ...this.config };
delete newConfig.layout_options;
this._yamlEditor?.setValue({});
fireEvent(this, "value-changed", { value: newConfig });
}
private _gridSizeChanged(ev: CustomEvent): void {
ev.stopPropagation();
const value = ev.detail.value as CardGridSize;
const newConfig: LovelaceCardConfig = {
...this.config,
layout_options: {
...this.config.layout_options,
grid_columns: value.columns,
grid_rows: value.rows,
grid_options: {
...this.config.grid_options,
columns: value.columns,
rows: value.rows,
},
};
if (newConfig.layout_options!.grid_columns === undefined) {
delete newConfig.layout_options!.grid_columns;
}
if (newConfig.layout_options!.grid_rows === undefined) {
delete newConfig.layout_options!.grid_rows;
}
if (Object.keys(newConfig.layout_options!).length === 0) {
delete newConfig.layout_options;
}
this._updateValue(newConfig);
}
fireEvent(this, "value-changed", { value: newConfig });
private _updateValue(value: LovelaceCardConfig): void {
if (value.grid_options!.columns === undefined) {
delete value.grid_options!.columns;
}
if (value.grid_options!.rows === undefined) {
delete value.grid_options!.rows;
}
if (Object.keys(value.grid_options!).length === 0) {
delete value.grid_options;
}
if (value.layout_options) {
delete value.layout_options;
}
fireEvent(this, "value-changed", { value });
}
private _valueChanged(ev: CustomEvent): void {
ev.stopPropagation();
const options = ev.detail.value as LovelaceLayoutOptions;
const options = ev.detail.value as LovelaceGridOptions;
const newConfig: LovelaceCardConfig = {
...this.config,
layout_options: options,
grid_options: options,
};
fireEvent(this, "value-changed", { value: newConfig });
this._updateValue(newConfig);
}
private _fullWidthChanged(ev): void {
@@ -265,14 +279,12 @@ export class HuiCardLayoutEditor extends LitElement {
const value = ev.target.checked;
const newConfig: LovelaceCardConfig = {
...this.config,
layout_options: {
...this.config.layout_options,
grid_columns: value
? "full"
: (this._defaultLayoutOptions?.grid_min_columns ?? 1),
grid_options: {
...this.config.grid_options,
columns: value ? "full" : (this._defaultGridOptions?.min_columns ?? 1),
},
};
fireEvent(this, "value-changed", { value: newConfig });
this._updateValue(newConfig);
}
static styles = [
@@ -1,6 +1,6 @@
import { CSSResultGroup, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { assert, assign, object, optional, string } from "superstruct";
import { assert, assign, boolean, object, optional, string } from "superstruct";
import { isComponentLoaded } from "../../../../common/config/is_component_loaded";
import { fireEvent } from "../../../../common/dom/fire_event";
import "../../../../components/ha-alert";
@@ -18,6 +18,7 @@ const cardConfigStruct = assign(
title: optional(string()),
theme: optional(string()),
entity: optional(string()),
hide_completed: optional(boolean()),
})
);
@@ -30,6 +31,7 @@ const SCHEMA = [
},
},
{ name: "theme", selector: { theme: {} } },
{ name: "hide_completed", selector: { boolean: {} } },
] as const;
@customElement("hui-todo-list-card-editor")
@@ -87,6 +89,10 @@ export class HuiTodoListEditor
)} (${this.hass!.localize(
"ui.panel.lovelace.editor.card.config.optional"
)})`;
case "hide_completed":
return this.hass!.localize(
"ui.panel.lovelace.editor.card.todo-list.hide_completed"
);
default:
return this.hass!.localize(
`ui.panel.lovelace.editor.card.generic.${schema.name}`
@@ -61,6 +61,11 @@ const actionConfigStructAssist = type({
start_listening: optional(boolean()),
});
const actionConfigStructMoreInfo = type({
action: literal("more-info"),
entity_id: optional(string()),
});
export const actionConfigStructType = object({
action: enums([
"none",
@@ -93,6 +98,9 @@ export const actionConfigStruct = dynamic<any>((value) => {
case "assist": {
return actionConfigStructAssist;
}
case "more-info": {
return actionConfigStructMoreInfo;
}
}
}
@@ -4,5 +4,6 @@ export const baseLovelaceCardConfig = object({
type: string(),
view_layout: any(),
layout_options: any(),
grid_options: any(),
visibility: any(),
});
@@ -84,9 +84,9 @@ export class GridSection extends LitElement implements LovelaceSectionElement {
(_cardConfig, idx) => {
const card = this.cards![idx];
card.layout = "grid";
const layoutOptions = card.getLayoutOptions();
const gridOptions = card.getGridOptions();
const { rows, columns } = computeCardGridSize(layoutOptions);
const { rows, columns } = computeCardGridSize(gridOptions);
return html`
<div
@@ -96,7 +96,7 @@ export class GridSection extends LitElement implements LovelaceSectionElement {
"--row-size": typeof rows === "number" ? rows : undefined,
})}
class="card ${classMap({
"fit-rows": typeof layoutOptions?.grid_rows === "number",
"fit-rows": typeof rows === "number",
"full-width": columns === "full",
})}"
>
@@ -165,7 +165,7 @@ export class GridSection extends LitElement implements LovelaceSectionElement {
haStyle,
css`
:host {
--base-column-count: 4;
--base-column-count: 12;
--row-gap: var(--ha-section-grid-row-gap, 8px);
--column-gap: var(--ha-section-grid-column-gap, 8px);
--row-height: var(--ha-section-grid-row-height, 56px);
@@ -230,8 +230,8 @@ export class GridSection extends LitElement implements LovelaceSectionElement {
.add {
outline: none;
grid-row: span var(--row-size, 1);
grid-column: span var(--column-size, 2);
grid-row: span 1;
grid-column: span 6;
background: none;
cursor: pointer;
border-radius: var(--ha-card-border-radius, 12px);
+11
View File
@@ -51,12 +51,23 @@ export type LovelaceLayoutOptions = {
grid_max_rows?: number;
};
export type LovelaceGridOptions = {
columns?: number | "full";
rows?: number | "auto";
max_columns?: number;
min_columns?: number;
min_rows?: number;
max_rows?: number;
};
export interface LovelaceCard extends HTMLElement {
hass?: HomeAssistant;
preview?: boolean;
layout?: string;
getCardSize(): number | Promise<number>;
/** @deprecated Use `getGridOptions` instead */
getLayoutOptions?(): LovelaceLayoutOptions;
getGridOptions?(): LovelaceGridOptions;
setConfig(config: LovelaceCardConfig): void;
}
+23 -16
View File
@@ -99,31 +99,38 @@ class StateDisplay extends LitElement {
if (content === "name") {
return html`${this.name || stateObj.attributes.friendly_name}`;
}
let relativeDateTime: string | undefined;
// Check last-changed for backwards compatibility
if (content === "last_changed" || content === "last-changed") {
return html`
<ha-relative-time
.hass=${this.hass}
.datetime=${stateObj.last_changed}
capitalize
></ha-relative-time>
`;
relativeDateTime = stateObj.last_changed;
}
// Check last_updated for backwards compatibility
if (content === "last_updated" || content === "last-updated") {
return html`
<ha-relative-time
.hass=${this.hass}
.datetime=${stateObj.last_updated}
capitalize
></ha-relative-time>
`;
relativeDateTime = stateObj.last_updated;
}
if (content === "last_triggered") {
if (
content === "last_triggered" ||
(domain === "calendar" &&
(content === "start_time" || content === "end_time")) ||
(domain === "sun" &&
(content === "next_dawn" ||
content === "next_dusk" ||
content === "next_midnight" ||
content === "next_noon" ||
content === "next_rising" ||
content === "next_setting"))
) {
relativeDateTime = stateObj.attributes[content];
}
if (relativeDateTime) {
return html`
<ha-relative-time
.hass=${this.hass}
.datetime=${stateObj.attributes.last_triggered}
.datetime=${relativeDateTime}
capitalize
></ha-relative-time>
`;
+7 -3
View File
@@ -1635,7 +1635,8 @@
"zigbee_information": "View the Zigbee information for the device."
},
"confirmations": {
"remove": "Are you sure that you want to remove the device?"
"remove_title": "Remove device",
"remove_text": "This device will be permanently removed from the Zigbee network."
},
"quirk": "Quirk",
"last_seen": "Last seen",
@@ -5644,7 +5645,9 @@
},
"layout": {
"full_width": "Full width card",
"full_width_helper": "Take up the full width of the section whatever its size"
"full_width_helper": "Take up the full width of the section whatever its size",
"precision_mode": "Precision mode",
"precision_mode_helper": "Change the card width with precision without limits"
}
},
"edit_badge": {
@@ -6131,7 +6134,8 @@
"todo-list": {
"name": "To-do list",
"description": "The to-do list card allows you to add, edit, check-off, and clear items from your to-do list.",
"integration_not_loaded": "This card requires the `todo` integration to be set up."
"integration_not_loaded": "This card requires the `todo` integration to be set up.",
"hide_completed": "Hide completed items"
},
"thermostat": {
"name": "Thermostat",
+815 -838
View File
File diff suppressed because it is too large Load Diff