diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index d6033abcd080..7f8ab1df13e7 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -8,6 +8,7 @@ "postCreateCommand": "sudo apt update && sudo apt upgrade -y && sudo apt install -y libpcap-dev", "postStartCommand": "script/bootstrap", "containerEnv": { + "DEV_CONTAINER": "1", "WORKSPACE_DIRECTORY": "${containerWorkspaceFolder}" }, "customizations": { diff --git a/build-scripts/env.cjs b/build-scripts/env.cjs index bb0d6b3d0028..6f208b779b33 100644 --- a/build-scripts/env.cjs +++ b/build-scripts/env.cjs @@ -32,4 +32,7 @@ module.exports = { } return version[1]; }, + isDevContainer() { + return process.env.DEV_CONTAINER === "1"; + }, }; diff --git a/build-scripts/gulp/webpack.js b/build-scripts/gulp/webpack.js index 51f062f316e1..a7eb3be20d8e 100644 --- a/build-scripts/gulp/webpack.js +++ b/build-scripts/gulp/webpack.js @@ -40,8 +40,12 @@ const runDevServer = async ({ compiler, contentBase, port, - listenHost = "localhost", + listenHost = undefined, }) => { + if (listenHost === undefined) { + // For dev container, we need to listen on all hosts + listenHost = env.isDevContainer() ? "0.0.0.0" : "localhost"; + } const server = new WebpackDevServer( { hot: false, diff --git a/cast/src/launcher/entrypoint.ts b/cast/src/launcher/entrypoint.ts index 423e3165e771..f7d615d45857 100644 --- a/cast/src/launcher/entrypoint.ts +++ b/cast/src/launcher/entrypoint.ts @@ -1,4 +1,3 @@ -import "../../../src/resources/safari-14-attachshadow-patch"; import "./layout/hc-connect"; import("../../../src/resources/ha-style"); diff --git a/demo/src/entrypoint.ts b/demo/src/entrypoint.ts index e02049326436..e153eabb128c 100644 --- a/demo/src/entrypoint.ts +++ b/demo/src/entrypoint.ts @@ -1,4 +1,3 @@ -import "../../src/resources/safari-14-attachshadow-patch"; import "./util/is_frontpage"; import "./ha-demo"; diff --git a/hassio/src/entrypoint.ts b/hassio/src/entrypoint.ts index 35a7e8647b13..eabfb7e5e590 100644 --- a/hassio/src/entrypoint.ts +++ b/hassio/src/entrypoint.ts @@ -1,6 +1,5 @@ // Compat needs to be first import import "../../src/resources/compatibility"; -import "../../src/resources/safari-14-attachshadow-patch"; import "./hassio-main"; import("../../src/resources/ha-style"); diff --git a/pyproject.toml b/pyproject.toml index a045f384a64a..6aa93e24f7a8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "home-assistant-frontend" -version = "20240702.0" +version = "20240703.0" license = {text = "Apache-2.0"} description = "The Home Assistant frontend" readme = "README.md" diff --git a/src/components/data-table/dialog-data-table-settings.ts b/src/components/data-table/dialog-data-table-settings.ts index 265abe0da0c5..1dfe8bbabf49 100644 --- a/src/components/data-table/dialog-data-table-settings.ts +++ b/src/components/data-table/dialog-data-table-settings.ts @@ -202,20 +202,53 @@ export class DialogDataTableSettings extends LitElement { const columns = this._sortedColumns( this._params.columns, this._columnOrder, - this._hiddenColumns + hidden ); if (!this._columnOrder) { this._columnOrder = columns.map((col) => col.key); } else { + const newOrder = this._columnOrder.filter((col) => col !== column); + + // Array.findLastIndex when supported or core-js polyfill + const findLastIndex = ( + arr: Array, + fn: (item: any, index: number, arr: Array) => boolean + ) => { + for (let i = arr.length - 1; i >= 0; i--) { + if (fn(arr[i], i, arr)) return i; + } + return -1; + }; + + let lastMoveable = findLastIndex( + newOrder, + (col) => + col !== column && + !hidden.includes(col) && + !this._params!.columns[col].main && + this._params!.columns[col].moveable !== false + ); + + if (lastMoveable === -1) { + lastMoveable = newOrder.length - 1; + } + columns.forEach((col) => { - if (!this._columnOrder!.includes(col.key)) { - this._columnOrder!.push(col.key); + if (!newOrder.includes(col.key)) { + if (col.moveable === false) { + newOrder.unshift(col.key); + } else { + newOrder.splice(lastMoveable + 1, 0, col.key); + } + if (col.defaultHidden) { hidden.push(col.key); } } }); + + this._columnOrder = newOrder; } this._hiddenColumns = hidden; diff --git a/src/entrypoints/authorize.ts b/src/entrypoints/authorize.ts index cc3e208d8194..047e80be1890 100644 --- a/src/entrypoints/authorize.ts +++ b/src/entrypoints/authorize.ts @@ -1,7 +1,6 @@ // Compat needs to be first import import "../resources/compatibility"; import "../auth/ha-authorize"; -import "../resources/safari-14-attachshadow-patch"; import("../resources/ha-style"); import("@polymer/polymer/lib/utils/settings").then( diff --git a/src/entrypoints/core.ts b/src/entrypoints/core.ts index bcd6138a0dc2..c0b83b90cc7a 100644 --- a/src/entrypoints/core.ts +++ b/src/entrypoints/core.ts @@ -25,7 +25,6 @@ import { subscribePanels } from "../data/ws-panels"; import { subscribeThemes } from "../data/ws-themes"; import { subscribeUser } from "../data/ws-user"; import type { ExternalAuth } from "../external_app/external_auth"; -import "../resources/safari-14-attachshadow-patch"; window.name = MAIN_WINDOW_NAME; (window as any).frontendVersion = __VERSION__; diff --git a/src/entrypoints/custom-panel.ts b/src/entrypoints/custom-panel.ts index b0b759ba775b..c12674a98e0a 100644 --- a/src/entrypoints/custom-panel.ts +++ b/src/entrypoints/custom-panel.ts @@ -1,6 +1,5 @@ // Compat needs to be first import import "../resources/compatibility"; -import "../resources/safari-14-attachshadow-patch"; import { CSSResult } from "lit"; import { fireEvent } from "../common/dom/fire_event"; diff --git a/src/entrypoints/onboarding.ts b/src/entrypoints/onboarding.ts index 9deb7359271c..87539e965a9b 100644 --- a/src/entrypoints/onboarding.ts +++ b/src/entrypoints/onboarding.ts @@ -1,7 +1,6 @@ // Compat needs to be first import import "../resources/compatibility"; import "../onboarding/ha-onboarding"; -import "../resources/safari-14-attachshadow-patch"; import("../resources/ha-style"); import("@polymer/polymer/lib/utils/settings").then( diff --git a/src/fake_data/entity.ts b/src/fake_data/entity.ts index 2ea81b3feacf..232b75de1ff5 100644 --- a/src/fake_data/entity.ts +++ b/src/fake_data/entity.ts @@ -10,6 +10,18 @@ const now = () => new Date().toISOString(); const randomTime = () => new Date(new Date().getTime() - Math.random() * 80 * 60 * 1000).toISOString(); +const CAPABILITY_ATTRIBUTES = [ + "friendly_name", + "unit_of_measurement", + "icon", + "entity_picture", + "supported_features", + "hidden", + "assumed_state", + "device_class", + "state_class", + "restored", +]; export class Entity { public domain: string; @@ -29,16 +41,28 @@ export class Entity { public hass?: any; - constructor(domain, objectId, state, baseAttributes) { + static CAPABILITY_ATTRIBUTES = new Set(CAPABILITY_ATTRIBUTES); + + constructor(domain, objectId, state, attributes) { this.domain = domain; this.objectId = objectId; this.entityId = `${domain}.${objectId}`; this.lastChanged = randomTime(); this.lastUpdated = randomTime(); this.state = String(state); + // These are the attributes that we always write to the state machine + const baseAttributes = {}; + const capabilityAttributes = + TYPES[domain]?.CAPABILITY_ATTRIBUTES || Entity.CAPABILITY_ATTRIBUTES; + for (const key of Object.keys(attributes)) { + if (capabilityAttributes.has(key)) { + baseAttributes[key] = attributes[key]; + } + } + this.baseAttributes = baseAttributes; - this.attributes = baseAttributes; + this.attributes = attributes; } public async handleService(domain, service, data: Record) { @@ -54,7 +78,7 @@ export class Entity { this.lastUpdated = now(); this.lastChanged = state === this.state ? this.lastChanged : this.lastUpdated; - this.attributes = { ...this.baseAttributes, ...attributes }; + this.attributes = { ...this.attributes, ...attributes }; // eslint-disable-next-line console.log("update", this.entityId, this); @@ -68,7 +92,7 @@ export class Entity { return { entity_id: this.entityId, state: this.state, - attributes: this.attributes, + attributes: this.state === "off" ? this.baseAttributes : this.attributes, last_changed: this.lastChanged, last_updated: this.lastUpdated, }; @@ -76,6 +100,16 @@ export class Entity { } class LightEntity extends Entity { + static CAPABILITY_ATTRIBUTES = new Set([ + ...CAPABILITY_ATTRIBUTES, + "min_color_temp_kelvin", + "max_color_temp_kelvin", + "min_mireds", + "max_mireds", + "effect_list", + "supported_color_modes", + ]); + public async handleService(domain, service, data) { if (!["homeassistant", this.domain].includes(domain)) { return; @@ -188,6 +222,12 @@ class AlarmControlPanelEntity extends Entity { } class MediaPlayerEntity extends Entity { + static CAPABILITY_ATTRIBUTES = new Set([ + ...CAPABILITY_ATTRIBUTES, + "source_list", + "sound_mode_list", + ]); + public async handleService( domain, service, @@ -223,7 +263,11 @@ class CoverEntity extends Entity { if (service === "open_cover") { this.update("open"); } else if (service === "close_cover") { - this.update("closing"); + this.update("closed"); + } else if (service === "set_cover_position") { + this.update(data.position > 0 ? "open" : "closed", { + current_position: data.position, + }); } else { super.handleService(domain, service, data); } @@ -288,6 +332,19 @@ class InputSelectEntity extends Entity { } class ClimateEntity extends Entity { + static CAPABILITY_ATTRIBUTES = new Set([ + ...CAPABILITY_ATTRIBUTES, + "hvac_modes", + "min_temp", + "max_temp", + "target_temp_step", + "fan_modes", + "preset_modes", + "swing_modes", + "min_humidity", + "max_humidity", + ]); + public async handleService(domain, service, data) { if (domain !== this.domain) { return; @@ -357,6 +414,14 @@ class ClimateEntity extends Entity { } class WaterHeaterEntity extends Entity { + static CAPABILITY_ATTRIBUTES = new Set([ + ...CAPABILITY_ATTRIBUTES, + "current_temperature", + "min_temp", + "max_temp", + "operation_list", + ]); + public async handleService(domain, service, data) { if (domain !== this.domain) { return; diff --git a/src/fake_data/provide_hass.ts b/src/fake_data/provide_hass.ts index 3723fa7321ec..62787c4fef93 100644 --- a/src/fake_data/provide_hass.ts +++ b/src/fake_data/provide_hass.ts @@ -278,6 +278,8 @@ export const provideHass = ( // @ts-ignore async callService(domain, service, data) { if (data && "entity_id" in data) { + // eslint-disable-next-line + console.log("Entity service call", domain, service, data); await Promise.all( ensureArray(data.entity_id).map((ent) => entities[ent].handleService(domain, service, data) diff --git a/src/panels/lovelace/cards/hui-area-card.ts b/src/panels/lovelace/cards/hui-area-card.ts index 5ebf4b4eae05..387110d9ae19 100644 --- a/src/panels/lovelace/cards/hui-area-card.ts +++ b/src/panels/lovelace/cards/hui-area-card.ts @@ -412,19 +412,19 @@ export class HuiAreaCard if (this._config.show_camera && "camera" in entitiesByDomain) { cameraEntityId = entitiesByDomain.camera[0].entity_id; } - cameraEntityId = "camera.demo_camera"; const imageClass = area.picture || cameraEntityId; - const ignoreAspectRatio = imageClass || this.layout === "grid"; + const ignoreAspectRatio = this.layout === "grid"; return html` ${area.picture || cameraEntityId @@ -435,8 +435,10 @@ export class HuiAreaCard .image=${area.picture ? area.picture : undefined} .cameraImage=${cameraEntityId} .cameraView=${this._config.camera_view} - .aspectRatio=${this._config.aspect_ratio || - DEFAULT_ASPECT_RATIO} + .aspectRatio=${ignoreAspectRatio + ? undefined + : this._config.aspect_ratio || DEFAULT_ASPECT_RATIO} + fitMode="cover" > ` : area.icon @@ -586,6 +588,10 @@ export class HuiAreaCard opacity: 0.12; } + .image hui-image { + height: 100%; + } + .icon-container { position: absolute; top: 0; diff --git a/src/panels/lovelace/cards/hui-media-control-card.ts b/src/panels/lovelace/cards/hui-media-control-card.ts index 4cd3e0f1058d..ffdfe2c34a2f 100644 --- a/src/panels/lovelace/cards/hui-media-control-card.ts +++ b/src/panels/lovelace/cards/hui-media-control-card.ts @@ -40,11 +40,7 @@ import { findEntities } from "../common/find-entities"; import { hasConfigOrEntityChanged } from "../common/has-changed"; import "../components/hui-marquee"; import { createEntityNotFoundWarning } from "../components/hui-warning"; -import type { - LovelaceCard, - LovelaceCardEditor, - LovelaceLayoutOptions, -} from "../types"; +import type { LovelaceCard, LovelaceCardEditor } from "../types"; import { MediaControlCardConfig } from "./types"; @customElement("hui-media-control-card") @@ -586,15 +582,6 @@ export class HuiMediaControlCard extends LitElement implements LovelaceCard { } } - public getLayoutOptions(): LovelaceLayoutOptions { - return { - grid_columns: 4, - grid_min_columns: 2, - grid_rows: 3, - grid_min_rows: 3, - }; - } - static get styles(): CSSResultGroup { return css` ha-card { diff --git a/src/resources/safari-14-attachshadow-patch.ts b/src/resources/safari-14-attachshadow-patch.ts deleted file mode 100644 index a3a41ce4bdab..000000000000 --- a/src/resources/safari-14-attachshadow-patch.ts +++ /dev/null @@ -1,17 +0,0 @@ -// https://github.com/home-assistant/frontend/pull/7031 - -export {}; // for Babel to treat as a module - -const isSafari14 = /^((?!chrome|android).)*version\/14\.0\s.*safari/i.test( - navigator.userAgent -); - -if (isSafari14) { - const origAttachShadow = window.Element.prototype.attachShadow; - window.Element.prototype.attachShadow = function (init) { - if (init && init.delegatesFocus) { - delete init.delegatesFocus; - } - return origAttachShadow.apply(this, [init]); - }; -}