Compare commits

..

32 Commits

Author SHA1 Message Date
Paulus Schoutsen
8ec2c38f72 20220902.0 (#13571) 2022-09-02 16:15:38 -04:00
Paulus Schoutsen
72aea57105 Bumped version to 20220902.0 2022-09-02 16:14:53 -04:00
Philip Allgaier
031ecf5be8 Align visuals of automation and script editor after redesign (#13567) 2022-09-02 13:03:53 -04:00
Paul Bottein
93e7927686 Fix automation trace link (#13563) 2022-09-02 13:02:50 -04:00
Paul Bottein
320d8e6190 Improve blueprint editor layout (#13564) 2022-09-02 13:02:36 -04:00
Paul Bottein
efa4f65686 Add information in overflow menu (#13570) 2022-09-02 13:02:01 -04:00
Paul Bottein
ec257710ff Add overflow menu to automation picker (#13569) 2022-09-02 13:01:05 -04:00
Paulus Schoutsen
ffad6f340f Fix some descriptions (#13562) 2022-09-01 21:43:30 -05:00
Paulus Schoutsen
0b637fc9bd Dev -> Master 20220901.0 (#13561) 2022-09-01 16:42:16 -04:00
Zack Barett
9f9b0b6457 Bumped version to 20220901.0 (#13560) 2022-09-01 20:11:10 +00:00
Zack Barett
a4227680de Fix Unable to select last slot (#13559) 2022-09-01 19:54:32 +00:00
Zack Barett
5cfd263617 Fix Sunday issue and add minutes to events (#13556) 2022-09-01 16:06:51 +00:00
Bram Kragten
430e671901 unique-id -> id (#13552) 2022-09-01 11:51:38 -04:00
Ernst Klamer
8fcd396445 Add icon for device class moisture (#13553) 2022-09-01 11:48:04 -04:00
Erik Montnemery
e273b6b659 Improve delete device button and confirmation dialog (#13500) 2022-09-01 10:38:56 -05:00
Bram Kragten
2751adf440 remove duplicate controls blueprint automation (#13554) 2022-09-01 10:37:22 -05:00
Matthias de Baat
d661450121 Typo on dialogs page (#13555) 2022-09-01 10:36:56 -05:00
Steve Repsher
4511ded205 Add semantic heading to script editor (#13546) 2022-09-01 16:19:08 +02:00
Erik Montnemery
2bf0c5d72d Only update device class if changed by user (#13551) 2022-09-01 12:25:32 +00:00
Steve Repsher
604e5d5e09 Add semantic headings to automation editor (#13542)
Co-authored-by: Bram Kragten <mail@bramkragten.nl>
2022-08-31 14:15:14 -05:00
Paulus Schoutsen
5466705d97 20220831.0 (#13541) 2022-08-31 12:45:33 -04:00
Bram Kragten
774aee406c Fix tag trigger (#13540)
* Fix tag trigger

* Fix
2022-08-31 12:28:50 -04:00
Zack Barett
775837b60f Bumped version to 20220831.0 (#13539) 2022-08-31 18:28:31 +02:00
Bram Kragten
030b2b921a Format duration object (#13538) 2022-08-31 16:24:24 +00:00
Bram Kragten
229bc26327 Don't fire change on tag picker when not changed (#13537) 2022-08-31 16:08:35 +00:00
Bram Kragten
99e85173eb Change padding buttons entity settings (#13536) 2022-08-31 15:48:54 +00:00
Bram Kragten
be0c22d7ae Fix header mobile automation editor (#13534) 2022-08-31 15:42:00 +00:00
Zack Barett
fee1092a08 Fix Selection on IOS (#13533) 2022-08-31 15:40:03 +00:00
Bram Kragten
d041bd9fd3 use device and area names in service call description (#13532) 2022-08-31 15:30:44 +00:00
Zack Barett
df4b83349e Merge pull request #13329 from home-assistant/dev 2022-08-02 10:20:21 -05:00
Zack Barett
61b42249ec Merge pull request #13302 from home-assistant/dev
Co-authored-by: Paulus Schoutsen <balloob@gmail.com>
Co-authored-by: Franck Nijhof <git@frenck.dev>
2022-07-28 10:31:47 -05:00
Bram Kragten
40616b6af2 Merge pull request #13286 from home-assistant/dev 2022-07-27 12:41:33 +02:00
25 changed files with 650 additions and 236 deletions

View File

@@ -61,6 +61,7 @@ class HaDemo extends HomeAssistantAppEl {
area_id: null, area_id: null,
disabled_by: null, disabled_by: null,
entity_id: "sensor.co2_intensity", entity_id: "sensor.co2_intensity",
id: "sensor.co2_intensity",
name: null, name: null,
icon: null, icon: null,
platform: "co2signal", platform: "co2signal",
@@ -74,6 +75,7 @@ class HaDemo extends HomeAssistantAppEl {
area_id: null, area_id: null,
disabled_by: null, disabled_by: null,
entity_id: "sensor.grid_fossil_fuel_percentage", entity_id: "sensor.grid_fossil_fuel_percentage",
id: "sensor.co2_intensity",
name: null, name: null,
icon: null, icon: null,
platform: "co2signal", platform: "co2signal",

View File

@@ -3,7 +3,7 @@ title: Dialgos
subtitle: Dialogs provide important prompts in a user flow. subtitle: Dialogs provide important prompts in a user flow.
--- ---
# Material Desing 3 # Material Design 3
Our dialogs are based on the latest version of Material Design. Specs and guidelines can be found on it's [website](https://m3.material.io/components/dialogs/overview). Our dialogs are based on the latest version of Material Design. Specs and guidelines can be found on it's [website](https://m3.material.io/components/dialogs/overview).

View File

@@ -191,6 +191,7 @@ const createEntityRegistryEntries = (
hidden_by: null, hidden_by: null,
entity_category: null, entity_category: null,
entity_id: "binary_sensor.updater", entity_id: "binary_sensor.updater",
id: "binary_sensor.updater",
name: null, name: null,
icon: null, icon: null,
platform: "updater", platform: "updater",

View File

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

View File

@@ -126,6 +126,7 @@ export const FIXED_DEVICE_CLASS_ICONS = {
gas: mdiGasCylinder, gas: mdiGasCylinder,
humidity: mdiWaterPercent, humidity: mdiWaterPercent,
illuminance: mdiBrightness5, illuminance: mdiBrightness5,
moisture: mdiWaterPercent,
monetary: mdiCash, monetary: mdiCash,
nitrogen_dioxide: mdiMolecule, nitrogen_dioxide: mdiMolecule,
nitrogen_monoxide: mdiMolecule, nitrogen_monoxide: mdiMolecule,

View File

@@ -0,0 +1,28 @@
import { HaDurationData } from "../../components/ha-duration-input";
const leftPad = (num: number) => (num < 10 ? `0${num}` : num);
export const formatDuration = (duration: HaDurationData) => {
const d = duration.days || 0;
const h = duration.hours || 0;
const m = duration.minutes || 0;
const s = duration.seconds || 0;
const ms = duration.milliseconds || 0;
if (d > 0) {
return `${d} days ${h}:${leftPad(m)}:${leftPad(s)}`;
}
if (h > 0) {
return `${h}:${leftPad(m)}:${leftPad(s)}`;
}
if (m > 0) {
return `${m}:${leftPad(s)}`;
}
if (s > 0) {
return `${s} seconds`;
}
if (ms > 0) {
return `${ms} milliseconds`;
}
return null;
};

View File

@@ -3,6 +3,8 @@ import { mdiDotsVertical } from "@mdi/js";
import "@polymer/paper-tooltip/paper-tooltip"; import "@polymer/paper-tooltip/paper-tooltip";
import { css, html, LitElement, TemplateResult } from "lit"; import { css, html, LitElement, TemplateResult } from "lit";
import { customElement, property } from "lit/decorators"; import { customElement, property } from "lit/decorators";
import { classMap } from "lit/directives/class-map";
import { haStyle } from "../resources/styles";
import { HomeAssistant } from "../types"; import { HomeAssistant } from "../types";
import "./ha-button-menu"; import "./ha-button-menu";
import "./ha-icon-button"; import "./ha-icon-button";
@@ -16,6 +18,7 @@ export interface IconOverflowMenuItem {
disabled?: boolean; disabled?: boolean;
tooltip?: string; tooltip?: string;
onClick: CallableFunction; onClick: CallableFunction;
warning?: boolean;
} }
@customElement("ha-icon-overflow-menu") @customElement("ha-icon-overflow-menu")
@@ -49,9 +52,13 @@ export class HaIconOverflowMenu extends LitElement {
graphic="icon" graphic="icon"
.disabled=${item.disabled} .disabled=${item.disabled}
@click=${item.action} @click=${item.action}
class=${classMap({ warning: Boolean(item.warning) })}
> >
<div slot="graphic"> <div slot="graphic">
<ha-svg-icon .path=${item.path}></ha-svg-icon> <ha-svg-icon
class=${classMap({ warning: Boolean(item.warning) })}
.path=${item.path}
></ha-svg-icon>
</div> </div>
${item.label} ${item.label}
</mwc-list-item> </mwc-list-item>
@@ -81,7 +88,8 @@ export class HaIconOverflowMenu extends LitElement {
`; `;
} }
protected _handleIconOverflowMenuOpened() { protected _handleIconOverflowMenuOpened(e) {
e.stopPropagation();
// If this component is used inside a data table, the z-index of the row // If this component is used inside a data table, the z-index of the row
// needs to be increased. Otherwise the ha-button-menu would be displayed // needs to be increased. Otherwise the ha-button-menu would be displayed
// underneath the next row in the table. // underneath the next row in the table.
@@ -99,12 +107,15 @@ export class HaIconOverflowMenu extends LitElement {
} }
static get styles() { static get styles() {
return css` return [
:host { haStyle,
display: flex; css`
justify-content: flex-end; :host {
} display: flex;
`; justify-content: flex-end;
}
`,
];
} }
} }

View File

@@ -1,7 +1,14 @@
import secondsToDuration from "../common/datetime/seconds_to_duration"; import secondsToDuration from "../common/datetime/seconds_to_duration";
import { ensureArray } from "../common/ensure-array";
import { computeStateName } from "../common/entity/compute_state_name"; import { computeStateName } from "../common/entity/compute_state_name";
import type { HomeAssistant } from "../types"; import type { HomeAssistant } from "../types";
import { Condition, Trigger } from "./automation"; import { Condition, Trigger } from "./automation";
import {
DeviceCondition,
DeviceTrigger,
localizeDeviceAutomationCondition,
localizeDeviceAutomationTrigger,
} from "./device_automation";
import { formatAttributeName } from "./entity_attributes"; import { formatAttributeName } from "./entity_attributes";
export const describeTrigger = ( export const describeTrigger = (
@@ -68,7 +75,7 @@ export const describeTrigger = (
} }
// State Trigger // State Trigger
if (trigger.platform === "state" && trigger.entity_id) { if (trigger.platform === "state") {
let base = "When"; let base = "When";
let entities = ""; let entities = "";
@@ -89,12 +96,17 @@ export const describeTrigger = (
} ${computeStateName(states[entity]) || entity}`; } ${computeStateName(states[entity]) || entity}`;
} }
} }
} else { } else if (trigger.entity_id) {
entities = states[trigger.entity_id] entities = states[trigger.entity_id]
? computeStateName(states[trigger.entity_id]) ? computeStateName(states[trigger.entity_id])
: trigger.entity_id; : trigger.entity_id;
} }
if (!entities) {
// no entity_id or empty array
entities = "something";
}
base += ` ${entities} changes`; base += ` ${entities} changes`;
if (trigger.from) { if (trigger.from) {
@@ -280,7 +292,7 @@ export const describeTrigger = (
} }
// MQTT Trigger // MQTT Trigger
if (trigger.platform === "mqtt") { if (trigger.platform === "mqtt") {
return "When a MQTT payload has been received"; return "When an MQTT message has been received";
} }
// Template Trigger // Template Trigger
@@ -292,7 +304,25 @@ export const describeTrigger = (
if (trigger.platform === "webhook") { if (trigger.platform === "webhook") {
return "When a Webhook payload has been received"; return "When a Webhook payload has been received";
} }
return `${trigger.platform || "Unknown"} trigger`;
if (trigger.platform === "device") {
if (!trigger.device_id) {
return "Device trigger";
}
const config = trigger as DeviceTrigger;
const localized = localizeDeviceAutomationTrigger(hass, config);
if (localized) {
return localized;
}
const stateObj = hass.states[config.entity_id as string];
return `${stateObj ? computeStateName(stateObj) : config.entity_id} ${
config.type
}`;
}
return `${
trigger.platform ? trigger.platform.replace(/_/g, " ") : "Unknown"
} trigger`;
}; };
export const describeCondition = ( export const describeCondition = (
@@ -304,15 +334,64 @@ export const describeCondition = (
return condition.alias; return condition.alias;
} }
if (["or", "and", "not"].includes(condition.condition)) { if (!condition.condition) {
return `multiple conditions using "${condition.condition}"`; const shorthands: Array<"and" | "or" | "not"> = ["and", "or", "not"];
for (const key of shorthands) {
if (!(key in condition)) {
continue;
}
if (ensureArray(condition[key])) {
condition = {
condition: key,
conditions: condition[key],
};
}
}
}
if (condition.condition === "or") {
const conditions = ensureArray(condition.conditions);
let count = "condition";
if (conditions && conditions.length > 0) {
count = `of ${conditions.length} conditions`;
}
return `Test if any ${count} matches`;
}
if (condition.condition === "and") {
const conditions = ensureArray(condition.conditions);
const count =
conditions && conditions.length > 0
? `${conditions.length} `
: "multiple";
return `Test if ${count} conditions match`;
}
if (condition.condition === "not") {
const conditions = ensureArray(condition.conditions);
const what =
conditions && conditions.length > 0
? `none of ${conditions.length} conditions match`
: "no condition matches";
return `Test if ${what}`;
} }
// State Condition // State Condition
if (condition.condition === "state" && condition.entity_id) { if (condition.condition === "state") {
let base = "Confirm"; let base = "Confirm";
const stateObj = hass.states[condition.entity_id]; const stateObj = hass.states[condition.entity_id];
const entity = stateObj ? computeStateName(stateObj) : condition.entity_id; const entity = stateObj
? computeStateName(stateObj)
: condition.entity_id
? condition.entity_id
: "an entity";
if ("attribute" in condition) { if ("attribute" in condition) {
base += ` ${condition.attribute} from`; base += ` ${condition.attribute} from`;
@@ -328,10 +407,14 @@ export const describeCondition = (
: "" : ""
} ${state}`; } ${state}`;
} }
} else { } else if (condition.state) {
states = condition.state.toString(); states = condition.state.toString();
} }
if (!states) {
states = "a state";
}
base += ` ${entity} is ${states}`; base += ` ${entity} is ${states}`;
if ("for" in condition) { if ("for" in condition) {
@@ -467,5 +550,22 @@ export const describeCondition = (
}`; }`;
} }
return `${condition.condition} condition`; if (condition.condition === "device") {
if (!condition.device_id) {
return "Device condition";
}
const config = condition as DeviceCondition;
const localized = localizeDeviceAutomationCondition(hass, config);
if (localized) {
return localized;
}
const stateObj = hass.states[config.entity_id as string];
return `${stateObj ? computeStateName(stateObj) : config.entity_id} ${
config.type
}`;
}
return `${
condition.condition ? condition.condition.replace(/_/g, " ") : "Unknown"
} condition`;
}; };

View File

@@ -1,11 +1,13 @@
import { Connection, createCollection } from "home-assistant-js-websocket"; import { Connection, createCollection } from "home-assistant-js-websocket";
import { Store } from "home-assistant-js-websocket/dist/store"; import { Store } from "home-assistant-js-websocket/dist/store";
import memoizeOne from "memoize-one";
import { computeStateName } from "../common/entity/compute_state_name"; import { computeStateName } from "../common/entity/compute_state_name";
import { caseInsensitiveStringCompare } from "../common/string/compare"; import { caseInsensitiveStringCompare } from "../common/string/compare";
import { debounce } from "../common/util/debounce"; import { debounce } from "../common/util/debounce";
import { HomeAssistant } from "../types"; import { HomeAssistant } from "../types";
export interface EntityRegistryEntry { export interface EntityRegistryEntry {
id: string;
entity_id: string; entity_id: string;
name: string | null; name: string | null;
icon: string | null; icon: string | null;
@@ -161,6 +163,16 @@ export const sortEntityRegistryByName = (entries: EntityRegistryEntry[]) =>
caseInsensitiveStringCompare(entry1.name || "", entry2.name || "") caseInsensitiveStringCompare(entry1.name || "", entry2.name || "")
); );
export const entityRegistryById = memoizeOne(
(entries: HomeAssistant["entities"]) => {
const entities: HomeAssistant["entities"] = {};
for (const entity of Object.values(entries)) {
entities[entity.id] = entity;
}
return entities;
}
);
export const getEntityPlatformLookup = ( export const getEntityPlatformLookup = (
entities: EntityRegistryEntry[] entities: EntityRegistryEntry[]
): Record<string, string> => { ): Record<string, string> => {

View File

@@ -1,3 +1,4 @@
import { formatDuration } from "../common/datetime/format_duration";
import secondsToDuration from "../common/datetime/seconds_to_duration"; import secondsToDuration from "../common/datetime/seconds_to_duration";
import { ensureArray } from "../common/ensure-array"; import { ensureArray } from "../common/ensure-array";
import { computeStateName } from "../common/entity/compute_state_name"; import { computeStateName } from "../common/entity/compute_state_name";
@@ -5,6 +6,13 @@ import { isTemplate } from "../common/string/has-template";
import { HomeAssistant } from "../types"; import { HomeAssistant } from "../types";
import { Condition } from "./automation"; import { Condition } from "./automation";
import { describeCondition, describeTrigger } from "./automation_i18n"; import { describeCondition, describeTrigger } from "./automation_i18n";
import { localizeDeviceAutomationAction } from "./device_automation";
import { computeDeviceName } from "./device_registry";
import {
computeEntityRegistryName,
entityRegistryById,
} from "./entity_registry";
import { domainToName } from "./integration";
import { import {
ActionType, ActionType,
ActionTypes, ActionTypes,
@@ -47,9 +55,13 @@ export const describeAction = <T extends ActionType>(
) { ) {
base = "Call a service based on a template"; base = "Call a service based on a template";
} else if (config.service) { } else if (config.service) {
base = `Call service ${config.service}`; const [domain, serviceName] = config.service.split(".", 2);
const service = hass.services[domain][serviceName];
base = service
? `${domainToName(hass.localize, domain)}: ${service.name}`
: `Call service: ${config.service}`;
} else { } else {
return actionType; return "Call a service";
} }
if (config.target) { if (config.target) {
const targets: string[] = []; const targets: string[] = [];
@@ -66,26 +78,49 @@ export const describeAction = <T extends ActionType>(
? config.target[key] ? config.target[key]
: [config.target[key]]; : [config.target[key]];
const values: string[] = [];
let renderValues = true;
for (const targetThing of keyConf) { for (const targetThing of keyConf) {
if (isTemplate(targetThing)) { if (isTemplate(targetThing)) {
targets.push(`templated ${label}`); targets.push(`templated ${label}`);
renderValues = false;
break; break;
} else if (key === "entity_id") {
if (targetThing.includes(".")) {
const state = hass.states[targetThing];
if (state) {
targets.push(computeStateName(state));
} else {
targets.push(targetThing);
}
} else {
const entityReg = entityRegistryById(hass.entities)[targetThing];
if (entityReg) {
targets.push(
computeEntityRegistryName(hass, entityReg) || targetThing
);
} else {
targets.push("unknown entity");
}
}
} else if (key === "device_id") {
const device = hass.devices[targetThing];
if (device) {
targets.push(computeDeviceName(device, hass));
} else {
targets.push("unknown device");
}
} else if (key === "area_id") {
const area = hass.areas[targetThing];
if (area?.name) {
targets.push(area.name);
} else {
targets.push("unknown area");
}
} else { } else {
values.push(targetThing); targets.push(targetThing);
} }
} }
if (renderValues) {
targets.push(`${label} ${values.join(", ")}`);
}
} }
if (targets.length > 0) { if (targets.length > 0) {
base += ` on ${targets.join(", ")}`; base += ` ${targets.join(", ")}`;
} }
} }
@@ -102,9 +137,11 @@ export const describeAction = <T extends ActionType>(
} else if (typeof config.delay === "string") { } else if (typeof config.delay === "string") {
duration = isTemplate(config.delay) duration = isTemplate(config.delay)
? "based on a template" ? "based on a template"
: `for ${config.delay}`; : `for ${config.delay || "a duration"}`;
} else if (config.delay) {
duration = `for ${formatDuration(config.delay)}`;
} else { } else {
duration = `for ${JSON.stringify(config.delay)}`; duration = "for a duration";
} }
return `Delay ${duration}`; return `Delay ${duration}`;
@@ -118,13 +155,12 @@ export const describeAction = <T extends ActionType>(
} else { } else {
entityId = config.target?.entity_id || config.entity_id; entityId = config.target?.entity_id || config.entity_id;
} }
if (!entityId) {
return "Activate a scene";
}
const sceneStateObj = entityId ? hass.states[entityId] : undefined; const sceneStateObj = entityId ? hass.states[entityId] : undefined;
return `Scene ${ return `Active scene ${
sceneStateObj sceneStateObj ? computeStateName(sceneStateObj) : entityId
? computeStateName(sceneStateObj)
: "scene" in config
? config.scene
: config.target?.entity_id || config.entity_id || ""
}`; }`;
} }
@@ -132,16 +168,22 @@ export const describeAction = <T extends ActionType>(
const config = action as PlayMediaAction; const config = action as PlayMediaAction;
const entityId = config.target?.entity_id || config.entity_id; const entityId = config.target?.entity_id || config.entity_id;
const mediaStateObj = entityId ? hass.states[entityId] : undefined; const mediaStateObj = entityId ? hass.states[entityId] : undefined;
return `Play ${config.metadata.title || config.data.media_content_id} on ${ return `Play ${
config.metadata.title || config.data.media_content_id || "media"
} on ${
mediaStateObj mediaStateObj
? computeStateName(mediaStateObj) ? computeStateName(mediaStateObj)
: config.target?.entity_id || config.entity_id : entityId || "a media player"
}`; }`;
} }
if (actionType === "wait_for_trigger") { if (actionType === "wait_for_trigger") {
const config = action as WaitForTriggerAction; const config = action as WaitForTriggerAction;
return `Wait for ${ensureArray(config.wait_for_trigger) const triggers = ensureArray(config.wait_for_trigger);
if (!triggers || triggers.length === 0) {
return "Wait for a trigger";
}
return `Wait for ${triggers
.map((trigger) => describeTrigger(trigger, hass)) .map((trigger) => describeTrigger(trigger, hass))
.join(", ")}`; .join(", ")}`;
} }
@@ -164,22 +206,26 @@ export const describeAction = <T extends ActionType>(
} }
if (actionType === "check_condition") { if (actionType === "check_condition") {
return `Test ${describeCondition(action as Condition, hass)}`; return describeCondition(action as Condition, hass);
} }
if (actionType === "stop") { if (actionType === "stop") {
const config = action as StopAction; const config = action as StopAction;
return `Stopped${config.stop ? ` because: ${config.stop}` : ""}`; return `Stop${config.stop ? ` because: ${config.stop}` : ""}`;
} }
if (actionType === "if") { if (actionType === "if") {
const config = action as IfAction; const config = action as IfAction;
return `Perform an action if: ${ return `Perform an action if: ${
typeof config.if === "string" !config.if
? ""
: typeof config.if === "string"
? config.if ? config.if
: ensureArray(config.if).length > 1 : ensureArray(config.if).length > 1
? `${ensureArray(config.if).length} conditions` ? `${ensureArray(config.if).length} conditions`
: describeCondition(ensureArray(config.if)[0], hass) : ensureArray(config.if).length
? describeCondition(ensureArray(config.if)[0], hass)
: ""
}${config.else ? " (or else!)" : ""}`; }${config.else ? " (or else!)" : ""}`;
} }
@@ -219,6 +265,13 @@ export const describeAction = <T extends ActionType>(
if (actionType === "device_action") { if (actionType === "device_action") {
const config = action as DeviceAction; const config = action as DeviceAction;
if (!config.device_id) {
return "Device action";
}
const localized = localizeDeviceAutomationAction(hass, config);
if (localized) {
return localized;
}
const stateObj = hass.states[config.entity_id as string]; const stateObj = hass.states[config.entity_id as string];
return `${config.type || "Perform action with"} ${ return `${config.type || "Perform action with"} ${
stateObj ? computeStateName(stateObj) : config.entity_id stateObj ? computeStateName(stateObj) : config.entity_id

View File

@@ -15,6 +15,8 @@ class HassSubpage extends LitElement {
@property({ type: String, attribute: "back-path" }) public backPath?: string; @property({ type: String, attribute: "back-path" }) public backPath?: string;
@property() public backCallback?: () => void;
@property({ type: Boolean, reflect: true }) public narrow = false; @property({ type: Boolean, reflect: true }) public narrow = false;
@property({ type: Boolean }) public supervisor = false; @property({ type: Boolean }) public supervisor = false;
@@ -52,6 +54,9 @@ class HassSubpage extends LitElement {
<slot name="toolbar-icon"></slot> <slot name="toolbar-icon"></slot>
</div> </div>
<div class="content" @scroll=${this._saveScrollPos}><slot></slot></div> <div class="content" @scroll=${this._saveScrollPos}><slot></slot></div>
<div id="fab">
<slot name="fab"></slot>
</div>
`; `;
} }
@@ -61,6 +66,10 @@ class HassSubpage extends LitElement {
} }
private _backTapped(): void { private _backTapped(): void {
if (this.backCallback) {
this.backCallback();
return;
}
history.back(); history.back();
} }
@@ -116,6 +125,29 @@ class HassSubpage extends LitElement {
overflow: auto; overflow: auto;
-webkit-overflow-scrolling: touch; -webkit-overflow-scrolling: touch;
} }
#fab {
position: fixed;
right: calc(16px + env(safe-area-inset-right));
bottom: calc(16px + env(safe-area-inset-bottom));
z-index: 1;
}
:host([narrow]) #fab.tabs {
bottom: calc(84px + env(safe-area-inset-bottom));
}
#fab[is-wide] {
bottom: 24px;
right: 24px;
}
:host([rtl]) #fab {
right: auto;
left: calc(16px + env(safe-area-inset-left));
}
:host([rtl][is-wide]) #fab {
bottom: 24px;
left: 24px;
right: auto;
}
`; `;
} }
} }

View File

@@ -157,13 +157,13 @@ export default class HaAutomationActionRow extends LitElement {
</div>` </div>`
: ""} : ""}
<ha-expansion-panel leftChevron> <ha-expansion-panel leftChevron>
<div slot="header"> <h3 slot="header">
<ha-svg-icon <ha-svg-icon
class="action-icon" class="action-icon"
.path=${ACTION_TYPES[type!]} .path=${ACTION_TYPES[type!]}
></ha-svg-icon> ></ha-svg-icon>
${capitalizeFirstLetter(describeAction(this.hass, this.action))} ${capitalizeFirstLetter(describeAction(this.hass, this.action))}
</div> </h3>
${this.index !== 0 ${this.index !== 0
? html` ? html`
@@ -230,6 +230,7 @@ export default class HaAutomationActionRow extends LitElement {
${this.hass.localize("ui.panel.config.automation.editor.edit_ui")} ${this.hass.localize("ui.panel.config.automation.editor.edit_ui")}
${!yamlMode ${!yamlMode
? html`<ha-svg-icon ? html`<ha-svg-icon
class="selected_menu_item"
slot="graphic" slot="graphic"
.path=${mdiCheck} .path=${mdiCheck}
></ha-svg-icon>` ></ha-svg-icon>`
@@ -242,6 +243,7 @@ export default class HaAutomationActionRow extends LitElement {
)} )}
${yamlMode ${yamlMode
? html`<ha-svg-icon ? html`<ha-svg-icon
class="selected_menu_item"
slot="graphic" slot="graphic"
.path=${mdiCheck} .path=${mdiCheck}
></ha-svg-icon>` ></ha-svg-icon>`
@@ -507,13 +509,18 @@ export default class HaAutomationActionRow extends LitElement {
--expansion-panel-summary-padding: 0 0 0 8px; --expansion-panel-summary-padding: 0 0 0 8px;
--expansion-panel-content-padding: 0; --expansion-panel-content-padding: 0;
} }
h3 {
margin: 0;
font-size: inherit;
font-weight: inherit;
}
.action-icon { .action-icon {
display: none; display: none;
} }
@media (min-width: 870px) { @media (min-width: 870px) {
.action-icon { .action-icon {
display: inline-block; display: inline-block;
color: var(--primary-color); color: var(--secondary-text-color);
opacity: 0.9; opacity: 0.9;
margin-right: 8px; margin-right: 8px;
} }
@@ -534,6 +541,12 @@ export default class HaAutomationActionRow extends LitElement {
.warning ul { .warning ul {
margin: 4px 0; margin: 4px 0;
} }
.selected_menu_item {
color: var(--primary-color);
}
li[role="separator"] {
border-bottom-color: var(--divider-color);
}
`, `,
]; ];
} }

View File

@@ -1,9 +1,8 @@
import "@material/mwc-button/mwc-button"; import "@material/mwc-button/mwc-button";
import { HassEntity } from "home-assistant-js-websocket"; import { HassEntity } from "home-assistant-js-websocket";
import { css, CSSResultGroup, html, LitElement, PropertyValues } from "lit"; import { css, CSSResultGroup, html, LitElement } from "lit";
import { customElement, property, state } from "lit/decorators"; import { customElement, property, state } from "lit/decorators";
import { fireEvent } from "../../../common/dom/fire_event"; import { fireEvent } from "../../../common/dom/fire_event";
import "../../../components/entity/ha-entity-toggle";
import "../../../components/ha-blueprint-picker"; import "../../../components/ha-blueprint-picker";
import "../../../components/ha-card"; import "../../../components/ha-card";
import "../../../components/ha-circular-progress"; import "../../../components/ha-circular-progress";
@@ -11,10 +10,7 @@ import "../../../components/ha-markdown";
import "../../../components/ha-selector/ha-selector"; import "../../../components/ha-selector/ha-selector";
import "../../../components/ha-settings-row"; import "../../../components/ha-settings-row";
import "../../../components/ha-textfield"; import "../../../components/ha-textfield";
import { import { BlueprintAutomationConfig } from "../../../data/automation";
BlueprintAutomationConfig,
triggerAutomationActions,
} from "../../../data/automation";
import { import {
BlueprintOrError, BlueprintOrError,
Blueprints, Blueprints,
@@ -38,8 +34,6 @@ export class HaBlueprintAutomationEditor extends LitElement {
@state() private _blueprints?: Blueprints; @state() private _blueprints?: Blueprints;
@state() private _showDescription = false;
protected firstUpdated(changedProps) { protected firstUpdated(changedProps) {
super.firstUpdated(changedProps); super.firstUpdated(changedProps);
this._getBlueprints(); this._getBlueprints();
@@ -52,88 +46,28 @@ export class HaBlueprintAutomationEditor extends LitElement {
return this._blueprints[this.config.use_blueprint.path]; return this._blueprints[this.config.use_blueprint.path];
} }
protected willUpdate(changedProps: PropertyValues): void {
super.willUpdate(changedProps);
if (
!this._showDescription &&
changedProps.has("config") &&
this.config.description
) {
this._showDescription = true;
}
}
protected render() { protected render() {
const blueprint = this._blueprint; const blueprint = this._blueprint;
return html` return html`
<ha-config-section vertical .isWide=${this.isWide}> <p class="introduction">
${!this.narrow ${this.hass.localize("ui.panel.config.automation.editor.introduction")}
? html` <span slot="header">${this.config.alias}</span> ` </p>
: ""} <ha-card outlined>
<span slot="introduction"> <div class="card-content">
${this.hass.localize( <ha-textarea
"ui.panel.config.automation.editor.introduction" .label=${this.hass.localize(
)} "ui.panel.config.automation.editor.description.label"
</span> )}
<ha-card outlined> .placeholder=${this.hass.localize(
<div class="card-content"> "ui.panel.config.automation.editor.description.placeholder"
${this._showDescription )}
? html` name="description"
<ha-textarea autogrow
.label=${this.hass.localize( .value=${this.config.description || ""}
"ui.panel.config.automation.editor.description.label" @change=${this._valueChanged}
)} ></ha-textarea>
.placeholder=${this.hass.localize( </div>
"ui.panel.config.automation.editor.description.placeholder" </ha-card>
)}
name="description"
autogrow
.value=${this.config.description || ""}
@change=${this._valueChanged}
></ha-textarea>
`
: html`
<div class="link-button-row">
<button class="link" @click=${this._addDescription}>
${this.hass.localize(
"ui.panel.config.automation.editor.description.add"
)}
</button>
</div>
`}
</div>
${this.stateObj
? html`
<div class="card-actions layout horizontal justified center">
<div class="layout horizontal center">
<ha-entity-toggle
.hass=${this.hass}
.stateObj=${this.stateObj!}
></ha-entity-toggle>
${this.hass.localize(
"ui.panel.config.automation.editor.enable_disable"
)}
</div>
<div>
<a href="/config/automation/trace/${this.config.id}">
<mwc-button>
${this.hass.localize(
"ui.panel.config.automation.editor.show_trace"
)}
</mwc-button>
</a>
<mwc-button
@click=${this._runActions}
.stateObj=${this.stateObj}
>
${this.hass.localize("ui.card.automation.trigger")}
</mwc-button>
</div>
</div>
`
: ""}
</ha-card>
</ha-config-section>
<ha-card <ha-card
outlined outlined
@@ -220,10 +154,6 @@ export class HaBlueprintAutomationEditor extends LitElement {
this._blueprints = await fetchBlueprints(this.hass, "automation"); this._blueprints = await fetchBlueprints(this.hass, "automation");
} }
private _runActions(ev: Event) {
triggerAutomationActions(this.hass, (ev.target as any).stateObj.entity_id);
}
private _blueprintChanged(ev) { private _blueprintChanged(ev) {
ev.stopPropagation(); ev.stopPropagation();
if (this.config.use_blueprint.path === ev.detail.value) { if (this.config.use_blueprint.path === ev.detail.value) {
@@ -284,16 +214,14 @@ export class HaBlueprintAutomationEditor extends LitElement {
}); });
} }
private _addDescription() {
this._showDescription = true;
}
static get styles(): CSSResultGroup { static get styles(): CSSResultGroup {
return [ return [
haStyle, haStyle,
css` css`
:host {
display: block;
}
ha-card.blueprint { ha-card.blueprint {
max-width: 1040px;
margin: 24px auto; margin: 24px auto;
} }
.padding { .padding {
@@ -313,15 +241,16 @@ export class HaBlueprintAutomationEditor extends LitElement {
h3 { h3 {
margin: 16px; margin: 16px;
} }
span[slot="introduction"] a { .introduction {
margin-top: 0;
margin-bottom: 12px;
}
.introduction a {
color: var(--primary-color); color: var(--primary-color);
} }
p { p {
margin-bottom: 0; margin-bottom: 0;
} }
ha-entity-toggle {
margin-right: 8px;
}
ha-settings-row { ha-settings-row {
--paper-time-input-justify-content: flex-end; --paper-time-input-justify-content: flex-end;
--settings-row-content-width: 100%; --settings-row-content-width: 100%;

View File

@@ -93,7 +93,7 @@ export default class HaAutomationConditionRow extends LitElement {
: ""} : ""}
<ha-expansion-panel leftChevron> <ha-expansion-panel leftChevron>
<div slot="header"> <h3 slot="header">
<ha-svg-icon <ha-svg-icon
class="condition-icon" class="condition-icon"
.path=${CONDITION_TYPES[this.condition.condition]} .path=${CONDITION_TYPES[this.condition.condition]}
@@ -101,7 +101,7 @@ export default class HaAutomationConditionRow extends LitElement {
${capitalizeFirstLetter( ${capitalizeFirstLetter(
describeCondition(this.condition, this.hass) describeCondition(this.condition, this.hass)
)} )}
</div> </h3>
<ha-button-menu <ha-button-menu
slot="icons" slot="icons"
@@ -145,6 +145,7 @@ export default class HaAutomationConditionRow extends LitElement {
${this.hass.localize("ui.panel.config.automation.editor.edit_ui")} ${this.hass.localize("ui.panel.config.automation.editor.edit_ui")}
${!this._yamlMode ${!this._yamlMode
? html`<ha-svg-icon ? html`<ha-svg-icon
class="selected_menu_item"
slot="graphic" slot="graphic"
.path=${mdiCheck} .path=${mdiCheck}
></ha-svg-icon>` ></ha-svg-icon>`
@@ -157,6 +158,7 @@ export default class HaAutomationConditionRow extends LitElement {
)} )}
${this._yamlMode ${this._yamlMode
? html`<ha-svg-icon ? html`<ha-svg-icon
class="selected_menu_item"
slot="graphic" slot="graphic"
.path=${mdiCheck} .path=${mdiCheck}
></ha-svg-icon>` ></ha-svg-icon>`
@@ -423,13 +425,18 @@ export default class HaAutomationConditionRow extends LitElement {
--expansion-panel-summary-padding: 0 0 0 8px; --expansion-panel-summary-padding: 0 0 0 8px;
--expansion-panel-content-padding: 0; --expansion-panel-content-padding: 0;
} }
h3 {
margin: 0;
font-size: inherit;
font-weight: inherit;
}
.condition-icon { .condition-icon {
display: none; display: none;
} }
@media (min-width: 870px) { @media (min-width: 870px) {
.condition-icon { .condition-icon {
display: inline-block; display: inline-block;
color: var(--primary-color); color: var(--secondary-text-color);
opacity: 0.9; opacity: 0.9;
margin-right: 8px; margin-right: 8px;
} }
@@ -472,6 +479,12 @@ export default class HaAutomationConditionRow extends LitElement {
.testing.pass { .testing.pass {
background-color: var(--success-color); background-color: var(--success-color);
} }
.selected_menu_item {
color: var(--primary-color);
}
li[role="separator"] {
border-bottom-color: var(--divider-color);
}
`, `,
]; ];
} }

View File

@@ -5,6 +5,7 @@ import {
mdiContentSave, mdiContentSave,
mdiDelete, mdiDelete,
mdiDotsVertical, mdiDotsVertical,
mdiInformationOutline,
mdiPencil, mdiPencil,
mdiPlay, mdiPlay,
mdiPlayCircleOutline, mdiPlayCircleOutline,
@@ -25,6 +26,7 @@ import {
} from "lit"; } from "lit";
import { property, state, query } from "lit/decorators"; import { property, state, query } from "lit/decorators";
import { classMap } from "lit/directives/class-map"; import { classMap } from "lit/directives/class-map";
import { fireEvent } from "../../../common/dom/fire_event";
import { navigate } from "../../../common/navigate"; import { navigate } from "../../../common/navigate";
import { copyToClipboard } from "../../../common/util/copy-clipboard"; import { copyToClipboard } from "../../../common/util/copy-clipboard";
import "../../../components/ha-button-menu"; import "../../../components/ha-button-menu";
@@ -49,7 +51,7 @@ import {
showPromptDialog, showPromptDialog,
} from "../../../dialogs/generic/show-dialog-box"; } from "../../../dialogs/generic/show-dialog-box";
import "../../../layouts/ha-app-layout"; import "../../../layouts/ha-app-layout";
import "../../../layouts/hass-tabs-subpage"; import "../../../layouts/hass-subpage";
import { KeyboardShortcutMixin } from "../../../mixins/keyboard-shortcut-mixin"; import { KeyboardShortcutMixin } from "../../../mixins/keyboard-shortcut-mixin";
import { haStyle } from "../../../resources/styles"; import { haStyle } from "../../../resources/styles";
import { HomeAssistant, Route } from "../../../types"; import { HomeAssistant, Route } from "../../../types";
@@ -111,7 +113,7 @@ export class HaAutomationEditor extends KeyboardShortcutMixin(LitElement) {
? this.hass.states[this._entityId] ? this.hass.states[this._entityId]
: undefined; : undefined;
return html` return html`
<hass-tabs-subpage <hass-subpage
.hass=${this.hass} .hass=${this.hass}
.narrow=${this.narrow} .narrow=${this.narrow}
.route=${this.route} .route=${this.route}
@@ -125,6 +127,14 @@ export class HaAutomationEditor extends KeyboardShortcutMixin(LitElement) {
.path=${mdiDotsVertical} .path=${mdiDotsVertical}
></ha-icon-button> ></ha-icon-button>
<mwc-list-item graphic="icon" @click=${this._showInfo}>
${this.hass.localize("ui.panel.config.automation.editor.show_info")}
<ha-svg-icon
slot="graphic"
.path=${mdiInformationOutline}
></ha-svg-icon>
</mwc-list-item>
<mwc-list-item <mwc-list-item
graphic="icon" graphic="icon"
.disabled=${!stateObj} .disabled=${!stateObj}
@@ -134,15 +144,9 @@ export class HaAutomationEditor extends KeyboardShortcutMixin(LitElement) {
<ha-svg-icon slot="graphic" .path=${mdiPlay}></ha-svg-icon> <ha-svg-icon slot="graphic" .path=${mdiPlay}></ha-svg-icon>
</mwc-list-item> </mwc-list-item>
${stateObj ${stateObj && this._config
? html`<a ? html`<a href="/config/automation/trace/${this._config.id}">
href="/config/automation/trace/${this._config <mwc-list-item graphic="icon">
? this._config.id
: ""}"
target="_blank"
.disabled=${!stateObj}
>
<mwc-list-item graphic="icon" .disabled=${!stateObj}>
${this.hass.localize( ${this.hass.localize(
"ui.panel.config.automation.editor.show_trace" "ui.panel.config.automation.editor.show_trace"
)} )}
@@ -230,6 +234,14 @@ export class HaAutomationEditor extends KeyboardShortcutMixin(LitElement) {
${this._config ${this._config
? html` ? html`
${this.narrow
? html`<span slot="header"
>${this._config!.alias ||
this.hass.localize(
"ui.panel.config.automation.editor.default_name"
)}</span
>`
: ""}
<div <div
class="content ${classMap({ class="content ${classMap({
"yaml-mode": this._mode === "yaml", "yaml-mode": this._mode === "yaml",
@@ -242,12 +254,7 @@ export class HaAutomationEditor extends KeyboardShortcutMixin(LitElement) {
${this._mode === "gui" ${this._mode === "gui"
? html` ? html`
${this.narrow ${this.narrow
? html`<span slot="header" ? ""
>${this._config!.alias ||
this.hass.localize(
"ui.panel.config.automation.editor.default_name"
)}</span
>`
: html` : html`
<div class="header-name"> <div class="header-name">
<h1> <h1>
@@ -329,7 +336,7 @@ export class HaAutomationEditor extends KeyboardShortcutMixin(LitElement) {
> >
<ha-svg-icon slot="icon" .path=${mdiContentSave}></ha-svg-icon> <ha-svg-icon slot="icon" .path=${mdiContentSave}></ha-svg-icon>
</ha-fab> </ha-fab>
</hass-tabs-subpage> </hass-subpage>
`; `;
} }
@@ -430,6 +437,13 @@ export class HaAutomationEditor extends KeyboardShortcutMixin(LitElement) {
this._errors = undefined; this._errors = undefined;
} }
private _showInfo() {
if (!this.hass || !this._entityId) {
return;
}
fireEvent(this, "hass-more-info", { entityId: this._entityId });
}
private _runActions() { private _runActions() {
if (!this.hass || !this._entityId) { if (!this.hass || !this._entityId) {
return; return;
@@ -637,7 +651,8 @@ export class HaAutomationEditor extends KeyboardShortcutMixin(LitElement) {
flex-direction: column; flex-direction: column;
padding-bottom: 0; padding-bottom: 0;
} }
manual-automation-editor { manual-automation-editor,
blueprint-automation-editor {
margin: 0 auto; margin: 0 auto;
max-width: 1040px; max-width: 1040px;
padding: 28px 20px 0; padding: 28px 20px 0;
@@ -678,15 +693,6 @@ export class HaAutomationEditor extends KeyboardShortcutMixin(LitElement) {
} }
h1 { h1 {
margin: 0; margin: 0;
font-family: var(--paper-font-headline_-_font-family);
-webkit-font-smoothing: var(
--paper-font-headline_-_-webkit-font-smoothing
);
font-size: var(--paper-font-headline_-_font-size);
font-weight: var(--paper-font-headline_-_font-weight);
letter-spacing: var(--paper-font-headline_-_letter-spacing);
line-height: var(--paper-font-headline_-_line-height);
opacity: var(--dark-primary-opacity);
} }
.header-name { .header-name {
display: flex; display: flex;

View File

@@ -1,4 +1,16 @@
import { mdiHelpCircle, mdiInformationOutline, mdiPlus } from "@mdi/js"; import {
mdiCancel,
mdiContentDuplicate,
mdiDelete,
mdiHelpCircle,
mdiInformationOutline,
mdiPlay,
mdiPlayCircleOutline,
mdiPlus,
mdiStopCircleOutline,
mdiTransitConnection,
} from "@mdi/js";
import "@polymer/paper-tooltip/paper-tooltip";
import { CSSResultGroup, html, LitElement, TemplateResult } from "lit"; import { CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { customElement, property, state } from "lit/decorators"; import { customElement, property, state } from "lit/decorators";
import memoizeOne from "memoize-one"; import memoizeOne from "memoize-one";
@@ -13,11 +25,22 @@ import type {
RowClickedEvent, RowClickedEvent,
} from "../../../components/data-table/ha-data-table"; } from "../../../components/data-table/ha-data-table";
import "../../../components/ha-button-related-filter-menu"; import "../../../components/ha-button-related-filter-menu";
import "../../../components/ha-chip";
import "../../../components/ha-fab"; import "../../../components/ha-fab";
import "../../../components/ha-icon-button"; import "../../../components/ha-icon-button";
import "../../../components/ha-icon-overflow-menu";
import "../../../components/ha-svg-icon"; import "../../../components/ha-svg-icon";
import type { AutomationEntity } from "../../../data/automation"; import {
import { showAlertDialog } from "../../../dialogs/generic/show-dialog-box"; AutomationEntity,
deleteAutomation,
getAutomationConfig,
showAutomationEditor,
triggerAutomationActions,
} from "../../../data/automation";
import {
showAlertDialog,
showConfirmationDialog,
} from "../../../dialogs/generic/show-dialog-box";
import "../../../layouts/hass-tabs-subpage-data-table"; import "../../../layouts/hass-tabs-subpage-data-table";
import { haStyle } from "../../../resources/styles"; import { haStyle } from "../../../resources/styles";
import { HomeAssistant, Route } from "../../../types"; import { HomeAssistant, Route } from "../../../types";
@@ -63,6 +86,7 @@ class HaAutomationPicker extends LitElement {
...automation, ...automation,
name: computeStateName(automation), name: computeStateName(automation),
last_triggered: automation.attributes.last_triggered || undefined, last_triggered: automation.attributes.last_triggered || undefined,
disabled: automation.state === "off",
})); }));
} }
); );
@@ -123,22 +147,105 @@ class HaAutomationPicker extends LitElement {
}, },
}; };
} }
columns.disabled = this.narrow
? {
title: "",
template: (disabled: boolean) =>
disabled
? html`
<paper-tooltip animation-delay="0" position="left">
${this.hass.localize(
"ui.panel.config.automation.picker.disabled"
)}
</paper-tooltip>
<ha-svg-icon
.path=${mdiCancel}
style="color: var(--secondary-text-color)"
></ha-svg-icon>
`
: "",
}
: {
width: "20%",
title: "",
template: (disabled: boolean) =>
disabled
? html`
<ha-chip>
${this.hass.localize(
"ui.panel.config.automation.picker.disabled"
)}
</ha-chip>
`
: "",
};
columns.actions = { columns.actions = {
title: "", title: "",
label: this.hass.localize( width: this.narrow ? undefined : "10%",
"ui.panel.config.automation.picker.headers.actions" type: "overflow-menu",
), template: (_: string, automation: any) =>
type: "icon-button", html`
template: (_info, automation: any) => html` <ha-icon-overflow-menu
<ha-icon-button .hass=${this.hass}
.automation=${automation} narrow
.label=${this.hass.localize( .items=${[
"ui.panel.config.automation.picker.headers.actions" {
)} path: mdiInformationOutline,
.path=${mdiInformationOutline} label: this.hass.localize(
@click=${this._showInfo} "ui.panel.config.automation.editor.show_info"
></ha-icon-button> ),
`, action: () => this._showInfo(automation),
},
{
path: mdiPlay,
label: this.hass.localize(
"ui.panel.config.automation.editor.run"
),
action: () => this._runActions(automation),
},
{
path: mdiTransitConnection,
label: this.hass.localize(
"ui.panel.config.automation.editor.show_trace"
),
action: () => this._showTrace(automation),
},
{
path: mdiContentDuplicate,
label: this.hass.localize(
"ui.panel.config.automation.picker.duplicate"
),
action: () => this.duplicate(automation),
},
{
path:
automation.state === "off"
? mdiPlayCircleOutline
: mdiStopCircleOutline,
label:
automation.state === "off"
? this.hass.localize(
"ui.panel.config.automation.editor.enable"
)
: this.hass.localize(
"ui.panel.config.automation.editor.disable"
),
action: () => this._toggle(automation),
},
{
label: this.hass.localize(
"ui.panel.config.automation.picker.delete"
),
path: mdiDelete,
action: () => this._deleteConfirm(automation),
warning: true,
},
]}
>
</ha-icon-overflow-menu>
`,
}; };
return columns; return columns;
} }
@@ -210,12 +317,52 @@ class HaAutomationPicker extends LitElement {
this._filterValue = undefined; this._filterValue = undefined;
} }
private _showInfo(ev) { private _showInfo(automation: any) {
ev.stopPropagation();
const automation = ev.currentTarget.automation;
fireEvent(this, "hass-more-info", { entityId: automation.entity_id }); fireEvent(this, "hass-more-info", { entityId: automation.entity_id });
} }
private _runActions(automation: any) {
triggerAutomationActions(this.hass, automation.entity_id);
}
private _showTrace(automation: any) {
navigate(`/config/automation/trace/${automation.attributes.id}`);
}
private async _toggle(automation): Promise<void> {
const service = automation.state === "off" ? "turn_on" : "turn_off";
await this.hass.callService("automation", service, {
entity_id: automation.entity_id,
});
}
private async _deleteConfirm(automation) {
showConfirmationDialog(this, {
text: this.hass.localize(
"ui.panel.config.automation.picker.delete_confirm"
),
confirmText: this.hass!.localize("ui.common.delete"),
dismissText: this.hass!.localize("ui.common.cancel"),
confirm: () => this._delete(automation),
});
}
private async _delete(automation) {
await deleteAutomation(this.hass, automation.attributes.id);
}
private async duplicate(automation) {
const config = await getAutomationConfig(
this.hass,
automation.attributes.id
);
showAutomationEditor({
...config,
id: undefined,
alias: undefined,
});
}
private _showHelp() { private _showHelp() {
showAlertDialog(this, { showAlertDialog(this, {
title: this.hass.localize("ui.panel.config.automation.caption"), title: this.hass.localize("ui.panel.config.automation.caption"),

View File

@@ -47,12 +47,12 @@ export class HaManualAutomationEditor extends LitElement {
: ""} : ""}
<ha-expansion-panel leftChevron> <ha-expansion-panel leftChevron>
<div slot="header"> <h3 slot="header">
<ha-svg-icon class="settings-icon" .path=${mdiRobot}></ha-svg-icon> <ha-svg-icon class="settings-icon" .path=${mdiRobot}></ha-svg-icon>
${this.hass.localize( ${this.hass.localize(
"ui.panel.config.automation.editor.automation_settings" "ui.panel.config.automation.editor.automation_settings"
)} )}
</div> </h3>
<div class="card-content"> <div class="card-content">
<ha-textarea <ha-textarea
.label=${this.hass.localize( .label=${this.hass.localize(
@@ -115,11 +115,11 @@ export class HaManualAutomationEditor extends LitElement {
</ha-card> </ha-card>
<div class="header"> <div class="header">
<div class="name"> <h2 id="triggers-heading" class="name">
${this.hass.localize( ${this.hass.localize(
"ui.panel.config.automation.editor.triggers.header" "ui.panel.config.automation.editor.triggers.header"
)} )}
</div> </h2>
<a <a
href=${documentationUrl(this.hass, "/docs/automation/trigger/")} href=${documentationUrl(this.hass, "/docs/automation/trigger/")}
target="_blank" target="_blank"
@@ -135,17 +135,19 @@ export class HaManualAutomationEditor extends LitElement {
</div> </div>
<ha-automation-trigger <ha-automation-trigger
role="region"
aria-labelledby="triggers-heading"
.triggers=${this.config.trigger} .triggers=${this.config.trigger}
@value-changed=${this._triggerChanged} @value-changed=${this._triggerChanged}
.hass=${this.hass} .hass=${this.hass}
></ha-automation-trigger> ></ha-automation-trigger>
<div class="header"> <div class="header">
<div class="name"> <h2 id="conditions-heading" class="name">
${this.hass.localize( ${this.hass.localize(
"ui.panel.config.automation.editor.conditions.header" "ui.panel.config.automation.editor.conditions.header"
)} )}
</div> </h2>
<a <a
href=${documentationUrl(this.hass, "/docs/automation/condition/")} href=${documentationUrl(this.hass, "/docs/automation/condition/")}
target="_blank" target="_blank"
@@ -161,17 +163,19 @@ export class HaManualAutomationEditor extends LitElement {
</div> </div>
<ha-automation-condition <ha-automation-condition
role="region"
aria-labelledby="conditions-heading"
.conditions=${this.config.condition || []} .conditions=${this.config.condition || []}
@value-changed=${this._conditionChanged} @value-changed=${this._conditionChanged}
.hass=${this.hass} .hass=${this.hass}
></ha-automation-condition> ></ha-automation-condition>
<div class="header"> <div class="header">
<div class="name"> <h2 id="actions-heading" class="name">
${this.hass.localize( ${this.hass.localize(
"ui.panel.config.automation.editor.actions.header" "ui.panel.config.automation.editor.actions.header"
)} )}
</div> </h2>
<a <a
href=${documentationUrl(this.hass, "/docs/automation/action/")} href=${documentationUrl(this.hass, "/docs/automation/action/")}
target="_blank" target="_blank"
@@ -187,6 +191,8 @@ export class HaManualAutomationEditor extends LitElement {
</div> </div>
<ha-automation-action <ha-automation-action
role="region"
aria-labelledby="actions-heading"
.actions=${this.config.action} .actions=${this.config.action}
@value-changed=${this._actionChanged} @value-changed=${this._actionChanged}
.hass=${this.hass} .hass=${this.hass}
@@ -292,7 +298,6 @@ export class HaManualAutomationEditor extends LitElement {
} }
.header { .header {
display: flex; display: flex;
margin: 16px 0;
align-items: center; align-items: center;
} }
.header .name { .header .name {
@@ -303,6 +308,11 @@ export class HaManualAutomationEditor extends LitElement {
.header a { .header a {
color: var(--secondary-text-color); color: var(--secondary-text-color);
} }
h3 {
margin: 0;
font-size: inherit;
font-weight: inherit;
}
ha-expansion-panel { ha-expansion-panel {
--expansion-panel-summary-padding: 0 0 0 8px; --expansion-panel-summary-padding: 0 0 0 8px;
--expansion-panel-content-padding: 0; --expansion-panel-content-padding: 0;
@@ -319,7 +329,7 @@ export class HaManualAutomationEditor extends LitElement {
@media (min-width: 870px) { @media (min-width: 870px) {
.settings-icon { .settings-icon {
display: inline-block; display: inline-block;
color: var(--primary-color); color: var(--secondary-text-color);
opacity: 0.9; opacity: 0.9;
margin-right: 8px; margin-right: 8px;
} }

View File

@@ -121,13 +121,13 @@ export default class HaAutomationTriggerRow extends LitElement {
: ""} : ""}
<ha-expansion-panel leftChevron> <ha-expansion-panel leftChevron>
<div slot="header"> <h3 slot="header">
<ha-svg-icon <ha-svg-icon
class="trigger-icon" class="trigger-icon"
.path=${TRIGGER_TYPES[this.trigger.platform]} .path=${TRIGGER_TYPES[this.trigger.platform]}
></ha-svg-icon> ></ha-svg-icon>
${capitalizeFirstLetter(describeTrigger(this.trigger, this.hass))} ${capitalizeFirstLetter(describeTrigger(this.trigger, this.hass))}
</div> </h3>
<ha-button-menu <ha-button-menu
slot="icons" slot="icons"
fixed fixed
@@ -170,6 +170,7 @@ export default class HaAutomationTriggerRow extends LitElement {
${this.hass.localize("ui.panel.config.automation.editor.edit_ui")} ${this.hass.localize("ui.panel.config.automation.editor.edit_ui")}
${!yamlMode ${!yamlMode
? html`<ha-svg-icon ? html`<ha-svg-icon
class="selected_menu_item"
slot="graphic" slot="graphic"
.path=${mdiCheck} .path=${mdiCheck}
></ha-svg-icon>` ></ha-svg-icon>`
@@ -182,6 +183,7 @@ export default class HaAutomationTriggerRow extends LitElement {
)} )}
${yamlMode ${yamlMode
? html`<ha-svg-icon ? html`<ha-svg-icon
class="selected_menu_item"
slot="graphic" slot="graphic"
.path=${mdiCheck} .path=${mdiCheck}
></ha-svg-icon>` ></ha-svg-icon>`
@@ -532,13 +534,18 @@ export default class HaAutomationTriggerRow extends LitElement {
--expansion-panel-summary-padding: 0 0 0 8px; --expansion-panel-summary-padding: 0 0 0 8px;
--expansion-panel-content-padding: 0; --expansion-panel-content-padding: 0;
} }
h3 {
margin: 0;
font-size: inherit;
font-weight: inherit;
}
.trigger-icon { .trigger-icon {
display: none; display: none;
} }
@media (min-width: 870px) { @media (min-width: 870px) {
.trigger-icon { .trigger-icon {
display: inline-block; display: inline-block;
color: var(--primary-color); color: var(--secondary-text-color);
opacity: 0.9; opacity: 0.9;
margin-right: 8px; margin-right: 8px;
} }
@@ -587,6 +594,12 @@ export default class HaAutomationTriggerRow extends LitElement {
display: block; display: block;
margin-bottom: 24px; margin-bottom: 24px;
} }
.selected_menu_item {
color: var(--primary-color);
}
li[role="separator"] {
border-bottom-color: var(--divider-color);
}
`, `,
]; ];
} }

View File

@@ -15,7 +15,7 @@ export class HaTagTrigger extends LitElement implements TriggerElement {
@property() public trigger!: TagTrigger; @property() public trigger!: TagTrigger;
@state() private _tags: Tag[] = []; @state() private _tags?: Tag[];
public static get defaultConfig() { public static get defaultConfig() {
return { tag_id: "" }; return { tag_id: "" };
@@ -27,14 +27,16 @@ export class HaTagTrigger extends LitElement implements TriggerElement {
} }
protected render() { protected render() {
const { tag_id } = this.trigger; if (!this._tags) {
return html``;
}
return html` return html`
<ha-select <ha-select
.label=${this.hass.localize( .label=${this.hass.localize(
"ui.panel.config.automation.editor.triggers.type.tag.label" "ui.panel.config.automation.editor.triggers.type.tag.label"
)} )}
.disabled=${this._tags.length === 0} .disabled=${this._tags.length === 0}
.value=${tag_id} .value=${this.trigger.tag_id}
@selected=${this._tagChanged} @selected=${this._tagChanged}
> >
${this._tags.map( ${this._tags.map(
@@ -49,13 +51,19 @@ export class HaTagTrigger extends LitElement implements TriggerElement {
} }
private async _fetchTags() { private async _fetchTags() {
this._tags = await fetchTags(this.hass); this._tags = (await fetchTags(this.hass)).sort((a, b) =>
this._tags.sort((a, b) =>
caseInsensitiveStringCompare(a.name || a.id, b.name || b.id) caseInsensitiveStringCompare(a.name || a.id, b.name || b.id)
); );
} }
private _tagChanged(ev) { private _tagChanged(ev) {
if (
!ev.detail.value ||
!this._tags ||
this.trigger.tag_id === ev.detail.value
) {
return;
}
fireEvent(this, "value-changed", { fireEvent(this, "value-changed", {
value: { value: {
...this.trigger, ...this.trigger,

View File

@@ -944,7 +944,18 @@ export class HaConfigDevicePage extends LitElement {
buttons.push({ buttons.push({
action: async () => { action: async () => {
const confirmed = await showConfirmationDialog(this, { const confirmed = await showConfirmationDialog(this, {
text: this.hass.localize("ui.panel.config.devices.confirm_delete"), text:
this._integrations(device, this.entries).length > 1
? this.hass.localize(
`ui.panel.config.devices.confirm_delete_integration`,
{
integration: domainToName(
this.hass.localize,
entry.domain
),
}
)
: this.hass.localize(`ui.panel.config.devices.confirm_delete`),
}); });
if (!confirmed) { if (!confirmed) {
@@ -960,7 +971,7 @@ export class HaConfigDevicePage extends LitElement {
classes: "warning", classes: "warning",
icon: mdiDelete, icon: mdiDelete,
label: label:
buttons.length > 1 this._integrations(device, this.entries).length > 1
? this.hass.localize( ? this.hass.localize(
`ui.panel.config.devices.delete_device_integration`, `ui.panel.config.devices.delete_device_integration`,
{ {

View File

@@ -872,10 +872,17 @@ export class EntityRegistrySettings extends SubscribeMixin(LitElement) {
name: this._name.trim() || null, name: this._name.trim() || null,
icon: this._icon.trim() || null, icon: this._icon.trim() || null,
area_id: this._areaId || null, area_id: this._areaId || null,
device_class: this._deviceClass || null,
new_entity_id: this._entityId.trim(), new_entity_id: this._entityId.trim(),
}; };
// Only update device class if changed by user
if (
this._deviceClass !==
(this.entry.device_class || this.entry.original_device_class)
) {
params.device_class = this._deviceClass;
}
const stateObj: HassEntity | undefined = const stateObj: HassEntity | undefined =
this.hass.states[this.entry.entity_id]; this.hass.states[this.entry.entity_id];
const domain = computeDomain(this.entry.entity_id); const domain = computeDomain(this.entry.entity_id);
@@ -1051,9 +1058,10 @@ export class EntityRegistrySettings extends SubscribeMixin(LitElement) {
.buttons { .buttons {
box-sizing: border-box; box-sizing: border-box;
display: flex; display: flex;
padding: 8px 24px 24px 24px; padding: 24px;
padding-top: 16px;
justify-content: space-between; justify-content: space-between;
padding-bottom: max(env(safe-area-inset-bottom), 16px); padding-bottom: max(env(safe-area-inset-bottom), 24px);
background-color: var(--mdc-theme-surface, #fff); background-color: var(--mdc-theme-surface, #fff);
border-top: 1px solid var(--divider-color); border-top: 1px solid var(--divider-color);
position: sticky; position: sticky;

View File

@@ -68,9 +68,10 @@ import type { HomeAssistant, Route } from "../../../types";
import { configSections } from "../ha-panel-config"; import { configSections } from "../ha-panel-config";
import "../integrations/ha-integration-overflow-menu"; import "../integrations/ha-integration-overflow-menu";
export interface StateEntity extends EntityRegistryEntry { export interface StateEntity extends Omit<EntityRegistryEntry, "id"> {
readonly?: boolean; readonly?: boolean;
selectable?: boolean; selectable?: boolean;
id?: string;
} }
export interface EntityRow extends StateEntity { export interface EntityRow extends StateEntity {
@@ -302,7 +303,7 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) {
private _filteredEntitiesAndDomains = memoize( private _filteredEntitiesAndDomains = memoize(
( (
entities: EntityRegistryEntry[], entities: StateEntity[],
devices: DeviceRegistryEntry[] | undefined, devices: DeviceRegistryEntry[] | undefined,
areas: AreaRegistryEntry[] | undefined, areas: AreaRegistryEntry[] | undefined,
stateEntities: StateEntity[], stateEntities: StateEntity[],
@@ -392,7 +393,10 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) {
result.push({ result.push({
...entry, ...entry,
entity, entity,
name: computeEntityRegistryName(this.hass!, entry), name: computeEntityRegistryName(
this.hass!,
entry as EntityRegistryEntry
),
unavailable, unavailable,
restored, restored,
area: area ? area.name : "—", area: area ? area.name : "—",

View File

@@ -36,7 +36,6 @@ const defaultFullCalendarConfig: CalendarOptions = {
selectOverlap: false, selectOverlap: false,
eventOverlap: false, eventOverlap: false,
allDaySlot: false, allDaySlot: false,
slotMinTime: "00:00:59",
height: "parent", height: "parent",
locales: allLocales, locales: allLocales,
firstDay: 1, firstDay: 1,
@@ -178,7 +177,7 @@ class HaScheduleForm extends LitElement {
}, },
eventTimeFormat: { eventTimeFormat: {
hour: useAmPm(this.hass.locale) ? "numeric" : "2-digit", hour: useAmPm(this.hass.locale) ? "numeric" : "2-digit",
minute: undefined, minute: useAmPm(this.hass.locale) ? "numeric" : "2-digit",
hour12: useAmPm(this.hass.locale), hour12: useAmPm(this.hass.locale),
meridiem: useAmPm(this.hass.locale) ? "narrow" : false, meridiem: useAmPm(this.hass.locale) ? "narrow" : false,
}, },
@@ -214,7 +213,8 @@ class HaScheduleForm extends LitElement {
} }
this[`_${day}`].forEach((item: ScheduleDay, index: number) => { this[`_${day}`].forEach((item: ScheduleDay, index: number) => {
const distance = i - currentDay; // Add 7 to 0 because we start the calendar on Monday
const distance = i - currentDay + (i === 0 ? 7 : 0);
const start = new Date(); const start = new Date();
start.setDate(start.getDate() + distance); start.setDate(start.getDate() + distance);
@@ -227,7 +227,9 @@ class HaScheduleForm extends LitElement {
end.setDate(end.getDate() + distance); end.setDate(end.getDate() + distance);
end.setHours( end.setHours(
parseInt(item.to.slice(0, 2)), parseInt(item.to.slice(0, 2)),
parseInt(item.to.slice(-2)) parseInt(item.to.slice(-2)),
0,
0
); );
events.push({ events.push({
@@ -381,6 +383,9 @@ class HaScheduleForm extends LitElement {
margin: 8px 0; margin: 8px 0;
height: 450px; height: 450px;
width: 100%; width: 100%;
-webkit-user-select: none;
-ms-user-select: none;
user-select: none;
} }
.fc-scroller { .fc-scroller {
overflow-x: visible !important; overflow-x: visible !important;

View File

@@ -210,7 +210,6 @@ export class HaScriptEditor extends KeyboardShortcutMixin(LitElement) {
"ui.panel.config.automation.editor.edit_yaml" "ui.panel.config.automation.editor.edit_yaml"
)} )}
graphic="icon" graphic="icon"
?activated=${this._mode === "yaml"}
> >
${this.hass.localize("ui.panel.config.automation.editor.edit_yaml")} ${this.hass.localize("ui.panel.config.automation.editor.edit_yaml")}
${this._mode === "yaml" ${this._mode === "yaml"
@@ -331,11 +330,11 @@ export class HaScriptEditor extends KeyboardShortcutMixin(LitElement) {
` `
: html` : html`
<div class="header"> <div class="header">
<div class="name"> <h2 id="sequence-heading" class="name">
${this.hass.localize( ${this.hass.localize(
"ui.panel.config.script.editor.sequence" "ui.panel.config.script.editor.sequence"
)} )}
</div> </h2>
<a <a
href=${documentationUrl( href=${documentationUrl(
this.hass, this.hass,
@@ -354,6 +353,8 @@ export class HaScriptEditor extends KeyboardShortcutMixin(LitElement) {
</div> </div>
<ha-automation-action <ha-automation-action
role="region"
aria-labelledby="sequence-heading"
.actions=${this._config.sequence} .actions=${this._config.sequence}
@value-changed=${this._sequenceChanged} @value-changed=${this._sequenceChanged}
.hass=${this.hass} .hass=${this.hass}
@@ -831,6 +832,9 @@ export class HaScriptEditor extends KeyboardShortcutMixin(LitElement) {
font-weight: 400; font-weight: 400;
flex: 1; flex: 1;
} }
.header a {
color: var(--secondary-text-color);
}
`, `,
]; ];
} }

View File

@@ -1793,6 +1793,7 @@
"delete": "Delete", "delete": "Delete",
"delete_confirm": "Are you sure you want to delete this automation?", "delete_confirm": "Are you sure you want to delete this automation?",
"duplicate": "Duplicate", "duplicate": "Duplicate",
"disabled": "Disabled",
"headers": { "headers": {
"toggle": "Enable/disable", "toggle": "Enable/disable",
"name": "Name", "name": "Name",
@@ -1822,6 +1823,7 @@
"run": "[%key:ui::panel::config::automation::editor::actions::run%]", "run": "[%key:ui::panel::config::automation::editor::actions::run%]",
"rename": "[%key:ui::panel::config::automation::editor::triggers::rename%]", "rename": "[%key:ui::panel::config::automation::editor::triggers::rename%]",
"show_trace": "Traces", "show_trace": "Traces",
"show_info": "Information",
"introduction": "Use automations to bring your home to life.", "introduction": "Use automations to bring your home to life.",
"default_name": "New Automation", "default_name": "New Automation",
"missing_name": "Cannot save automation without a name", "missing_name": "Cannot save automation without a name",
@@ -2576,7 +2578,7 @@
"download_diagnostics": "Download diagnostics", "download_diagnostics": "Download diagnostics",
"download_diagnostics_integration": "Download {integration} diagnostics", "download_diagnostics_integration": "Download {integration} diagnostics",
"delete_device": "Delete", "delete_device": "Delete",
"delete_device_integration": "Remove {integration} from device", "delete_device_integration": "Remove device from {integration}",
"type": { "type": {
"device_heading": "Device", "device_heading": "Device",
"device": "device", "device": "device",
@@ -2653,6 +2655,7 @@
}, },
"delete": "Delete", "delete": "Delete",
"confirm_delete": "Are you sure you want to delete this device?", "confirm_delete": "Are you sure you want to delete this device?",
"confirm_delete_integration": "Are you sure you want to remove this device from {integration}?",
"picker": { "picker": {
"search": "Search devices", "search": "Search devices",
"filter": { "filter": {