mirror of
https://github.com/home-assistant/frontend.git
synced 2025-07-19 07:16:39 +00:00
Add play media action (#11702)
Co-authored-by: Zack Barett <zackbarett@hey.com> Co-authored-by: Paulus Schoutsen <balloob@gmail.com>
This commit is contained in:
parent
cbd0ef6b65
commit
5c5459bcaf
@ -3,10 +3,20 @@ import { html, css, LitElement, TemplateResult } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import "../../../../src/components/ha-card";
|
||||
import { describeAction } from "../../../../src/data/script_i18n";
|
||||
import { getEntity } from "../../../../src/fake_data/entity";
|
||||
import { provideHass } from "../../../../src/fake_data/provide_hass";
|
||||
import { HomeAssistant } from "../../../../src/types";
|
||||
|
||||
const actions = [
|
||||
const ENTITIES = [
|
||||
getEntity("scene", "kitchen_morning", "scening", {
|
||||
friendly_name: "Kitchen Morning",
|
||||
}),
|
||||
getEntity("media_player", "kitchen", "playing", {
|
||||
friendly_name: "Sonos Kitchen",
|
||||
}),
|
||||
];
|
||||
|
||||
const ACTIONS = [
|
||||
{ wait_template: "{{ true }}", alias: "Something with an alias" },
|
||||
{ delay: "0:05" },
|
||||
{ wait_template: "{{ true }}" },
|
||||
@ -19,8 +29,20 @@ const actions = [
|
||||
device_id: "abcdefgh",
|
||||
domain: "plex",
|
||||
entity_id: "media_player.kitchen",
|
||||
type: "turn_on",
|
||||
},
|
||||
{ scene: "scene.kitchen_morning" },
|
||||
{
|
||||
service: "scene.turn_on",
|
||||
target: { entity_id: "scene.kitchen_morning" },
|
||||
metadata: {},
|
||||
},
|
||||
{
|
||||
service: "media_player.play_media",
|
||||
target: { entity_id: "media_player.kitchen" },
|
||||
data: { media_content_id: "", media_content_type: "" },
|
||||
metadata: { title: "Happy Song" },
|
||||
},
|
||||
{
|
||||
wait_for_trigger: [
|
||||
{
|
||||
@ -52,7 +74,7 @@ export class DemoAutomationDescribeAction extends LitElement {
|
||||
}
|
||||
return html`
|
||||
<ha-card header="Actions">
|
||||
${actions.map(
|
||||
${ACTIONS.map(
|
||||
(conf) => html`
|
||||
<div class="action">
|
||||
<span>${describeAction(this.hass, conf as any)}</span>
|
||||
@ -68,6 +90,7 @@ export class DemoAutomationDescribeAction extends LitElement {
|
||||
super.firstUpdated(changedProps);
|
||||
const hass = provideHass(this);
|
||||
hass.updateTranslations(null, "en");
|
||||
hass.addEntities(ENTITIES);
|
||||
}
|
||||
|
||||
static get styles() {
|
||||
|
@ -14,7 +14,7 @@ import { HaDelayAction } from "../../../../src/panels/config/automation/action/t
|
||||
import { HaDeviceAction } from "../../../../src/panels/config/automation/action/types/ha-automation-action-device_id";
|
||||
import { HaEventAction } from "../../../../src/panels/config/automation/action/types/ha-automation-action-event";
|
||||
import { HaRepeatAction } from "../../../../src/panels/config/automation/action/types/ha-automation-action-repeat";
|
||||
import { HaSceneAction } from "../../../../src/panels/config/automation/action/types/ha-automation-action-scene";
|
||||
import { HaSceneAction } from "../../../../src/panels/config/automation/action/types/ha-automation-action-activate_scene";
|
||||
import { HaServiceAction } from "../../../../src/panels/config/automation/action/types/ha-automation-action-service";
|
||||
import { HaWaitForTriggerAction } from "../../../../src/panels/config/automation/action/types/ha-automation-action-wait_for_trigger";
|
||||
import { HaWaitAction } from "../../../../src/panels/config/automation/action/types/ha-automation-action-wait_template";
|
||||
|
@ -36,6 +36,8 @@ const SCHEMAS: {
|
||||
text_multiline: "Text Multiline",
|
||||
object: "Object",
|
||||
select: "Select",
|
||||
icon: "Icon",
|
||||
media: "Media",
|
||||
},
|
||||
schema: [
|
||||
{ name: "addon", selector: { addon: {} } },
|
||||
@ -67,6 +69,12 @@ const SCHEMAS: {
|
||||
icon: {},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "media",
|
||||
selector: {
|
||||
media: {},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
|
@ -12,6 +12,100 @@ import { mockEntityRegistry } from "../../../../demo/src/stubs/entity_registry";
|
||||
import { mockDeviceRegistry } from "../../../../demo/src/stubs/device_registry";
|
||||
import { mockAreaRegistry } from "../../../../demo/src/stubs/area_registry";
|
||||
import { mockHassioSupervisor } from "../../../../demo/src/stubs/hassio_supervisor";
|
||||
import { getEntity } from "../../../../src/fake_data/entity";
|
||||
import { ProvideHassElement } from "../../../../src/mixins/provide-hass-lit-mixin";
|
||||
import { showDialog } from "../../../../src/dialogs/make-dialog-manager";
|
||||
|
||||
const ENTITIES = [
|
||||
getEntity("alarm_control_panel", "alarm", "disarmed", {
|
||||
friendly_name: "Alarm",
|
||||
}),
|
||||
getEntity("media_player", "livingroom", "playing", {
|
||||
friendly_name: "Livingroom",
|
||||
}),
|
||||
getEntity("media_player", "lounge", "idle", {
|
||||
friendly_name: "Lounge",
|
||||
supported_features: 444983,
|
||||
}),
|
||||
getEntity("light", "bedroom", "on", {
|
||||
friendly_name: "Bedroom",
|
||||
}),
|
||||
getEntity("switch", "coffee", "off", {
|
||||
friendly_name: "Coffee",
|
||||
}),
|
||||
];
|
||||
|
||||
const DEVICES = [
|
||||
{
|
||||
area_id: "bedroom",
|
||||
configuration_url: null,
|
||||
config_entries: ["config_entry_1"],
|
||||
connections: [],
|
||||
disabled_by: null,
|
||||
entry_type: null,
|
||||
id: "device_1",
|
||||
identifiers: [["demo", "volume1"] as [string, string]],
|
||||
manufacturer: null,
|
||||
model: null,
|
||||
name_by_user: null,
|
||||
name: "Dishwasher",
|
||||
sw_version: null,
|
||||
hw_version: null,
|
||||
via_device_id: null,
|
||||
},
|
||||
{
|
||||
area_id: "backyard",
|
||||
configuration_url: null,
|
||||
config_entries: ["config_entry_2"],
|
||||
connections: [],
|
||||
disabled_by: null,
|
||||
entry_type: null,
|
||||
id: "device_2",
|
||||
identifiers: [["demo", "pwm1"] as [string, string]],
|
||||
manufacturer: null,
|
||||
model: null,
|
||||
name_by_user: null,
|
||||
name: "Lamp",
|
||||
sw_version: null,
|
||||
hw_version: null,
|
||||
via_device_id: null,
|
||||
},
|
||||
{
|
||||
area_id: null,
|
||||
configuration_url: null,
|
||||
config_entries: ["config_entry_3"],
|
||||
connections: [],
|
||||
disabled_by: null,
|
||||
entry_type: null,
|
||||
id: "device_3",
|
||||
identifiers: [["demo", "pwm1"] as [string, string]],
|
||||
manufacturer: null,
|
||||
model: null,
|
||||
name_by_user: "User name",
|
||||
name: "Technical name",
|
||||
sw_version: null,
|
||||
hw_version: null,
|
||||
via_device_id: null,
|
||||
},
|
||||
];
|
||||
|
||||
const AREAS = [
|
||||
{
|
||||
area_id: "backyard",
|
||||
name: "Backyard",
|
||||
picture: null,
|
||||
},
|
||||
{
|
||||
area_id: "bedroom",
|
||||
name: "Bedroom",
|
||||
picture: null,
|
||||
},
|
||||
{
|
||||
area_id: "livingroom",
|
||||
name: "Livingroom",
|
||||
picture: null,
|
||||
},
|
||||
];
|
||||
|
||||
const SCHEMAS: {
|
||||
name: string;
|
||||
@ -73,13 +167,14 @@ const SCHEMAS: {
|
||||
selector: { select: { options: ["Option 1", "Option 2"] } },
|
||||
},
|
||||
icon: { name: "Icon", selector: { icon: {} } },
|
||||
media: { name: "Media", selector: { media: {} } },
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
@customElement("demo-components-ha-selector")
|
||||
class DemoHaSelector extends LitElement {
|
||||
@state() private hass!: HomeAssistant;
|
||||
class DemoHaSelector extends LitElement implements ProvideHassElement {
|
||||
@state() public hass!: HomeAssistant;
|
||||
|
||||
private data = SCHEMAS.map(() => ({}));
|
||||
|
||||
@ -88,12 +183,130 @@ class DemoHaSelector extends LitElement {
|
||||
const hass = provideHass(this);
|
||||
hass.updateTranslations(null, "en");
|
||||
hass.updateTranslations("config", "en");
|
||||
hass.addEntities(ENTITIES);
|
||||
mockEntityRegistry(hass);
|
||||
mockDeviceRegistry(hass);
|
||||
mockAreaRegistry(hass);
|
||||
mockDeviceRegistry(hass, DEVICES);
|
||||
mockAreaRegistry(hass, AREAS);
|
||||
mockHassioSupervisor(hass);
|
||||
hass.mockWS("auth/sign_path", (params) => params);
|
||||
hass.mockWS("media_player/browse_media", this._browseMedia);
|
||||
}
|
||||
|
||||
public provideHass(el) {
|
||||
el.hass = this.hass;
|
||||
}
|
||||
|
||||
public connectedCallback() {
|
||||
super.connectedCallback();
|
||||
this.addEventListener("show-dialog", this._dialogManager);
|
||||
}
|
||||
|
||||
public disconnectedCallback() {
|
||||
super.disconnectedCallback();
|
||||
this.removeEventListener("show-dialog", this._dialogManager);
|
||||
}
|
||||
|
||||
private _browseMedia = ({ media_content_id }) => {
|
||||
if (media_content_id === undefined) {
|
||||
return {
|
||||
title: "Media",
|
||||
media_class: "directory",
|
||||
media_content_type: "",
|
||||
media_content_id: "media-source://media_source/local/.",
|
||||
can_play: false,
|
||||
can_expand: true,
|
||||
children_media_class: "directory",
|
||||
thumbnail: null,
|
||||
children: [
|
||||
{
|
||||
title: "Misc",
|
||||
media_class: "directory",
|
||||
media_content_type: "",
|
||||
media_content_id: "media-source://media_source/local/misc",
|
||||
can_play: false,
|
||||
can_expand: true,
|
||||
children_media_class: null,
|
||||
thumbnail: null,
|
||||
},
|
||||
{
|
||||
title: "Movies",
|
||||
media_class: "directory",
|
||||
media_content_type: "",
|
||||
media_content_id: "media-source://media_source/local/movies",
|
||||
can_play: true,
|
||||
can_expand: true,
|
||||
children_media_class: "movie",
|
||||
thumbnail: null,
|
||||
},
|
||||
{
|
||||
title: "Music",
|
||||
media_class: "album",
|
||||
media_content_type: "",
|
||||
media_content_id: "media-source://media_source/local/music",
|
||||
can_play: false,
|
||||
can_expand: true,
|
||||
children_media_class: "music",
|
||||
thumbnail: "/images/album_cover_2.jpg",
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
return {
|
||||
title: "Subfolder",
|
||||
media_class: "directory",
|
||||
media_content_type: "",
|
||||
media_content_id: "media-source://media_source/local/sub",
|
||||
can_play: false,
|
||||
can_expand: true,
|
||||
children_media_class: "directory",
|
||||
thumbnail: null,
|
||||
children: [
|
||||
{
|
||||
title: "audio.mp3",
|
||||
media_class: "music",
|
||||
media_content_type: "audio/mpeg",
|
||||
media_content_id: "media-source://media_source/local/audio.mp3",
|
||||
can_play: true,
|
||||
can_expand: false,
|
||||
children_media_class: null,
|
||||
thumbnail: "/images/album_cover.jpg",
|
||||
},
|
||||
{
|
||||
title: "image.jpg",
|
||||
media_class: "image",
|
||||
media_content_type: "image/jpeg",
|
||||
media_content_id: "media-source://media_source/local/image.jpg",
|
||||
can_play: true,
|
||||
can_expand: false,
|
||||
children_media_class: null,
|
||||
thumbnail: null,
|
||||
},
|
||||
{
|
||||
title: "movie.mp4",
|
||||
media_class: "movie",
|
||||
media_content_type: "image/jpeg",
|
||||
media_content_id: "media-source://media_source/local/movie.mp4",
|
||||
can_play: true,
|
||||
can_expand: false,
|
||||
children_media_class: null,
|
||||
thumbnail: null,
|
||||
},
|
||||
],
|
||||
};
|
||||
};
|
||||
|
||||
private _dialogManager = (e) => {
|
||||
const { dialogTag, dialogImport, dialogParams, addHistory } = e.detail;
|
||||
showDialog(
|
||||
this,
|
||||
this.shadowRoot!,
|
||||
dialogTag,
|
||||
dialogParams,
|
||||
dialogImport,
|
||||
addHistory
|
||||
);
|
||||
};
|
||||
|
||||
protected render(): TemplateResult {
|
||||
return html`
|
||||
${SCHEMAS.map((info, idx) => {
|
||||
@ -132,7 +345,6 @@ class DemoHaSelector extends LitElement {
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
paper-input,
|
||||
ha-selector {
|
||||
width: 60;
|
||||
}
|
||||
|
264
src/components/ha-selector/ha-selector-media.ts
Normal file
264
src/components/ha-selector/ha-selector-media.ts
Normal file
@ -0,0 +1,264 @@
|
||||
import { mdiPlayBox, mdiPlus } from "@mdi/js";
|
||||
import { css, CSSResultGroup, html, LitElement, PropertyValues } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { classMap } from "lit/directives/class-map";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import { supportsFeature } from "../../common/entity/supports-feature";
|
||||
import { getSignedPath } from "../../data/auth";
|
||||
import {
|
||||
MediaClassBrowserSettings,
|
||||
MediaPickedEvent,
|
||||
SUPPORT_BROWSE_MEDIA,
|
||||
} from "../../data/media-player";
|
||||
import type { MediaSelector, MediaSelectorValue } from "../../data/selector";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import "../ha-alert";
|
||||
import "../ha-form/ha-form";
|
||||
import type { HaFormSchema } from "../ha-form/types";
|
||||
import { showMediaBrowserDialog } from "../media-player/show-media-browser-dialog";
|
||||
|
||||
const MANUAL_SCHEMA = [
|
||||
{ name: "media_content_id", required: false, selector: { text: {} } },
|
||||
{ name: "media_content_type", required: false, selector: { text: {} } },
|
||||
];
|
||||
|
||||
@customElement("ha-selector-media")
|
||||
export class HaMediaSelector extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public selector!: MediaSelector;
|
||||
|
||||
@property({ attribute: false }) public value?: MediaSelectorValue;
|
||||
|
||||
@property() public label?: string;
|
||||
|
||||
@property({ type: Boolean, reflect: true }) public disabled = false;
|
||||
|
||||
@state() private _thumbnailUrl?: string | null;
|
||||
|
||||
willUpdate(changedProps: PropertyValues<this>) {
|
||||
if (changedProps.has("value")) {
|
||||
const thumbnail = this.value?.metadata?.thumbnail;
|
||||
const oldThumbnail = (changedProps.get("value") as this["value"])
|
||||
?.metadata?.thumbnail;
|
||||
if (thumbnail === oldThumbnail) {
|
||||
return;
|
||||
}
|
||||
if (thumbnail && thumbnail.startsWith("/")) {
|
||||
this._thumbnailUrl = undefined;
|
||||
// Thumbnails served by local API require authentication
|
||||
getSignedPath(this.hass, thumbnail).then((signedPath) => {
|
||||
this._thumbnailUrl = signedPath.path;
|
||||
});
|
||||
} else {
|
||||
this._thumbnailUrl = thumbnail;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected render() {
|
||||
const stateObj = this.value?.entity_id
|
||||
? this.hass.states[this.value.entity_id]
|
||||
: undefined;
|
||||
|
||||
const supportsBrowse =
|
||||
!this.value?.entity_id ||
|
||||
(stateObj && supportsFeature(stateObj, SUPPORT_BROWSE_MEDIA));
|
||||
|
||||
return html`<ha-entity-picker
|
||||
.hass=${this.hass}
|
||||
.value=${this.value?.entity_id}
|
||||
.label=${this.label ||
|
||||
this.hass.localize("ui.components.selectors.media.pick_media_player")}
|
||||
.disabled=${this.disabled}
|
||||
include-domains='["media_player"]'
|
||||
allow-custom-entity
|
||||
@value-changed=${this._entityChanged}
|
||||
></ha-entity-picker>
|
||||
${!supportsBrowse
|
||||
? html`<ha-alert>
|
||||
${this.hass.localize(
|
||||
"ui.components.selectors.media.browse_not_supported"
|
||||
)}
|
||||
</ha-alert>
|
||||
<ha-form
|
||||
.hass=${this.hass}
|
||||
.data=${this.value}
|
||||
.schema=${MANUAL_SCHEMA}
|
||||
.computeLabel=${this._computeLabelCallback}
|
||||
></ha-form>`
|
||||
: html`<ha-card
|
||||
outlined
|
||||
@click=${this._pickMedia}
|
||||
class=${this.disabled || !this.value?.entity_id ? "disabled" : ""}
|
||||
>
|
||||
<div
|
||||
class="thumbnail ${classMap({
|
||||
portrait:
|
||||
!!this.value?.metadata?.media_class &&
|
||||
MediaClassBrowserSettings[
|
||||
this.value.metadata.children_media_class ||
|
||||
this.value.metadata.media_class
|
||||
].thumbnail_ratio === "portrait",
|
||||
})}"
|
||||
>
|
||||
${this.value?.metadata?.thumbnail
|
||||
? html`
|
||||
<div
|
||||
class="${classMap({
|
||||
"centered-image":
|
||||
!!this.value.metadata.media_class &&
|
||||
["app", "directory"].includes(
|
||||
this.value.metadata.media_class
|
||||
),
|
||||
})}
|
||||
image"
|
||||
style=${this._thumbnailUrl
|
||||
? `background-image: url(${this._thumbnailUrl});`
|
||||
: ""}
|
||||
></div>
|
||||
`
|
||||
: html`
|
||||
<div class="icon-holder image">
|
||||
<ha-svg-icon
|
||||
class="folder"
|
||||
.path=${!this.value?.media_content_id
|
||||
? mdiPlus
|
||||
: this.value?.metadata?.media_class
|
||||
? MediaClassBrowserSettings[
|
||||
this.value.metadata.media_class === "directory"
|
||||
? this.value.metadata.children_media_class ||
|
||||
this.value.metadata.media_class
|
||||
: this.value.metadata.media_class
|
||||
].icon
|
||||
: mdiPlayBox}
|
||||
></ha-svg-icon>
|
||||
</div>
|
||||
`}
|
||||
</div>
|
||||
<div class="title">
|
||||
${!this.value?.media_content_id
|
||||
? this.hass.localize("ui.components.selectors.media.pick_media")
|
||||
: this.value.metadata?.title || this.value.media_content_id}
|
||||
</div>
|
||||
</ha-card>`}`;
|
||||
}
|
||||
|
||||
private _computeLabelCallback = (schema: HaFormSchema): string =>
|
||||
this.hass.localize(`ui.components.selectors.media.${schema.name}`);
|
||||
|
||||
private _entityChanged(ev: CustomEvent) {
|
||||
ev.stopPropagation();
|
||||
fireEvent(this, "value-changed", {
|
||||
value: {
|
||||
entity_id: ev.detail.value,
|
||||
media_content_id: "",
|
||||
media_content_type: "",
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
private _pickMedia() {
|
||||
showMediaBrowserDialog(this, {
|
||||
action: "pick",
|
||||
entityId: this.value!.entity_id!,
|
||||
navigateIds: this.value!.metadata?.navigateIds,
|
||||
mediaPickedCallback: (pickedMedia: MediaPickedEvent) => {
|
||||
fireEvent(this, "value-changed", {
|
||||
value: {
|
||||
...this.value,
|
||||
media_content_id: pickedMedia.item.media_content_id,
|
||||
media_content_type: pickedMedia.item.media_content_type,
|
||||
metadata: {
|
||||
title: pickedMedia.item.title,
|
||||
thumbnail: pickedMedia.item.thumbnail,
|
||||
media_class: pickedMedia.item.media_class,
|
||||
children_media_class: pickedMedia.item.children_media_class,
|
||||
navigateIds: pickedMedia.navigateIds?.map((id) => ({
|
||||
media_content_type: id.media_content_type,
|
||||
media_content_id: id.media_content_id,
|
||||
})),
|
||||
},
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return css`
|
||||
ha-entity-picker {
|
||||
display: block;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
mwc-button {
|
||||
margin-top: 8px;
|
||||
}
|
||||
ha-alert {
|
||||
display: block;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
ha-card {
|
||||
position: relative;
|
||||
width: 200px;
|
||||
box-sizing: border-box;
|
||||
cursor: pointer;
|
||||
}
|
||||
ha-card.disabled {
|
||||
pointer-events: none;
|
||||
color: var(--disabled-text-color);
|
||||
}
|
||||
ha-card .thumbnail {
|
||||
width: 100%;
|
||||
position: relative;
|
||||
box-sizing: border-box;
|
||||
transition: padding-bottom 0.1s ease-out;
|
||||
padding-bottom: 100%;
|
||||
}
|
||||
ha-card .thumbnail.portrait {
|
||||
padding-bottom: 150%;
|
||||
}
|
||||
ha-card .image {
|
||||
border-radius: 3px 3px 0 0;
|
||||
}
|
||||
.folder {
|
||||
--mdc-icon-size: calc(var(--media-browse-item-size, 175px) * 0.4);
|
||||
}
|
||||
.title {
|
||||
font-size: 16px;
|
||||
padding-top: 16px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
margin-bottom: 16px;
|
||||
padding-left: 16px;
|
||||
padding-right: 4px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.image {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
background-size: cover;
|
||||
background-repeat: no-repeat;
|
||||
background-position: center;
|
||||
}
|
||||
.centered-image {
|
||||
margin: 0 8px;
|
||||
background-size: contain;
|
||||
}
|
||||
.icon-holder {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-selector-media": HaMediaSelector;
|
||||
}
|
||||
}
|
@ -18,6 +18,7 @@ import "./ha-selector-target";
|
||||
import "./ha-selector-text";
|
||||
import "./ha-selector-time";
|
||||
import "./ha-selector-icon";
|
||||
import "./ha-selector-media";
|
||||
|
||||
@customElement("ha-selector")
|
||||
export class HaSelector extends LitElement {
|
||||
|
@ -28,10 +28,10 @@ class DialogMediaPlayerBrowse extends LitElement {
|
||||
|
||||
public showDialog(params: MediaPlayerBrowseDialogParams): void {
|
||||
this._params = params;
|
||||
this._navigateIds = [
|
||||
this._navigateIds = params.navigateIds || [
|
||||
{
|
||||
media_content_id: this._params.mediaContentId,
|
||||
media_content_type: this._params.mediaContentType,
|
||||
media_content_id: undefined,
|
||||
media_content_type: undefined,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
@ -11,12 +11,26 @@ import {
|
||||
getCloudTtsLanguages,
|
||||
getCloudTtsSupportedGenders,
|
||||
} from "../../data/cloud/tts";
|
||||
import { MediaPlayerBrowseAction } from "../../data/media-player";
|
||||
import {
|
||||
MediaPlayerBrowseAction,
|
||||
MediaPlayerItem,
|
||||
} from "../../data/media-player";
|
||||
import { HomeAssistant } from "../../types";
|
||||
import "../ha-textarea";
|
||||
import { buttonLinkStyle } from "../../resources/styles";
|
||||
import { showAlertDialog } from "../../dialogs/generic/show-dialog-box";
|
||||
import { LocalStorage } from "../../common/decorators/local-storage";
|
||||
import { stopPropagation } from "../../common/dom/stop_propagation";
|
||||
|
||||
export interface TtsMediaPickedEvent {
|
||||
item: MediaPlayerItem;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HASSDomEvents {
|
||||
"tts-picked": TtsMediaPickedEvent;
|
||||
}
|
||||
}
|
||||
|
||||
@customElement("ha-browse-media-tts")
|
||||
class BrowseMediaTTS extends LitElement {
|
||||
@ -32,40 +46,55 @@ class BrowseMediaTTS extends LitElement {
|
||||
|
||||
@state() private _cloudTTSInfo?: CloudTTSInfo;
|
||||
|
||||
@LocalStorage("cloudTtsTryMessage", false, false) private _message!: string;
|
||||
@LocalStorage("cloudTtsTryMessage", true, false) private _message!: string;
|
||||
|
||||
protected render() {
|
||||
return html`
|
||||
return html`<ha-card>
|
||||
<div class="card-content">
|
||||
<ha-textarea
|
||||
autogrow
|
||||
.label=${this.hass.localize("ui.panel.media-browser.tts.message")}
|
||||
.label=${this.hass.localize(
|
||||
"ui.components.media-browser.tts.message"
|
||||
)}
|
||||
.value=${this._message ||
|
||||
this.hass.localize("ui.panel.media-browser.tts.example_message", {
|
||||
this.hass.localize(
|
||||
"ui.components.media-browser.tts.example_message",
|
||||
{
|
||||
name: this.hass.user?.name || "",
|
||||
})}
|
||||
}
|
||||
)}
|
||||
>
|
||||
</ha-textarea>
|
||||
${this._cloudDefaultOptions ? this._renderCloudOptions() : ""}
|
||||
<div class="actions">
|
||||
</div>
|
||||
<div class="card-actions">
|
||||
${this._cloudDefaultOptions &&
|
||||
(this._cloudDefaultOptions![0] !== this._cloudOptions![0] ||
|
||||
this._cloudDefaultOptions![1] !== this._cloudOptions![1])
|
||||
? html`
|
||||
<button class="link" @click=${this._storeDefaults}>
|
||||
${this.hass.localize(
|
||||
"ui.panel.media-browser.tts.set_as_default"
|
||||
"ui.components.media-browser.tts.set_as_default"
|
||||
)}
|
||||
</button>
|
||||
`
|
||||
: html`<span></span>`}
|
||||
<mwc-button raised label="Say" @click=${this._ttsClicked}></mwc-button>
|
||||
|
||||
<mwc-button @click=${this._ttsClicked}>
|
||||
${this.hass.localize(
|
||||
`ui.components.media-browser.tts.action_${this.action}`
|
||||
)}
|
||||
</mwc-button>
|
||||
</div>
|
||||
`;
|
||||
</ha-card> `;
|
||||
}
|
||||
|
||||
private _renderCloudOptions() {
|
||||
if (!this._cloudTTSInfo || !this._cloudOptions) {
|
||||
return "";
|
||||
}
|
||||
const languages = this.getLanguages(this._cloudTTSInfo);
|
||||
const selectedVoice = this._cloudOptions!;
|
||||
const selectedVoice = this._cloudOptions;
|
||||
const genders = this.getSupportedGenders(
|
||||
selectedVoice[0],
|
||||
this._cloudTTSInfo,
|
||||
@ -77,9 +106,12 @@ class BrowseMediaTTS extends LitElement {
|
||||
<mwc-select
|
||||
fixedMenuPosition
|
||||
naturalMenuWidth
|
||||
.label=${this.hass.localize("ui.panel.media-browser.tts.language")}
|
||||
.label=${this.hass.localize(
|
||||
"ui.components.media-browser.tts.language"
|
||||
)}
|
||||
.value=${selectedVoice[0]}
|
||||
@selected=${this._handleLanguageChange}
|
||||
@closed=${stopPropagation}
|
||||
>
|
||||
${languages.map(
|
||||
([key, label]) =>
|
||||
@ -90,9 +122,10 @@ class BrowseMediaTTS extends LitElement {
|
||||
<mwc-select
|
||||
fixedMenuPosition
|
||||
naturalMenuWidth
|
||||
.label=${this.hass.localize("ui.panel.media-browser.tts.gender")}
|
||||
.label=${this.hass.localize("ui.components.media-browser.tts.gender")}
|
||||
.value=${selectedVoice[1]}
|
||||
@selected=${this._handleGenderChange}
|
||||
@closed=${stopPropagation}
|
||||
>
|
||||
${genders.map(
|
||||
([key, label]) =>
|
||||
@ -106,6 +139,37 @@ class BrowseMediaTTS extends LitElement {
|
||||
protected override willUpdate(changedProps: PropertyValues): void {
|
||||
super.willUpdate(changedProps);
|
||||
|
||||
if (changedProps.has("item")) {
|
||||
if (this.item.media_content_id) {
|
||||
const params = new URLSearchParams(
|
||||
this.item.media_content_id.split("?")[1]
|
||||
);
|
||||
const message = params.get("message");
|
||||
const language = params.get("language");
|
||||
const gender = params.get("gender");
|
||||
if (message) {
|
||||
this._message = message;
|
||||
}
|
||||
if (language && gender) {
|
||||
this._cloudOptions = [language, gender];
|
||||
}
|
||||
}
|
||||
|
||||
if (this.isCloudItem && !this._cloudTTSInfo) {
|
||||
getCloudTTSInfo(this.hass).then((info) => {
|
||||
this._cloudTTSInfo = info;
|
||||
});
|
||||
fetchCloudStatus(this.hass).then((status) => {
|
||||
if (status.logged_in) {
|
||||
this._cloudDefaultOptions = status.prefs.tts_default_voice;
|
||||
if (!this._cloudOptions) {
|
||||
this._cloudOptions = { ...this._cloudDefaultOptions };
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (changedProps.has("message")) {
|
||||
return;
|
||||
}
|
||||
@ -133,30 +197,12 @@ class BrowseMediaTTS extends LitElement {
|
||||
this._cloudOptions = [this._cloudOptions![0], ev.target.value];
|
||||
}
|
||||
|
||||
protected updated(changedProps: PropertyValues): void {
|
||||
super.updated(changedProps);
|
||||
|
||||
if (changedProps.has("item")) {
|
||||
if (this.isCloudItem && !this._cloudTTSInfo) {
|
||||
getCloudTTSInfo(this.hass).then((info) => {
|
||||
this._cloudTTSInfo = info;
|
||||
});
|
||||
fetchCloudStatus(this.hass).then((status) => {
|
||||
if (status.logged_in) {
|
||||
this._cloudDefaultOptions = status.prefs.tts_default_voice;
|
||||
this._cloudOptions = { ...this._cloudDefaultOptions };
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private getLanguages = memoizeOne(getCloudTtsLanguages);
|
||||
|
||||
private getSupportedGenders = memoizeOne(getCloudTtsSupportedGenders);
|
||||
|
||||
private get isCloudItem(): boolean {
|
||||
return this.item.media_content_id === "media-source://tts/cloud";
|
||||
return this.item.media_content_id.startsWith("media-source://tts/cloud");
|
||||
}
|
||||
|
||||
private async _ttsClicked(): Promise<void> {
|
||||
@ -169,9 +215,12 @@ class BrowseMediaTTS extends LitElement {
|
||||
query.append("language", this._cloudOptions[0]);
|
||||
query.append("gender", this._cloudOptions[1]);
|
||||
}
|
||||
item.media_content_id += `?${query.toString()}`;
|
||||
item.media_content_id = `${
|
||||
item.media_content_id.split("?")[0]
|
||||
}?${query.toString()}`;
|
||||
item.can_play = true;
|
||||
fireEvent(this, "media-picked", { item });
|
||||
item.title = message;
|
||||
fireEvent(this, "tts-picked", { item });
|
||||
}
|
||||
|
||||
private async _storeDefaults() {
|
||||
@ -185,7 +234,7 @@ class BrowseMediaTTS extends LitElement {
|
||||
this._cloudDefaultOptions = oldDefaults;
|
||||
showAlertDialog(this, {
|
||||
text: this.hass.localize(
|
||||
"ui.panel.media-browser.tts.faild_to_store_defaults",
|
||||
"ui.components.media-browser.tts.faild_to_store_defaults",
|
||||
{ error: err.message || err }
|
||||
),
|
||||
});
|
||||
@ -210,15 +259,16 @@ class BrowseMediaTTS extends LitElement {
|
||||
.cloud-options mwc-select {
|
||||
width: 48%;
|
||||
}
|
||||
|
||||
.actions {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin-top: 16px;
|
||||
ha-textarea {
|
||||
width: 100%;
|
||||
}
|
||||
button.link {
|
||||
color: var(--primary-color);
|
||||
}
|
||||
.card-actions {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
@ -49,7 +49,7 @@ import "../ha-svg-icon";
|
||||
import "../ha-fab";
|
||||
import { browseLocalMediaPlayer } from "../../data/media_source";
|
||||
import { isTTSMediaSource } from "../../data/tts";
|
||||
import "./ha-browse-media-tts";
|
||||
import { TtsMediaPickedEvent } from "./ha-browse-media-tts";
|
||||
|
||||
declare global {
|
||||
interface HASSDomEvents {
|
||||
@ -260,6 +260,7 @@ export class HaMediaPlayerBrowse extends LitElement {
|
||||
.item=${currentItem}
|
||||
.hass=${this.hass}
|
||||
.action=${this.action}
|
||||
@tts-picked=${this._ttsPicked}
|
||||
></ha-browse-media-tts>
|
||||
`
|
||||
: !currentItem.children?.length
|
||||
@ -562,7 +563,17 @@ export class HaMediaPlayerBrowse extends LitElement {
|
||||
}
|
||||
|
||||
private _runAction(item: MediaPlayerItem): void {
|
||||
fireEvent(this, "media-picked", { item });
|
||||
fireEvent(this, "media-picked", { item, navigateIds: this.navigateIds });
|
||||
}
|
||||
|
||||
private _ttsPicked(ev: CustomEvent<TtsMediaPickedEvent>): void {
|
||||
ev.stopPropagation();
|
||||
const navigateIds = this.navigateIds.slice(0, -1);
|
||||
navigateIds.push(ev.detail.item);
|
||||
fireEvent(this, "media-picked", {
|
||||
...ev.detail,
|
||||
navigateIds,
|
||||
});
|
||||
}
|
||||
|
||||
private async _childClicked(ev: MouseEvent): Promise<void> {
|
||||
|
@ -3,13 +3,13 @@ import {
|
||||
MediaPickedEvent,
|
||||
MediaPlayerBrowseAction,
|
||||
} from "../../data/media-player";
|
||||
import { MediaPlayerItemId } from "./ha-media-player-browse";
|
||||
|
||||
export interface MediaPlayerBrowseDialogParams {
|
||||
action: MediaPlayerBrowseAction;
|
||||
entityId: string;
|
||||
mediaPickedCallback: (pickedMedia: MediaPickedEvent) => void;
|
||||
mediaContentId?: string;
|
||||
mediaContentType?: string;
|
||||
navigateIds?: MediaPlayerItemId[];
|
||||
}
|
||||
|
||||
export const showMediaBrowserDialog = (
|
||||
|
@ -28,6 +28,7 @@ import type {
|
||||
HassEntityBase,
|
||||
} from "home-assistant-js-websocket";
|
||||
import { supportsFeature } from "../common/entity/supports-feature";
|
||||
import { MediaPlayerItemId } from "../components/media-player/ha-media-player-browse";
|
||||
import type { HomeAssistant } from "../types";
|
||||
import { UNAVAILABLE_STATES } from "./entity";
|
||||
|
||||
@ -147,6 +148,7 @@ export const MediaClassBrowserSettings: {
|
||||
|
||||
export interface MediaPickedEvent {
|
||||
item: MediaPlayerItem;
|
||||
navigateIds: MediaPlayerItemId[];
|
||||
}
|
||||
|
||||
export interface MediaPlayerThumbnail {
|
||||
|
@ -3,6 +3,17 @@ import {
|
||||
HassEntityBase,
|
||||
HassServiceTarget,
|
||||
} from "home-assistant-js-websocket";
|
||||
import {
|
||||
object,
|
||||
optional,
|
||||
string,
|
||||
union,
|
||||
array,
|
||||
assign,
|
||||
literal,
|
||||
is,
|
||||
Describe,
|
||||
} from "superstruct";
|
||||
import { computeObjectId } from "../common/entity/compute_object_id";
|
||||
import { navigate } from "../common/navigate";
|
||||
import { HomeAssistant } from "../types";
|
||||
@ -12,6 +23,48 @@ import { BlueprintInput } from "./blueprint";
|
||||
export const MODES = ["single", "restart", "queued", "parallel"] as const;
|
||||
export const MODES_MAX = ["queued", "parallel"];
|
||||
|
||||
export const baseActionStruct = object({
|
||||
alias: optional(string()),
|
||||
});
|
||||
|
||||
const targetStruct = object({
|
||||
entity_id: optional(union([string(), array(string())])),
|
||||
device_id: optional(union([string(), array(string())])),
|
||||
area_id: optional(union([string(), array(string())])),
|
||||
});
|
||||
|
||||
export const serviceActionStruct: Describe<ServiceAction> = assign(
|
||||
baseActionStruct,
|
||||
object({
|
||||
service: optional(string()),
|
||||
service_template: optional(string()),
|
||||
entity_id: optional(string()),
|
||||
target: optional(targetStruct),
|
||||
data: optional(object()),
|
||||
})
|
||||
);
|
||||
|
||||
const playMediaActionStruct: Describe<PlayMediaAction> = assign(
|
||||
baseActionStruct,
|
||||
object({
|
||||
service: literal("media_player.play_media"),
|
||||
target: optional(object({ entity_id: optional(string()) })),
|
||||
entity_id: optional(string()),
|
||||
data: object({ media_content_id: string(), media_content_type: string() }),
|
||||
metadata: object(),
|
||||
})
|
||||
);
|
||||
|
||||
const activateSceneActionStruct: Describe<ServiceSceneAction> = assign(
|
||||
baseActionStruct,
|
||||
object({
|
||||
service: literal("scene.turn_on"),
|
||||
target: optional(object({ entity_id: optional(string()) })),
|
||||
entity_id: optional(string()),
|
||||
metadata: object(),
|
||||
})
|
||||
);
|
||||
|
||||
export interface ScriptEntity extends HassEntityBase {
|
||||
attributes: HassEntityAttributeBase & {
|
||||
last_triggered: string;
|
||||
@ -48,11 +101,12 @@ export interface ServiceAction {
|
||||
service_template?: string;
|
||||
entity_id?: string;
|
||||
target?: HassServiceTarget;
|
||||
data?: Record<string, any>;
|
||||
data?: Record<string, unknown>;
|
||||
}
|
||||
|
||||
export interface DeviceAction {
|
||||
alias?: string;
|
||||
type: string;
|
||||
device_id: string;
|
||||
domain: string;
|
||||
entity_id: string;
|
||||
@ -70,9 +124,12 @@ export interface DelayAction {
|
||||
delay: number | Partial<DelayActionParts> | string;
|
||||
}
|
||||
|
||||
export interface ServiceSceneAction extends ServiceAction {
|
||||
export interface ServiceSceneAction {
|
||||
alias?: string;
|
||||
service: "scene.turn_on";
|
||||
metadata: Record<string, any>;
|
||||
target?: { entity_id?: string };
|
||||
entity_id?: string;
|
||||
metadata: Record<string, unknown>;
|
||||
}
|
||||
export interface LegacySceneAction {
|
||||
alias?: string;
|
||||
@ -94,6 +151,15 @@ export interface WaitForTriggerAction {
|
||||
continue_on_timeout?: boolean;
|
||||
}
|
||||
|
||||
export interface PlayMediaAction {
|
||||
alias?: string;
|
||||
service: "media_player.play_media";
|
||||
target?: { entity_id?: string };
|
||||
entity_id?: string;
|
||||
data: { media_content_id: string; media_content_type: string };
|
||||
metadata: Record<string, unknown>;
|
||||
}
|
||||
|
||||
export interface RepeatAction {
|
||||
alias?: string;
|
||||
repeat: CountRepeat | WhileRepeat | UntilRepeat;
|
||||
@ -150,6 +216,7 @@ export type Action =
|
||||
| RepeatAction
|
||||
| ChooseAction
|
||||
| VariablesAction
|
||||
| PlayMediaAction
|
||||
| UnknownAction;
|
||||
|
||||
export interface ActionTypes {
|
||||
@ -158,13 +225,13 @@ export interface ActionTypes {
|
||||
check_condition: Condition;
|
||||
fire_event: EventAction;
|
||||
device_action: DeviceAction;
|
||||
legacy_activate_scene: LegacySceneAction;
|
||||
activate_scene: ServiceSceneAction;
|
||||
activate_scene: SceneAction;
|
||||
repeat: RepeatAction;
|
||||
choose: ChooseAction;
|
||||
wait_for_trigger: WaitForTriggerAction;
|
||||
variables: VariablesAction;
|
||||
service: ServiceAction;
|
||||
play_media: PlayMediaAction;
|
||||
unknown: UnknownAction;
|
||||
}
|
||||
|
||||
@ -224,7 +291,7 @@ export const getActionType = (action: Action): ActionType => {
|
||||
return "device_action";
|
||||
}
|
||||
if ("scene" in action) {
|
||||
return "legacy_activate_scene";
|
||||
return "activate_scene";
|
||||
}
|
||||
if ("repeat" in action) {
|
||||
return "repeat";
|
||||
@ -240,12 +307,12 @@ export const getActionType = (action: Action): ActionType => {
|
||||
}
|
||||
if ("service" in action) {
|
||||
if ("metadata" in action) {
|
||||
if (
|
||||
(action as ServiceAction).service === "scene.turn_on" &&
|
||||
!Array.isArray((action as ServiceAction)?.target?.entity_id)
|
||||
) {
|
||||
if (is(action, activateSceneActionStruct)) {
|
||||
return "activate_scene";
|
||||
}
|
||||
if (is(action, playMediaActionStruct)) {
|
||||
return "play_media";
|
||||
}
|
||||
}
|
||||
return "service";
|
||||
}
|
||||
|
@ -9,10 +9,11 @@ import {
|
||||
ActionType,
|
||||
ActionTypes,
|
||||
DelayAction,
|
||||
DeviceAction,
|
||||
EventAction,
|
||||
getActionType,
|
||||
LegacySceneAction,
|
||||
ServiceSceneAction,
|
||||
PlayMediaAction,
|
||||
SceneAction,
|
||||
VariablesAction,
|
||||
WaitForTriggerAction,
|
||||
} from "./script";
|
||||
@ -103,19 +104,32 @@ export const describeAction = <T extends ActionType>(
|
||||
return `Delay ${duration}`;
|
||||
}
|
||||
|
||||
if (actionType === "legacy_activate_scene") {
|
||||
const config = action as LegacySceneAction;
|
||||
const sceneStateObj = hass.states[config.scene];
|
||||
if (actionType === "activate_scene") {
|
||||
const config = action as SceneAction;
|
||||
let entityId: string | undefined;
|
||||
if ("scene" in config) {
|
||||
entityId = config.scene;
|
||||
} else {
|
||||
entityId = config.target?.entity_id || config.entity_id;
|
||||
}
|
||||
const sceneStateObj = entityId ? hass.states[entityId] : undefined;
|
||||
return `Activate scene ${
|
||||
sceneStateObj ? computeStateName(sceneStateObj) : config.scene
|
||||
sceneStateObj
|
||||
? computeStateName(sceneStateObj)
|
||||
: "scene" in config
|
||||
? config.scene
|
||||
: config.target?.entity_id || config.entity_id
|
||||
}`;
|
||||
}
|
||||
|
||||
if (actionType === "activate_scene") {
|
||||
const config = action as ServiceSceneAction;
|
||||
const sceneStateObj = hass.states[config.target!.entity_id as string];
|
||||
return `Activate scene ${
|
||||
sceneStateObj ? computeStateName(sceneStateObj) : config.target!.entity_id
|
||||
if (actionType === "play_media") {
|
||||
const config = action as PlayMediaAction;
|
||||
const entityId = config.target?.entity_id || config.entity_id;
|
||||
const mediaStateObj = entityId ? hass.states[entityId] : undefined;
|
||||
return `Play ${config.metadata.title || config.data.media_content_id} on ${
|
||||
mediaStateObj
|
||||
? computeStateName(mediaStateObj)
|
||||
: config.target?.entity_id || config.entity_id
|
||||
}`;
|
||||
}
|
||||
|
||||
@ -147,5 +161,13 @@ export const describeAction = <T extends ActionType>(
|
||||
return `Test ${describeCondition(action as Condition)}`;
|
||||
}
|
||||
|
||||
if (actionType === "device_action") {
|
||||
const config = action as DeviceAction;
|
||||
const stateObj = hass.states[config.entity_id as string];
|
||||
return `${config.type || "Perform action with"} ${
|
||||
stateObj ? computeStateName(stateObj) : config.entity_id
|
||||
}`;
|
||||
}
|
||||
|
||||
return actionType;
|
||||
};
|
||||
|
@ -13,7 +13,8 @@ export type Selector =
|
||||
| StringSelector
|
||||
| ObjectSelector
|
||||
| SelectSelector
|
||||
| IconSelector;
|
||||
| IconSelector
|
||||
| MediaSelector;
|
||||
|
||||
export interface EntitySelector {
|
||||
entity: {
|
||||
@ -149,3 +150,21 @@ export interface IconSelector {
|
||||
// eslint-disable-next-line @typescript-eslint/ban-types
|
||||
icon: {};
|
||||
}
|
||||
|
||||
export interface MediaSelector {
|
||||
// eslint-disable-next-line @typescript-eslint/ban-types
|
||||
media: {};
|
||||
}
|
||||
|
||||
export interface MediaSelectorValue {
|
||||
entity_id?: string;
|
||||
media_content_id?: string;
|
||||
media_content_type?: string;
|
||||
metadata?: {
|
||||
title?: string;
|
||||
thumbnail?: string | null;
|
||||
media_class?: string;
|
||||
children_media_class?: string | null;
|
||||
navigateIds?: { media_content_type: string; media_content_id: string }[];
|
||||
};
|
||||
}
|
||||
|
@ -1,8 +1,8 @@
|
||||
import { ActionDetail } from "@material/mwc-list/mwc-list-foundation";
|
||||
import "@material/mwc-list/mwc-list-item";
|
||||
import { mdiArrowDown, mdiArrowUp, mdiDotsVertical } from "@mdi/js";
|
||||
import "@material/mwc-select";
|
||||
import type { Select } from "@material/mwc-select";
|
||||
import { mdiArrowDown, mdiArrowUp, mdiDotsVertical } from "@mdi/js";
|
||||
import { css, CSSResultGroup, html, LitElement, PropertyValues } from "lit";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
@ -11,22 +11,23 @@ import { fireEvent } from "../../../../common/dom/fire_event";
|
||||
import { stringCompare } from "../../../../common/string/compare";
|
||||
import { handleStructError } from "../../../../common/structs/handle-errors";
|
||||
import { LocalizeFunc } from "../../../../common/translations/localize";
|
||||
import "../../../../components/ha-alert";
|
||||
import "../../../../components/ha-button-menu";
|
||||
import "../../../../components/ha-card";
|
||||
import "../../../../components/ha-alert";
|
||||
import "../../../../components/ha-icon-button";
|
||||
import type { HaYamlEditor } from "../../../../components/ha-yaml-editor";
|
||||
import type { Action, ServiceSceneAction } from "../../../../data/script";
|
||||
import { Action, getActionType } from "../../../../data/script";
|
||||
import { showConfirmationDialog } from "../../../../dialogs/generic/show-dialog-box";
|
||||
import { haStyle } from "../../../../resources/styles";
|
||||
import type { HomeAssistant } from "../../../../types";
|
||||
import "./types/ha-automation-action-activate_scene";
|
||||
import "./types/ha-automation-action-choose";
|
||||
import "./types/ha-automation-action-condition";
|
||||
import "./types/ha-automation-action-delay";
|
||||
import "./types/ha-automation-action-device_id";
|
||||
import "./types/ha-automation-action-event";
|
||||
import "./types/ha-automation-action-play_media";
|
||||
import "./types/ha-automation-action-repeat";
|
||||
import "./types/ha-automation-action-scene";
|
||||
import "./types/ha-automation-action-service";
|
||||
import "./types/ha-automation-action-wait_for_trigger";
|
||||
import "./types/ha-automation-action-wait_template";
|
||||
@ -35,7 +36,8 @@ const OPTIONS = [
|
||||
"condition",
|
||||
"delay",
|
||||
"event",
|
||||
"scene",
|
||||
"play_media",
|
||||
"activate_scene",
|
||||
"service",
|
||||
"wait_template",
|
||||
"wait_for_trigger",
|
||||
@ -48,21 +50,8 @@ const getType = (action: Action | undefined) => {
|
||||
if (!action) {
|
||||
return undefined;
|
||||
}
|
||||
if ("metadata" in action && action.service) {
|
||||
switch (action.service) {
|
||||
case "scene.turn_on":
|
||||
// we dont support arrays of entities
|
||||
if (
|
||||
!Array.isArray(
|
||||
(action as unknown as ServiceSceneAction).target?.entity_id
|
||||
)
|
||||
) {
|
||||
return "scene";
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
if ("service" in action || "scene" in action) {
|
||||
return getActionType(action);
|
||||
}
|
||||
return OPTIONS.find((option) => option in action);
|
||||
};
|
||||
@ -133,24 +122,30 @@ export default class HaAutomationActionRow extends LitElement {
|
||||
).sort((a, b) => stringCompare(a[1], b[1]))
|
||||
);
|
||||
|
||||
protected willUpdate(changedProperties: PropertyValues) {
|
||||
if (!changedProperties.has("action")) {
|
||||
return;
|
||||
}
|
||||
this._uiModeAvailable = getType(this.action) !== undefined;
|
||||
if (!this._uiModeAvailable && !this._yamlMode) {
|
||||
this._yamlMode = true;
|
||||
}
|
||||
}
|
||||
|
||||
protected updated(changedProperties: PropertyValues) {
|
||||
if (!changedProperties.has("action")) {
|
||||
return;
|
||||
}
|
||||
this._uiModeAvailable = Boolean(getType(this.action));
|
||||
if (!this._uiModeAvailable && !this._yamlMode) {
|
||||
this._yamlMode = true;
|
||||
}
|
||||
|
||||
if (this._yamlMode) {
|
||||
const yamlEditor = this._yamlEditor;
|
||||
if (this._yamlMode && yamlEditor && yamlEditor.value !== this.action) {
|
||||
if (yamlEditor && yamlEditor.value !== this.action) {
|
||||
yamlEditor.setValue(this.action);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected render() {
|
||||
const type = getType(this.action);
|
||||
const selected = type ? OPTIONS.indexOf(type) : -1;
|
||||
const yamlMode = this._yamlMode;
|
||||
|
||||
return html`
|
||||
@ -225,7 +220,7 @@ export default class HaAutomationActionRow extends LitElement {
|
||||
: ""}
|
||||
${yamlMode
|
||||
? html`
|
||||
${selected === -1
|
||||
${type === undefined
|
||||
? html`
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.automation.editor.actions.unsupported_action",
|
||||
|
@ -9,7 +9,7 @@ import { ActionElement } from "../ha-automation-action-row";
|
||||
|
||||
const includeDomains = ["scene"];
|
||||
|
||||
@customElement("ha-automation-action-scene")
|
||||
@customElement("ha-automation-action-activate_scene")
|
||||
export class HaSceneAction extends LitElement implements ActionElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@ -61,6 +61,6 @@ export class HaSceneAction extends LitElement implements ActionElement {
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-automation-action-scene": HaSceneAction;
|
||||
"ha-automation-action-activate_scene": HaSceneAction;
|
||||
}
|
||||
}
|
@ -0,0 +1,68 @@
|
||||
import "@polymer/paper-input/paper-input";
|
||||
import { html, LitElement } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { fireEvent } from "../../../../../common/dom/fire_event";
|
||||
import "../../../../../components/ha-selector/ha-selector-media";
|
||||
import { PlayMediaAction } from "../../../../../data/script";
|
||||
import type { MediaSelectorValue } from "../../../../../data/selector";
|
||||
import type { HomeAssistant } from "../../../../../types";
|
||||
import { ActionElement } from "../ha-automation-action-row";
|
||||
|
||||
@customElement("ha-automation-action-play_media")
|
||||
export class HaPlayMediaAction extends LitElement implements ActionElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public action!: PlayMediaAction;
|
||||
|
||||
@property({ type: Boolean }) public narrow = false;
|
||||
|
||||
public static get defaultConfig(): PlayMediaAction {
|
||||
return {
|
||||
service: "media_player.play_media",
|
||||
target: { entity_id: "" },
|
||||
data: { media_content_id: "", media_content_type: "" },
|
||||
metadata: {},
|
||||
};
|
||||
}
|
||||
|
||||
private _getSelectorValue = memoizeOne(
|
||||
(action: PlayMediaAction): MediaSelectorValue => ({
|
||||
entity_id: action.target?.entity_id || action.entity_id,
|
||||
media_content_id: action.data?.media_content_id,
|
||||
media_content_type: action.data?.media_content_type,
|
||||
metadata: action.metadata,
|
||||
})
|
||||
);
|
||||
|
||||
protected render() {
|
||||
return html`
|
||||
<ha-selector-media
|
||||
.hass=${this.hass}
|
||||
.value=${this._getSelectorValue(this.action)}
|
||||
@value-changed=${this._valueChanged}
|
||||
></ha-selector-media>
|
||||
`;
|
||||
}
|
||||
|
||||
private _valueChanged(ev: CustomEvent<{ value: MediaSelectorValue }>) {
|
||||
ev.stopPropagation();
|
||||
fireEvent(this, "value-changed", {
|
||||
value: {
|
||||
service: "media_player.play_media",
|
||||
target: { entity_id: ev.detail.value.entity_id },
|
||||
data: {
|
||||
media_content_id: ev.detail.value.media_content_id,
|
||||
media_content_type: ev.detail.value.media_content_type,
|
||||
},
|
||||
metadata: ev.detail.value.metadata || {},
|
||||
} as PlayMediaAction,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-automation-action-play_media": HaPlayMediaAction;
|
||||
}
|
||||
}
|
@ -30,7 +30,7 @@ export class HaServiceAction extends LitElement implements ActionElement {
|
||||
return { service: "", data: {} };
|
||||
}
|
||||
|
||||
protected updated(changedProperties: PropertyValues) {
|
||||
protected willUpdate(changedProperties: PropertyValues) {
|
||||
if (!changedProperties.has("action")) {
|
||||
return;
|
||||
}
|
||||
|
@ -317,6 +317,17 @@
|
||||
"copied_clipboard": "Copied to clipboard"
|
||||
},
|
||||
"components": {
|
||||
"selectors": {
|
||||
"media": {
|
||||
"pick_media_player": "Select media player",
|
||||
"browse_not_supported": "Media player does not support browsing media.",
|
||||
"pick_media": "Pick media",
|
||||
"browse_media": "Browse media",
|
||||
"manual": "Manually enter Media ID",
|
||||
"media_content_id": "Media content ID",
|
||||
"media_content_type": "Media content type"
|
||||
}
|
||||
},
|
||||
"logbook": {
|
||||
"entries_not_found": "No logbook events found.",
|
||||
"by": "by",
|
||||
@ -505,6 +516,18 @@
|
||||
"clear": "Clear"
|
||||
},
|
||||
"media-browser": {
|
||||
"tts": {
|
||||
"message": "Message",
|
||||
"example_message": "Hello {name}, you can play any text on any supported media player!",
|
||||
"language": "Language",
|
||||
"gender": "Gender",
|
||||
"gender_male": "Male",
|
||||
"gender_female": "Female",
|
||||
"action_play": "Say",
|
||||
"action_pick": "Select",
|
||||
"set_as_default": "Set as default options",
|
||||
"faild_to_store_defaults": "Failed to store defaults: {error}"
|
||||
},
|
||||
"pick": "Pick",
|
||||
"play": "Play",
|
||||
"play-media": "Play Media",
|
||||
@ -1798,6 +1821,9 @@
|
||||
"service": {
|
||||
"label": "Call service"
|
||||
},
|
||||
"play_media": {
|
||||
"label": "Play media"
|
||||
},
|
||||
"delay": {
|
||||
"label": "Wait for time to pass (delay)",
|
||||
"delay": "Duration"
|
||||
@ -1836,7 +1862,7 @@
|
||||
"flash": "Flash"
|
||||
}
|
||||
},
|
||||
"scene": {
|
||||
"activate_scene": {
|
||||
"label": "Activate scene"
|
||||
},
|
||||
"repeat": {
|
||||
@ -3699,18 +3725,6 @@
|
||||
"media-browser": {
|
||||
"error": {
|
||||
"player_not_exist": "Media player {name} does not exist"
|
||||
},
|
||||
"tts": {
|
||||
"message": "Message",
|
||||
"example_message": "Hello {name}, you can play any text on any supported media player!",
|
||||
"language": "Language",
|
||||
"gender": "Gender",
|
||||
"gender_male": "Male",
|
||||
"gender_female": "Female",
|
||||
"action_play": "Say",
|
||||
"action_pick": "Select",
|
||||
"set_as_default": "Set as default options",
|
||||
"faild_to_store_defaults": "Failed to store defaults: {error}"
|
||||
}
|
||||
},
|
||||
"map": {
|
||||
|
Loading…
x
Reference in New Issue
Block a user