Merge branch 'home-assistant:dev' into raulcodes/import-calendar-events

This commit is contained in:
Raul Camacho 2024-04-27 15:53:03 -05:00 committed by GitHub
commit c3efca0709
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
92 changed files with 2876 additions and 889 deletions

View File

@ -21,7 +21,7 @@ jobs:
url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }}
steps:
- name: Check out files from GitHub
uses: actions/checkout@v4.1.2
uses: actions/checkout@v4.1.3
with:
ref: dev
@ -57,7 +57,7 @@ jobs:
url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }}
steps:
- name: Check out files from GitHub
uses: actions/checkout@v4.1.2
uses: actions/checkout@v4.1.3
with:
ref: master

View File

@ -24,7 +24,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Check out files from GitHub
uses: actions/checkout@v4.1.2
uses: actions/checkout@v4.1.3
- name: Setup Node
uses: actions/setup-node@v4.0.2
with:
@ -58,7 +58,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Check out files from GitHub
uses: actions/checkout@v4.1.2
uses: actions/checkout@v4.1.3
- name: Setup Node
uses: actions/setup-node@v4.0.2
with:
@ -76,7 +76,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Check out files from GitHub
uses: actions/checkout@v4.1.2
uses: actions/checkout@v4.1.3
- name: Setup Node
uses: actions/setup-node@v4.0.2
with:
@ -89,7 +89,7 @@ jobs:
env:
IS_TEST: "true"
- name: Upload bundle stats
uses: actions/upload-artifact@v4.3.1
uses: actions/upload-artifact@v4.3.2
with:
name: frontend-bundle-stats
path: build/stats/*.json
@ -100,7 +100,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Check out files from GitHub
uses: actions/checkout@v4.1.2
uses: actions/checkout@v4.1.3
- name: Setup Node
uses: actions/setup-node@v4.0.2
with:
@ -113,7 +113,7 @@ jobs:
env:
IS_TEST: "true"
- name: Upload bundle stats
uses: actions/upload-artifact@v4.3.1
uses: actions/upload-artifact@v4.3.2
with:
name: supervisor-bundle-stats
path: build/stats/*.json

View File

@ -23,7 +23,7 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@v4.1.2
uses: actions/checkout@v4.1.3
with:
# We must fetch at least the immediate parents so that if this is
# a pull request then we can checkout the head.

View File

@ -22,7 +22,7 @@ jobs:
url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }}
steps:
- name: Check out files from GitHub
uses: actions/checkout@v4.1.2
uses: actions/checkout@v4.1.3
with:
ref: dev
@ -58,7 +58,7 @@ jobs:
url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }}
steps:
- name: Check out files from GitHub
uses: actions/checkout@v4.1.2
uses: actions/checkout@v4.1.3
with:
ref: master

View File

@ -16,7 +16,7 @@ jobs:
url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }}
steps:
- name: Check out files from GitHub
uses: actions/checkout@v4.1.2
uses: actions/checkout@v4.1.3
- name: Setup Node
uses: actions/setup-node@v4.0.2

View File

@ -21,7 +21,7 @@ jobs:
if: github.repository == 'home-assistant/frontend' && contains(github.event.pull_request.labels.*.name, 'needs design preview')
steps:
- name: Check out files from GitHub
uses: actions/checkout@v4.1.2
uses: actions/checkout@v4.1.3
- name: Setup Node
uses: actions/setup-node@v4.0.2

View File

@ -20,7 +20,7 @@ jobs:
contents: write
steps:
- name: Checkout the repository
uses: actions/checkout@v4.1.2
uses: actions/checkout@v4.1.3
- name: Set up Python ${{ env.PYTHON_VERSION }}
uses: actions/setup-python@v5
@ -57,14 +57,14 @@ jobs:
run: tar -czvf translations.tar.gz translations
- name: Upload build artifacts
uses: actions/upload-artifact@v4.3.1
uses: actions/upload-artifact@v4.3.2
with:
name: wheels
path: dist/home_assistant_frontend*.whl
if-no-files-found: error
- name: Upload translations
uses: actions/upload-artifact@v4.3.1
uses: actions/upload-artifact@v4.3.2
with:
name: translations
path: translations.tar.gz

View File

@ -23,7 +23,7 @@ jobs:
contents: write # Required to upload release assets
steps:
- name: Checkout the repository
uses: actions/checkout@v4.1.2
uses: actions/checkout@v4.1.3
- name: Verify version
uses: home-assistant/actions/helpers/verify-version@master

View File

@ -13,7 +13,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout the repository
uses: actions/checkout@v4.1.2
uses: actions/checkout@v4.1.3
- name: Upload Translations
run: |

View File

@ -10,6 +10,7 @@ const WebpackBar = require("webpackbar");
const {
TransformAsyncModulesPlugin,
} = require("transform-async-modules-webpack-plugin");
const { dependencies } = require("../package.json");
const paths = require("./paths.cjs");
const bundle = require("./bundle.cjs");
@ -156,7 +157,10 @@ const createWebpackConfig = ({
transform: (stats) => JSON.stringify(filterStats(stats)),
}),
!latestBuild &&
new TransformAsyncModulesPlugin({ browserslistEnv: "legacy" }),
new TransformAsyncModulesPlugin({
browserslistEnv: "legacy",
runtime: { version: dependencies["@babel/runtime"] },
}),
].filter(Boolean),
resolve: {
extensions: [".ts", ".js", ".json"],

View File

@ -10,6 +10,7 @@ import {
import { HomeAssistantAppEl } from "../../src/layouts/home-assistant";
import { HomeAssistant } from "../../src/types";
import { selectedDemoConfig } from "./configs/demo-configs";
import { mockAreaRegistry } from "./stubs/area_registry";
import { mockAuth } from "./stubs/auth";
import { mockConfigEntries } from "./stubs/config_entries";
import { mockEnergy } from "./stubs/energy";
@ -23,10 +24,10 @@ import { mockLovelace } from "./stubs/lovelace";
import { mockMediaPlayer } from "./stubs/media_player";
import { mockPersistentNotification } from "./stubs/persistent_notification";
import { mockRecorder } from "./stubs/recorder";
import { mockTodo } from "./stubs/todo";
import { mockSensor } from "./stubs/sensor";
import { mockSystemLog } from "./stubs/system_log";
import { mockTemplate } from "./stubs/template";
import { mockTodo } from "./stubs/todo";
import { mockTranslations } from "./stubs/translations";
@customElement("ha-demo")
@ -62,6 +63,7 @@ export class HaDemo extends HomeAssistantAppEl {
mockEnergy(hass);
mockPersistentNotification(hass);
mockConfigEntries(hass);
mockAreaRegistry(hass);
mockEntityRegistry(hass, [
{
config_entry_id: "co2signal",

View File

@ -2,6 +2,7 @@ import { html, LitElement, PropertyValues, TemplateResult } from "lit";
import { customElement, query } from "lit/decorators";
import { CoverEntityFeature } from "../../../../src/data/cover";
import { LightColorMode } from "../../../../src/data/light";
import { LockEntityFeature } from "../../../../src/data/lock";
import { VacuumEntityFeature } from "../../../../src/data/vacuum";
import { getEntity } from "../../../../src/fake_data/entity";
import { provideHass } from "../../../../src/fake_data/provide_hass";
@ -20,6 +21,11 @@ const ENTITIES = [
getEntity("light", "unavailable", "unavailable", {
friendly_name: "Unavailable entity",
}),
getEntity("lock", "front_door", "locked", {
friendly_name: "Front Door Lock",
device_class: "lock",
supported_features: LockEntityFeature.OPEN,
}),
getEntity("climate", "thermostat", "heat", {
current_temperature: 73,
min_temp: 45,
@ -138,6 +144,24 @@ const CONFIGS = [
- type: "color-temp"
`,
},
{
heading: "Lock commands feature",
config: `
- type: tile
entity: lock.front_door
features:
- type: "lock-commands"
`,
},
{
heading: "Lock open door feature",
config: `
- type: tile
entity: lock.front_door
features:
- type: "lock-open-door"
`,
},
{
heading: "Vacuum commands feature",
config: `

View File

@ -36,6 +36,8 @@ const createConfigEntry = (
pref_disable_new_entities: false,
pref_disable_polling: false,
reason: null,
error_reason_translation_key: null,
error_reason_translation_placeholders: null,
...override,
});

View File

@ -28,7 +28,7 @@
"@babel/runtime": "7.24.4",
"@braintree/sanitize-url": "7.0.1",
"@codemirror/autocomplete": "6.16.0",
"@codemirror/commands": "6.3.3",
"@codemirror/commands": "6.5.0",
"@codemirror/language": "6.10.1",
"@codemirror/legacy-modes": "6.4.0",
"@codemirror/search": "6.5.6",
@ -81,7 +81,7 @@
"@material/mwc-top-app-bar": "0.27.0",
"@material/mwc-top-app-bar-fixed": "0.27.0",
"@material/top-app-bar": "=14.0.0-canary.53b3cad2f.0",
"@material/web": "1.4.0",
"@material/web": "1.4.1",
"@mdi/js": "7.4.47",
"@mdi/svg": "7.4.47",
"@polymer/paper-item": "3.0.1",
@ -101,17 +101,17 @@
"chart.js": "4.4.2",
"color-name": "2.0.0",
"comlink": "4.4.1",
"core-js": "3.36.1",
"cropperjs": "1.6.1",
"core-js": "3.37.0",
"cropperjs": "1.6.2",
"date-fns": "3.6.0",
"date-fns-tz": "3.0.1",
"date-fns-tz": "3.1.3",
"deep-clone-simple": "1.1.1",
"deep-freeze": "0.0.1",
"element-internals-polyfill": "1.3.11",
"fuse.js": "7.0.0",
"google-timezones-json": "1.2.0",
"hls.js": "patch:hls.js@npm%3A1.5.7#~/.yarn/patches/hls.js-npm-1.5.7-f5bbd3d060.patch",
"home-assistant-js-websocket": "9.2.1",
"home-assistant-js-websocket": "9.3.0",
"idb-keyval": "6.2.1",
"intl-messageformat": "10.5.11",
"js-yaml": "4.1.0",
@ -119,7 +119,7 @@
"leaflet-draw": "1.0.4",
"lit": "2.8.0",
"luxon": "3.4.4",
"marked": "12.0.1",
"marked": "12.0.2",
"memoize-one": "6.0.0",
"node-vibrant": "3.2.1-alpha.1",
"proxy-polyfill": "0.3.2",
@ -141,26 +141,26 @@
"vue": "2.7.16",
"vue2-daterange-picker": "0.6.8",
"weekstart": "2.0.0",
"workbox-cacheable-response": "7.0.0",
"workbox-core": "7.0.0",
"workbox-expiration": "7.0.0",
"workbox-precaching": "7.0.0",
"workbox-routing": "7.0.0",
"workbox-strategies": "7.0.0",
"workbox-cacheable-response": "7.1.0",
"workbox-core": "7.1.0",
"workbox-expiration": "7.1.0",
"workbox-precaching": "7.1.0",
"workbox-routing": "7.1.0",
"workbox-strategies": "7.1.0",
"xss": "1.0.15"
},
"devDependencies": {
"@babel/core": "7.24.4",
"@babel/helper-define-polyfill-provider": "0.6.1",
"@babel/helper-define-polyfill-provider": "0.6.2",
"@babel/plugin-proposal-decorators": "7.24.1",
"@babel/plugin-transform-runtime": "7.24.3",
"@babel/preset-env": "7.24.4",
"@babel/preset-typescript": "7.24.1",
"@bundle-stats/plugin-webpack-filter": "4.12.2",
"@koa/cors": "5.0.0",
"@lokalise/node-api": "12.4.0",
"@octokit/auth-oauth-device": "7.1.0",
"@octokit/plugin-retry": "7.1.0",
"@lokalise/node-api": "12.4.1",
"@octokit/auth-oauth-device": "7.1.1",
"@octokit/plugin-retry": "7.1.1",
"@octokit/rest": "20.1.0",
"@open-wc/dev-server-hmr": "0.1.4",
"@rollup/plugin-babel": "6.0.4",
@ -169,24 +169,24 @@
"@rollup/plugin-node-resolve": "15.2.3",
"@rollup/plugin-replace": "5.0.5",
"@types/babel__plugin-transform-runtime": "7.9.5",
"@types/chromecast-caf-receiver": "6.0.13",
"@types/chromecast-caf-receiver": "6.0.14",
"@types/chromecast-caf-sender": "1.0.9",
"@types/color-name": "1.1.3",
"@types/color-name": "1.1.4",
"@types/glob": "8.1.0",
"@types/html-minifier-terser": "7.0.2",
"@types/js-yaml": "4.0.9",
"@types/leaflet": "1.9.9",
"@types/leaflet": "1.9.12",
"@types/leaflet-draw": "1.0.11",
"@types/luxon": "3.4.2",
"@types/mocha": "10.0.6",
"@types/qrcode": "1.5.5",
"@types/serve-handler": "6.1.4",
"@types/sortablejs": "1.15.8",
"@types/tar": "6.1.12",
"@types/tar": "6.1.13",
"@types/ua-parser-js": "0.7.39",
"@types/webspeechapi": "0.0.29",
"@typescript-eslint/eslint-plugin": "7.6.0",
"@typescript-eslint/parser": "7.6.0",
"@typescript-eslint/eslint-plugin": "7.7.1",
"@typescript-eslint/parser": "7.7.1",
"@web/dev-server": "0.1.38",
"@web/dev-server-rollup": "0.4.1",
"babel-loader": "9.1.3",
@ -219,7 +219,7 @@
"lint-staged": "15.2.2",
"lit-analyzer": "2.0.3",
"lodash.template": "4.5.0",
"magic-string": "0.30.9",
"magic-string": "0.30.10",
"map-stream": "0.0.7",
"mocha": "10.4.0",
"object-hash": "3.0.0",
@ -234,9 +234,9 @@
"sinon": "17.0.1",
"source-map-url": "0.4.1",
"systemjs": "6.14.3",
"tar": "7.0.0",
"tar": "7.0.1",
"terser-webpack-plugin": "5.3.10",
"transform-async-modules-webpack-plugin": "1.0.4",
"transform-async-modules-webpack-plugin": "1.1.0",
"ts-lit-plugin": "2.0.2",
"typescript": "5.4.5",
"webpack": "5.91.0",
@ -245,7 +245,7 @@
"webpack-manifest-plugin": "5.0.0",
"webpack-stats-plugin": "1.1.3",
"webpackbar": "6.0.1",
"workbox-build": "7.0.0"
"workbox-build": "7.1.0"
},
"_comment": "Polymer 3.2 contained a bug, fixed in https://github.com/Polymer/polymer/pull/5569, add as patch",
"resolutions": {

View File

@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "home-assistant-frontend"
version = "20240404.1"
version = "20240426.0"
license = {text = "Apache-2.0"}
description = "The Home Assistant frontend"
readme = "README.md"

View File

@ -187,11 +187,14 @@ export const computeStateDisplayFromEntityAttributes = (
if (
[
"button",
"conversation",
"event",
"image",
"input_button",
"notify",
"scene",
"stt",
"tag",
"tts",
"wake_word",
].includes(domain) ||

View File

@ -1,13 +1,13 @@
import { mdiArrowDown, mdiArrowUp } from "@mdi/js";
import { mdiArrowDown, mdiArrowUp, mdiChevronUp } from "@mdi/js";
import deepClone from "deep-clone-simple";
import {
css,
CSSResultGroup,
html,
LitElement,
nothing,
PropertyValues,
TemplateResult,
css,
html,
nothing,
} from "lit";
import {
customElement,
@ -22,7 +22,9 @@ import { styleMap } from "lit/directives/style-map";
import memoizeOne from "memoize-one";
import { restoreScroll } from "../../common/decorators/restore-scroll";
import { fireEvent } from "../../common/dom/fire_event";
import { stringCompare } from "../../common/string/compare";
import { debounce } from "../../common/util/debounce";
import { groupBy } from "../../common/util/group-by";
import { nextRender } from "../../common/util/render-status";
import { haStyleScrollbar } from "../../resources/styles";
import { loadVirtualizer } from "../../resources/virtualizer";
@ -32,17 +34,6 @@ import type { HaCheckbox } from "../ha-checkbox";
import "../ha-svg-icon";
import "../search-input";
import { filterData, sortData } from "./sort-filter";
import { groupBy } from "../../common/util/group-by";
import { stringCompare } from "../../common/string/compare";
declare global {
// for fire event
interface HASSDomEvents {
"selection-changed": SelectionChangedEvent;
"row-click": RowClickedEvent;
"sorting-changed": SortingChangedEvent;
}
}
export interface RowClickedEvent {
id: string;
@ -52,6 +43,10 @@ export interface SelectionChangedEvent {
value: string[];
}
export interface CollapsedChangedEvent {
value: string[];
}
export interface SortingChangedEvent {
column: string;
direction: SortingDirection;
@ -142,10 +137,14 @@ export class HaDataTable extends LitElement {
@property() public groupColumn?: string;
@property({ attribute: false }) public groupOrder?: string[];
@property() public sortColumn?: string;
@property() public sortDirection: SortingDirection = null;
@property({ attribute: false }) public initialCollapsedGroups?: string[];
@state() private _filterable = false;
@state() private _filter = "";
@ -158,6 +157,8 @@ export class HaDataTable extends LitElement {
@state() private _items: DataTableRowData[] = [];
@state() private _collapsedGroups: string[] = [];
private _checkableRowsCount?: number;
private _checkedRows: string[] = [];
@ -213,17 +214,19 @@ export class HaDataTable extends LitElement {
(column) => column.filterable
);
for (const columnId in this.columns) {
if (this.columns[columnId].direction) {
this.sortDirection = this.columns[columnId].direction!;
this.sortColumn = columnId;
if (!this.sortColumn) {
for (const columnId in this.columns) {
if (this.columns[columnId].direction) {
this.sortDirection = this.columns[columnId].direction!;
this.sortColumn = columnId;
fireEvent(this, "sorting-changed", {
column: columnId,
direction: this.sortDirection,
});
fireEvent(this, "sorting-changed", {
column: columnId,
direction: this.sortDirection,
});
break;
break;
}
}
}
@ -248,13 +251,23 @@ export class HaDataTable extends LitElement {
).length;
}
if (!this.hasUpdated && this.initialCollapsedGroups) {
this._collapsedGroups = this.initialCollapsedGroups;
fireEvent(this, "collapsed-changed", { value: this._collapsedGroups });
} else if (properties.has("groupColumn")) {
this._collapsedGroups = [];
fireEvent(this, "collapsed-changed", { value: this._collapsedGroups });
}
if (
properties.has("data") ||
properties.has("columns") ||
properties.has("_filter") ||
properties.has("sortColumn") ||
properties.has("sortDirection") ||
properties.has("groupColumn")
properties.has("groupColumn") ||
properties.has("groupOrder") ||
properties.has("_collapsedGroups")
) {
this._sortFilterData();
}
@ -447,6 +460,8 @@ export class HaDataTable extends LitElement {
}
return html`
<div
@mouseover=${this._setTitle}
@focus=${this._setTitle}
role=${column.main ? "rowheader" : "cell"}
class="mdc-data-table__cell ${classMap({
"mdc-data-table__cell--flex": column.type === "flex",
@ -514,11 +529,7 @@ export class HaDataTable extends LitElement {
}
if (this.appendRow || this.hasFab || this.groupColumn) {
const items = [...data];
if (this.appendRow) {
items.push({ append: true, content: this.appendRow });
}
let items = [...data];
if (this.groupColumn) {
const grouped = groupBy(items, (item) => item[this.groupColumn!]);
@ -530,13 +541,24 @@ export class HaDataTable extends LitElement {
const sorted: {
[key: string]: DataTableRowData[];
} = Object.keys(grouped)
.sort((a, b) =>
stringCompare(
.sort((a, b) => {
const orderA = this.groupOrder?.indexOf(a) ?? -1;
const orderB = this.groupOrder?.indexOf(b) ?? -1;
if (orderA !== orderB) {
if (orderA === -1) {
return 1;
}
if (orderB === -1) {
return -1;
}
return orderA - orderB;
}
return stringCompare(
["", "-", "—"].includes(a) ? "zzz" : a,
["", "-", "—"].includes(b) ? "zzz" : b,
this.hass.locale.language
)
)
);
})
.reduce((obj, key) => {
obj[key] = grouped[key];
return obj;
@ -552,23 +574,39 @@ export class HaDataTable extends LitElement {
content: html`<div
class="mdc-data-table__cell group-header"
role="cell"
.group=${groupName}
@click=${this._collapseGroup}
>
${groupName === UNDEFINED_GROUP_KEY ? "" : groupName || ""}
<ha-icon-button
.path=${mdiChevronUp}
class=${this._collapsedGroups.includes(groupName)
? "collapsed"
: ""}
>
</ha-icon-button>
${groupName === UNDEFINED_GROUP_KEY
? this.hass.localize("ui.components.data-table.ungrouped")
: groupName || ""}
</div>`,
});
}
groupedItems.push(...rows);
if (!this._collapsedGroups.includes(groupName)) {
groupedItems.push(...rows);
}
});
this._items = groupedItems;
} else {
this._items = items;
items = groupedItems;
}
if (this.appendRow) {
items.push({ append: true, content: this.appendRow });
}
if (this.hasFab) {
this._items = [...this._items, { empty: true }];
items.push({ empty: true });
}
this._items = items;
} else {
this._items = data;
}
@ -649,6 +687,13 @@ export class HaDataTable extends LitElement {
fireEvent(this, "row-click", { id: rowId }, { bubbles: false });
};
private _setTitle(ev: Event) {
const target = ev.currentTarget as HTMLElement;
if (target.scrollWidth > target.offsetWidth) {
target.setAttribute("title", target.innerText);
}
}
private _checkedRowsChanged() {
// force scroller to update, change it's items
if (this._items.length) {
@ -679,6 +724,18 @@ export class HaDataTable extends LitElement {
this._savedScrollPos = (e.target as HTMLDivElement).scrollTop;
}
private _collapseGroup = (ev: Event) => {
const groupName = (ev.currentTarget as any).group;
if (this._collapsedGroups.includes(groupName)) {
this._collapsedGroups = this._collapsedGroups.filter(
(grp) => grp !== groupName
);
} else {
this._collapsedGroups = [...this._collapsedGroups, groupName];
}
fireEvent(this, "collapsed-changed", { value: this._collapsedGroups });
};
static get styles(): CSSResultGroup {
return [
haStyleScrollbar,
@ -931,8 +988,21 @@ export class HaDataTable extends LitElement {
.group-header {
padding-top: 12px;
padding-left: 12px;
padding-inline-start: 12px;
width: 100%;
font-weight: 500;
display: flex;
align-items: center;
cursor: pointer;
}
.group-header ha-icon-button {
transition: transform 0.2s ease;
}
.group-header ha-icon-button.collapsed {
transform: rotate(180deg);
}
:host {
@ -1031,4 +1101,12 @@ declare global {
interface HTMLElementTagNameMap {
"ha-data-table": HaDataTable;
}
// for fire event
interface HASSDomEvents {
"selection-changed": SelectionChangedEvent;
"row-click": RowClickedEvent;
"sorting-changed": SortingChangedEvent;
"collapsed-changed": CollapsedChangedEvent;
}
}

View File

@ -409,7 +409,7 @@ export class HaEntityPicker extends LitElement {
ev.stopPropagation();
const newValue = ev.detail.value;
if (newValue.startsWith(CREATE_ID)) {
if (newValue && newValue.startsWith(CREATE_ID)) {
const domain = newValue.substring(CREATE_ID.length);
showHelperDetailDialog(this, {
domain,

View File

@ -67,6 +67,9 @@ export class HaControlSlider extends LitElement {
@property({ attribute: "tooltip-mode" })
public tooltipMode: TooltipMode = "interaction";
@property({ attribute: "touch-action" })
public touchAction?: string;
@property({ type: Number })
public value?: number;
@ -152,7 +155,7 @@ export class HaControlSlider extends LitElement {
setupListeners() {
if (this.slider && !this._mc) {
this._mc = new Manager(this.slider, {
touchAction: this.vertical ? "pan-x" : "pan-y",
touchAction: this.touchAction ?? (this.vertical ? "pan-x" : "pan-y"),
});
this._mc.add(
new Pan({

View File

@ -33,6 +33,9 @@ export class HaControlSwitch extends LitElement {
// SVG icon path (if you need a non SVG icon instead, use the provided off icon slot to pass an <ha-icon slot="icon-off"> in)
@property({ type: String }) pathOff?: string;
@property({ attribute: "touch-action" })
public touchAction?: string;
private _mc?: HammerManager;
protected firstUpdated(changedProperties: PropertyValues): void {
@ -73,7 +76,7 @@ export class HaControlSwitch extends LitElement {
setupListeners() {
if (this.switch && !this._mc) {
this._mc = new Manager(this.switch, {
touchAction: this.vertical ? "pan-x" : "pan-y",
touchAction: this.touchAction ?? (this.vertical ? "pan-x" : "pan-y"),
});
this._mc.add(
new Swipe({

View File

@ -69,7 +69,7 @@ export class HaFilterDevices extends LitElement {
@value-changed=${this._handleSearchChange}
>
</search-input-outlined>
<mwc-list class="ha-scrollbar">
<mwc-list class="ha-scrollbar" multi>
<lit-virtualizer
.items=${this._devices(
this.hass.devices,
@ -94,7 +94,7 @@ export class HaFilterDevices extends LitElement {
? nothing
: html`<ha-check-list-item
.value=${device.id}
.selected=${this.value?.includes(device.id)}
.selected=${this.value?.includes(device.id) ?? false}
>
${computeDeviceName(device, this.hass)}
</ha-check-list-item>`;

View File

@ -0,0 +1,198 @@
import { mdiFilterVariantRemove } from "@mdi/js";
import { css, CSSResultGroup, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { repeat } from "lit/directives/repeat";
import memoizeOne from "memoize-one";
import { fireEvent } from "../common/dom/fire_event";
import { stringCompare } from "../common/string/compare";
import { domainToName } from "../data/integration";
import { haStyleScrollbar } from "../resources/styles";
import type { HomeAssistant } from "../types";
import "./ha-domain-icon";
import "./search-input-outlined";
import { computeDomain } from "../common/entity/compute_domain";
@customElement("ha-filter-domains")
export class HaFilterDomains extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public value?: string[];
@property({ type: Boolean }) public narrow = false;
@property({ type: Boolean, reflect: true }) public expanded = false;
@state() private _shouldRender = false;
@state() private _filter?: string;
protected render() {
return html`
<ha-expansion-panel
leftChevron
.expanded=${this.expanded}
@expanded-will-change=${this._expandedWillChange}
@expanded-changed=${this._expandedChanged}
>
<div slot="header" class="header">
${this.hass.localize(
"ui.panel.config.entities.picker.headers.domain"
)}
${this.value?.length
? html`<div class="badge">${this.value?.length}</div>
<ha-icon-button
.path=${mdiFilterVariantRemove}
@click=${this._clearFilter}
></ha-icon-button>`
: nothing}
</div>
${this._shouldRender
? html`<search-input-outlined
.hass=${this.hass}
.filter=${this._filter}
@value-changed=${this._handleSearchChange}
>
</search-input-outlined>
<mwc-list
class="ha-scrollbar"
@click=${this._handleItemClick}
multi
>
${repeat(
this._domains(this.hass.states, this._filter),
(i) => i,
(domain) =>
html`<ha-check-list-item
.value=${domain}
.selected=${(this.value || []).includes(domain)}
graphic="icon"
>
<ha-domain-icon
slot="graphic"
.hass=${this.hass}
.domain=${domain}
brandFallback
></ha-domain-icon>
${domainToName(this.hass.localize, domain)}
</ha-check-list-item>`
)}
</mwc-list> `
: nothing}
</ha-expansion-panel>
`;
}
private _domains = memoizeOne((states, filter) => {
const domains = new Set<string>();
Object.keys(states).forEach((entityId) => {
domains.add(computeDomain(entityId));
});
return Array.from(domains)
.filter((domain) => !filter || domain.toLowerCase().includes(filter))
.sort((a, b) => stringCompare(a, b, this.hass.locale.language));
});
protected updated(changed) {
if (changed.has("expanded") && this.expanded) {
setTimeout(() => {
if (!this.expanded) return;
this.renderRoot.querySelector("mwc-list")!.style.height =
`${this.clientHeight - 49 - 32}px`; // 32px is the height of the search input
}, 300);
}
}
private _expandedWillChange(ev) {
this._shouldRender = ev.detail.expanded;
}
private _expandedChanged(ev) {
this.expanded = ev.detail.expanded;
}
private _handleItemClick(ev) {
const listItem = ev.target.closest("ha-check-list-item");
const value = listItem?.value;
if (!value) {
return;
}
if (this.value?.includes(value)) {
this.value = this.value?.filter((val) => val !== value);
} else {
this.value = [...(this.value || []), value];
}
listItem.selected = this.value.includes(value);
fireEvent(this, "data-table-filter-changed", {
value: this.value,
items: undefined,
});
}
private _clearFilter(ev) {
ev.preventDefault();
this.value = undefined;
fireEvent(this, "data-table-filter-changed", {
value: undefined,
items: undefined,
});
}
private _handleSearchChange(ev: CustomEvent) {
this._filter = ev.detail.value.toLowerCase();
}
static get styles(): CSSResultGroup {
return [
haStyleScrollbar,
css`
:host {
border-bottom: 1px solid var(--divider-color);
}
:host([expanded]) {
flex: 1;
height: 0;
}
ha-expansion-panel {
--ha-card-border-radius: 0;
--expansion-panel-content-padding: 0;
}
.header {
display: flex;
align-items: center;
}
.header ha-icon-button {
margin-inline-start: auto;
margin-inline-end: 8px;
}
.badge {
display: inline-block;
margin-left: 8px;
margin-inline-start: 8px;
margin-inline-end: 0;
min-width: 16px;
box-sizing: border-box;
border-radius: 50%;
font-weight: 400;
font-size: 11px;
background-color: var(--primary-color);
line-height: 16px;
text-align: center;
padding: 0px 2px;
color: var(--text-primary-color);
}
search-input-outlined {
display: block;
padding: 0 8px;
}
`,
];
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-filter-domains": HaFilterDomains;
}
}

View File

@ -71,7 +71,7 @@ export class HaFilterEntities extends LitElement {
@value-changed=${this._handleSearchChange}
>
</search-input-outlined>
<mwc-list class="ha-scrollbar">
<mwc-list class="ha-scrollbar" multi>
<lit-virtualizer
.items=${this._entities(
this.hass.states,
@ -108,7 +108,7 @@ export class HaFilterEntities extends LitElement {
? nothing
: html`<ha-check-list-item
.value=${entity.entity_id}
.selected=${this.value?.includes(entity.entity_id)}
.selected=${this.value?.includes(entity.entity_id) ?? false}
graphic="icon"
>
<ha-state-icon

View File

@ -55,7 +55,11 @@ export class HaFilterIntegrations extends LitElement {
@value-changed=${this._handleSearchChange}
>
</search-input-outlined>
<mwc-list class="ha-scrollbar" @click=${this._handleItemClick}>
<mwc-list
class="ha-scrollbar"
@click=${this._handleItemClick}
multi
>
${repeat(
this._integrations(this._manifests, this._filter, this.value),
(i) => i.domain,

View File

@ -62,8 +62,8 @@ export class HaFilterStates extends LitElement {
(item) =>
html`<ha-check-list-item
.value=${item.value}
.selected=${this.value?.includes(item.value)}
.graphic=${hasIcon ? "icon" : undefined}
.selected=${this.value?.includes(item.value) ?? false}
.graphic=${hasIcon ? "icon" : null}
>
${item.icon
? html`<ha-icon

View File

@ -71,6 +71,10 @@ export const computeInitialHaFormData = (
if (selector.country?.countries?.length) {
data[field.name] = selector.country.countries[0];
}
} else if ("language" in selector) {
if (selector.language?.languages?.length) {
data[field.name] = selector.language.languages[0];
}
} else if ("duration" in selector) {
data[field.name] = {
hours: 0,
@ -93,7 +97,9 @@ export const computeInitialHaFormData = (
) {
data[field.name] = {};
} else {
throw new Error("Selector not supported in initial form data");
throw new Error(
`Selector ${Object.keys(selector)[0]} not supported in initial form data`
);
}
}
});

View File

@ -1,13 +1,29 @@
import { FormfieldBase } from "@material/mwc-formfield/mwc-formfield-base";
import { styles } from "@material/mwc-formfield/mwc-formfield.css";
import { css } from "lit";
import { css, html } from "lit";
import { customElement, property } from "lit/decorators";
import { classMap } from "lit/directives/class-map";
import { fireEvent } from "../common/dom/fire_event";
@customElement("ha-formfield")
export class HaFormfield extends FormfieldBase {
@property({ type: Boolean, reflect: true }) public disabled = false;
protected override render() {
const classes = {
"mdc-form-field--align-end": this.alignEnd,
"mdc-form-field--space-between": this.spaceBetween,
"mdc-form-field--nowrap": this.nowrap,
};
return html` <div class="mdc-form-field ${classMap(classes)}">
<slot></slot>
<label class="mdc-label" @click=${this._labelClick}
><slot name="label">${this.label}</slot></label
>
</div>`;
}
protected _labelClick() {
const input = this.input as HTMLInputElement | undefined;
if (!input) return;
@ -39,6 +55,9 @@ export class HaFormfield extends FormfieldBase {
margin-inline-end: 10px;
margin-inline-start: inline;
}
.mdc-form-field {
align-items: var(--ha-formfield-align-items, center);
}
.mdc-form-field > label {
direction: var(--direction);
margin-inline-start: 0;

View File

@ -82,7 +82,7 @@ export class HaSortable extends LitElement {
public connectedCallback() {
super.connectedCallback();
this._shouldBeDestroy = false;
if (this.hasUpdated) {
if (this.hasUpdated && !this.disabled) {
this._createSortable();
}
}

View File

@ -19,7 +19,7 @@ import { customElement, property, query, state } from "lit/decorators";
import memoizeOne from "memoize-one";
import { fireEvent } from "../../common/dom/fire_event";
import type { LeafletModuleType } from "../../common/dom/setup-leaflet-map";
import type { HomeAssistant } from "../../types";
import type { HomeAssistant, ThemeMode } from "../../types";
import "../ha-input-helper-text";
import "./ha-map";
import type { HaMap } from "./ha-map";
@ -61,7 +61,8 @@ export class HaLocationsEditor extends LitElement {
@property({ type: Number }) public zoom = 16;
@property({ type: Boolean }) public darkMode = false;
@property({ attribute: "theme-mode", type: String })
public themeMode: ThemeMode = "auto";
@state() private _locationMarkers?: Record<string, Marker | Circle>;
@ -133,7 +134,7 @@ export class HaLocationsEditor extends LitElement {
.layers=${this._getLayers(this._circles, this._locationMarkers)}
.zoom=${this.zoom}
.autoFit=${this.autoFit}
?darkMode=${this.darkMode}
.themeMode=${this.themeMode}
></ha-map>
${this.helper
? html`<ha-input-helper-text>${this.helper}</ha-input-helper-text>`

View File

@ -1,32 +1,32 @@
import { isToday } from "date-fns";
import type {
Circle,
CircleMarker,
LatLngTuple,
LatLngExpression,
LatLngTuple,
Layer,
Map,
Marker,
Polyline,
} from "leaflet";
import { isToday } from "date-fns";
import { css, CSSResultGroup, PropertyValues, ReactiveElement } from "lit";
import { CSSResultGroup, PropertyValues, ReactiveElement, css } from "lit";
import { customElement, property, state } from "lit/decorators";
import { formatDateTime } from "../../common/datetime/format_date_time";
import {
formatTimeWeekday,
formatTimeWithSeconds,
} from "../../common/datetime/format_time";
import {
LeafletModuleType,
setupLeafletMap,
} from "../../common/dom/setup-leaflet-map";
import {
formatTimeWithSeconds,
formatTimeWeekday,
} from "../../common/datetime/format_time";
import { formatDateTime } from "../../common/datetime/format_date_time";
import { computeStateDomain } from "../../common/entity/compute_state_domain";
import { computeStateName } from "../../common/entity/compute_state_name";
import { loadPolyfillIfNeeded } from "../../resources/resize-observer.polyfill";
import { HomeAssistant } from "../../types";
import { HomeAssistant, ThemeMode } from "../../types";
import { isTouch } from "../../util/is_touch";
import "../ha-icon-button";
import "./ha-entity-marker";
import { isTouch } from "../../util/is_touch";
const getEntityId = (entity: string | HaMapEntity): string =>
typeof entity === "string" ? entity : entity.entity_id;
@ -69,7 +69,8 @@ export class HaMap extends ReactiveElement {
@property({ type: Boolean }) public fitZones = false;
@property({ type: Boolean }) public darkMode = false;
@property({ attribute: "theme-mode", type: String })
public themeMode: ThemeMode = "auto";
@property({ type: Number }) public zoom = 14;
@ -154,7 +155,7 @@ export class HaMap extends ReactiveElement {
}
if (
!changedProps.has("darkMode") &&
!changedProps.has("themeMode") &&
(!changedProps.has("hass") ||
(oldHass && oldHass.themes?.darkMode === this.hass.themes?.darkMode))
) {
@ -163,12 +164,18 @@ export class HaMap extends ReactiveElement {
this._updateMapStyle();
}
private get _darkMode() {
return (
this.themeMode === "dark" ||
(this.themeMode === "auto" && Boolean(this.hass.themes.darkMode))
);
}
private _updateMapStyle(): void {
const darkMode = this.darkMode || (this.hass.themes.darkMode ?? false);
const forcedDark = this.darkMode;
const map = this.renderRoot.querySelector("#map");
map!.classList.toggle("dark", darkMode);
map!.classList.toggle("forced-dark", forcedDark);
map!.classList.toggle("dark", this._darkMode);
map!.classList.toggle("forced-dark", this.themeMode === "dark");
map!.classList.toggle("forced-light", this.themeMode === "light");
}
private async _loadMap(): Promise<void> {
@ -398,8 +405,7 @@ export class HaMap extends ReactiveElement {
"--dark-primary-color"
);
const className =
this.darkMode || this.hass.themes.darkMode ? "dark" : "light";
const className = this._darkMode ? "dark" : "light";
for (const entity of this.entities) {
const stateObj = hass.states[getEntityId(entity)];
@ -543,27 +549,30 @@ export class HaMap extends ReactiveElement {
background: #090909;
}
#map.forced-dark {
color: #ffffff;
--map-filter: invert(0.9) hue-rotate(170deg) brightness(1.5)
contrast(1.2) saturate(0.3);
}
#map.forced-light {
background: #ffffff;
color: #000000;
--map-filter: invert(0);
}
#map:active {
cursor: grabbing;
cursor: -moz-grabbing;
cursor: -webkit-grabbing;
}
.light {
color: #000000;
}
.dark {
color: #ffffff;
}
.leaflet-tile-pane {
filter: var(--map-filter);
}
.dark .leaflet-bar a {
background-color: var(--card-background-color, #1c1c1c);
background-color: #1c1c1c;
color: #ffffff;
}
.dark .leaflet-bar a:hover {
background-color: #313131;
}
.leaflet-marker-draggable {
cursor: move !important;
}

View File

@ -797,6 +797,7 @@ export class HaAutomationTracer extends LitElement {
description: html`${this.hass.localize(
`ui.panel.config.automation.trace.messages.${message}`,
{
reason: this.trace.script_execution,
time: renderFinishedAt(),
executiontime: renderRuntime(),
}

View File

@ -1,6 +1,8 @@
import { EntityFilter } from "../common/entity/entity_filter";
import { HomeAssistant } from "../types";
type StrictConnectionMode = "disabled" | "guard_page" | "drop_connection";
interface CloudStatusNotLoggedIn {
logged_in: false;
cloud: "disconnected" | "connecting" | "connected";
@ -19,6 +21,7 @@ export interface CloudPreferences {
alexa_enabled: boolean;
remote_enabled: boolean;
remote_allow_remote_enable: boolean;
strict_connection: StrictConnectionMode;
google_secure_devices_pin: string | undefined;
cloudhooks: { [webhookId: string]: CloudWebhook };
alexa_report_state: boolean;
@ -141,6 +144,7 @@ export const updateCloudPref = (
google_secure_devices_pin?: CloudPreferences["google_secure_devices_pin"];
tts_default_voice?: CloudPreferences["tts_default_voice"];
remote_allow_remote_enable?: CloudPreferences["remote_allow_remote_enable"];
strict_connection?: CloudPreferences["strict_connection"];
}
) =>
hass.callWS({

View File

@ -23,6 +23,8 @@ export interface ConfigEntry {
pref_disable_polling: boolean;
disabled_by: "user" | null;
reason: string | null;
error_reason_translation_key: string | null;
error_reason_translation_placeholders: Record<string, string> | null;
}
export type ConfigEntryMutableParams = Partial<

View File

@ -422,7 +422,8 @@ export const computeHistory = (
entityIds: string[],
localize: LocalizeFunc,
sensorNumericalDeviceClasses: string[],
splitDeviceClasses = false
splitDeviceClasses = false,
forceNumeric = false
): HistoryResult => {
const lineChartDevices: { [unit: string]: HistoryStates } = {};
const timelineDevices: TimelineEntity[] = [];
@ -468,6 +469,7 @@ export const computeHistory = (
let unit: string | undefined;
const isNumeric =
forceNumeric ||
isNumericFromDomain(domain) ||
(currentState != null &&
isNumericFromAttributes(currentState.attributes)) ||

View File

@ -5,9 +5,7 @@ import {
import { getExtendedEntityRegistryEntry } from "./entity_registry";
import { showEnterCodeDialog } from "../dialogs/enter-code/show-enter-code-dialog";
import { HomeAssistant } from "../types";
export const FORMAT_TEXT = "text";
export const FORMAT_NUMBER = "number";
import { UNAVAILABLE } from "./entity";
export const enum LockEntityFeature {
OPEN = 1,
@ -24,6 +22,33 @@ export interface LockEntity extends HassEntityBase {
type ProtectedLockService = "lock" | "unlock" | "open";
export function isLocked(stateObj: LockEntity) {
return stateObj.state === "locked";
}
export function isUnlocking(stateObj: LockEntity) {
return stateObj.state === "unlocking";
}
export function isLocking(stateObj: LockEntity) {
return stateObj.state === "locking";
}
export function isJammed(stateObj: LockEntity) {
return stateObj.state === "jammed";
}
export function isAvailable(stateObj: LockEntity) {
if (stateObj.state === UNAVAILABLE) {
return false;
}
const assumedState = stateObj.attributes.assumed_state === true;
return (
assumedState ||
(!isLocking(stateObj) && !isUnlocking(stateObj) && !isJammed(stateObj))
);
}
export const callProtectedLockService = async (
element: HTMLElement,
hass: HomeAssistant,

View File

@ -78,6 +78,7 @@ class LightColorTempPicker extends LitElement {
return html`
<ha-control-slider
touch-action="none"
inverted
vertical
.value=${this._ctPickerValue}

View File

@ -16,21 +16,23 @@ class MoreInfoCounter extends LitElement {
return nothing;
}
const disabled = isUnavailableState(this.stateObj!.state);
const disabled = isUnavailableState(this.stateObj.state);
return html`
<div class="actions">
<mwc-button
.action=${"increment"}
@click=${this._handleActionClick}
.disabled=${disabled}
.disabled=${disabled ||
Number(this.stateObj.state) === this.stateObj.attributes.maximum}
>
${this.hass!.localize("ui.card.counter.actions.increment")}
</mwc-button>
<mwc-button
.action=${"decrement"}
@click=${this._handleActionClick}
.disabled=${disabled}
.disabled=${disabled ||
Number(this.stateObj.state) === this.stateObj.attributes.minimum}
>
${this.hass!.localize("ui.card.counter.actions.decrement")}
</mwc-button>

View File

@ -189,6 +189,7 @@ class MoreInfoLawnMower extends LitElement {
.flex-horizontal {
display: flex;
flex-direction: row;
justify-content: space-between;
}
.space-around {
justify-content: space-around;

View File

@ -9,11 +9,12 @@ import "../../../components/ha-control-button";
import "../../../components/ha-control-button-group";
import "../../../components/ha-outlined-icon-button";
import "../../../components/ha-state-icon";
import { UNAVAILABLE } from "../../../data/entity";
import {
LockEntity,
LockEntityFeature,
callProtectedLockService,
isAvailable,
isJammed,
} from "../../../data/lock";
import "../../../state-control/lock/ha-state-control-lock-toggle";
import type { HomeAssistant } from "../../../types";
@ -85,15 +86,13 @@ class MoreInfoLock extends LitElement {
"--state-color": color,
};
const isJammed = this.stateObj.state === "jammed";
return html`
<ha-more-info-state-header
.hass=${this.hass}
.stateObj=${this.stateObj}
></ha-more-info-state-header>
<div class="controls" style=${styleMap(style)}>
${this.stateObj.state === "jammed"
${isJammed(this.stateObj)
? html`
<div class="status">
<span></span>
@ -125,7 +124,7 @@ class MoreInfoLock extends LitElement {
`
: html`
<ha-control-button
.disabled=${this.stateObj.state === UNAVAILABLE}
.disabled=${!isAvailable(this.stateObj)}
class="open-button ${this._buttonState}"
@click=${this._open}
>
@ -139,7 +138,7 @@ class MoreInfoLock extends LitElement {
: nothing}
</div>
<div>
${isJammed
${isJammed(this.stateObj)
? html`
<ha-control-button-group class="jammed">
<ha-control-button @click=${this._unlock}>

View File

@ -19,6 +19,7 @@ import "../../../components/ha-select";
import "../../../components/ha-slider";
import "../../../components/ha-svg-icon";
import { showMediaBrowserDialog } from "../../../components/media-player/show-media-browser-dialog";
import { isUnavailableState } from "../../../data/entity";
import {
MediaPickedEvent,
MediaPlayerEntity,
@ -62,7 +63,8 @@ class MoreInfoMediaPlayer extends LitElement {
`
)}
</div>
${supportsFeature(stateObj, MediaPlayerEntityFeature.BROWSE_MEDIA)
${!isUnavailableState(stateObj.state) &&
supportsFeature(stateObj, MediaPlayerEntityFeature.BROWSE_MEDIA)
? html`
<mwc-button
.label=${this.hass.localize(

View File

@ -142,7 +142,7 @@ class MoreInfoVacuum extends LitElement {
"ui.dialogs.more_info_control.vacuum.commands"
)}
</div>
<div class="flex-horizontal">
<div class="flex-horizontal space-around">
${VACUUM_COMMANDS.filter((item) =>
item.isVisible(stateObj)
).map(
@ -327,6 +327,9 @@ class MoreInfoVacuum extends LitElement {
flex-direction: row;
justify-content: space-between;
}
.space-around {
justify-content: space-around;
}
`;
}
}

View File

@ -9,7 +9,19 @@ import { fireEvent } from "../common/dom/fire_event";
import { mainWindow } from "../common/dom/get_main_window";
import { showAutomationEditor } from "../data/automation";
import { HomeAssistantMain } from "../layouts/home-assistant-main";
import type { EMIncomingMessageCommands } from "./external_messaging";
import type {
EMIncomingMessageBarCodeScanAborted,
EMIncomingMessageBarCodeScanResult,
EMIncomingMessageCommands,
} from "./external_messaging";
const barCodeListeners = new Set<
(
msg:
| EMIncomingMessageBarCodeScanResult
| EMIncomingMessageBarCodeScanAborted
) => boolean
>();
export const attachExternalToApp = (hassMainEl: HomeAssistantMain) => {
window.addEventListener("haptic", (ev) =>
@ -24,6 +36,19 @@ export const attachExternalToApp = (hassMainEl: HomeAssistantMain) => {
);
};
export const addExternalBarCodeListener = (
listener: (
msg:
| EMIncomingMessageBarCodeScanResult
| EMIncomingMessageBarCodeScanAborted
) => boolean
) => {
barCodeListeners.add(listener);
return () => {
barCodeListeners.delete(listener);
};
};
const handleExternalMessage = (
hassMainEl: HomeAssistantMain,
msg: EMIncomingMessageCommands
@ -88,6 +113,22 @@ const handleExternalMessage = (
success: true,
result: null,
});
} else if (msg.command === "bar_code/scan_result") {
barCodeListeners.forEach((listener) => listener(msg));
bus.fireMessage({
id: msg.id,
type: "result",
success: true,
result: null,
});
} else if (msg.command === "bar_code/aborted") {
barCodeListeners.forEach((listener) => listener(msg));
bus.fireMessage({
id: msg.id,
type: "result",
success: true,
result: null,
});
} else {
return false;
}

View File

@ -37,9 +37,11 @@ interface EMOutgoingMessageConfigGet extends EMMessage {
interface EMOutgoingMessageBarCodeScan extends EMMessage {
type: "bar_code/scan";
title: string;
description: string;
alternative_option_label?: string;
payload: {
title: string;
description: string;
alternative_option_label?: string;
};
}
interface EMOutgoingMessageBarCodeClose extends EMMessage {
@ -48,7 +50,9 @@ interface EMOutgoingMessageBarCodeClose extends EMMessage {
interface EMOutgoingMessageBarCodeNotify extends EMMessage {
type: "bar_code/notify";
message: string;
payload: {
message: string;
};
}
interface EMOutgoingMessageMatterCommission extends EMMessage {

View File

@ -41,14 +41,6 @@ import type { HomeAssistant, Route } from "../types";
import "./hass-tabs-subpage";
import type { PageNavigation } from "./hass-tabs-subpage";
declare global {
// for fire event
interface HASSDomEvents {
"search-changed": { value: string };
"clear-filter": undefined;
}
}
@customElement("hass-tabs-subpage-data-table")
export class HaTabsSubpageDataTable extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@ -63,6 +55,8 @@ export class HaTabsSubpageDataTable extends LitElement {
@property({ type: Boolean, attribute: "main-page" }) public mainPage = false;
@property({ attribute: false }) public initialCollapsedGroups: string[] = [];
/**
* Object with the columns.
* @type {Object}
@ -166,8 +160,15 @@ export class HaTabsSubpageDataTable extends LitElement {
@property({ type: Boolean }) public showFilters = false;
@property({ attribute: false }) public initialSorting?: {
column: string;
direction: SortingDirection;
};
@property() public initialGroupColumn?: string;
@property({ attribute: false }) public groupOrder?: string[];
@state() private _sortColumn?: string;
@state() private _sortDirection: SortingDirection = null;
@ -190,9 +191,16 @@ export class HaTabsSubpageDataTable extends LitElement {
this._dataTable.clearSelection();
}
protected firstUpdated() {
protected willUpdate() {
if (this.hasUpdated) {
return;
}
if (this.initialGroupColumn) {
this._groupColumn = this.initialGroupColumn;
this._setGroupColumn(this.initialGroupColumn);
}
if (this.initialSorting) {
this._sortColumn = this.initialSorting.column;
this._sortDirection = this.initialSorting.direction;
}
}
@ -418,6 +426,8 @@ export class HaTabsSubpageDataTable extends LitElement {
.sortColumn=${this._sortColumn}
.sortDirection=${this._sortDirection}
.groupColumn=${this._groupColumn}
.groupOrder=${this.groupOrder}
.initialCollapsedGroups=${this.initialCollapsedGroups}
>
${!this.narrow
? html`
@ -560,10 +570,20 @@ export class HaTabsSubpageDataTable extends LitElement {
this._sortDirection = null;
}
this._sortColumn = this._sortDirection === null ? undefined : columnId;
fireEvent(this, "sorting-changed", {
column: columnId,
direction: this._sortDirection,
});
}
private _handleGroupBy(ev) {
this._groupColumn = ev.currentTarget.value;
this._setGroupColumn(ev.currentTarget.value);
}
private _setGroupColumn(columnId: string) {
this._groupColumn = columnId;
fireEvent(this, "grouping-changed", { value: columnId });
}
private _enableSelectMode() {
@ -819,4 +839,11 @@ declare global {
interface HTMLElementTagNameMap {
"hass-tabs-subpage-data-table": HaTabsSubpageDataTable;
}
// for fire event
interface HASSDomEvents {
"search-changed": { value: string };
"grouping-changed": { value: string };
"clear-filter": undefined;
}
}

View File

@ -169,10 +169,6 @@ export class HomeAssistantAppEl extends QuickBarMixin(HassElement) {
// @ts-ignore
this._loadHassTranslations(this.hass!.language, "entity");
// Backwards compatibility for custom integrations
// @ts-ignore
this._loadHassTranslations(this.hass!.language, "state");
document.addEventListener(
"visibilitychange",
() => this._checkVisibility(),

View File

@ -41,7 +41,7 @@ import type { HomeAssistant } from "../types";
import { onBoardingStyles } from "./styles";
const AMSTERDAM: [number, number] = [52.3731339, 4.8903147];
const mql = matchMedia("(prefers-color-scheme: dark)");
const darkMql = matchMedia("(prefers-color-scheme: dark)");
const LOCATION_MARKER_ID = "location";
@customElement("onboarding-location")
@ -199,7 +199,7 @@ class OnboardingLocation extends LitElement {
this._highlightedMarker
)}
zoom="14"
.darkMode=${mql.matches}
.themeMode=${darkMql.matches ? "dark" : "light"}
.disabled=${this._working}
@location-updated=${this._locationChanged}
@marker-clicked=${this._markerClicked}

View File

@ -511,6 +511,7 @@ export class HaConfigAreasDashboard extends SubscribeMixin(LitElement) {
display: flex;
justify-content: space-between;
align-items: center;
overflow-wrap: anywhere;
}
.warning {
color: var(--error-color);

View File

@ -15,6 +15,7 @@ import {
mdiPlus,
mdiRobotHappy,
mdiTag,
mdiTextureBox,
mdiToggleSwitch,
mdiToggleSwitchOffOutline,
mdiTransitConnection,
@ -37,15 +38,21 @@ import { computeCssColor } from "../../../common/color/compute-color";
import { isComponentLoaded } from "../../../common/config/is_component_loaded";
import { formatShortDateTime } from "../../../common/datetime/format_date_time";
import { relativeTime } from "../../../common/datetime/relative_time";
import { storage } from "../../../common/decorators/storage";
import { HASSDomEvent, fireEvent } from "../../../common/dom/fire_event";
import { computeStateName } from "../../../common/entity/compute_state_name";
import { navigate } from "../../../common/navigate";
import { LocalizeFunc } from "../../../common/translations/localize";
import {
hasRejectedItems,
rejectedItems,
} from "../../../common/util/promise-all-settled-results";
import "../../../components/chips/ha-assist-chip";
import type {
DataTableColumnContainer,
RowClickedEvent,
SelectionChangedEvent,
SortingChangedEvent,
} from "../../../components/data-table/ha-data-table";
import "../../../components/data-table/ha-data-table-labels";
import "../../../components/entity/ha-entity-toggle";
@ -63,6 +70,7 @@ import type { HaMenu } from "../../../components/ha-menu";
import "../../../components/ha-menu-item";
import "../../../components/ha-sub-menu";
import "../../../components/ha-svg-icon";
import { createAreaRegistryEntry } from "../../../data/area_registry";
import {
AutomationEntity,
deleteAutomation,
@ -100,15 +108,12 @@ import { haStyle } from "../../../resources/styles";
import { HomeAssistant, Route, ServiceCallResponse } from "../../../types";
import { documentationUrl } from "../../../util/documentation-url";
import { turnOnOffEntity } from "../../lovelace/common/entity/turn-on-off-entity";
import { showAreaRegistryDetailDialog } from "../areas/show-dialog-area-registry-detail";
import { showAssignCategoryDialog } from "../category/show-dialog-assign-category";
import { showCategoryRegistryDetailDialog } from "../category/show-dialog-category-registry-detail";
import { configSections } from "../ha-panel-config";
import { showLabelDetailDialog } from "../labels/show-dialog-label-detail";
import { showNewAutomationDialog } from "./show-dialog-new-automation";
import {
hasRejectedItems,
rejectedItems,
} from "../../../common/util/promise-all-settled-results";
type AutomationItem = AutomationEntity & {
name: string;
@ -156,6 +161,19 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) {
@state() private _overflowAutomation?: AutomationItem;
@storage({ key: "automation-table-sort", state: false, subscribe: false })
private _activeSorting?: SortingChangedEvent;
@storage({ key: "automation-table-grouping", state: false, subscribe: false })
private _activeGrouping?: string;
@storage({
key: "automation-table-collapsed",
state: false,
subscribe: false,
})
private _activeCollapsed?: string;
@query("#overflow-menu") private _overflowMenu!: HaMenu;
private _sizeController = new ResizeController(this, {
@ -388,6 +406,7 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) {
${this.hass.localize("ui.panel.config.category.editor.add")}
</div>
</ha-menu-item>`;
const labelItems = html`${this._labels?.map((label) => {
const color = label.color ? computeCssColor(label.color) : undefined;
const selected = this._selected.every((entityId) =>
@ -424,9 +443,46 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) {
${this.hass.localize("ui.panel.config.labels.add_label")}
</div></ha-menu-item
>`;
const labelsInOverflow =
(this._sizeController.value && this._sizeController.value < 700) ||
const areaItems = html`${Object.values(this.hass.areas).map(
(area) =>
html`<ha-menu-item
.value=${area.area_id}
@click=${this._handleBulkArea}
>
${area.icon
? html`<ha-icon slot="start" .icon=${area.icon}></ha-icon>`
: html`<ha-svg-icon
slot="start"
.path=${mdiTextureBox}
></ha-svg-icon>`}
<div slot="headline">${area.name}</div>
</ha-menu-item>`
)}
<ha-menu-item .value=${null} @click=${this._handleBulkArea}>
<div slot="headline">
${this.hass.localize(
"ui.panel.config.devices.picker.bulk_actions.no_area"
)}
</div>
</ha-menu-item>
<md-divider role="separator" tabindex="-1"></md-divider>
<ha-menu-item @click=${this._bulkCreateArea}>
<div slot="headline">
${this.hass.localize(
"ui.panel.config.devices.picker.bulk_actions.add_area"
)}
</div>
</ha-menu-item>`;
const areasInOverflow =
(this._sizeController.value && this._sizeController.value < 900) ||
(!this._sizeController.value && this.hass.dockedSidebar === "docked");
const labelsInOverflow =
areasInOverflow &&
(!this._sizeController.value || this._sizeController.value < 700);
const automations = this._automations(
this.automations,
this._entityReg,
@ -468,7 +524,12 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) {
this.hass.localize,
this.hass.locale
)}
initialGroupColumn="category"
.initialGroupColumn=${this._activeGrouping || "category"}
.initialCollapsedGroups=${this._activeCollapsed}
.initialSorting=${this._activeSorting}
@sorting-changed=${this._handleSortingChanged}
@grouping-changed=${this._handleGroupingChanged}
@collapsed-changed=${this._handleCollapseChanged}
.data=${automations}
.empty=${!this.automations.length}
@row-click=${this._handleRowClicked}
@ -576,6 +637,22 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) {
></ha-svg-icon>
</ha-assist-chip>
${labelItems}
</ha-button-menu-new>`}
${areasInOverflow
? nothing
: html`<ha-button-menu-new slot="selection-bar">
<ha-assist-chip
slot="trigger"
.label=${this.hass.localize(
"ui.panel.config.devices.picker.bulk_actions.move_area"
)}
>
<ha-svg-icon
slot="trailing-icon"
.path=${mdiMenuDown}
></ha-svg-icon>
</ha-assist-chip>
${areaItems}
</ha-button-menu-new>`}`
: nothing
}
@ -640,6 +717,24 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) {
</ha-sub-menu>`
: nothing
}
${
this.narrow || areasInOverflow
? html`<ha-sub-menu>
<ha-menu-item slot="item">
<div slot="headline">
${this.hass.localize(
"ui.panel.config.devices.picker.bulk_actions.move_area"
)}
</div>
<ha-svg-icon
slot="end"
.path=${mdiChevronRight}
></ha-svg-icon>
</ha-menu-item>
<ha-menu slot="menu">${areaItems}</ha-menu>
</ha-sub-menu>`
: nothing
}
<ha-menu-item @click=${this._handleBulkEnable}>
<ha-svg-icon slot="start" .path=${mdiToggleSwitch}></ha-svg-icon>
<div slot="headline">
@ -1169,6 +1264,46 @@ ${rejected
}
}
private async _handleBulkArea(ev) {
const area = ev.currentTarget.value;
this._bulkAddArea(area);
}
private async _bulkAddArea(area: string) {
const promises: Promise<UpdateEntityRegistryEntryResult>[] = [];
this._selected.forEach((entityId) => {
promises.push(
updateEntityRegistryEntry(this.hass, entityId, {
area_id: area,
})
);
});
const result = await Promise.allSettled(promises);
if (hasRejectedItems(result)) {
const rejected = rejectedItems(result);
showAlertDialog(this, {
title: this.hass.localize("ui.panel.config.common.multiselect.failed", {
number: rejected.length,
}),
text: html`<pre>
${rejected
.map((r) => r.reason.message || r.reason.code || r.reason)
.join("\r\n")}</pre
>`,
});
}
}
private async _bulkCreateArea() {
showAreaRegistryDetailDialog(this, {
createEntry: async (values) => {
const area = await createAreaRegistryEntry(this.hass, values);
this._bulkAddArea(area.area_id);
return area;
},
});
}
private async _handleBulkEnable() {
const promises: Promise<ServiceCallResponse>[] = [];
this._selected.forEach((entityId) => {
@ -1236,6 +1371,18 @@ ${rejected
});
}
private _handleSortingChanged(ev: CustomEvent) {
this._activeSorting = ev.detail;
}
private _handleGroupingChanged(ev: CustomEvent) {
this._activeGrouping = ev.detail.value;
}
private _handleCollapseChanged(ev: CustomEvent) {
this._activeCollapsed = ev.detail.value;
}
static get styles(): CSSResultGroup {
return [
haStyle,

View File

@ -97,7 +97,7 @@ export class HaManualAutomationEditor extends LitElement {
<ha-automation-trigger
role="region"
aria-labelledby="triggers-heading"
.triggers=${this.config.trigger}
.triggers=${this.config.trigger || []}
.path=${["trigger"]}
@value-changed=${this._triggerChanged}
@item-moved=${this._itemMoved}

View File

@ -6,6 +6,7 @@ import { fireEvent } from "../../../../../common/dom/fire_event";
import "../../../../../components/device/ha-device-picker";
import "../../../../../components/device/ha-device-trigger-picker";
import "../../../../../components/ha-form/ha-form";
import { computeInitialHaFormData } from "../../../../../components/ha-form/compute-initial-ha-form-data";
import { fullEntitiesContext } from "../../../../../data/context";
import {
deviceAutomationsEqual,
@ -44,7 +45,9 @@ export class HaDeviceTrigger extends LitElement {
private _extraFieldsData = memoizeOne(
(trigger: DeviceTrigger, capabilities: DeviceCapabilities) => {
const extraFieldsData: Record<string, any> = {};
const extraFieldsData = computeInitialHaFormData(
capabilities.extra_fields
);
capabilities.extra_fields.forEach((item) => {
if (trigger[item.name] !== undefined) {
extraFieldsData![item.name] = trigger[item.name];

View File

@ -24,6 +24,7 @@ import { extractSearchParam } from "../../../common/url/search-params";
import {
DataTableColumnContainer,
RowClickedEvent,
SortingChangedEvent,
} from "../../../components/data-table/ha-data-table";
import "../../../components/entity/ha-entity-toggle";
import "../../../components/ha-button";
@ -54,6 +55,7 @@ import { documentationUrl } from "../../../util/documentation-url";
import { showToast } from "../../../util/toast";
import { configSections } from "../ha-panel-config";
import { showAddBlueprintDialog } from "./show-dialog-import-blueprint";
import { storage } from "../../../common/decorators/storage";
type BlueprintMetaDataPath = BlueprintMetaData & {
path: string;
@ -92,8 +94,24 @@ class HaBlueprintOverview extends LitElement {
Blueprints
>;
@storage({ key: "blueprint-table-sort", state: false, subscribe: false })
private _activeSorting?: SortingChangedEvent;
@storage({ key: "blueprint-table-grouping", state: false, subscribe: false })
private _activeGrouping?: string;
@storage({
key: "blueprint-table-collapsed",
state: false,
subscribe: false,
})
private _activeCollapsed?: string;
private _processedBlueprints = memoizeOne(
(blueprints: Record<string, Blueprints>): BlueprintMetaDataPath[] => {
(
blueprints: Record<string, Blueprints>,
localize: LocalizeFunc
): BlueprintMetaDataPath[] => {
const result: any[] = [];
Object.entries(blueprints).forEach(([type, typeBlueprints]) =>
Object.entries(typeBlueprints).forEach(([path, blueprint]) => {
@ -101,6 +119,9 @@ class HaBlueprintOverview extends LitElement {
result.push({
name: blueprint.error,
type,
translated_type: localize(
`ui.panel.config.blueprint.overview.types.${type as "automation" | "script"}`
),
error: true,
path,
fullpath: `${type}/${path}`,
@ -109,6 +130,9 @@ class HaBlueprintOverview extends LitElement {
result.push({
...blueprint.metadata,
type,
translated_type: localize(
`ui.panel.config.blueprint.overview.types.${type as "automation" | "script"}`
),
error: false,
path,
fullpath: `${type}/${path}`,
@ -140,14 +164,11 @@ class HaBlueprintOverview extends LitElement {
`
: undefined,
},
type: {
translated_type: {
title: localize("ui.panel.config.blueprint.overview.headers.type"),
template: (blueprint) =>
html`${this.hass.localize(
`ui.panel.config.blueprint.overview.types.${blueprint.type}`
)}`,
sortable: true,
filterable: true,
groupable: true,
hidden: narrow,
direction: "asc",
width: "10%",
@ -256,7 +277,7 @@ class HaBlueprintOverview extends LitElement {
this.hass.language,
this.hass.localize
)}
.data=${this._processedBlueprints(this.blueprints)}
.data=${this._processedBlueprints(this.blueprints, this.hass.localize)}
id="fullpath"
.noDataText=${this.hass.localize(
"ui.panel.config.blueprint.overview.no_blueprints"
@ -281,6 +302,12 @@ class HaBlueprintOverview extends LitElement {
>
</a>
</div>`}
.initialGroupColumn=${this._activeGrouping}
.initialCollapsedGroups=${this._activeCollapsed}
.initialSorting=${this._activeSorting}
@sorting-changed=${this._handleSortingChanged}
@grouping-changed=${this._handleGroupingChanged}
@collapsed-changed=${this._handleCollapseChanged}
>
<ha-icon-button
slot="toolbar-icon"
@ -341,9 +368,10 @@ class HaBlueprintOverview extends LitElement {
}
private _handleRowClicked(ev: HASSDomEvent<RowClickedEvent>) {
const blueprint = this._processedBlueprints(this.blueprints).find(
(b) => b.fullpath === ev.detail.id
)!;
const blueprint = this._processedBlueprints(
this.blueprints,
this.hass.localize
).find((b) => b.fullpath === ev.detail.id)!;
if (blueprint.error) {
showAlertDialog(this, {
title: this.hass.localize("ui.panel.config.blueprint.overview.error", {
@ -502,6 +530,18 @@ class HaBlueprintOverview extends LitElement {
fireEvent(this, "reload-blueprints");
};
private _handleSortingChanged(ev: CustomEvent) {
this._activeSorting = ev.detail;
}
private _handleGroupingChanged(ev: CustomEvent) {
this._activeGrouping = ev.detail.value;
}
private _handleCollapseChanged(ev: CustomEvent) {
this._activeCollapsed = ev.detail.value;
}
static get styles(): CSSResultGroup {
return haStyle;
}

View File

@ -1,16 +1,19 @@
import { mdiContentCopy, mdiHelpCircle } from "@mdi/js";
import { mdiContentCopy, mdiEye, mdiEyeOff, mdiHelpCircle } from "@mdi/js";
import { CSSResultGroup, LitElement, css, html, nothing } from "lit";
import { customElement, property } from "lit/decorators";
import { customElement, property, state } from "lit/decorators";
import { fireEvent } from "../../../../common/dom/fire_event";
import { copyToClipboard } from "../../../../common/util/copy-clipboard";
import "../../../../components/ha-alert";
import "../../../../components/ha-button";
import "../../../../components/ha-card";
import "../../../../components/ha-expansion-panel";
import "../../../../components/ha-formfield";
import "../../../../components/ha-radio";
import "../../../../components/ha-settings-row";
import "../../../../components/ha-switch";
// eslint-disable-next-line
import { formatDate } from "../../../../common/datetime/format_date";
import type { HaRadio } from "../../../../components/ha-radio";
import type { HaSwitch } from "../../../../components/ha-switch";
import {
CloudStatusLoggedIn,
@ -20,6 +23,7 @@ import {
} from "../../../../data/cloud";
import type { HomeAssistant } from "../../../../types";
import { showToast } from "../../../../util/toast";
import { showAlertDialog } from "../../../lovelace/custom-card-helpers";
import { showCloudCertificateDialog } from "../dialog-cloud-certificate/show-dialog-cloud-certificate";
@customElement("cloud-remote-pref")
@ -28,12 +32,14 @@ export class CloudRemotePref extends LitElement {
@property({ attribute: false }) public cloudStatus?: CloudStatusLoggedIn;
@state() private _unmaskedUrl = false;
protected render() {
if (!this.cloudStatus) {
return nothing;
}
const { remote_enabled, remote_allow_remote_enable } =
const { remote_enabled, remote_allow_remote_enable, strict_connection } =
this.cloudStatus.prefs;
const {
@ -108,35 +114,180 @@ export class CloudRemotePref extends LitElement {
)}
></ha-alert>
`
: ""}
${this.hass.localize("ui.panel.config.cloud.account.remote.info")}
${this.hass.localize(
`ui.panel.config.cloud.account.remote.${
remote_connected
? "instance_is_available"
: "instance_will_be_available"
}`
)}
<a
href="https://${remote_domain}"
target="_blank"
class="break-word"
rel="noreferrer"
>${this.hass.localize(
"ui.panel.config.cloud.account.remote.nabu_casa_url"
)}</a
>.
<ha-svg-icon
.url=${`https://${remote_domain}`}
@click=${this._copyURL}
.path=${mdiContentCopy}
></ha-svg-icon>
: strict_connection === "drop_connection"
? html`<ha-alert
alert-type="warning"
.title=${this.hass.localize(
`ui.panel.config.cloud.account.remote.drop_connection_warning_title`
)}
>${this.hass.localize(
`ui.panel.config.cloud.account.remote.drop_connection_warning`
)}</ha-alert
>`
: nothing}
<p>
${this.hass.localize("ui.panel.config.cloud.account.remote.info")}
</p>
${remote_connected
? nothing
: html`
<p>
${this.hass.localize(
"ui.panel.config.cloud.account.remote.info_instance_will_be_available"
)}
</p>
`}
<div class="url-container">
<div class="textfield-container">
<ha-textfield
.value=${this._unmaskedUrl
? `https://${remote_domain}`
: "https://•••••••••••••••••.ui.nabu.casa"}
readonly
.suffix=${
// reserve some space for the icon.
html`<div style="width: 24px"></div>`
}
></ha-textfield>
<ha-icon-button
class="toggle-unmasked-url"
toggles
.label=${this.hass.localize(
`ui.panel.config.cloud.account.remote.${this._unmaskedUrl ? "hide" : "show"}_url`
)}
@click=${this._toggleUnmaskedUrl}
.path=${this._unmaskedUrl ? mdiEyeOff : mdiEye}
></ha-icon-button>
</div>
<ha-button
.url=${`https://${remote_domain}`}
@click=${this._copyURL}
unelevated
>
<ha-svg-icon slot="icon" .path=${mdiContentCopy}></ha-svg-icon>
${this.hass.localize(
"ui.panel.config.cloud.account.remote.copy_link"
)}
</ha-button>
</div>
<ha-expansion-panel
outlined
.header=${this.hass.localize(
"ui.panel.config.cloud.account.remote.advanced_options"
"ui.panel.config.cloud.account.remote.security_options"
)}
>
<ha-settings-row>
<span slot="heading">
${this.hass.localize(
"ui.panel.config.cloud.account.remote.strict_connection"
)}
</span>
<span slot="description">
${this.hass.localize(
"ui.panel.config.cloud.account.remote.strict_connection_secondary"
)}
</span>
</ha-settings-row>
<div class="strict-connection-container">
<ha-formfield>
<ha-radio
name="strict-connection-mode"
value="disabled"
.checked=${strict_connection === "disabled"}
@change=${this._strictConnectionModeChanged}
></ha-radio>
<div slot="label">
<div class="primary">
${this.hass.localize(
"ui.panel.config.cloud.account.remote.strict_connection_option_disabled"
)}
</div>
<div class="secondary">
${this.hass.localize(
"ui.panel.config.cloud.account.remote.strict_connection_option_disabled_secondary"
)}
</div>
</div>
</ha-formfield>
<ha-formfield>
<ha-radio
name="strict-connection-mode"
value="guard_page"
.checked=${strict_connection === "guard_page"}
@change=${this._strictConnectionModeChanged}
></ha-radio>
<div slot="label">
<div class="primary">
${this.hass.localize(
"ui.panel.config.cloud.account.remote.strict_connection_option_guard_page"
)}
</div>
<div class="secondary">
${this.hass.localize(
"ui.panel.config.cloud.account.remote.strict_connection_option_guard_page_secondary"
)}
<br /><br />
${this.hass.localize(
"ui.panel.config.cloud.account.remote.strict_connection_option_guard_page_warning"
)}
</div>
</div>
</ha-formfield>
<ha-formfield>
<ha-radio
name="strict-connection-mode"
value="drop_connection"
.checked=${strict_connection === "drop_connection"}
@change=${this._strictConnectionModeChanged}
></ha-radio>
<div slot="label">
<div class="primary">
${this.hass.localize(
"ui.panel.config.cloud.account.remote.strict_connection_option_drop_connection"
)}
</div>
<div class="secondary">
${this.hass.localize(
"ui.panel.config.cloud.account.remote.strict_connection_option_drop_connection_secondary"
)}
<br /><br />
${this.hass.localize(
"ui.panel.config.cloud.account.remote.strict_connection_option_drop_connection_warning"
)}
</div>
</div>
</ha-formfield>
</div>
${strict_connection !== "disabled"
? html`
<ha-settings-row>
<span slot="heading"
>${this.hass.localize(
"ui.panel.config.cloud.account.remote.strict_connection_link"
)}</span
>
<span slot="description"
>${this.hass.localize(
"ui.panel.config.cloud.account.remote.strict_connection_link_secondary"
)}</span
>
<ha-button @click=${this._createLoginUrl}
>${this.hass.localize(
"ui.panel.config.cloud.account.remote.strict_connection_create_link"
)}</ha-button
>
</ha-settings-row>
`
: nothing}
<hr />
<ha-settings-row>
<span slot="heading"
>${this.hass.localize(
@ -153,6 +304,7 @@ export class CloudRemotePref extends LitElement {
@change=${this._toggleAllowRemoteEnabledChanged}
></ha-switch>
</ha-settings-row>
<hr />
<ha-settings-row>
<span slot="heading"
>${this.hass.localize(
@ -193,6 +345,10 @@ export class CloudRemotePref extends LitElement {
});
}
private _toggleUnmaskedUrl(): void {
this._unmaskedUrl = !this._unmaskedUrl;
}
private async _toggleChanged(ev) {
const toggle = ev.target as HaSwitch;
@ -223,6 +379,24 @@ export class CloudRemotePref extends LitElement {
}
}
private async _strictConnectionModeChanged(ev) {
const toggle = ev.target as HaRadio;
if (ev.target.value === this.cloudStatus?.prefs.strict_connection) {
return;
}
try {
await updateCloudPref(this.hass, {
strict_connection: ev.target.value,
});
fireEvent(this, "ha-refresh-cloud-status");
} catch (err: any) {
alert(err.message);
toggle.checked = !toggle.checked;
}
}
private async _copyURL(ev): Promise<void> {
const url = ev.currentTarget.url;
await copyToClipboard(url);
@ -231,6 +405,45 @@ export class CloudRemotePref extends LitElement {
});
}
private async _createLoginUrl() {
try {
const result = await this.hass.callService(
"cloud",
"create_temporary_strict_connection_url",
undefined,
undefined,
false,
true
);
showAlertDialog(this, {
title: this.hass.localize(
"ui.panel.config.cloud.account.remote.strict_connection_link"
),
text: html`${this.hass.localize(
"ui.panel.config.cloud.account.remote.strict_connection_link_created_message"
)}
<div
style="display: flex; align-items: center; gap: 8px; margin-top: 8px;"
>
<ha-textfield .value=${result.response.url} readonly></ha-textfield>
<ha-button
style="flex-basis: 180px;"
.url=${result.response.url}
@click=${this._copyURL}
unelevated
>
<ha-svg-icon slot="icon" .path=${mdiContentCopy}></ha-svg-icon>
${this.hass.localize(
"ui.panel.config.cloud.account.remote.copy_link"
)}
</ha-button>
</div>`,
});
} catch (err: any) {
showAlertDialog(this, { text: err.message });
}
}
static get styles(): CSSResultGroup {
return css`
.preparing {
@ -241,8 +454,8 @@ export class CloudRemotePref extends LitElement {
}
.header-actions {
position: absolute;
right: 24px;
inset-inline-end: 24px;
right: 16px;
inset-inline-end: 16px;
inset-inline-start: initial;
top: 24px;
display: flex;
@ -276,17 +489,71 @@ export class CloudRemotePref extends LitElement {
.card-actions a {
text-decoration: none;
}
ha-svg-icon {
--mdc-icon-size: 18px;
color: var(--secondary-text-color);
cursor: pointer;
ha-expansion-panel {
margin-top: 16px;
}
ha-formfield {
margin-top: 8px;
ha-settings-row {
padding: 0;
}
ha-expansion-panel {
--expansion-panel-content-padding: 0 16px;
--expansion-panel-summary-padding: 0 16px;
}
ha-alert {
display: block;
margin-bottom: 16px;
}
.url-container {
display: flex;
align-items: center;
gap: 8px;
margin-top: 8px;
}
.textfield-container {
position: relative;
flex: 1;
}
.textfield-container ha-textfield {
display: block;
}
.toggle-unmasked-url {
position: absolute;
top: 8px;
right: 8px;
inset-inline-start: initial;
inset-inline-end: 8px;
--mdc-icon-button-size: 40px;
--mdc-icon-size: 20px;
color: var(--secondary-text-color);
direction: var(--direction);
}
ha-formfield {
margin-left: -12px;
margin-inline-start: -12px;
--ha-formfield-align-items: start;
}
.strict-connection-container {
gap: 16px;
display: flex;
flex-direction: column;
}
.strict-connection-container ha-formfield {
--ha-formfield-align-items: start;
}
.strict-connection-container .primary {
font-size: 14px;
margin-top: 12px;
}
.strict-connection-container .secondary {
color: var(--secondary-text-color);
font-size: 12px;
}
hr {
border: none;
height: 1px;
background-color: var(--divider-color);
margin: 8px 0;
}
`;
}
}

View File

@ -82,11 +82,11 @@ class HaConfigSectionUpdates extends LitElement {
>
${this.hass.localize("ui.panel.config.updates.show_skipped")}
</ha-check-list-item>
${this._supervisorInfo?.channel !== "dev"
${this._supervisorInfo && this._supervisorInfo.channel !== "dev"
? html`
<li divider role="separator"></li>
<mwc-list-item @request-selected=${this._toggleBeta}>
${this._supervisorInfo?.channel === "stable"
${this._supervisorInfo.channel === "stable"
? this.hass.localize("ui.panel.config.updates.join_beta")
: this.hass.localize(
"ui.panel.config.updates.leave_beta"

View File

@ -1,6 +1,12 @@
import { consume } from "@lit-labs/context";
import "@lrnwebcomponents/simple-tooltip/simple-tooltip";
import { mdiChevronRight, mdiMenuDown, mdiPlus } from "@mdi/js";
import {
mdiChevronRight,
mdiDotsVertical,
mdiMenuDown,
mdiPlus,
mdiTextureBox,
} from "@mdi/js";
import {
CSSResultGroup,
LitElement,
@ -10,10 +16,12 @@ import {
nothing,
} from "lit";
import { ResizeController } from "@lit-labs/observers/resize-controller";
import { UnsubscribeFunc } from "home-assistant-js-websocket";
import { customElement, property, state } from "lit/decorators";
import memoizeOne from "memoize-one";
import { computeCssColor } from "../../../common/color/compute-color";
import { storage } from "../../../common/decorators/storage";
import { HASSDomEvent } from "../../../common/dom/fire_event";
import { computeStateDomain } from "../../../common/entity/compute_state_domain";
import {
@ -22,10 +30,15 @@ import {
} from "../../../common/integrations/protocolIntegrationPicked";
import { navigate } from "../../../common/navigate";
import { LocalizeFunc } from "../../../common/translations/localize";
import {
hasRejectedItems,
rejectedItems,
} from "../../../common/util/promise-all-settled-results";
import {
DataTableColumnContainer,
RowClickedEvent,
SelectionChangedEvent,
SortingChangedEvent,
} from "../../../components/data-table/ha-data-table";
import "../../../components/data-table/ha-data-table-labels";
import "../../../components/entity/ha-battery-icon";
@ -41,6 +54,7 @@ import "../../../components/ha-filter-states";
import "../../../components/ha-icon-button";
import "../../../components/ha-menu-item";
import "../../../components/ha-sub-menu";
import { createAreaRegistryEntry } from "../../../data/area_registry";
import { ConfigEntry, sortConfigEntries } from "../../../data/config_entries";
import { fullEntitiesContext } from "../../../data/context";
import {
@ -65,15 +79,12 @@ import { SubscribeMixin } from "../../../mixins/subscribe-mixin";
import { haStyle } from "../../../resources/styles";
import { HomeAssistant, Route } from "../../../types";
import { brandsUrl } from "../../../util/brands-url";
import { showAreaRegistryDetailDialog } from "../areas/show-dialog-area-registry-detail";
import { configSections } from "../ha-panel-config";
import "../integrations/ha-integration-overflow-menu";
import { showAddIntegrationDialog } from "../integrations/show-add-integration-dialog";
import { showLabelDetailDialog } from "../labels/show-dialog-label-detail";
import {
hasRejectedItems,
rejectedItems,
} from "../../../common/util/promise-all-settled-results";
import { showAlertDialog } from "../../lovelace/custom-card-helpers";
import { showAlertDialog } from "../../../dialogs/generic/show-dialog-box";
interface DeviceRowData extends DeviceRegistryEntry {
device?: DeviceRowData;
@ -117,6 +128,19 @@ export class HaConfigDeviceDashboard extends SubscribeMixin(LitElement) {
@state()
_labels!: LabelRegistryEntry[];
@storage({ key: "devices-table-sort", state: false, subscribe: false })
private _activeSorting?: SortingChangedEvent;
@storage({ key: "devices-table-grouping", state: false, subscribe: false })
private _activeGrouping?: string;
@storage({ key: "devices-table-collapsed", state: false, subscribe: false })
private _activeCollapsed?: string;
private _sizeController = new ResizeController(this, {
callback: (entries) => entries[0]?.contentRect.width,
});
private _ignoreLocationChange = false;
public connectedCallback() {
@ -549,6 +573,41 @@ export class HaConfigDeviceDashboard extends SubscribeMixin(LitElement) {
this._labels
);
const areasInOverflow =
(this._sizeController.value && this._sizeController.value < 700) ||
(!this._sizeController.value && this.hass.dockedSidebar === "docked");
const areaItems = html`${Object.values(this.hass.areas).map(
(area) =>
html`<ha-menu-item
.value=${area.area_id}
@click=${this._handleBulkArea}
>
${area.icon
? html`<ha-icon slot="start" .icon=${area.icon}></ha-icon>`
: html`<ha-svg-icon
slot="start"
.path=${mdiTextureBox}
></ha-svg-icon>`}
<div slot="headline">${area.name}</div>
</ha-menu-item>`
)}
<ha-menu-item .value=${null} @click=${this._handleBulkArea}>
<div slot="headline">
${this.hass.localize(
"ui.panel.config.devices.picker.bulk_actions.no_area"
)}
</div>
</ha-menu-item>
<md-divider role="separator" tabindex="-1"></md-divider>
<ha-menu-item @click=${this._bulkCreateArea}>
<div slot="headline">
${this.hass.localize(
"ui.panel.config.devices.picker.bulk_actions.add_area"
)}
</div>
</ha-menu-item>`;
const labelItems = html`${this._labels?.map((label) => {
const color = label.color ? computeCssColor(label.color) : undefined;
const selected = this._selected.every((deviceId) =>
@ -614,8 +673,14 @@ export class HaConfigDeviceDashboard extends SubscribeMixin(LitElement) {
Array.isArray(val) ? val.length : val
)
).length}
.initialGroupColumn=${this._activeGrouping}
.initialCollapsedGroups=${this._activeCollapsed}
.initialSorting=${this._activeSorting}
@clear-filter=${this._clearFilter}
@search-changed=${this._handleSearchChange}
@sorting-changed=${this._handleSortingChanged}
@grouping-changed=${this._handleGroupingChanged}
@collapsed-changed=${this._handleCollapseChanged}
@row-click=${this._handleRowClicked}
clickable
hasFab
@ -684,36 +749,77 @@ export class HaConfigDeviceDashboard extends SubscribeMixin(LitElement) {
${!this.narrow
? html`<ha-button-menu-new slot="selection-bar">
<ha-assist-chip
slot="trigger"
.label=${this.hass.localize(
"ui.panel.config.automation.picker.bulk_actions.add_label"
)}
>
<ha-svg-icon
slot="trailing-icon"
.path=${mdiMenuDown}
></ha-svg-icon>
</ha-assist-chip>
${labelItems}
</ha-button-menu-new>`
: html` <ha-button-menu-new has-overflow slot="selection-bar"
><ha-assist-chip
.label=${this.hass.localize(
"ui.panel.config.automation.picker.bulk_action"
)}
slot="trigger"
>
<ha-svg-icon
slot="trailing-icon"
.path=${mdiMenuDown}
></ha-svg-icon>
</ha-assist-chip>
<ha-assist-chip
slot="trigger"
.label=${this.hass.localize(
"ui.panel.config.automation.picker.bulk_actions.add_label"
)}
>
<ha-svg-icon
slot="trailing-icon"
.path=${mdiMenuDown}
></ha-svg-icon>
</ha-assist-chip>
${labelItems}
</ha-button-menu-new>
${areasInOverflow
? nothing
: html`<ha-button-menu-new slot="selection-bar">
<ha-assist-chip
slot="trigger"
.label=${this.hass.localize(
"ui.panel.config.devices.picker.bulk_actions.move_area"
)}
>
<ha-svg-icon
slot="trailing-icon"
.path=${mdiMenuDown}
></ha-svg-icon>
</ha-assist-chip>
${areaItems}
</ha-button-menu-new>`}`
: nothing}
${this.narrow || areasInOverflow
? html`<ha-button-menu-new has-overflow slot="selection-bar">
${this.narrow
? html`<ha-assist-chip
.label=${this.hass.localize(
"ui.panel.config.automation.picker.bulk_action"
)}
slot="trigger"
>
<ha-svg-icon
slot="trailing-icon"
.path=${mdiMenuDown}
></ha-svg-icon>
</ha-assist-chip>`
: html`<ha-icon-button
.path=${mdiDotsVertical}
.label=${"ui.panel.config.automation.picker.bulk_action"}
slot="trigger"
></ha-icon-button>`}
${this.narrow
? html` <ha-sub-menu>
<ha-menu-item slot="item">
<div slot="headline">
${this.hass.localize(
"ui.panel.config.automation.picker.bulk_actions.add_label"
)}
</div>
<ha-svg-icon
slot="end"
.path=${mdiChevronRight}
></ha-svg-icon>
</ha-menu-item>
<ha-menu slot="menu">${labelItems}</ha-menu>
</ha-sub-menu>`
: nothing}
<ha-sub-menu>
<ha-menu-item slot="item">
<div slot="headline">
${this.hass.localize(
"ui.panel.config.automation.picker.bulk_actions.add_label"
"ui.panel.config.devices.picker.bulk_actions.move_area"
)}
</div>
<ha-svg-icon
@ -721,9 +827,10 @@ export class HaConfigDeviceDashboard extends SubscribeMixin(LitElement) {
.path=${mdiChevronRight}
></ha-svg-icon>
</ha-menu-item>
<ha-menu slot="menu">${labelItems}</ha-menu>
<ha-menu slot="menu">${areaItems}</ha-menu>
</ha-sub-menu>
</ha-button-menu-new>`}
</ha-button-menu-new>`
: nothing}
</hass-tabs-subpage-data-table>
`;
}
@ -809,6 +916,46 @@ export class HaConfigDeviceDashboard extends SubscribeMixin(LitElement) {
this._selected = ev.detail.value;
}
private async _handleBulkArea(ev) {
const area = ev.currentTarget.value;
this._bulkAddArea(area);
}
private async _bulkAddArea(area: string) {
const promises: Promise<DeviceRegistryEntry>[] = [];
this._selected.forEach((deviceId) => {
promises.push(
updateDeviceRegistryEntry(this.hass, deviceId, {
area_id: area,
})
);
});
const result = await Promise.allSettled(promises);
if (hasRejectedItems(result)) {
const rejected = rejectedItems(result);
showAlertDialog(this, {
title: this.hass.localize("ui.panel.config.common.multiselect.failed", {
number: rejected.length,
}),
text: html`<pre>
${rejected
.map((r) => r.reason.message || r.reason.code || r.reason)
.join("\r\n")}</pre
>`,
});
}
}
private async _bulkCreateArea() {
showAreaRegistryDetailDialog(this, {
createEntry: async (values) => {
const area = await createAreaRegistryEntry(this.hass, values);
this._bulkAddArea(area.area_id);
return area;
},
});
}
private async _handleBulkLabel(ev) {
const label = ev.currentTarget.value;
const action = ev.currentTarget.action;
@ -855,9 +1002,24 @@ ${rejected
});
}
private _handleSortingChanged(ev: CustomEvent) {
this._activeSorting = ev.detail;
}
private _handleGroupingChanged(ev: CustomEvent) {
this._activeGrouping = ev.detail.value;
}
private _handleCollapseChanged(ev: CustomEvent) {
this._activeCollapsed = ev.detail.value;
}
static get styles(): CSSResultGroup {
return [
css`
:host {
display: block;
}
hass-tabs-subpage-data-table {
--data-table-row-height: 60px;
}

View File

@ -29,6 +29,7 @@ import { ifDefined } from "lit/directives/if-defined";
import { styleMap } from "lit/directives/style-map";
import memoize from "memoize-one";
import { computeCssColor } from "../../../common/color/compute-color";
import { storage } from "../../../common/decorators/storage";
import type { HASSDomEvent } from "../../../common/dom/fire_event";
import { computeDomain } from "../../../common/entity/compute_domain";
import { computeStateName } from "../../../common/entity/compute_state_name";
@ -37,16 +38,22 @@ import {
protocolIntegrationPicked,
} from "../../../common/integrations/protocolIntegrationPicked";
import { LocalizeFunc } from "../../../common/translations/localize";
import {
hasRejectedItems,
rejectedItems,
} from "../../../common/util/promise-all-settled-results";
import type {
DataTableColumnContainer,
RowClickedEvent,
SelectionChangedEvent,
SortingChangedEvent,
} from "../../../components/data-table/ha-data-table";
import "../../../components/data-table/ha-data-table-labels";
import "../../../components/ha-alert";
import "../../../components/ha-button-menu";
import "../../../components/ha-check-list-item";
import "../../../components/ha-filter-devices";
import "../../../components/ha-filter-domains";
import "../../../components/ha-filter-floor-areas";
import "../../../components/ha-filter-integrations";
import "../../../components/ha-filter-labels";
@ -66,6 +73,11 @@ import {
removeEntityRegistryEntry,
updateEntityRegistryEntry,
} from "../../../data/entity_registry";
import {
EntitySources,
fetchEntitySourcesWithCache,
} from "../../../data/entity_sources";
import { domainToName } from "../../../data/integration";
import {
LabelRegistryEntry,
createLabelRegistryEntry,
@ -86,14 +98,6 @@ import { configSections } from "../ha-panel-config";
import "../integrations/ha-integration-overflow-menu";
import { showAddIntegrationDialog } from "../integrations/show-add-integration-dialog";
import { showLabelDetailDialog } from "../labels/show-dialog-label-detail";
import {
EntitySources,
fetchEntitySourcesWithCache,
} from "../../../data/entity_sources";
import {
hasRejectedItems,
rejectedItems,
} from "../../../common/util/promise-all-settled-results";
export interface StateEntity
extends Omit<EntityRegistryEntry, "id" | "unique_id"> {
@ -110,6 +114,7 @@ export interface EntityRow extends StateEntity {
status: string | undefined;
area?: string;
localized_platform: string;
domain: string;
label_entries: LabelRegistryEntry[];
}
@ -149,6 +154,19 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) {
@state() private _entitySources?: EntitySources;
@storage({ key: "entities-table-sort", state: false, subscribe: false })
private _activeSorting?: SortingChangedEvent;
@storage({ key: "entities-table-grouping", state: false, subscribe: false })
private _activeGrouping?: string;
@storage({
key: "entities-table-collapsed",
state: false,
subscribe: false,
})
private _activeCollapsed?: string;
@query("hass-tabs-subpage-data-table", true)
private _dataTable!: HaTabsSubpageDataTable;
@ -261,6 +279,13 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) {
filterable: true,
width: "20%",
},
domain: {
title: localize("ui.panel.config.entities.picker.headers.domain"),
sortable: false,
hidden: true,
filterable: true,
groupable: true,
},
area: {
title: localize("ui.panel.config.entities.picker.headers.area"),
sortable: true,
@ -419,6 +444,10 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) {
entryIds.includes(entity.config_entry_id))
);
filter.value!.forEach((domain) => filteredDomains.add(domain));
} else if (key === "ha-filter-domains" && filter.value?.length) {
filteredEntities = filteredEntities.filter((entity) =>
filter.value?.includes(computeDomain(entity.entity_id))
);
} else if (key === "ha-filter-labels" && filter.value?.length) {
filteredEntities = filteredEntities.filter((entity) =>
entity.labels.some((lbl) => filter.value!.includes(lbl))
@ -467,9 +496,9 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) {
),
unavailable,
restored,
localized_platform:
localize(`component.${entry.platform}.title`) || entry.platform,
localized_platform: domainToName(localize, entry.platform),
area: area ? area.name : "—",
domain: domainToName(localize, computeDomain(entry.entity_id)),
status: restored
? localize("ui.panel.config.entities.picker.status.restored")
: unavailable
@ -594,6 +623,12 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) {
.filter=${this._filter}
selectable
.selected=${this._selected.length}
.initialGroupColumn=${this._activeGrouping}
.initialCollapsedGroups=${this._activeCollapsed}
.initialSorting=${this._activeSorting}
@sorting-changed=${this._handleSortingChanged}
@grouping-changed=${this._handleGroupingChanged}
@collapsed-changed=${this._handleCollapseChanged}
@selection-changed=${this._handleSelectionChanged}
clickable
@clear-filter=${this._clearFilter}
@ -752,6 +787,15 @@ ${
.narrow=${this.narrow}
@expanded-changed=${this._filterExpanded}
></ha-filter-devices>
<ha-filter-domains
.hass=${this.hass}
.value=${this._filters["ha-filter-domains"]?.value}
@data-table-filter-changed=${this._filterChanged}
slot="filter-pane"
.expanded=${this._expandedFilter === "ha-filter-domains"}
.narrow=${this.narrow}
@expanded-changed=${this._filterExpanded}
></ha-filter-domains>
<ha-filter-integrations
.hass=${this.hass}
.value=${this._filters["ha-filter-integrations"]?.value}
@ -1196,6 +1240,18 @@ ${rejected
});
}
private _handleSortingChanged(ev: CustomEvent) {
this._activeSorting = ev.detail;
}
private _handleGroupingChanged(ev: CustomEvent) {
this._activeGrouping = ev.detail.value;
}
private _handleCollapseChanged(ev: CustomEvent) {
this._activeCollapsed = ev.detail.value;
}
static get styles(): CSSResultGroup {
return [
haStyle,

View File

@ -24,6 +24,7 @@ import {
import { customElement, property, state } from "lit/decorators";
import memoizeOne from "memoize-one";
import { computeCssColor } from "../../../common/color/compute-color";
import { storage } from "../../../common/decorators/storage";
import { HASSDomEvent } from "../../../common/dom/fire_event";
import { computeStateDomain } from "../../../common/entity/compute_state_domain";
import { navigate } from "../../../common/navigate";
@ -40,6 +41,7 @@ import {
DataTableColumnContainer,
RowClickedEvent,
SelectionChangedEvent,
SortingChangedEvent,
} from "../../../components/data-table/ha-data-table";
import "../../../components/data-table/ha-data-table-labels";
import "../../../components/ha-fab";
@ -139,6 +141,19 @@ export class HaConfigHelpers extends SubscribeMixin(LitElement) {
@property({ attribute: false }) public route!: Route;
@storage({ key: "helpers-table-sort", state: false, subscribe: false })
private _activeSorting?: SortingChangedEvent;
@storage({ key: "helpers-table-grouping", state: false, subscribe: false })
private _activeGrouping?: string;
@storage({
key: "helpers-table-collapsed",
state: false,
subscribe: false,
})
private _activeCollapsed?: string;
@state() private _stateItems: HassEntity[] = [];
@state() private _entityEntries?: Record<string, EntityRegistryEntry>;
@ -525,7 +540,12 @@ export class HaConfigHelpers extends SubscribeMixin(LitElement) {
).length}
.columns=${this._columns(this.narrow, this.hass.localize)}
.data=${helpers}
initialGroupColumn="category"
.initialGroupColumn=${this._activeGrouping || "category"}
.initialCollapsedGroups=${this._activeCollapsed}
.initialSorting=${this._activeSorting}
@sorting-changed=${this._handleSortingChanged}
@grouping-changed=${this._handleGroupingChanged}
@collapsed-changed=${this._handleCollapseChanged}
.activeFilters=${this._activeFilters}
@clear-filter=${this._clearFilter}
@row-click=${this._openEditDialog}
@ -1020,6 +1040,18 @@ ${rejected
});
}
private _handleSortingChanged(ev: CustomEvent) {
this._activeSorting = ev.detail;
}
private _handleGroupingChanged(ev: CustomEvent) {
this._activeGrouping = ev.detail.value;
}
private _handleCollapseChanged(ev: CustomEvent) {
this._activeCollapsed = ev.detail.value;
}
static get styles(): CSSResultGroup {
return [
haStyle,

View File

@ -37,6 +37,7 @@ import {
} from "lit";
import { customElement, property, state } from "lit/decorators";
import { classMap } from "lit/directives/class-map";
import { until } from "lit/directives/until";
import memoizeOne from "memoize-one";
import { isComponentLoaded } from "../../../common/config/is_component_loaded";
import { isDevVersion } from "../../../common/config/version";
@ -550,10 +551,24 @@ class HaConfigIntegrationPage extends SubscribeMixin(LitElement) {
`ui.panel.config.integrations.config_entry.state.${item.state}`,
];
if (item.reason) {
this.hass.loadBackendTranslation("config", item.domain);
stateTextExtra = html`${this.hass.localize(
`component.${item.domain}.config.error.${item.reason}`
) || item.reason}`;
if (item.error_reason_translation_key) {
const lokalisePromExc = this.hass
.loadBackendTranslation("exceptions", item.domain)
.then((localize) =>
localize(
`component.${item.domain}.exceptions.${item.error_reason_translation_key}.message`,
item.error_reason_translation_placeholders ?? undefined
)
);
stateTextExtra = html`${until(lokalisePromExc)}`;
} else {
const lokalisePromError = this.hass
.loadBackendTranslation("config", item.domain)
.then((localize) =>
localize(`component.${item.domain}.config.error.${item.reason}`)
);
stateTextExtra = html`${until(lokalisePromError, item.reason)}`;
}
} else {
stateTextExtra = html`
<br />

View File

@ -47,6 +47,7 @@ class DialogZHAReconfigureDevice extends LitElement {
public showDialog(params: ZHAReconfigureDeviceDialogParams): void {
this._params = params;
this._clusterConfigurationStatuses = new Map();
this._stages = undefined;
}

View File

@ -424,7 +424,7 @@ export class ZHANetworkVisualizationPage extends LitElement {
? {
physics: {
barnesHut: {
springConstant: 0.05,
springConstant: 0,
avoidOverlap: 10,
damping: 0.09,
},

View File

@ -10,15 +10,17 @@ import { LitElement, PropertyValues, html, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import memoizeOne from "memoize-one";
import { computeCssColor } from "../../../common/color/compute-color";
import { navigate } from "../../../common/navigate";
import { LocalizeFunc } from "../../../common/translations/localize";
import {
DataTableColumnContainer,
RowClickedEvent,
SortingChangedEvent,
} from "../../../components/data-table/ha-data-table";
import "../../../components/ha-fab";
import "../../../components/ha-icon-button";
import "../../../components/ha-relative-time";
import "../../../components/ha-icon-overflow-menu";
import "../../../components/ha-relative-time";
import {
LabelRegistryEntry,
LabelRegistryEntryMutableParams,
@ -35,7 +37,7 @@ import "../../../layouts/hass-tabs-subpage-data-table";
import { HomeAssistant, Route } from "../../../types";
import { configSections } from "../ha-panel-config";
import { showLabelDetailDialog } from "./show-dialog-label-detail";
import { navigate } from "../../../common/navigate";
import { storage } from "../../../common/decorators/storage";
@customElement("ha-config-labels")
export class HaConfigLabels extends LitElement {
@ -49,6 +51,13 @@ export class HaConfigLabels extends LitElement {
@state() private _labels: LabelRegistryEntry[] = [];
@storage({
key: "labels-table-sort",
state: false,
subscribe: false,
})
private _activeSorting?: SortingChangedEvent;
private _columns = memoizeOne((localize: LocalizeFunc) => {
const columns: DataTableColumnContainer<LabelRegistryEntry> = {
icon: {
@ -149,6 +158,8 @@ export class HaConfigLabels extends LitElement {
.data=${this._data(this._labels)}
.noDataText=${this.hass.localize("ui.panel.config.labels.no_labels")}
hasFab
.initialSorting=${this._activeSorting}
@sorting-changed=${this._handleSortingChanged}
@row-click=${this._editLabel}
clickable
id="label_id"
@ -268,6 +279,10 @@ export class HaConfigLabels extends LitElement {
`/config/automation/dashboard?historyBack=1&label=${label.label_id}`
);
}
private _handleSortingChanged(ev: CustomEvent) {
this._activeSorting = ev.detail;
}
}
declare global {

View File

@ -16,6 +16,7 @@ import { stringCompare } from "../../../../common/string/compare";
import {
DataTableColumnContainer,
RowClickedEvent,
SortingChangedEvent,
} from "../../../../components/data-table/ha-data-table";
import "../../../../components/ha-clickable-list-item";
import "../../../../components/ha-fab";
@ -46,6 +47,7 @@ import { showNewDashboardDialog } from "../../dashboard/show-dialog-new-dashboar
import { lovelaceTabs } from "../ha-config-lovelace";
import { showDashboardConfigureStrategyDialog } from "./show-dialog-lovelace-dashboard-configure-strategy";
import { showDashboardDetailDialog } from "./show-dialog-lovelace-dashboard-detail";
import { storage } from "../../../../common/decorators/storage";
type DataTableItem = Pick<
LovelaceDashboard,
@ -68,6 +70,13 @@ export class HaConfigLovelaceDashboards extends LitElement {
@state() private _dashboards: LovelaceDashboard[] = [];
@storage({
key: "lovelace-dashboards-table-sort",
state: false,
subscribe: false,
})
private _activeSorting?: SortingChangedEvent;
public willUpdate() {
if (!this.hasUpdated) {
this.hass.loadFragmentTranslation("lovelace");
@ -293,6 +302,8 @@ export class HaConfigLovelaceDashboards extends LitElement {
this.hass.localize
)}
.data=${this._getItems(this._dashboards)}
.initialSorting=${this._activeSorting}
@sorting-changed=${this._handleSortingChanged}
@row-click=${this._editDashboard}
id="url_path"
hasFab
@ -440,6 +451,10 @@ export class HaConfigLovelaceDashboards extends LitElement {
},
});
}
private _handleSortingChanged(ev: CustomEvent) {
this._activeSorting = ev.detail;
}
}
declare global {

View File

@ -10,9 +10,11 @@ import {
import { customElement, property, state } from "lit/decorators";
import memoize from "memoize-one";
import { stringCompare } from "../../../../common/string/compare";
import { LocalizeFunc } from "../../../../common/translations/localize";
import {
DataTableColumnContainer,
RowClickedEvent,
SortingChangedEvent,
} from "../../../../components/data-table/ha-data-table";
import "../../../../components/ha-card";
import "../../../../components/ha-fab";
@ -33,10 +35,10 @@ import "../../../../layouts/hass-subpage";
import "../../../../layouts/hass-tabs-subpage-data-table";
import { haStyle } from "../../../../resources/styles";
import { HomeAssistant, Route } from "../../../../types";
import { LocalizeFunc } from "../../../../common/translations/localize";
import { loadLovelaceResources } from "../../../lovelace/common/load-resources";
import { lovelaceResourcesTabs } from "../ha-config-lovelace";
import { showResourceDetailDialog } from "./show-dialog-lovelace-resource-detail";
import { storage } from "../../../../common/decorators/storage";
@customElement("ha-config-lovelace-resources")
export class HaConfigLovelaceRescources extends LitElement {
@ -50,6 +52,13 @@ export class HaConfigLovelaceRescources extends LitElement {
@state() private _resources: LovelaceResource[] = [];
@storage({
key: "lovelace-resources-table-sort",
state: false,
subscribe: false,
})
private _activeSorting?: SortingChangedEvent;
private _columns = memoize(
(
_language,
@ -127,6 +136,8 @@ export class HaConfigLovelaceRescources extends LitElement {
.noDataText=${this.hass.localize(
"ui.panel.config.lovelace.resources.picker.no_resources"
)}
.initialSorting=${this._activeSorting}
@sorting-changed=${this._handleSortingChanged}
@row-click=${this._editResource}
hasFab
clickable
@ -237,6 +248,10 @@ export class HaConfigLovelaceRescources extends LitElement {
});
}
private _handleSortingChanged(ev: CustomEvent) {
this._activeSorting = ev.detail;
}
static get styles(): CSSResultGroup {
return [
haStyle,

View File

@ -15,6 +15,7 @@ import {
mdiPlay,
mdiPlus,
mdiTag,
mdiTextureBox,
} from "@mdi/js";
import { differenceInDays } from "date-fns";
import { UnsubscribeFunc } from "home-assistant-js-websocket";
@ -32,14 +33,20 @@ import memoizeOne from "memoize-one";
import { computeCssColor } from "../../../common/color/compute-color";
import { formatShortDateTime } from "../../../common/datetime/format_date_time";
import { relativeTime } from "../../../common/datetime/relative_time";
import { storage } from "../../../common/decorators/storage";
import { HASSDomEvent, fireEvent } from "../../../common/dom/fire_event";
import { computeStateName } from "../../../common/entity/compute_state_name";
import { navigate } from "../../../common/navigate";
import { LocalizeFunc } from "../../../common/translations/localize";
import {
hasRejectedItems,
rejectedItems,
} from "../../../common/util/promise-all-settled-results";
import {
DataTableColumnContainer,
RowClickedEvent,
SelectionChangedEvent,
SortingChangedEvent,
} from "../../../components/data-table/ha-data-table";
import "../../../components/data-table/ha-data-table-labels";
import "../../../components/ha-button";
@ -55,6 +62,7 @@ import "../../../components/ha-menu-item";
import "../../../components/ha-state-icon";
import "../../../components/ha-sub-menu";
import "../../../components/ha-svg-icon";
import { createAreaRegistryEntry } from "../../../data/area_registry";
import {
CategoryRegistryEntry,
createCategoryRegistryEntry,
@ -91,14 +99,11 @@ import { haStyle } from "../../../resources/styles";
import { HomeAssistant, Route } from "../../../types";
import { documentationUrl } from "../../../util/documentation-url";
import { showToast } from "../../../util/toast";
import { showAreaRegistryDetailDialog } from "../areas/show-dialog-area-registry-detail";
import { showAssignCategoryDialog } from "../category/show-dialog-assign-category";
import { showCategoryRegistryDetailDialog } from "../category/show-dialog-category-registry-detail";
import { configSections } from "../ha-panel-config";
import { showLabelDetailDialog } from "../labels/show-dialog-label-detail";
import {
hasRejectedItems,
rejectedItems,
} from "../../../common/util/promise-all-settled-results";
type SceneItem = SceneEntity & {
name: string;
@ -144,6 +149,19 @@ class HaSceneDashboard extends SubscribeMixin(LitElement) {
@consume({ context: fullEntitiesContext, subscribe: true })
_entityReg!: EntityRegistryEntry[];
@storage({ key: "scene-table-sort", state: false, subscribe: false })
private _activeSorting?: SortingChangedEvent;
@storage({ key: "scene-table-grouping", state: false, subscribe: false })
private _activeGrouping?: string;
@storage({
key: "scene-table-collapsed",
state: false,
subscribe: false,
})
private _activeCollapsed?: string;
private _sizeController = new ResizeController(this, {
callback: (entries) => entries[0]?.contentRect.width,
});
@ -391,6 +409,7 @@ class HaSceneDashboard extends SubscribeMixin(LitElement) {
${this.hass.localize("ui.panel.config.category.editor.add")}
</div>
</ha-menu-item>`;
const labelItems = html` ${this._labels?.map((label) => {
const color = label.color ? computeCssColor(label.color) : undefined;
const selected = this._selected.every((entityId) =>
@ -427,9 +446,46 @@ class HaSceneDashboard extends SubscribeMixin(LitElement) {
${this.hass.localize("ui.panel.config.labels.add_label")}
</div></ha-menu-item
>`;
const labelsInOverflow =
(this._sizeController.value && this._sizeController.value < 700) ||
const areaItems = html`${Object.values(this.hass.areas).map(
(area) =>
html`<ha-menu-item
.value=${area.area_id}
@click=${this._handleBulkArea}
>
${area.icon
? html`<ha-icon slot="start" .icon=${area.icon}></ha-icon>`
: html`<ha-svg-icon
slot="start"
.path=${mdiTextureBox}
></ha-svg-icon>`}
<div slot="headline">${area.name}</div>
</ha-menu-item>`
)}
<ha-menu-item .value=${null} @click=${this._handleBulkArea}>
<div slot="headline">
${this.hass.localize(
"ui.panel.config.devices.picker.bulk_actions.no_area"
)}
</div>
</ha-menu-item>
<md-divider role="separator" tabindex="-1"></md-divider>
<ha-menu-item @click=${this._bulkCreateArea}>
<div slot="headline">
${this.hass.localize(
"ui.panel.config.devices.picker.bulk_actions.add_area"
)}
</div>
</ha-menu-item>`;
const areasInOverflow =
(this._sizeController.value && this._sizeController.value < 900) ||
(!this._sizeController.value && this.hass.dockedSidebar === "docked");
const labelsInOverflow =
areasInOverflow &&
(!this._sizeController.value || this._sizeController.value < 700);
const scenes = this._scenes(
this.scenes,
this._entityReg,
@ -438,6 +494,7 @@ class HaSceneDashboard extends SubscribeMixin(LitElement) {
this._labels,
this._filteredScenes
);
return html`
<hass-tabs-subpage-data-table
.hass=${this.hass}
@ -463,7 +520,12 @@ class HaSceneDashboard extends SubscribeMixin(LitElement) {
).length}
.columns=${this._columns(this.narrow, this.hass.localize)}
id="entity_id"
initialGroupColumn="category"
.initialGroupColumn=${this._activeGrouping || "category"}
.initialCollapsedGroups=${this._activeCollapsed}
.initialSorting=${this._activeSorting}
@sorting-changed=${this._handleSortingChanged}
@grouping-changed=${this._handleGroupingChanged}
@collapsed-changed=${this._handleCollapseChanged}
.data=${scenes}
.empty=${!this.scenes.length}
.activeFilters=${this._activeFilters}
@ -562,9 +624,25 @@ class HaSceneDashboard extends SubscribeMixin(LitElement) {
></ha-svg-icon>
</ha-assist-chip>
${labelItems}
</ha-button-menu-new>`}
${areasInOverflow
? nothing
: html`<ha-button-menu-new slot="selection-bar">
<ha-assist-chip
slot="trigger"
.label=${this.hass.localize(
"ui.panel.config.devices.picker.bulk_actions.move_area"
)}
>
<ha-svg-icon
slot="trailing-icon"
.path=${mdiMenuDown}
></ha-svg-icon>
</ha-assist-chip>
${areaItems}
</ha-button-menu-new>`}`
: nothing}
${this.narrow || labelsInOverflow
${this.narrow || areasInOverflow
? html`
<ha-button-menu-new has-overflow slot="selection-bar">
${
@ -610,8 +688,8 @@ class HaSceneDashboard extends SubscribeMixin(LitElement) {
: nothing
}
${
this.narrow || this.hass.dockedSidebar === "docked"
? html` <ha-sub-menu>
this.narrow || labelsInOverflow
? html`<ha-sub-menu>
<ha-menu-item slot="item">
<div slot="headline">
${this.hass.localize(
@ -627,6 +705,24 @@ class HaSceneDashboard extends SubscribeMixin(LitElement) {
</ha-sub-menu>`
: nothing
}
${
this.narrow || areasInOverflow
? html`<ha-sub-menu>
<ha-menu-item slot="item">
<div slot="headline">
${this.hass.localize(
"ui.panel.config.devices.picker.bulk_actions.move_area"
)}
</div>
<ha-svg-icon
slot="end"
.path=${mdiChevronRight}
></ha-svg-icon>
</ha-menu-item>
<ha-menu slot="menu">${areaItems}</ha-menu>
</ha-sub-menu>`
: nothing
}
</ha-button-menu-new>`
: nothing}
${!this.scenes.length
@ -855,6 +951,46 @@ ${rejected
}
}
private async _handleBulkArea(ev) {
const area = ev.currentTarget.value;
this._bulkAddArea(area);
}
private async _bulkAddArea(area: string) {
const promises: Promise<UpdateEntityRegistryEntryResult>[] = [];
this._selected.forEach((entityId) => {
promises.push(
updateEntityRegistryEntry(this.hass, entityId, {
area_id: area,
})
);
});
const result = await Promise.allSettled(promises);
if (hasRejectedItems(result)) {
const rejected = rejectedItems(result);
showAlertDialog(this, {
title: this.hass.localize("ui.panel.config.common.multiselect.failed", {
number: rejected.length,
}),
text: html`<pre>
${rejected
.map((r) => r.reason.message || r.reason.code || r.reason)
.join("\r\n")}</pre
>`,
});
}
}
private async _bulkCreateArea() {
showAreaRegistryDetailDialog(this, {
createEntry: async (values) => {
const area = await createAreaRegistryEntry(this.hass, values);
this._bulkAddArea(area.area_id);
return area;
},
});
}
private _editCategory(scene: any) {
const entityReg = this._entityReg.find(
(reg) => reg.entity_id === scene.entity_id
@ -975,6 +1111,18 @@ ${rejected
});
}
private _handleSortingChanged(ev: CustomEvent) {
this._activeSorting = ev.detail;
}
private _handleGroupingChanged(ev: CustomEvent) {
this._activeGrouping = ev.detail.value;
}
private _handleCollapseChanged(ev: CustomEvent) {
this._activeCollapsed = ev.detail.value;
}
static get styles(): CSSResultGroup {
return [
haStyle,

View File

@ -13,6 +13,7 @@ import {
mdiPlus,
mdiScriptText,
mdiTag,
mdiTextureBox,
mdiTransitConnection,
} from "@mdi/js";
import { differenceInDays } from "date-fns";
@ -33,14 +34,20 @@ import { computeCssColor } from "../../../common/color/compute-color";
import { isComponentLoaded } from "../../../common/config/is_component_loaded";
import { formatShortDateTime } from "../../../common/datetime/format_date_time";
import { relativeTime } from "../../../common/datetime/relative_time";
import { storage } from "../../../common/decorators/storage";
import { HASSDomEvent, fireEvent } from "../../../common/dom/fire_event";
import { computeStateName } from "../../../common/entity/compute_state_name";
import { navigate } from "../../../common/navigate";
import { LocalizeFunc } from "../../../common/translations/localize";
import {
hasRejectedItems,
rejectedItems,
} from "../../../common/util/promise-all-settled-results";
import {
DataTableColumnContainer,
RowClickedEvent,
SelectionChangedEvent,
SortingChangedEvent,
} from "../../../components/data-table/ha-data-table";
import "../../../components/data-table/ha-data-table-labels";
import "../../../components/ha-fab";
@ -55,6 +62,7 @@ import "../../../components/ha-icon-overflow-menu";
import "../../../components/ha-menu-item";
import "../../../components/ha-sub-menu";
import "../../../components/ha-svg-icon";
import { createAreaRegistryEntry } from "../../../data/area_registry";
import {
CategoryRegistryEntry,
createCategoryRegistryEntry,
@ -92,15 +100,12 @@ import { haStyle } from "../../../resources/styles";
import { HomeAssistant, Route } from "../../../types";
import { documentationUrl } from "../../../util/documentation-url";
import { showToast } from "../../../util/toast";
import { showAreaRegistryDetailDialog } from "../areas/show-dialog-area-registry-detail";
import { showNewAutomationDialog } from "../automation/show-dialog-new-automation";
import { showAssignCategoryDialog } from "../category/show-dialog-assign-category";
import { showCategoryRegistryDetailDialog } from "../category/show-dialog-category-registry-detail";
import { configSections } from "../ha-panel-config";
import { showLabelDetailDialog } from "../labels/show-dialog-label-detail";
import {
hasRejectedItems,
rejectedItems,
} from "../../../common/util/promise-all-settled-results";
type ScriptItem = ScriptEntity & {
name: string;
@ -148,6 +153,19 @@ class HaScriptPicker extends SubscribeMixin(LitElement) {
@consume({ context: fullEntitiesContext, subscribe: true })
_entityReg!: EntityRegistryEntry[];
@storage({ key: "script-table-sort", state: false, subscribe: false })
private _activeSorting?: SortingChangedEvent;
@storage({ key: "script-table-grouping", state: false, subscribe: false })
private _activeGrouping?: string;
@storage({
key: "script-table-collapsed",
state: false,
subscribe: false,
})
private _activeCollapsed?: string;
private _sizeController = new ResizeController(this, {
callback: (entries) => entries[0]?.contentRect.width,
});
@ -403,6 +421,7 @@ class HaScriptPicker extends SubscribeMixin(LitElement) {
${this.hass.localize("ui.panel.config.category.editor.add")}
</div>
</ha-menu-item>`;
const labelItems = html`${this._labels?.map((label) => {
const color = label.color ? computeCssColor(label.color) : undefined;
const selected = this._selected.every((entityId) =>
@ -439,9 +458,46 @@ class HaScriptPicker extends SubscribeMixin(LitElement) {
${this.hass.localize("ui.panel.config.labels.add_label")}
</div></ha-menu-item
>`;
const labelsInOverflow =
(this._sizeController.value && this._sizeController.value < 700) ||
const areaItems = html`${Object.values(this.hass.areas).map(
(area) =>
html`<ha-menu-item
.value=${area.area_id}
@click=${this._handleBulkArea}
>
${area.icon
? html`<ha-icon slot="start" .icon=${area.icon}></ha-icon>`
: html`<ha-svg-icon
slot="start"
.path=${mdiTextureBox}
></ha-svg-icon>`}
<div slot="headline">${area.name}</div>
</ha-menu-item>`
)}
<ha-menu-item .value=${null} @click=${this._handleBulkArea}>
<div slot="headline">
${this.hass.localize(
"ui.panel.config.devices.picker.bulk_actions.no_area"
)}
</div>
</ha-menu-item>
<md-divider role="separator" tabindex="-1"></md-divider>
<ha-menu-item @click=${this._bulkCreateArea}>
<div slot="headline">
${this.hass.localize(
"ui.panel.config.devices.picker.bulk_actions.add_area"
)}
</div>
</ha-menu-item>`;
const areasInOverflow =
(this._sizeController.value && this._sizeController.value < 900) ||
(!this._sizeController.value && this.hass.dockedSidebar === "docked");
const labelsInOverflow =
areasInOverflow &&
(!this._sizeController.value || this._sizeController.value < 700);
const scripts = this._scripts(
this.scripts,
this._entityReg,
@ -462,7 +518,12 @@ class HaScriptPicker extends SubscribeMixin(LitElement) {
{ number: scripts.length }
)}
hasFilters
initialGroupColumn="category"
.initialGroupColumn=${this._activeGrouping || "category"}
.initialCollapsedGroups=${this._activeCollapsed}
.initialSorting=${this._activeSorting}
@sorting-changed=${this._handleSortingChanged}
@grouping-changed=${this._handleGroupingChanged}
@collapsed-changed=${this._handleCollapseChanged}
selectable
.selected=${this._selected.length}
@selection-changed=${this._handleSelectionChanged}
@ -588,9 +649,25 @@ class HaScriptPicker extends SubscribeMixin(LitElement) {
></ha-svg-icon>
</ha-assist-chip>
${labelItems}
</ha-button-menu-new>`}
${areasInOverflow
? nothing
: html`<ha-button-menu-new slot="selection-bar">
<ha-assist-chip
slot="trigger"
.label=${this.hass.localize(
"ui.panel.config.devices.picker.bulk_actions.move_area"
)}
>
<ha-svg-icon
slot="trailing-icon"
.path=${mdiMenuDown}
></ha-svg-icon>
</ha-assist-chip>
${areaItems}
</ha-button-menu-new>`}`
: nothing}
${this.narrow || labelsInOverflow
${this.narrow || areasInOverflow
? html`
<ha-button-menu-new has-overflow slot="selection-bar">
${
@ -636,8 +713,8 @@ class HaScriptPicker extends SubscribeMixin(LitElement) {
: nothing
}
${
this.narrow || this.hass.dockedSidebar === "docked"
? html` <ha-sub-menu>
this.narrow || labelsInOverflow
? html`<ha-sub-menu>
<ha-menu-item slot="item">
<div slot="headline">
${this.hass.localize(
@ -653,6 +730,24 @@ class HaScriptPicker extends SubscribeMixin(LitElement) {
</ha-sub-menu>`
: nothing
}
${
this.narrow || areasInOverflow
? html`<ha-sub-menu>
<ha-menu-item slot="item">
<div slot="headline">
${this.hass.localize(
"ui.panel.config.devices.picker.bulk_actions.move_area"
)}
</div>
<ha-svg-icon
slot="end"
.path=${mdiChevronRight}
></ha-svg-icon>
</ha-menu-item>
<ha-menu slot="menu">${areaItems}</ha-menu>
</ha-sub-menu>`
: nothing
}
</ha-button-menu-new>`
: nothing}
${!this.scripts.length
@ -1091,6 +1186,58 @@ ${rejected
});
}
private async _handleBulkArea(ev) {
const area = ev.currentTarget.value;
this._bulkAddArea(area);
}
private async _bulkAddArea(area: string) {
const promises: Promise<UpdateEntityRegistryEntryResult>[] = [];
this._selected.forEach((entityId) => {
promises.push(
updateEntityRegistryEntry(this.hass, entityId, {
area_id: area,
})
);
});
const result = await Promise.allSettled(promises);
if (hasRejectedItems(result)) {
const rejected = rejectedItems(result);
showAlertDialog(this, {
title: this.hass.localize("ui.panel.config.common.multiselect.failed", {
number: rejected.length,
}),
text: html`<pre>
${rejected
.map((r) => r.reason.message || r.reason.code || r.reason)
.join("\r\n")}</pre
>`,
});
}
}
private async _bulkCreateArea() {
showAreaRegistryDetailDialog(this, {
createEntry: async (values) => {
const area = await createAreaRegistryEntry(this.hass, values);
this._bulkAddArea(area.area_id);
return area;
},
});
}
private _handleSortingChanged(ev: CustomEvent) {
this._activeSorting = ev.detail;
}
private _handleGroupingChanged(ev: CustomEvent) {
this._activeGrouping = ev.detail.value;
}
private _handleCollapseChanged(ev: CustomEvent) {
this._activeCollapsed = ev.detail.value;
}
static get styles(): CSSResultGroup {
return [
haStyle,

View File

@ -122,7 +122,7 @@ export class HaManualScriptEditor extends LitElement {
<ha-automation-action
role="region"
aria-labelledby="sequence-heading"
.actions=${this.config.sequence}
.actions=${this.config.sequence || []}
.path=${["sequence"]}
@value-changed=${this._sequenceChanged}
@item-moved=${this._itemMoved}

View File

@ -7,6 +7,7 @@ import { LocalizeFunc } from "../../../common/translations/localize";
import {
DataTableColumnContainer,
RowClickedEvent,
SortingChangedEvent,
} from "../../../components/data-table/ha-data-table";
import "../../../components/data-table/ha-data-table-icon";
import "../../../components/ha-fab";
@ -25,6 +26,7 @@ import { HomeAssistant, Route } from "../../../types";
import { configSections } from "../ha-panel-config";
import { showAddUserDialog } from "./show-dialog-add-user";
import { showUserDetailDialog } from "./show-dialog-user-detail";
import { storage } from "../../../common/decorators/storage";
@customElement("ha-config-users")
export class HaConfigUsers extends LitElement {
@ -38,6 +40,19 @@ export class HaConfigUsers extends LitElement {
@state() private _users: User[] = [];
@storage({ key: "users-table-sort", state: false, subscribe: false })
private _activeSorting?: SortingChangedEvent;
@storage({ key: "users-table-grouping", state: false, subscribe: false })
private _activeGrouping?: string;
@storage({
key: "users-table-collapsed",
state: false,
subscribe: false,
})
private _activeCollapsed?: string;
private _columns = memoizeOne(
(narrow: boolean, localize: LocalizeFunc): DataTableColumnContainer => {
const columns: DataTableColumnContainer<User> = {
@ -70,16 +85,14 @@ export class HaConfigUsers extends LitElement {
hidden: narrow,
template: (user) => html`${user.username || "—"}`,
},
group_ids: {
group: {
title: localize("ui.panel.config.users.picker.headers.group"),
sortable: true,
filterable: true,
groupable: true,
width: "20%",
direction: "asc",
hidden: narrow,
template: (user) => html`
${localize(`groups.${user.group_ids[0]}`)}
`,
},
is_active: {
title: this.hass.localize(
@ -164,7 +177,13 @@ export class HaConfigUsers extends LitElement {
backPath="/config"
.tabs=${configSections.persons}
.columns=${this._columns(this.narrow, this.hass.localize)}
.data=${this._users}
.data=${this._userData(this._users, this.hass.localize)}
.initialGroupColumn=${this._activeGrouping}
.initialCollapsedGroups=${this._activeCollapsed}
.initialSorting=${this._activeSorting}
@sorting-changed=${this._handleSortingChanged}
@grouping-changed=${this._handleGroupingChanged}
@collapsed-changed=${this._handleCollapseChanged}
@row-click=${this._editUser}
hasFab
clickable
@ -181,6 +200,13 @@ export class HaConfigUsers extends LitElement {
`;
}
private _userData = memoizeOne((users: User[], localize: LocalizeFunc) =>
users.map((user) => ({
...user,
group: localize(`groups.${user.group_ids[0]}`),
}))
);
private async _fetchUsers() {
this._users = await fetchUsers(this.hass);
@ -245,6 +271,18 @@ export class HaConfigUsers extends LitElement {
},
});
}
private _handleSortingChanged(ev: CustomEvent) {
this._activeSorting = ev.detail;
}
private _handleGroupingChanged(ev: CustomEvent) {
this._activeGrouping = ev.detail.value;
}
private _handleCollapseChanged(ev: CustomEvent) {
this._activeCollapsed = ev.detail.value;
}
}
declare global {

View File

@ -23,6 +23,7 @@ import {
DataTableRowData,
RowClickedEvent,
SelectionChangedEvent,
SortingChangedEvent,
} from "../../../components/data-table/ha-data-table";
import "../../../components/ha-fab";
import { AlexaEntity, fetchCloudAlexaEntities } from "../../../data/alexa";
@ -52,6 +53,7 @@ import "./expose/expose-assistant-icon";
import { voiceAssistantTabs } from "./ha-config-voice-assistants";
import { showExposeEntityDialog } from "./show-dialog-expose-entity";
import { showVoiceSettingsDialog } from "./show-dialog-voice-settings";
import { storage } from "../../../common/decorators/storage";
@customElement("ha-config-voice-assistants-expose")
export class VoiceAssistantsExpose extends LitElement {
@ -87,6 +89,13 @@ export class VoiceAssistantsExpose extends LitElement {
string[] | undefined
>;
@storage({
key: "voice-expose-table-sort",
state: false,
subscribe: false,
})
private _activeSorting?: SortingChangedEvent;
@query("hass-tabs-subpage-data-table", true)
private _dataTable!: HaTabsSubpageDataTable;
@ -505,6 +514,8 @@ export class VoiceAssistantsExpose extends LitElement {
selectable
.selected=${this._selectedEntities.length}
clickable
.initialSorting=${this._activeSorting}
@sorting-changed=${this._handleSortingChanged}
@selection-changed=${this._handleSelectionChanged}
@clear-filter=${this._clearFilter}
@search-changed=${this._handleSearchChange}
@ -696,6 +707,10 @@ export class VoiceAssistantsExpose extends LitElement {
navigate(window.location.pathname, { replace: true });
}
private _handleSortingChanged(ev: CustomEvent) {
this._activeSorting = ev.detail;
}
static get styles(): CSSResultGroup {
return [
haStyle,

View File

@ -421,6 +421,7 @@ class HaPanelHistory extends LitElement {
[],
this.hass.localize,
sensorNumericDeviceClasses,
true,
true
);
// remap states array to statistics array

View File

@ -0,0 +1,127 @@
import { mdiLock, mdiLockOpen } from "@mdi/js";
import { HassEntity } from "home-assistant-js-websocket";
import { css, CSSResultGroup, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { classMap } from "lit/directives/class-map";
import { computeDomain } from "../../../common/entity/compute_domain";
import "../../../components/ha-control-button";
import "../../../components/ha-control-button-group";
import {
callProtectedLockService,
isAvailable,
isLocking,
isUnlocking,
isLocked,
} from "../../../data/lock";
import { HomeAssistant } from "../../../types";
import { LovelaceCardFeature } from "../types";
import { LockCommandsCardFeatureConfig } from "./types";
import { forwardHaptic } from "../../../data/haptics";
export const supportsLockCommandsCardFeature = (stateObj: HassEntity) => {
const domain = computeDomain(stateObj.entity_id);
return domain === "lock";
};
@customElement("hui-lock-commands-card-feature")
class HuiLockCommandsCardFeature
extends LitElement
implements LovelaceCardFeature
{
@property({ attribute: false }) public hass?: HomeAssistant;
@property({ attribute: false }) public stateObj?: HassEntity;
@state() private _config?: LockCommandsCardFeatureConfig;
static getStubConfig(): LockCommandsCardFeatureConfig {
return {
type: "lock-commands",
};
}
public setConfig(config: LockCommandsCardFeatureConfig): void {
if (!config) {
throw new Error("Invalid configuration");
}
this._config = config;
}
private _onTap(ev): void {
ev.stopPropagation();
const service = ev.target.dataset.service;
if (!this.hass || !this.stateObj || !service) {
return;
}
forwardHaptic("light");
callProtectedLockService(this, this.hass, this.stateObj, service);
}
protected render() {
if (
!this._config ||
!this.hass ||
!this.stateObj ||
!supportsLockCommandsCardFeature(this.stateObj)
) {
return nothing;
}
return html`
<ha-control-button-group>
<ha-control-button
.label=${this.hass.localize("ui.card.lock.lock")}
.disabled=${!isAvailable(this.stateObj) || isLocked(this.stateObj)}
@click=${this._onTap}
data-service="lock"
class=${classMap({
pulse: isLocking(this.stateObj) || isUnlocking(this.stateObj),
})}
>
<ha-svg-icon .path=${mdiLock}></ha-svg-icon>
</ha-control-button>
<ha-control-button
.label=${this.hass.localize("ui.card.lock.unlock")}
.disabled=${!isAvailable(this.stateObj) || !isLocked(this.stateObj)}
@click=${this._onTap}
data-service="unlock"
class=${classMap({
pulse: isLocking(this.stateObj) || isUnlocking(this.stateObj),
})}
>
<ha-svg-icon .path=${mdiLockOpen}></ha-svg-icon>
</ha-control-button>
</ha-control-button-group>
`;
}
static get styles(): CSSResultGroup {
return css`
@keyframes pulse {
0% {
opacity: 1;
}
50% {
opacity: 0;
}
100% {
opacity: 1;
}
}
.pulse {
animation: pulse 1s infinite;
}
ha-control-button-group {
margin: 0 12px 12px 12px;
--control-button-group-spacing: 12px;
}
`;
}
}
declare global {
interface HTMLElementTagNameMap {
"hui-lock-commands-card-feature": HuiLockCommandsCardFeature;
}
}

View File

@ -0,0 +1,154 @@
import { mdiCheck } from "@mdi/js";
import { HassEntity } from "home-assistant-js-websocket";
import { css, CSSResultGroup, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { computeDomain } from "../../../common/entity/compute_domain";
import { supportsFeature } from "../../../common/entity/supports-feature";
import "../../../components/ha-control-button";
import "../../../components/ha-control-button-group";
import {
LockEntityFeature,
callProtectedLockService,
isAvailable,
} from "../../../data/lock";
import { HomeAssistant } from "../../../types";
import { LovelaceCardFeature } from "../types";
import { LockOpenDoorCardFeatureConfig } from "./types";
export const supportsLockOpenDoorCardFeature = (stateObj: HassEntity) => {
const domain = computeDomain(stateObj.entity_id);
return domain === "lock" && supportsFeature(stateObj, LockEntityFeature.OPEN);
};
const CONFIRM_TIMEOUT_SECOND = 5;
const OPENED_TIMEOUT_SECOND = 3;
type ButtonState = "normal" | "confirm" | "success";
@customElement("hui-lock-open-door-card-feature")
class HuiLockOpenDoorCardFeature
extends LitElement
implements LovelaceCardFeature
{
@property({ attribute: false }) public hass?: HomeAssistant;
@property({ attribute: false }) public stateObj?: HassEntity;
@state() public _buttonState: ButtonState = "normal";
@state() private _config?: LockOpenDoorCardFeatureConfig;
private _buttonTimeout?: number;
static getStubConfig(): LockOpenDoorCardFeatureConfig {
return {
type: "lock-open-door",
};
}
public setConfig(config: LockOpenDoorCardFeatureConfig): void {
if (!config) {
throw new Error("Invalid configuration");
}
this._config = config;
}
private _setButtonState(buttonState: ButtonState, timeoutSecond?: number) {
clearTimeout(this._buttonTimeout);
this._buttonState = buttonState;
if (timeoutSecond) {
this._buttonTimeout = window.setTimeout(() => {
this._buttonState = "normal";
}, timeoutSecond * 1000);
}
}
private async _open() {
if (this._buttonState !== "confirm") {
this._setButtonState("confirm", CONFIRM_TIMEOUT_SECOND);
return;
}
if (!this.hass || !this.stateObj) {
return;
}
callProtectedLockService(this, this.hass, this.stateObj!, "open");
this._setButtonState("success", OPENED_TIMEOUT_SECOND);
}
protected render() {
if (
!this._config ||
!this.hass ||
!this.stateObj ||
!supportsLockOpenDoorCardFeature(this.stateObj)
) {
return nothing;
}
return html`
${this._buttonState === "success"
? html`
<p class="open-success">
<ha-svg-icon path=${mdiCheck}></ha-svg-icon>
${this.hass.localize("ui.card.lock.open_door_success")}
</p>
`
: html`
<ha-control-button-group>
<ha-control-button
.disabled=${!isAvailable(this.stateObj)}
class="open-button ${this._buttonState}"
@click=${this._open}
>
${this._buttonState === "confirm"
? this.hass.localize("ui.card.lock.open_door_confirm")
: this.hass.localize("ui.card.lock.open_door")}
</ha-control-button>
</ha-control-button-group>
`}
`;
}
static get styles(): CSSResultGroup {
return css`
ha-control-button {
font-size: 14px;
}
ha-control-button-group {
margin: 0 12px 12px 12px;
--control-button-group-spacing: 12px;
}
.open-button {
width: 130px;
}
.open-button.confirm {
--control-button-background-color: var(--warning-color);
}
.open-success {
font-size: 14px;
line-height: 14px;
display: flex;
align-items: center;
justify-content: center;
flex-direction: row;
gap: 8px;
font-weight: 500;
color: var(--success-color);
margin: 0 12px 12px 12px;
height: 40px;
text-align: center;
}
ha-control-button-group + ha-attributes:not([empty]) {
margin-top: 16px;
}
`;
}
}
declare global {
interface HTMLElementTagNameMap {
"hui-lock-open-door-card-feature": HuiLockOpenDoorCardFeature;
}
}

View File

@ -26,6 +26,14 @@ export interface LightColorTempCardFeatureConfig {
type: "light-color-temp";
}
export interface LockCommandsCardFeatureConfig {
type: "lock-commands";
}
export interface LockOpenDoorCardFeatureConfig {
type: "lock-open-door";
}
export interface FanPresetModesCardFeatureConfig {
type: "fan-preset-modes";
style?: "dropdown" | "icons";
@ -143,6 +151,8 @@ export type LovelaceCardFeatureConfig =
| LawnMowerCommandsCardFeatureConfig
| LightBrightnessCardFeatureConfig
| LightColorTempCardFeatureConfig
| LockCommandsCardFeatureConfig
| LockOpenDoorCardFeatureConfig
| NumericInputCardFeatureConfig
| SelectOptionsCardFeatureConfig
| TargetHumidityCardFeatureConfig

View File

@ -138,7 +138,7 @@ class HuiMapCard extends LitElement implements LovelaceCard {
includeDomains
);
return { type: "map", entities: foundEntities };
return { type: "map", entities: foundEntities, theme_mode: "auto" };
}
protected render() {
@ -151,6 +151,17 @@ class HuiMapCard extends LitElement implements LovelaceCard {
(${this._error.code})
</ha-alert>`;
}
const isDarkMode =
this._config.dark_mode || this._config.theme_mode === "dark"
? true
: this._config.theme_mode === "light"
? false
: this.hass.themes.darkMode;
const themeMode =
this._config.theme_mode || (this._config.dark_mode ? "dark" : "auto");
return html`
<ha-card id="card" .header=${this._config.title}>
<div id="root">
@ -161,7 +172,7 @@ class HuiMapCard extends LitElement implements LovelaceCard {
.paths=${this._getHistoryPaths(this._config, this._stateHistory)}
.autoFit=${this._config.auto_fit || false}
.fitZones=${this._config.fit_zones}
?darkMode=${this._config.dark_mode}
.themeMode=${themeMode}
interactiveZones
renderPassive
></ha-map>
@ -170,6 +181,7 @@ class HuiMapCard extends LitElement implements LovelaceCard {
"ui.panel.lovelace.cards.map.reset_focus"
)}
.path=${mdiImageFilterCenterFocus}
style=${isDarkMode ? "color:#ffffff" : "color:#000000"}
@click=${this._fitMap}
tabindex="0"
></ha-icon-button>

View File

@ -3,7 +3,7 @@ import { ActionConfig } from "../../../data/lovelace/config/action";
import { LovelaceCardConfig } from "../../../data/lovelace/config/card";
import { Statistic, StatisticType } from "../../../data/recorder";
import { ForecastType } from "../../../data/weather";
import { FullCalendarView, TranslationDict } from "../../../types";
import { FullCalendarView, ThemeMode, TranslationDict } from "../../../types";
import { LovelaceCardFeatureConfig } from "../card-features/types";
import { LegacyStateFilter } from "../common/evaluate-filter";
import { Condition, LegacyCondition } from "../common/validate-condition";
@ -314,6 +314,7 @@ export interface MapCardConfig extends LovelaceCardConfig {
hours_to_show?: number;
geo_location_sources?: string[];
dark_mode?: boolean;
theme_mode?: ThemeMode;
}
export interface MarkdownCardConfig extends LovelaceCardConfig {

View File

@ -14,6 +14,8 @@ import "../card-features/hui-humidifier-toggle-card-feature";
import "../card-features/hui-lawn-mower-commands-card-feature";
import "../card-features/hui-light-brightness-card-feature";
import "../card-features/hui-light-color-temp-card-feature";
import "../card-features/hui-lock-commands-card-feature";
import "../card-features/hui-lock-open-door-card-feature";
import "../card-features/hui-numeric-input-card-feature";
import "../card-features/hui-select-options-card-feature";
import "../card-features/hui-target-temperature-card-feature";
@ -45,6 +47,8 @@ const TYPES: Set<LovelaceCardFeatureConfig["type"]> = new Set([
"lawn-mower-commands",
"light-brightness",
"light-color-temp",
"lock-commands",
"lock-open-door",
"numeric-input",
"select-options",
"target-humidity",

View File

@ -35,6 +35,8 @@ import { supportsHumidifierToggleCardFeature } from "../../card-features/hui-hum
import { supportsLawnMowerCommandCardFeature } from "../../card-features/hui-lawn-mower-commands-card-feature";
import { supportsLightBrightnessCardFeature } from "../../card-features/hui-light-brightness-card-feature";
import { supportsLightColorTempCardFeature } from "../../card-features/hui-light-color-temp-card-feature";
import { supportsLockCommandsCardFeature } from "../../card-features/hui-lock-commands-card-feature";
import { supportsLockOpenDoorCardFeature } from "../../card-features/hui-lock-open-door-card-feature";
import { supportsNumericInputCardFeature } from "../../card-features/hui-numeric-input-card-feature";
import { supportsSelectOptionsCardFeature } from "../../card-features/hui-select-options-card-feature";
import { supportsTargetHumidityCardFeature } from "../../card-features/hui-target-humidity-card-feature";
@ -56,8 +58,8 @@ const UI_FEATURE_TYPES = [
"climate-preset-modes",
"cover-open-close",
"cover-position",
"cover-tilt-position",
"cover-tilt",
"cover-tilt-position",
"fan-preset-modes",
"fan-speed",
"humidifier-modes",
@ -65,6 +67,8 @@ const UI_FEATURE_TYPES = [
"lawn-mower-commands",
"light-brightness",
"light-color-temp",
"lock-commands",
"lock-open-door",
"numeric-input",
"select-options",
"target-humidity",
@ -111,6 +115,8 @@ const SUPPORTS_FEATURE_TYPES: Record<
"lawn-mower-commands": supportsLawnMowerCommandCardFeature,
"light-brightness": supportsLightBrightnessCardFeature,
"light-color-temp": supportsLightColorTempCardFeature,
"lock-commands": supportsLockCommandsCardFeature,
"lock-open-door": supportsLockOpenDoorCardFeature,
"numeric-input": supportsNumericInputCardFeature,
"select-options": supportsSelectOptionsCardFeature,
"target-humidity": supportsTargetHumidityCardFeature,

View File

@ -1,3 +1,4 @@
import { mdiPalette } from "@mdi/js";
import { css, CSSResultGroup, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import {
@ -11,6 +12,7 @@ import {
string,
union,
} from "superstruct";
import memoizeOne from "memoize-one";
import { fireEvent } from "../../../../common/dom/fire_event";
import { hasLocation } from "../../../../common/entity/has_location";
import "../../../../components/ha-form/ha-form";
@ -28,6 +30,7 @@ import { processEditorEntities } from "../process-editor-entities";
import { baseLovelaceCardConfig } from "../structs/base-card-struct";
import { EntitiesEditorEvent } from "../types";
import { configElementStyle } from "./config-elements-style";
import { LocalizeFunc } from "../../../../common/translations/localize";
export const mapEntitiesConfigStruct = union([
object({
@ -50,30 +53,11 @@ const cardConfigStruct = assign(
hours_to_show: optional(number()),
geo_location_sources: optional(array(string())),
auto_fit: optional(boolean()),
theme_mode: optional(string()),
})
);
const SCHEMA = [
{ name: "title", selector: { text: {} } },
{
name: "",
type: "grid",
schema: [
{ name: "aspect_ratio", selector: { text: {} } },
{
name: "default_zoom",
default: DEFAULT_ZOOM,
selector: { number: { mode: "box", min: 0 } },
},
{ name: "dark_mode", selector: { boolean: {} } },
{
name: "hours_to_show",
default: DEFAULT_HOURS_TO_SHOW,
selector: { number: { mode: "box", min: 0 } },
},
],
},
] as const;
const themeModes = ["auto", "light", "dark"] as const;
@customElement("hui-map-card-editor")
export class HuiMapCardEditor extends LitElement implements LovelaceCardEditor {
@ -83,8 +67,68 @@ export class HuiMapCardEditor extends LitElement implements LovelaceCardEditor {
@state() private _configEntities?: EntityConfig[];
private _schema = memoizeOne(
(localize: LocalizeFunc) =>
[
{ name: "title", selector: { text: {} } },
{
name: "",
type: "expandable",
iconPath: mdiPalette,
title: localize(`ui.panel.lovelace.editor.card.map.appearance`),
schema: [
{
name: "",
type: "grid",
schema: [
{ name: "aspect_ratio", selector: { text: {} } },
{
name: "default_zoom",
default: DEFAULT_ZOOM,
selector: { number: { mode: "box", min: 0 } },
},
{
name: "theme_mode",
default: "auto",
selector: {
select: {
mode: "dropdown",
options: themeModes.map((themeMode) => ({
value: themeMode,
label: localize(
`ui.panel.lovelace.editor.card.map.theme_modes.${themeMode}`
),
})),
},
},
},
{
name: "hours_to_show",
default: DEFAULT_HOURS_TO_SHOW,
selector: { number: { mode: "box", min: 0 } },
},
],
},
],
},
] as const
);
public setConfig(config: MapCardConfig): void {
assert(config, cardConfigStruct);
// Migrate legacy dark_mode to theme_mode
if (!this._config && !("theme_mode" in config)) {
config = { ...config };
if (config.dark_mode) {
config.theme_mode = "dark";
} else {
config.theme_mode = "auto";
}
delete config.dark_mode;
fireEvent(this, "config-changed", { config: config });
}
this._config = config;
this._configEntities = config.entities
? processEditorEntities(config.entities)
@ -104,33 +148,32 @@ export class HuiMapCardEditor extends LitElement implements LovelaceCardEditor {
<ha-form
.hass=${this.hass}
.data=${this._config}
.schema=${SCHEMA}
.schema=${this._schema(this.hass.localize)}
.computeLabel=${this._computeLabelCallback}
@value-changed=${this._valueChanged}
></ha-form>
<div class="card-config">
<hui-entity-editor
.hass=${this.hass}
.entities=${this._configEntities}
.entityFilter=${hasLocation}
@entities-changed=${this._entitiesValueChanged}
></hui-entity-editor>
<h3>
${this.hass.localize(
"ui.panel.lovelace.editor.card.map.geo_location_sources"
)}
</h3>
<div class="geo_location_sources">
<hui-input-list-editor
.inputLabel=${this.hass.localize(
"ui.panel.lovelace.editor.card.map.source"
)}
.hass=${this.hass}
.value=${this._geo_location_sources}
@value-changed=${this._geoSourcesChanged}
></hui-input-list-editor>
</div>
</div>
<hui-entity-editor
.hass=${this.hass}
.entities=${this._configEntities}
.entityFilter=${hasLocation}
@entities-changed=${this._entitiesValueChanged}
></hui-entity-editor>
<h3>
${this.hass.localize(
"ui.panel.lovelace.editor.card.map.geo_location_sources"
)}
</h3>
<hui-input-list-editor
.inputLabel=${this.hass.localize(
"ui.panel.lovelace.editor.card.map.source"
)}
.hass=${this.hass}
.value=${this._geo_location_sources}
@value-changed=${this._geoSourcesChanged}
></hui-input-list-editor>
`;
}
@ -170,9 +213,14 @@ export class HuiMapCardEditor extends LitElement implements LovelaceCardEditor {
fireEvent(this, "config-changed", { config: ev.detail.value });
}
private _computeLabelCallback = (schema: SchemaUnion<typeof SCHEMA>) => {
private _computeLabelCallback = (
schema: SchemaUnion<ReturnType<typeof this._schema>>
) => {
switch (schema.name) {
case "dark_mode":
case "theme_mode":
return this.hass!.localize(
`ui.panel.lovelace.editor.card.map.${schema.name}`
);
case "default_zoom":
return this.hass!.localize(
`ui.panel.lovelace.editor.card.map.${schema.name}`
@ -185,16 +233,7 @@ export class HuiMapCardEditor extends LitElement implements LovelaceCardEditor {
};
static get styles(): CSSResultGroup {
return [
configElementStyle,
css`
.geo_location_sources {
padding-left: 20px;
padding-inline-start: 20px;
direction: var(--direction);
}
`,
];
return [configElementStyle, css``];
}
}

View File

@ -102,6 +102,7 @@ export const derivedStyles = {
"mdc-theme-error": "var(--error-color)",
"app-header-text-color": "var(--text-primary-color)",
"app-header-background-color": "var(--primary-color)",
"app-theme-color": "var(--primary-color)",
"mdc-checkbox-unchecked-color": "rgba(var(--rgb-primary-text-color), 0.54)",
"mdc-checkbox-disabled-color": "var(--disabled-text-color)",
"mdc-radio-unchecked-color": "rgba(var(--rgb-primary-text-color), 0.54)",

View File

@ -41,6 +41,7 @@ export class HaStateControlCoverPosition extends LitElement {
return html`
<ha-control-slider
touch-action="none"
vertical
.value=${this.value}
min="0"

View File

@ -78,6 +78,7 @@ export class HaStateControlInfoCoverTiltPosition extends LitElement {
return html`
<ha-control-slider
touch-action="none"
vertical
.value=${this.value}
min="0"

View File

@ -106,6 +106,7 @@ export class HaStateControlCoverToggle extends LitElement {
return html`
<ha-control-switch
touch-action="none"
vertical
reversed
.checked=${isOn}

View File

@ -111,6 +111,7 @@ export class HaStateControlFanSpeed extends LitElement {
return html`
<ha-control-slider
touch-action="none"
vertical
min="0"
max="100"

View File

@ -108,6 +108,7 @@ export class HaStateControlToggle extends LitElement {
return html`
<ha-control-switch
touch-action="none"
.pathOn=${this.iconPathOn || mdiFlash}
.pathOff=${this.iconPathOff || mdiFlashOff}
vertical

View File

@ -59,6 +59,7 @@ export class HaStateControlLightBrightness extends LitElement {
return html`
<ha-control-slider
touch-action="none"
vertical
.value=${this.value}
min="1"

View File

@ -118,6 +118,7 @@ export class HaStateControlLockToggle extends LitElement {
return html`
<ha-control-switch
touch-action="none"
vertical
reversed
.checked=${this._isOn}

View File

@ -40,6 +40,7 @@ export class HaStateControlValvePosition extends LitElement {
return html`
<ha-control-slider
touch-action="none"
vertical
.value=${this.value}
min="0"

View File

@ -106,6 +106,7 @@ export class HaStateControlValveToggle extends LitElement {
return html`
<ha-control-switch
touch-action="none"
vertical
reversed
.checked=${isOn}

View File

@ -83,7 +83,8 @@ export const connectionMixin = <T extends Constructor<HassBaseEl>>(
service,
serviceData,
target,
notifyOnError = true
notifyOnError = true,
returnResponse = false
) => {
if (__DEV__ || this.hass?.debugConnection) {
// eslint-disable-next-line no-console
@ -101,7 +102,8 @@ export const connectionMixin = <T extends Constructor<HassBaseEl>>(
domain,
service,
serviceData ?? {},
target
target,
returnResponse
)) as ServiceCallResponse;
} catch (err: any) {
if (

View File

@ -130,9 +130,8 @@ export default <T extends Constructor<HassBaseEl>>(superClass: T) =>
const themeMeta = document.querySelector("meta[name=theme-color]");
const computedStyles = getComputedStyle(document.documentElement);
const headerColor = computedStyles.getPropertyValue(
"--app-header-background-color"
);
const themeMetaColor =
computedStyles.getPropertyValue("--app-theme-color");
document.documentElement.style.backgroundColor =
computedStyles.getPropertyValue("--primary-background-color");
@ -145,7 +144,7 @@ export default <T extends Constructor<HassBaseEl>>(superClass: T) =>
);
}
const themeColor =
headerColor?.trim() ||
themeMetaColor?.trim() ||
(themeMeta.getAttribute("default-content") as string);
themeMeta.setAttribute("content", themeColor);
}

View File

@ -72,7 +72,7 @@ export default <T extends Constructor<HassBaseEl>>(superClass: T) =>
// eslint-disable-next-line: variable-name
private __coreProgress?: string;
private __loadedFragmetTranslations: Set<string> = new Set();
private __loadedFragmentTranslations: Set<string> = new Set();
private __loadedTranslations: {
// track what things have been loaded
@ -262,7 +262,7 @@ export default <T extends Constructor<HassBaseEl>>(superClass: T) =>
document.querySelector("html")!.setAttribute("lang", hass.language);
this._applyDirection(hass);
this._loadCoreTranslations(hass.language);
this.__loadedFragmetTranslations = new Set();
this.__loadedFragmentTranslations = new Set();
this._loadFragmentTranslations(hass.language, hass.panelUrl);
}
@ -385,12 +385,12 @@ export default <T extends Constructor<HassBaseEl>>(superClass: T) =>
return undefined;
}
if (this.__loadedFragmetTranslations.has(fragment)) {
if (this.__loadedFragmentTranslations.has(fragment)) {
return this.hass!.localize;
}
this.__loadedFragmetTranslations.add(fragment);
this.__loadedFragmentTranslations.add(fragment);
const result = await getTranslation(fragment, language);
return this._updateResources(result.language, result.data);
return this._updateResources(language, result.data);
}
private async _loadCoreTranslations(language: string) {
@ -402,7 +402,7 @@ export default <T extends Constructor<HassBaseEl>>(superClass: T) =>
this.__coreProgress = language;
try {
const result = await getTranslation(null, language);
await this._updateResources(result.language, result.data);
await this._updateResources(language, result.data);
} finally {
this.__coreProgress = undefined;
}

View File

@ -783,7 +783,8 @@
"no-data": "No data",
"filtering_by": "Filtering by",
"hidden": "{number} hidden",
"clear": "Clear"
"clear": "Clear",
"ungrouped": "Ungrouped"
},
"media-browser": {
"tts": {
@ -2949,7 +2950,7 @@
"event": "[%key:ui::panel::config::automation::editor::triggers::type::homeassistant::event%]",
"sunrise": "Sunrise",
"sunset": "Sunset",
"offset": "Offset (optional)",
"offset": "Offset in seconds or HH:MM:SS (optional)",
"description": {
"picker": "When the sun sets or rises.",
"sets": "When the sun sets{hasDuration, select, \n true { offset by {duration}} \n other {}\n }",
@ -3829,25 +3830,42 @@
}
},
"remote": {
"title": "Remote control",
"title": "Remote access",
"connected": "Connected",
"not_connected": "Not connected",
"reconnecting": "Not connected. Trying to reconnect.",
"access_is_being_prepared": "Remote control is being prepared. We will notify you when it's ready.",
"access_is_being_prepared": "Remote access is being prepared. We will notify you when it's ready.",
"cerificate_loading": "Your certificate is loading.",
"cerificate_loaded": "Your certificate is loaded, waiting for validation.",
"cerificate_error": "There was an error generating the certificate, check your logs.",
"info": "Home Assistant Cloud provides a secure remote connection to your instance while away from home.",
"instance_is_available": "Your instance is available at your",
"instance_will_be_available": "Your instance will be available at your",
"info": "Home Assistant Cloud provides a secure remote access to your instance while away from home. For more information on remote access and these settings visit our security documentation.",
"info_instance_will_be_available": "Your instance will be available at your Nabu Casa URL.",
"link_learn_how_it_works": "Learn how it works",
"nabu_casa_url": "Nabu Casa URL",
"advanced_options": "Advanced options",
"external_activation": "Allow external activation of remote control",
"external_activation_secondary": "Allows you to turn on remote control from your Nabu Casa account page, even if you're outside your local network",
"certificate_info": "Certificate info",
"certificate_expire": "Will be renewed at {date}",
"more_info": "More info"
"show_url": "Show full URL",
"hide_url": "Hide URL",
"copy_link": "Copy link",
"security_options": "Security options",
"strict_connection": "Remote login access",
"strict_connection_secondary": "Choose what happens when new devices visit your remote access link.",
"strict_connection_option_disabled": "Show login page",
"strict_connection_option_disabled_secondary": "Any new device visiting your remote access link are presented with a login page.",
"strict_connection_option_guard_page": "Block remote logins",
"strict_connection_option_guard_page_secondary": "New devices must log in with a temporary access link. Devices accessing the link that are not logged in will be presented with a page explaining the restrictions.",
"strict_connection_option_guard_page_warning": "This prevents outsiders from trying to log in to your system but also your own devices if they have not logged in previously.",
"strict_connection_option_drop_connection": "Block remote logins and show nothing",
"strict_connection_option_drop_connection_secondary": "This is the same as the above setting but instead provides a blank page for additional security.",
"strict_connection_option_drop_connection_warning": "This prevents outsiders from snooping the remote web address and trying to log in, but it may appear as if there is no system running when users try to access it.",
"external_activation": "Allow external activation of remote access",
"external_activation_secondary": "If you disable remote access on this page, having this setting enabled allows you to reactivate it remotely via your Nabu Casa account.",
"drop_connection_warning_title": "Remote log in has been deactivated",
"drop_connection_warning": "The below security options may make it appear the system is not running.",
"strict_connection_link": "Provide temporary login access",
"strict_connection_link_secondary": "This provides a link for new devices to login for the next hour.",
"strict_connection_create_link": "Create link",
"strict_connection_link_created_message": "Give this link to the person you want to give remote access to the login page of your Home Assistant instance.",
"certificate_info": "Certificate information",
"certificate_expire": "Certificate renewal at {date}",
"more_info": "More details"
},
"alexa": {
"title": "Alexa",
@ -4034,7 +4052,12 @@
"confirm_delete_integration": "Are you sure you want to remove this device from {integration}?",
"picker": {
"search": "Search {number} devices",
"state": "State"
"state": "State",
"bulk_actions": {
"move_area": "Move to area",
"no_area": "No area",
"add_area": "Add area"
}
}
},
"entities": {
@ -4061,7 +4084,8 @@
"integration": "Integration",
"area": "Area",
"disabled_by": "Disabled by",
"status": "Status"
"status": "Status",
"domain": "Domain"
},
"selected": "{number} selected",
"enable_selected": {
@ -5824,7 +5848,14 @@
"name": "Map",
"geo_location_sources": "Geolocation sources",
"dark_mode": "Dark mode?",
"default_zoom": "Default zoom",
"appearance": "Appearance",
"theme_mode": "Theme Mode",
"theme_modes": {
"auto": "Auto",
"light": "Light",
"dark": "Dark"
},
"default_zoom": "Default Zoom",
"source": "Source",
"description": "The Map card that allows you to display entities on a map."
},
@ -5950,6 +5981,12 @@
"light-color-temp": {
"label": "Light color temperature"
},
"lock-commands": {
"label": "Lock commands"
},
"lock-open-door": {
"label": "Lock open door"
},
"vacuum-commands": {
"label": "Vacuum commands",
"commands": "Commands",

View File

@ -139,6 +139,8 @@ export type FullCalendarView =
| "dayGridDay"
| "listWeek";
export type ThemeMode = "auto" | "light" | "dark";
export interface ToggleButton {
label: string;
iconPath?: string;
@ -190,6 +192,7 @@ export interface Context {
export interface ServiceCallResponse {
context: Context;
response?: any;
}
export interface ServiceCallRequest {
@ -241,7 +244,8 @@ export interface HomeAssistant {
service: ServiceCallRequest["service"],
serviceData?: ServiceCallRequest["serviceData"],
target?: ServiceCallRequest["target"],
notifyOnError?: boolean
notifyOnError?: boolean,
returnResponse?: boolean
): Promise<ServiceCallResponse>;
callApi<T>(
method: "GET" | "POST" | "PUT" | "DELETE",

990
yarn.lock

File diff suppressed because it is too large Load Diff