Compare commits

..

2 Commits

Author SHA1 Message Date
Paul Bottein
e642c80003 Fix device automations 2024-10-09 19:28:50 +02:00
Paul Bottein
cc07d51613 Add sequence support to dashboard action 2024-10-09 18:53:41 +02:00
39 changed files with 756 additions and 718 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.2.1
uses: actions/checkout@v4.2.0
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.2.1
uses: actions/checkout@v4.2.0
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.2.1
uses: actions/checkout@v4.2.0
- name: Setup Node
uses: actions/setup-node@v4.0.4
with:
@@ -37,7 +37,7 @@ jobs:
- name: Build resources
run: ./node_modules/.bin/gulp gen-icons-json build-translations build-locale-data gather-gallery-pages
- name: Setup lint cache
uses: actions/cache@v4.1.1
uses: actions/cache@v4.1.0
with:
path: |
node_modules/.cache/prettier
@@ -58,7 +58,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Check out files from GitHub
uses: actions/checkout@v4.2.1
uses: actions/checkout@v4.2.0
- name: Setup Node
uses: actions/setup-node@v4.0.4
with:
@@ -76,7 +76,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Check out files from GitHub
uses: actions/checkout@v4.2.1
uses: actions/checkout@v4.2.0
- name: Setup Node
uses: actions/setup-node@v4.0.4
with:
@@ -89,7 +89,7 @@ jobs:
env:
IS_TEST: "true"
- name: Upload bundle stats
uses: actions/upload-artifact@v4.4.3
uses: actions/upload-artifact@v4.4.0
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.2.1
uses: actions/checkout@v4.2.0
- name: Setup Node
uses: actions/setup-node@v4.0.4
with:
@@ -113,7 +113,7 @@ jobs:
env:
IS_TEST: "true"
- name: Upload bundle stats
uses: actions/upload-artifact@v4.4.3
uses: actions/upload-artifact@v4.4.0
with:
name: supervisor-bundle-stats
path: build/stats/*.json

View File

@@ -23,7 +23,7 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@v4.2.1
uses: actions/checkout@v4.2.0
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.2.1
uses: actions/checkout@v4.2.0
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.2.1
uses: actions/checkout@v4.2.0
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.2.1
uses: actions/checkout@v4.2.0
- name: Setup Node
uses: actions/setup-node@v4.0.4

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.2.1
uses: actions/checkout@v4.2.0
- name: Setup Node
uses: actions/setup-node@v4.0.4

View File

@@ -20,7 +20,7 @@ jobs:
contents: write
steps:
- name: Checkout the repository
uses: actions/checkout@v4.2.1
uses: actions/checkout@v4.2.0
- 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.4.3
uses: actions/upload-artifact@v4.4.0
with:
name: wheels
path: dist/home_assistant_frontend*.whl
if-no-files-found: error
- name: Upload translations
uses: actions/upload-artifact@v4.4.3
uses: actions/upload-artifact@v4.4.0
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.2.1
uses: actions/checkout@v4.2.0
- 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.2.1
uses: actions/checkout@v4.2.0
- name: Upload Translations
run: |

View File

@@ -28,21 +28,21 @@
"@babel/runtime": "7.25.7",
"@braintree/sanitize-url": "7.1.0",
"@codemirror/autocomplete": "6.18.1",
"@codemirror/commands": "6.7.0",
"@codemirror/commands": "6.6.2",
"@codemirror/language": "6.10.3",
"@codemirror/legacy-modes": "6.4.1",
"@codemirror/search": "6.5.6",
"@codemirror/state": "6.4.1",
"@codemirror/view": "6.34.1",
"@egjs/hammerjs": "2.0.17",
"@formatjs/intl-datetimeformat": "6.13.0",
"@formatjs/intl-displaynames": "6.6.9",
"@formatjs/intl-datetimeformat": "6.12.5",
"@formatjs/intl-displaynames": "6.6.8",
"@formatjs/intl-getcanonicallocales": "2.3.0",
"@formatjs/intl-listformat": "7.5.8",
"@formatjs/intl-locale": "4.0.1",
"@formatjs/intl-numberformat": "8.11.0",
"@formatjs/intl-pluralrules": "5.2.15",
"@formatjs/intl-relativetimeformat": "11.2.15",
"@formatjs/intl-listformat": "7.5.7",
"@formatjs/intl-locale": "4.0.0",
"@formatjs/intl-numberformat": "8.10.3",
"@formatjs/intl-pluralrules": "5.2.14",
"@formatjs/intl-relativetimeformat": "11.2.14",
"@fullcalendar/core": "6.1.15",
"@fullcalendar/daygrid": "6.1.15",
"@fullcalendar/interaction": "6.1.15",
@@ -89,8 +89,8 @@
"@polymer/polymer": "3.5.1",
"@replit/codemirror-indentation-markers": "6.5.3",
"@thomasloven/round-slider": "0.6.0",
"@vaadin/combo-box": "24.4.11",
"@vaadin/vaadin-themable-mixin": "24.4.11",
"@vaadin/combo-box": "24.4.10",
"@vaadin/vaadin-themable-mixin": "24.4.10",
"@vibrant/color": "3.2.1-alpha.1",
"@vibrant/core": "3.2.1-alpha.1",
"@vibrant/quantizer-mmcq": "3.2.1-alpha.1",
@@ -114,7 +114,7 @@
"hls.js": "patch:hls.js@npm%3A1.5.7#~/.yarn/patches/hls.js-npm-1.5.7-f5bbd3d060.patch",
"home-assistant-js-websocket": "9.4.0",
"idb-keyval": "6.2.1",
"intl-messageformat": "10.6.0",
"intl-messageformat": "10.5.14",
"js-yaml": "4.1.0",
"leaflet": "1.9.4",
"leaflet-draw": "patch:leaflet-draw@npm%3A1.0.4#./.yarn/patches/leaflet-draw-npm-1.0.4-0ca0ebcf65.patch",
@@ -151,11 +151,11 @@
"xss": "1.0.15"
},
"devDependencies": {
"@babel/core": "7.25.8",
"@babel/core": "7.25.7",
"@babel/helper-define-polyfill-provider": "0.6.2",
"@babel/plugin-proposal-decorators": "7.25.7",
"@babel/plugin-transform-runtime": "7.25.7",
"@babel/preset-env": "7.25.8",
"@babel/preset-env": "7.25.7",
"@babel/preset-typescript": "7.25.7",
"@bundle-stats/plugin-webpack-filter": "4.15.1",
"@koa/cors": "5.0.0",
@@ -195,7 +195,7 @@
"babel-plugin-template-html-minifier": "4.1.0",
"browserslist-useragent-regexp": "4.1.3",
"chai": "5.1.1",
"del": "8.0.0",
"del": "7.1.0",
"eslint": "8.57.1",
"eslint-config-airbnb-base": "15.0.0",
"eslint-config-airbnb-typescript": "18.0.0",
@@ -205,7 +205,7 @@
"eslint-plugin-lit": "1.15.0",
"eslint-plugin-lit-a11y": "4.1.4",
"eslint-plugin-unused-imports": "4.1.4",
"eslint-plugin-wc": "2.2.0",
"eslint-plugin-wc": "2.1.1",
"fancy-log": "2.0.0",
"fs-extra": "11.2.0",
"glob": "11.0.0",
@@ -222,7 +222,7 @@
"lit-analyzer": "2.0.3",
"lodash.merge": "4.6.2",
"lodash.template": "4.5.0",
"magic-string": "0.30.12",
"magic-string": "0.30.11",
"map-stream": "0.0.7",
"mocha": "10.5.0",
"object-hash": "3.0.0",
@@ -240,7 +240,7 @@
"terser-webpack-plugin": "5.3.10",
"transform-async-modules-webpack-plugin": "1.1.1",
"ts-lit-plugin": "2.0.2",
"typescript": "5.6.3",
"typescript": "5.6.2",
"webpack": "5.95.0",
"webpack-cli": "5.1.4",
"webpack-dev-server": "5.1.0",

View File

@@ -1,10 +1,10 @@
[build-system]
requires = ["setuptools~=75.1"]
requires = ["setuptools~=68.0", "wheel~=0.40.0"]
build-backend = "setuptools.build_meta"
[project]
name = "home-assistant-frontend"
version = "20241010.0"
version = "20241002.2"
license = {text = "Apache-2.0"}
description = "The Home Assistant frontend"
readme = "README.md"

View File

@@ -18,9 +18,5 @@ if [[ -n "$DEVCONTAINER" ]]; then
fi
fi
if ! command -v yarn &> /dev/null; then
echo "Error: yarn not found. Please install it following the official instructions: https://yarnpkg.com/getting-started/install" >&2
exit 1
fi
# Install node modules
yarn install
yarn install

View File

@@ -20,15 +20,6 @@ function findNestedItem(
}, obj);
}
function updateNestedItem(obj: any, path: ItemPath): any {
const lastKey = path.pop()!;
const parent = findNestedItem(obj, path);
parent[lastKey] = Array.isArray(parent[lastKey])
? [...parent[lastKey]]
: [parent[lastKey]];
return obj;
}
export function nestedArrayMove<A>(
obj: A,
oldIndex: number,
@@ -36,18 +27,14 @@ export function nestedArrayMove<A>(
oldPath?: ItemPath,
newPath?: ItemPath
): A {
let newObj = (Array.isArray(obj) ? [...obj] : { ...obj }) as A;
if (oldPath) {
newObj = updateNestedItem(newObj, [...oldPath]);
}
if (newPath) {
newObj = updateNestedItem(newObj, [...newPath]);
}
const newObj = (Array.isArray(obj) ? [...obj] : { ...obj }) as A;
const from = oldPath ? findNestedItem(newObj, oldPath) : newObj;
const to = newPath ? findNestedItem(newObj, newPath, true) : newObj;
if (!Array.isArray(from) || !Array.isArray(to)) {
return obj;
}
const item = from.splice(oldIndex, 1)[0];
to.splice(newIndex, 0, item);

View File

@@ -3,6 +3,7 @@ import "@material/mwc-list/mwc-list-item";
import { css, CSSResultGroup, html, LitElement, nothing } from "lit";
import { property, state } from "lit/decorators";
import { fireEvent } from "../../common/dom/fire_event";
import { stopPropagation } from "../../common/dom/stop_propagation";
import { fullEntitiesContext } from "../../data/context";
import {
DeviceAutomation,
@@ -103,6 +104,7 @@ export abstract class HaDeviceAutomationPicker<
.label=${this.label}
.value=${value}
@selected=${this._automationChanged}
@closed=${stopPropagation}
.disabled=${this._automations.length === 0}
>
${value === NO_AUTOMATION_KEY

View File

@@ -174,6 +174,7 @@ export class HaServiceControl extends LitElement {
if (this._value && serviceData) {
const loadDefaults = this.value && !("data" in this.value);
// Set mandatory bools without a default value to false
this._value = { ...this._value };
if (!this._value.data) {
this._value.data = {};
}
@@ -499,23 +500,8 @@ export class HaServiceControl extends LitElement {
.defaultValue=${this._value?.data}
@value-changed=${this._dataChanged}
></ha-yaml-editor>`
: serviceData?.fields.map((dataField) => {
if (!dataField.fields) {
return this._renderField(
dataField,
hasOptional,
domain,
serviceName,
targetEntities
);
}
const fields = Object.entries(dataField.fields).map(
([key, field]) => ({ key, ...field })
);
return fields.length &&
this._hasFilteredFields(fields, targetEntities)
: serviceData?.fields.map((dataField) =>
dataField.fields
? html`<ha-expansion-panel
leftChevron
.expanded=${!dataField.collapsed}
@@ -546,8 +532,14 @@ export class HaServiceControl extends LitElement {
)
)}
</ha-expansion-panel>`
: nothing;
})} `;
: this._renderField(
dataField,
hasOptional,
domain,
serviceName,
targetEntities
)
)} `;
}
private _getSectionDescription(
@@ -560,16 +552,6 @@ export class HaServiceControl extends LitElement {
);
}
private _hasFilteredFields(
dataFields: ExtHassService["fields"],
targetEntities: string[]
) {
return dataFields.some(
(dataField) =>
!dataField.filter || this._filterField(dataField.filter, targetEntities)
);
}
private _renderField = (
dataField: ExtHassService["fields"][number],
hasOptional: boolean,

View File

@@ -167,7 +167,7 @@ export interface TagTrigger extends BaseTrigger {
export interface TimeTrigger extends BaseTrigger {
trigger: "time";
at: string | { entity_id: string; offset?: string };
at: string;
}
export interface TemplateTrigger extends BaseTrigger {

View File

@@ -8,7 +8,6 @@ import {
import secondsToDuration from "../common/datetime/seconds_to_duration";
import { computeAttributeNameDisplay } from "../common/entity/compute_attribute_display";
import { computeStateName } from "../common/entity/compute_state_name";
import { isValidEntityId } from "../common/entity/valid_entity_id";
import type { HomeAssistant } from "../types";
import { Condition, ForDict, Trigger } from "./automation";
import {
@@ -372,22 +371,13 @@ const tryDescribeTrigger = (
// Time Trigger
if (trigger.trigger === "time" && trigger.at) {
const result = ensureArray(trigger.at).map((at) => {
if (typeof at === "string") {
if (isValidEntityId(at)) {
return `entity ${hass.states[at] ? computeStateName(hass.states[at]) : at}`;
}
return localizeTimeString(at, hass.locale, hass.config);
}
const entityStr = `entity ${hass.states[at.entity_id] ? computeStateName(hass.states[at.entity_id]) : at.entity_id}`;
const offsetStr = at.offset
? " " +
hass.localize(`${triggerTranslationBaseKey}.time.offset_by`, {
offset: describeDuration(hass.locale, at.offset),
})
: "";
return `${entityStr}${offsetStr}`;
});
const result = ensureArray(trigger.at).map((at) =>
typeof at !== "string"
? at
: at.includes(".")
? `entity ${hass.states[at] ? computeStateName(hass.states[at]) : at}`
: localizeTimeString(at, hass.locale, hass.config)
);
return hass.localize(`${triggerTranslationBaseKey}.time.description.full`, {
time: formatListWithOrs(hass.locale, result),

View File

@@ -1,4 +1,5 @@
import type { HassServiceTarget } from "home-assistant-js-websocket";
import { Action } from "../../script";
export interface ToggleActionConfig extends BaseActionConfig {
action: "toggle";
@@ -31,6 +32,11 @@ export interface MoreInfoActionConfig extends BaseActionConfig {
entity_id?: string;
}
export interface SequenceActionConfig extends BaseActionConfig {
action: "sequence";
actions?: Action[];
}
export interface AssistActionConfig extends BaseActionConfig {
action: "assist";
pipeline_id?: string;
@@ -67,4 +73,5 @@ export type ActionConfig =
| MoreInfoActionConfig
| AssistActionConfig
| NoActionConfig
| CustomActionConfig;
| CustomActionConfig
| SequenceActionConfig;

View File

@@ -17,10 +17,6 @@ export interface LovelaceSectionConfig extends LovelaceBaseSectionConfig {
cards?: LovelaceCardConfig[];
}
export interface LovelaceGridSectionConfig extends LovelaceSectionConfig {
grid_base?: number;
}
export interface LovelaceStrategySectionConfig
extends LovelaceBaseSectionConfig {
strategy: LovelaceStrategyConfig;

View File

@@ -51,7 +51,6 @@ export class HuiPersistentNotificationItem extends LitElement {
static get styles(): CSSResultGroup {
return css`
.time {
position: relative;
display: flex;
justify-content: flex-end;
margin-top: 6px;

View File

@@ -208,7 +208,6 @@ class DialogAddAutomationElement extends LitElement implements HassDialog {
const options: IFuseOptions<ListItem> = {
keys: ["key", "name", "description"],
isCaseSensitive: false,
ignoreLocation: true,
minMatchCharLength: Math.min(filter.length, 2),
threshold: 0.2,
getFn: getStripDiacriticsFn,

View File

@@ -9,9 +9,6 @@ import type { TimeTrigger } from "../../../../../data/automation";
import type { HomeAssistant } from "../../../../../types";
import type { TriggerElement } from "../ha-automation-trigger-row";
const MODE_TIME = "time";
const MODE_ENTITY = "entity";
@customElement("ha-automation-trigger-time")
export class HaTimeTrigger extends LitElement implements TriggerElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@@ -20,60 +17,48 @@ export class HaTimeTrigger extends LitElement implements TriggerElement {
@property({ type: Boolean }) public disabled = false;
@state() private _inputMode:
| undefined
| typeof MODE_TIME
| typeof MODE_ENTITY;
@state() private _inputMode?: boolean;
public static get defaultConfig(): TimeTrigger {
return { trigger: "time", at: "" };
}
private _schema = memoizeOne(
(
localize: LocalizeFunc,
inputMode: typeof MODE_TIME | typeof MODE_ENTITY,
showOffset: boolean
) =>
[
(localize: LocalizeFunc, inputMode?: boolean) => {
const atSelector = inputMode
? {
entity: {
filter: [
{ domain: "input_datetime" },
{ domain: "sensor", device_class: "timestamp" },
],
},
}
: { time: {} };
return [
{
name: "mode",
type: "select",
required: true,
options: [
[
MODE_TIME,
"value",
localize(
"ui.panel.config.automation.editor.triggers.type.time.type_value"
),
],
[
MODE_ENTITY,
"input",
localize(
"ui.panel.config.automation.editor.triggers.type.time.type_input"
),
],
],
},
...(inputMode === MODE_TIME
? ([{ name: "time", selector: { time: {} } }] as const)
: ([
{
name: "entity",
selector: {
entity: {
filter: [
{ domain: "input_datetime" },
{ domain: "sensor", device_class: "timestamp" },
],
},
},
},
] as const)),
...(showOffset
? ([{ name: "offset", selector: { text: {} } }] as const)
: ([] as const)),
] as const
{ name: "at", selector: atSelector },
] as const;
}
);
public willUpdate(changedProperties: PropertyValues) {
@@ -90,46 +75,23 @@ export class HaTimeTrigger extends LitElement implements TriggerElement {
}
}
private _data = memoizeOne(
(
inputMode: undefined | typeof MODE_ENTITY | typeof MODE_TIME,
at:
| string
| { entity_id: string | undefined; offset?: string | undefined }
): {
mode: typeof MODE_TIME | typeof MODE_ENTITY;
entity: string | undefined;
time: string | undefined;
offset: string | undefined;
} => {
const entity =
typeof at === "object"
? at.entity_id
: at?.startsWith("input_datetime.") || at?.startsWith("sensor.")
? at
: undefined;
const time = entity ? undefined : (at as string | undefined);
const offset = typeof at === "object" ? at.offset : undefined;
const mode = inputMode ?? (entity ? MODE_ENTITY : MODE_TIME);
return {
mode,
entity,
time,
offset,
};
}
);
protected render() {
const at = this.trigger.at;
if (Array.isArray(at)) {
return nothing;
}
const data = this._data(this._inputMode, at);
const showOffset =
data.mode === MODE_ENTITY && data.entity?.startsWith("sensor.");
const schema = this._schema(this.hass.localize, data.mode, !!showOffset);
const inputMode =
this._inputMode ??
(at?.startsWith("input_datetime.") || at?.startsWith("sensor."));
const schema = this._schema(this.hass.localize, inputMode);
const data = {
mode: inputMode ? "input" : "value",
...this.trigger,
};
return html`
<ha-form
@@ -145,43 +107,26 @@ export class HaTimeTrigger extends LitElement implements TriggerElement {
private _valueChanged(ev: CustomEvent): void {
ev.stopPropagation();
const newValue = { ...ev.detail.value };
this._inputMode = newValue.mode;
if (newValue.mode === MODE_TIME) {
delete newValue.entity;
delete newValue.offset;
} else {
delete newValue.time;
if (!newValue.entity?.startsWith("sensor.")) {
delete newValue.offset;
}
}
fireEvent(this, "value-changed", {
value: {
...this.trigger,
at: newValue.offset
? {
entity_id: newValue.entity,
offset: newValue.offset,
}
: newValue.entity || newValue.time,
},
});
const newValue = ev.detail.value;
this._inputMode = newValue.mode === "input";
delete newValue.mode;
Object.keys(newValue).forEach((key) =>
newValue[key] === undefined || newValue[key] === ""
? delete newValue[key]
: {}
);
fireEvent(this, "value-changed", { value: newValue });
}
private _computeLabelCallback = (
schema: SchemaUnion<ReturnType<typeof this._schema>>
): string => {
switch (schema.name) {
case "time":
return this.hass.localize(
`ui.panel.config.automation.editor.triggers.type.time.at`
);
}
return this.hass.localize(
): string =>
this.hass.localize(
`ui.panel.config.automation.editor.triggers.type.time.${schema.name}`
);
};
}
declare global {

View File

@@ -83,15 +83,9 @@ export const getZHADeviceActions = async (
classes: "warning",
action: async () => {
const confirmed = await showConfirmationDialog(el, {
title: hass.localize(
"ui.dialogs.zha_device_info.confirmations.remove_title"
),
text: hass.localize(
"ui.dialogs.zha_device_info.confirmations.remove_text"
"ui.dialogs.zha_device_info.confirmations.remove"
),
confirmText: hass.localize("ui.common.remove"),
dismissText: hass.localize("ui.common.cancel"),
destructive: true,
});
if (!confirmed) {

View File

@@ -482,9 +482,7 @@ export class ThreadConfigPanel extends SubscribeMixin(LitElement) {
const network = (ev.currentTarget as any).network as ThreadNetwork;
const router = (ev.currentTarget as any).router as ThreadRouter;
const otbr = (ev.currentTarget as any).otbr as OTBRInfo;
const index = network.dataset
? Number(ev.detail.index)
: Number(ev.detail.index) + 1;
const index = Number(ev.detail.index);
switch (index) {
case 0:
this._setPreferredBorderAgent(network.dataset!, router);

View File

@@ -83,6 +83,8 @@ export class HaScriptEditor extends KeyboardShortcutMixin(LitElement) {
@state() private _config?: ScriptConfig;
@state() private _idError = false;
@state() private _dirty = false;
@state() private _errors?: string;
@@ -412,18 +414,6 @@ export class HaScriptEditor extends KeyboardShortcutMixin(LitElement) {
this._loadConfig();
}
if (
(changedProps.has("scriptId") || changedProps.has("entityRegistry")) &&
this.scriptId &&
this.entityRegistry
) {
// find entity for when script entity id changed
const entity = this.entityRegistry.find(
(ent) => ent.platform === "script" && ent.unique_id === this.scriptId
);
this._entityId = entity?.entity_id;
}
if (changedProps.has("scriptId") && !this.scriptId && this.hass) {
const initData = getScriptEditorInitData();
this._dirty = !!initData;
@@ -458,6 +448,15 @@ export class HaScriptEditor extends KeyboardShortcutMixin(LitElement) {
}
}
private _setEntityId(id?: string) {
this._entityId = id;
if (this.hass.states[`script.${this._entityId}`]) {
this._idError = true;
} else {
this._idError = false;
}
}
private async _checkValidation() {
this._validationErrors = undefined;
if (!this._entityId || !this._config) {
@@ -767,12 +766,28 @@ export class HaScriptEditor extends KeyboardShortcutMixin(LitElement) {
}
private async _saveScript(): Promise<void> {
if (this._idError) {
showToast(this, {
message: this.hass.localize(
"ui.panel.config.script.editor.id_already_exists_save_error"
),
dismissable: false,
duration: -1,
action: {
action: () => {},
text: this.hass.localize("ui.dialogs.generic.ok"),
},
});
return;
}
if (!this.scriptId) {
const saved = await this._promptScriptAlias();
if (!saved) {
return;
}
this._entityId = this._computeEntityIdFromAlias(this._config!.alias);
const entityId = this._computeEntityIdFromAlias(this._config!.alias);
this._setEntityId(entityId);
}
const id = this.scriptId || this._entityId || Date.now();

View File

@@ -68,16 +68,6 @@ class HaPanelDevAction extends LitElement {
@query("#yaml-editor") private _yamlEditor?: HaYamlEditor;
protected willUpdate() {
if (
!this.hasUpdated &&
this._serviceData?.action &&
typeof this._serviceData.action !== "string"
) {
this._serviceData.action = "";
}
}
protected firstUpdated(params) {
super.firstUpdated(params);
this.hass.loadBackendTranslation("services");

View File

@@ -187,6 +187,7 @@ export class HuiHeadingCard extends LitElement implements LovelaceCard {
}
.content p {
margin: 0;
font-family: Roboto;
font-style: normal;
white-space: nowrap;
overflow: hidden;

View File

@@ -275,7 +275,7 @@ export class HuiTodoListCard extends LitElement implements LovelaceCard {
"ui.panel.lovelace.cards.todo-list.no_unchecked_items"
)}
</p>`}
${!this._config.hide_completed && checkedItems.length
${checkedItems.length
? html`
<div role="separator">
<div class="divider"></div>

View File

@@ -453,7 +453,6 @@ export interface TodoListCardConfig extends LovelaceCardConfig {
title?: string;
theme?: string;
entity?: string;
hide_completed?: boolean;
}
export interface StackCardConfig extends LovelaceCardConfig {

View File

@@ -3,6 +3,7 @@ import { navigate } from "../../../common/navigate";
import { forwardHaptic } from "../../../data/haptics";
import { domainToName } from "../../../data/integration";
import { ActionConfig } from "../../../data/lovelace/config/action";
import { callExecuteScript } from "../../../data/service";
import { showConfirmationDialog } from "../../../dialogs/generic/show-dialog-box";
import { showVoiceCommandDialog } from "../../../dialogs/voice-command-dialog/show-ha-voice-command-dialog";
import { HomeAssistant } from "../../../types";
@@ -177,6 +178,13 @@ export const handleAction = async (
});
break;
}
case "sequence": {
if (!actionConfig.actions) {
return;
}
callExecuteScript(hass, actionConfig.actions);
break;
}
case "fire-dom-event": {
fireEvent(node, "ll-custom", actionConfig);
}

View File

@@ -1,3 +1,5 @@
import { ContextProvider } from "@lit-labs/context";
import { UnsubscribeFunc } from "home-assistant-js-websocket";
import {
css,
CSSResultGroup,
@@ -14,7 +16,10 @@ import "../../../components/ha-assist-pipeline-picker";
import { HaFormSchema, SchemaUnion } from "../../../components/ha-form/types";
import "../../../components/ha-help-tooltip";
import "../../../components/ha-navigation-picker";
import { HaSelect } from "../../../components/ha-select";
import "../../../components/ha-service-control";
import { fullEntitiesContext } from "../../../data/context";
import { subscribeEntityRegistry } from "../../../data/entity_registry";
import {
ActionConfig,
CallServiceActionConfig,
@@ -22,9 +27,9 @@ import {
UrlActionConfig,
} from "../../../data/lovelace/config/action";
import { ServiceAction } from "../../../data/script";
import { SubscribeMixin } from "../../../mixins/subscribe-mixin";
import { HomeAssistant } from "../../../types";
import { EditorTarget } from "../editor/types";
import { HaSelect } from "../../../components/ha-select";
export type UiAction = Exclude<ActionConfig["action"], "fire-dom-event">;
@@ -34,6 +39,7 @@ const DEFAULT_ACTIONS: UiAction[] = [
"navigate",
"url",
"perform-action",
"sequence",
"assist",
"none",
];
@@ -70,8 +76,17 @@ const ASSIST_SCHEMA = [
},
] as const satisfies readonly HaFormSchema[];
const SEQUENCE_SCHEMA = [
{
name: "actions",
selector: {
action: {},
},
},
] as const satisfies readonly HaFormSchema[];
@customElement("hui-action-editor")
export class HuiActionEditor extends LitElement {
export class HuiActionEditor extends SubscribeMixin(LitElement) {
@property({ attribute: false }) public config?: ActionConfig;
@property() public label?: string;
@@ -86,6 +101,19 @@ export class HuiActionEditor extends LitElement {
@query("ha-select") private _select!: HaSelect;
private _entitiesContext = new ContextProvider(this, {
context: fullEntitiesContext,
initialValue: [],
});
public hassSubscribe(): UnsubscribeFunc[] {
return [
subscribeEntityRegistry(this.hass!.connection!, (entities) => {
this._entitiesContext.setValue(entities);
}),
];
}
get _navigation_path(): string {
const config = this.config as NavigateActionConfig | undefined;
return config?.navigation_path || "";
@@ -120,6 +148,11 @@ export class HuiActionEditor extends LitElement {
}
}
protected firstUpdated(_changedProperties: PropertyValues): void {
this.hass!.loadFragmentTranslation("config");
this.hass!.loadBackendTranslation("device_automation");
}
protected render() {
if (!this.hass) {
return nothing;
@@ -218,6 +251,17 @@ export class HuiActionEditor extends LitElement {
</ha-form>
`
: nothing}
${this.config?.action === "sequence"
? html`
<ha-form
.hass=${this.hass}
.schema=${SEQUENCE_SCHEMA}
.data=${this.config}
.computeLabel=${this._computeFormLabel}
@value-changed=${this._formValueChanged}
></ha-form>
`
: nothing}
`;
}
@@ -289,7 +333,15 @@ export class HuiActionEditor extends LitElement {
});
}
private _computeFormLabel(schema: SchemaUnion<typeof ASSIST_SCHEMA>) {
private _computeFormLabel(
schema:
| SchemaUnion<typeof ASSIST_SCHEMA>
| SchemaUnion<typeof NAVIGATE_SCHEMA>
| SchemaUnion<typeof SEQUENCE_SCHEMA>
) {
if (schema.name === "actions") {
return "";
}
return this.hass?.localize(
`ui.panel.lovelace.editor.action-editor.${schema.name}`
);

View File

@@ -3,15 +3,12 @@ import "@material/mwc-tab/mwc-tab";
import { CSSResultGroup, TemplateResult, css, html, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { LovelaceCardConfig } from "../../../../data/lovelace/config/card";
import {
LovelaceGridSectionConfig,
LovelaceSectionConfig,
} from "../../../../data/lovelace/config/section";
import { getCardElementClass } from "../../create-element/create-card-element";
import type { LovelaceCardEditor, LovelaceConfigForm } from "../../types";
import { HuiTypedElementEditor } from "../hui-typed-element-editor";
import "./hui-card-layout-editor";
import "./hui-card-visibility-editor";
import { LovelaceSectionConfig } from "../../../../data/lovelace/config/section";
const tabs = ["config", "visibility", "layout"] as const;
@@ -62,7 +59,7 @@ export class HuiCardElementEditor extends HuiTypedElementEditor<LovelaceCardConf
protected renderConfigElement(): TemplateResult {
const displayedTabs: string[] = ["config"];
if (this.showVisibilityTab) displayedTabs.push("visibility");
if (this.sectionConfig?.type === "grid") displayedTabs.push("layout");
if (this._showLayoutTab) displayedTabs.push("layout");
if (displayedTabs.length === 1) return super.renderConfigElement();
@@ -86,8 +83,8 @@ export class HuiCardElementEditor extends HuiTypedElementEditor<LovelaceCardConf
<hui-card-layout-editor
.hass=${this.hass}
.config=${this.value}
.sectionConfig=${this.sectionConfig!}
@value-changed=${this._configChanged}
.sectionConfig=${this.sectionConfig as LovelaceGridSectionConfig}
>
</hui-card-layout-editor>
`;

View File

@@ -19,10 +19,7 @@ import "../../../../components/ha-switch";
import "../../../../components/ha-yaml-editor";
import type { HaYamlEditor } from "../../../../components/ha-yaml-editor";
import { LovelaceCardConfig } from "../../../../data/lovelace/config/card";
import {
LovelaceGridSectionConfig,
LovelaceSectionConfig,
} from "../../../../data/lovelace/config/section";
import { LovelaceSectionConfig } from "../../../../data/lovelace/config/section";
import { haStyle } from "../../../../resources/styles";
import { HomeAssistant } from "../../../../types";
import { HuiCard } from "../../cards/hui-card";
@@ -30,7 +27,6 @@ import {
CardGridSize,
computeCardGridSize,
} from "../../common/compute-card-grid-size";
import { DEFAULT_GRID_BASE } from "../../sections/hui-grid-section";
import { LovelaceLayoutOptions } from "../../types";
@customElement("hui-card-layout-editor")
@@ -76,10 +72,7 @@ export class HuiCardLayoutEditor extends LitElement {
const value = this._computeCardGridSize(options);
const totalColumns =
(this.sectionConfig.column_span ?? 1) *
((this.sectionConfig as LovelaceGridSectionConfig).grid_base ||
DEFAULT_GRID_BASE);
const totalColumns = (this.sectionConfig.column_span ?? 1) * 4;
return html`
<div class="header">

View File

@@ -1,6 +1,6 @@
import { CSSResultGroup, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { assert, assign, boolean, object, optional, string } from "superstruct";
import { assert, assign, object, optional, string } from "superstruct";
import { isComponentLoaded } from "../../../../common/config/is_component_loaded";
import { fireEvent } from "../../../../common/dom/fire_event";
import "../../../../components/ha-alert";
@@ -18,7 +18,6 @@ const cardConfigStruct = assign(
title: optional(string()),
theme: optional(string()),
entity: optional(string()),
hide_completed: optional(boolean()),
})
);
@@ -31,7 +30,6 @@ const SCHEMA = [
},
},
{ name: "theme", selector: { theme: {} } },
{ name: "hide_completed", selector: { boolean: {} } },
] as const;
@customElement("hui-todo-list-card-editor")
@@ -89,10 +87,6 @@ export class HuiTodoListEditor
)} (${this.hass!.localize(
"ui.panel.lovelace.editor.card.config.optional"
)})`;
case "hide_completed":
return this.hass!.localize(
"ui.panel.lovelace.editor.card.todo-list.hide_completed"
);
default:
return this.hass!.localize(
`ui.panel.lovelace.editor.card.generic.${schema.name}`

View File

@@ -1,4 +1,4 @@
import { html, LitElement } from "lit";
import { LitElement, html } from "lit";
import { customElement, property } from "lit/decorators";
import memoizeOne from "memoize-one";
import { fireEvent } from "../../../../common/dom/fire_event";
@@ -6,21 +6,12 @@ import {
HaFormSchema,
SchemaUnion,
} from "../../../../components/ha-form/types";
import {
isStrategySection,
LovelaceGridSectionConfig,
LovelaceSectionRawConfig,
} from "../../../../data/lovelace/config/section";
import { LovelaceSectionRawConfig } from "../../../../data/lovelace/config/section";
import { LovelaceViewConfig } from "../../../../data/lovelace/config/view";
import { HomeAssistant } from "../../../../types";
import { LocalizeFunc } from "../../../../common/translations/localize";
import { DEFAULT_GRID_BASE } from "../../sections/hui-grid-section";
type GridDensity = "default" | "dense" | "custom";
type SettingsData = {
column_span?: number;
grid_density?: GridDensity;
};
@customElement("hui-section-settings-editor")
@@ -32,89 +23,27 @@ export class HuiDialogEditSection extends LitElement {
@property({ attribute: false }) public viewConfig!: LovelaceViewConfig;
private _schema = memoizeOne(
(
maxColumns: number,
localize: LocalizeFunc,
type?: string | undefined,
columnDensity?: GridDensity,
columnBase?: number
) =>
(maxColumns: number) =>
[
{
name: "title",
selector: { text: {} },
name: "column_span",
selector: {
number: {
min: 1,
max: maxColumns,
slider_ticks: true,
},
},
},
...(type === "grid"
? ([
{
name: "grid_density",
default: "default",
selector: {
select: {
mode: "list",
options: [
{
label: localize(
`ui.panel.lovelace.editor.edit_section.settings.grid_density_options.default`,
{ count: 4 }
),
value: "default",
},
{
label: localize(
`ui.panel.lovelace.editor.edit_section.settings.grid_density_options.dense`,
{ count: 6 }
),
value: "dense",
},
...(columnDensity === "custom" && columnBase
? [
{
label: localize(
`ui.panel.lovelace.editor.edit_section.settings.grid_density_options.custom`,
{ count: columnBase }
),
value: "custom",
},
]
: []),
],
},
},
},
] as const satisfies readonly HaFormSchema[])
: []),
] as const satisfies HaFormSchema[]
);
private _isGridSectionConfig(
config: LovelaceSectionRawConfig
): config is LovelaceGridSectionConfig {
return !isStrategySection(config) && config.type === "grid";
}
render() {
const gridBase = this._isGridSectionConfig(this.config)
? this.config.grid_base || DEFAULT_GRID_BASE
: undefined;
const columnDensity =
gridBase === 6 ? "dense" : gridBase === 4 ? "default" : "custom";
const data: SettingsData = {
column_span: this.config.column_span || 1,
grid_density: columnDensity,
};
const type = "type" in this.config ? this.config.type : undefined;
const schema = this._schema(
this.viewConfig.max_columns || 4,
this.hass.localize,
type,
columnDensity,
gridBase
);
const schema = this._schema(this.viewConfig.max_columns || 4);
return html`
<ha-form
@@ -146,26 +75,11 @@ export class HuiDialogEditSection extends LitElement {
ev.stopPropagation();
const newData = ev.detail.value as SettingsData;
const { column_span, grid_density } = newData;
const newConfig: LovelaceSectionRawConfig = {
...this.config,
column_span: column_span,
column_span: newData.column_span,
};
if (this._isGridSectionConfig(newConfig)) {
const gridBase =
grid_density === "default"
? 4
: grid_density === "dense"
? 6
: undefined;
if (gridBase) {
(newConfig as LovelaceGridSectionConfig).grid_base = gridBase;
}
}
fireEvent(this, "value-changed", { value: newConfig });
}
}

View File

@@ -48,6 +48,12 @@ const actionConfigStructService = object({
confirmation: optional(actionConfigStructConfirmation),
});
const actionConfigStructSequence = object({
action: literal("sequence"),
actions: optional(array(object())),
confirmation: optional(actionConfigStructConfirmation),
});
const actionConfigStructNavigate = object({
action: literal("navigate"),
navigation_path: string(),
@@ -101,6 +107,9 @@ export const actionConfigStruct = dynamic<any>((value) => {
case "more-info": {
return actionConfigStructMoreInfo;
}
case "sequence": {
return actionConfigStructSequence;
}
}
}

View File

@@ -8,7 +8,7 @@ import { fireEvent } from "../../../common/dom/fire_event";
import type { HaSortableOptions } from "../../../components/ha-sortable";
import { LovelaceSectionElement } from "../../../data/lovelace";
import { LovelaceCardConfig } from "../../../data/lovelace/config/card";
import type { LovelaceGridSectionConfig } from "../../../data/lovelace/config/section";
import type { LovelaceSectionConfig } from "../../../data/lovelace/config/section";
import { haStyle } from "../../../resources/styles";
import type { HomeAssistant } from "../../../types";
import { HuiCard } from "../cards/hui-card";
@@ -24,8 +24,6 @@ const CARD_SORTABLE_OPTIONS: HaSortableOptions = {
invertedSwapThreshold: 0.7,
} as HaSortableOptions;
export const DEFAULT_GRID_BASE = 4;
export class GridSection extends LitElement implements LovelaceSectionElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@@ -39,11 +37,11 @@ export class GridSection extends LitElement implements LovelaceSectionElement {
@property({ attribute: false }) public cards: HuiCard[] = [];
@state() _config?: LovelaceGridSectionConfig;
@state() _config?: LovelaceSectionConfig;
@state() _dragging = false;
public setConfig(config: LovelaceGridSectionConfig): void {
public setConfig(config: LovelaceSectionConfig): void {
this._config = config;
}
@@ -66,8 +64,6 @@ export class GridSection extends LitElement implements LovelaceSectionElement {
const editMode = Boolean(this.lovelace?.editMode && !this.isStrategy);
const columnCount = this._config.grid_base ?? DEFAULT_GRID_BASE;
return html`
<ha-sortable
.disabled=${!editMode}
@@ -81,10 +77,7 @@ export class GridSection extends LitElement implements LovelaceSectionElement {
.options=${CARD_SORTABLE_OPTIONS}
invert-swap
>
<div
class="container ${classMap({ "edit-mode": editMode })}"
style=${styleMap({ "--column-count": columnCount })}
>
<div class="container ${classMap({ "edit-mode": editMode })}">
${repeat(
cardsConfig,
(cardConfig) => this._getKey(cardConfig),
@@ -172,6 +165,7 @@ export class GridSection extends LitElement implements LovelaceSectionElement {
haStyle,
css`
:host {
--base-column-count: 4;
--row-gap: var(--ha-section-grid-row-gap, 8px);
--column-gap: var(--ha-section-grid-column-gap, 8px);
--row-height: var(--ha-section-grid-row-height, 56px);
@@ -181,7 +175,7 @@ export class GridSection extends LitElement implements LovelaceSectionElement {
}
.container {
--grid-column-count: calc(
var(--column-count, 4) * var(--column-span, 1)
var(--base-column-count) * var(--column-span, 1)
);
display: grid;
grid-template-columns: repeat(

View File

@@ -1635,8 +1635,7 @@
"zigbee_information": "View the Zigbee information for the device."
},
"confirmations": {
"remove_title": "Remove device",
"remove_text": "This device will be permanently removed from the Zigbee network."
"remove": "Are you sure that you want to remove the device?"
},
"quirk": "Quirk",
"last_seen": "Last seen",
@@ -2811,7 +2810,7 @@
"migrate": "Migrate",
"duplicate": "[%key:ui::common::duplicate%]",
"take_control": "Take control",
"confirm_take_control": "You are viewing a preview of the automation config, do you want to take control?",
"confirm_take_control": "Your are viewing a preview of the automation config, do you want to take control?",
"run": "[%key:ui::panel::config::automation::editor::actions::run%]",
"rename": "[%key:ui::panel::config::automation::editor::triggers::rename%]",
"show_trace": "Traces",
@@ -3054,9 +3053,6 @@
"type_input": "Value of a date/time helper or timestamp-class sensor",
"label": "Time",
"at": "At time",
"offset": "[%key:ui::panel::config::automation::editor::triggers::type::sun::offset%]",
"entity": "Entity with timestamp",
"offset_by": "offset by {offset}",
"mode": "Mode",
"description": {
"picker": "At a specific time, or on a specific date.",
@@ -3688,13 +3684,16 @@
"editor": {
"alias": "Name",
"icon": "Icon",
"id": "Entity ID",
"id_already_exists_save_error": "You can't save this script because the ID is not unique, pick another ID or leave it blank to automatically generate one.",
"id_already_exists": "This ID already exists",
"introduction": "Use scripts to run a sequence of actions.",
"show_trace": "[%key:ui::panel::config::automation::editor::show_trace%]",
"show_info": "[%key:ui::panel::config::automation::editor::show_info%]",
"rename": "[%key:ui::panel::config::automation::editor::triggers::rename%]",
"change_mode": "[%key:ui::panel::config::automation::editor::change_mode%]",
"take_control": "[%key:ui::panel::config::automation::editor::take_control%]",
"confirm_take_control": "You are viewing a preview of the script config, do you want to take control?",
"confirm_take_control": "Your are viewing a preview of the script config, do you want to take control?",
"read_only": "This script cannot be edited from the UI, because it is not stored in the ''scripts.yaml'' file.",
"unavailable": "Script is unavailable",
"migrate": "Migrate",
@@ -5714,13 +5713,7 @@
"title": "Title",
"title_helper": "The title will appear at the top of section. Leave empty to hide the title.",
"column_span": "Width",
"column_span_helper": "Larger sections will be made smaller to fit the display. (e.g. on mobile devices)",
"grid_density": "Grid density",
"grid_density_options": {
"default": "Default ({count} columns)",
"dense": "Dense ({count} columns)",
"custom": "Custom ({count} {count, plural,\n one {column}\n other {columns}\n})"
}
"column_span_helper": "Larger sections will be made smaller to fit the display. (e.g. on mobile devices)"
},
"visibility": {
"explanation": "The section will be shown when ALL conditions below are fulfilled. If no conditions are set, the section will always be shown."
@@ -5759,10 +5752,12 @@
"more-info": "More info",
"toggle": "Toggle",
"navigate": "Navigate",
"sequence": "Sequence",
"assist": "Assist",
"url": "URL",
"none": "Nothing"
}
},
"sequence_actions": "Actions"
},
"condition-editor": {
"explanation": "The card will be shown when ALL conditions below are fulfilled.",
@@ -6138,8 +6133,7 @@
"todo-list": {
"name": "To-do list",
"description": "The to-do list card allows you to add, edit, check-off, and clear items from your to-do list.",
"integration_not_loaded": "This card requires the `todo` integration to be set up.",
"hide_completed": "Hide completed items"
"integration_not_loaded": "This card requires the `todo` integration to be set up."
},
"thermostat": {
"name": "Thermostat",

815
yarn.lock

File diff suppressed because it is too large Load Diff