@@ -111,15 +153,23 @@ class HassioSupervisorInfo extends LitElement {
box-sizing: border-box;
height: calc(100% - 47px);
}
- .info {
+ .info,
+ .options {
width: 100%;
}
.info td:nth-child(2) {
text-align: right;
}
- .errors {
- color: var(--error-color);
- margin-top: 16px;
+ ha-settings-row {
+ padding: 0;
+ }
+ button.link {
+ color: var(--primary-color);
+ }
+ .diagnostics-description {
+ white-space: normal;
+ padding: 0;
+ color: var(--secondary-text-color);
}
`,
];
@@ -181,6 +231,40 @@ class HassioSupervisorInfo extends LitElement {
this._errors = `Error joining beta channel, ${err.body?.message || err}`;
}
}
+
+ private async _diagnosticsInformationDialog() {
+ await showAlertDialog(this, {
+ title: "Help Improve Home Assistant",
+ text: html`Would you want to automatically share crash reports and
+ diagnostic information when the supervisor encounters unexpected errors?
+
+ This will allow us to fix the problems, the information is only
+ accessible to the Home Assistant Core team and will not be shared with
+ others.
+
+ The data does not include any private/sensitive information and you can
+ disable this in settings at any time you want.`,
+ });
+ }
+
+ private async _toggleDiagnostics() {
+ try {
+ const data: SupervisorOptions = {
+ diagnostics: !this.supervisorInfo?.diagnostics,
+ };
+ await setSupervisorOption(this.hass, data);
+ const eventdata = {
+ success: true,
+ response: undefined,
+ path: "option",
+ };
+ fireEvent(this, "hass-api-called", eventdata);
+ } catch (err) {
+ this._errors = `Error changing supervisor setting, ${
+ err.body?.message || err
+ }`;
+ }
+ }
}
declare global {
diff --git a/hassio/src/system/hassio-system.ts b/hassio/src/system/hassio-system.ts
index 00a304ed03..3a52347a08 100644
--- a/hassio/src/system/hassio-system.ts
+++ b/hassio/src/system/hassio-system.ts
@@ -56,6 +56,7 @@ class HassioSystem extends LitElement {
span::selection,
+ .cm-s-default .CodeMirror-line>span>span::selection {
+ background: rgba(var(--rgb-primary-color), 0.2);
+ }
+
+ .cm-s-default .cm-keyword {
+ color: var(--codemirror-keyword, #6262FF);
+ }
+
+ .cm-s-default .cm-operator {
+ color: var(--codemirror-operator, #cda869);
+ }
+
+ .cm-s-default .cm-variable-2 {
+ color: var(--codemirror-variable-2, #690);
+ }
+
+ .cm-s-default .cm-builtin {
+ color: var(--codemirror-builtin, #9B7536);
+ }
+
+ .cm-s-default .cm-atom {
+ color: var(--codemirror-atom, #F90);
+ }
+
+ .cm-s-default .cm-number {
+ color: var(--codemirror-number, #ca7841);
+ }
+
+ .cm-s-default .cm-def {
+ color: var(--codemirror-def, #8DA6CE);
+ }
+
+ .cm-s-default .cm-string {
+ color: var(--codemirror-string, #07a);
+ }
+
+ .cm-s-default .cm-string-2 {
+ color: var(--codemirror-string-2, #bd6b18);
+ }
+
+ .cm-s-default .cm-comment {
+ color: var(--codemirror-comment, #777);
+ }
+
+ .cm-s-default .cm-variable {
+ color: var(--codemirror-variable, #07a);
+ }
+
+ .cm-s-default .cm-tag {
+ color: var(--codemirror-tag, #997643);
+ }
+
+ .cm-s-default .cm-meta {
+ color: var(--codemirror-meta, #000);
+ }
+
+ .cm-s-default .cm-attribute {
+ color: var(--codemirror-attribute, #d6bb6d);
+ }
+
+ .cm-s-default .cm-property {
+ color: var(--codemirror-property, #905);
+ }
+
+ .cm-s-default .cm-qualifier {
+ color: var(--codemirror-qualifier, #690);
+ }
+
+ .cm-s-default .cm-variable-3 {
+ color: var(--codemirror-variable-3, #07a);
+ }
+
+ .cm-s-default .cm-type {
+ color: var(--codemirror-type, #07a);
+ }
`;
this.codemirror = codeMirror(shadowRoot, {
diff --git a/src/components/ha-picture-upload.ts b/src/components/ha-picture-upload.ts
new file mode 100644
index 0000000000..411984d555
--- /dev/null
+++ b/src/components/ha-picture-upload.ts
@@ -0,0 +1,226 @@
+import "@material/mwc-icon-button/mwc-icon-button";
+import { mdiClose, mdiImagePlus } from "@mdi/js";
+import "@polymer/iron-input/iron-input";
+import "@polymer/paper-input/paper-input-container";
+import {
+ css,
+ customElement,
+ html,
+ internalProperty,
+ LitElement,
+ property,
+ PropertyValues,
+ TemplateResult,
+} from "lit-element";
+import { classMap } from "lit-html/directives/class-map";
+import { fireEvent } from "../common/dom/fire_event";
+import { createImage, generateImageThumbnailUrl } from "../data/image";
+import { HomeAssistant } from "../types";
+import "./ha-circular-progress";
+import "./ha-svg-icon";
+import {
+ showImageCropperDialog,
+ CropOptions,
+} from "../dialogs/image-cropper-dialog/show-image-cropper-dialog";
+
+@customElement("ha-picture-upload")
+export class HaPictureUpload extends LitElement {
+ public hass!: HomeAssistant;
+
+ @property() public value: string | null = null;
+
+ @property() public label?: string;
+
+ @property({ type: Boolean }) public crop = false;
+
+ @property({ attribute: false }) public cropOptions?: CropOptions;
+
+ @property({ type: Number }) public size = 512;
+
+ @internalProperty() private _error = "";
+
+ @internalProperty() private _uploading = false;
+
+ @internalProperty() private _drag = false;
+
+ protected updated(changedProperties: PropertyValues) {
+ if (changedProperties.has("_drag")) {
+ (this.shadowRoot!.querySelector(
+ "paper-input-container"
+ ) as any)._setFocused(this._drag);
+ }
+ }
+
+ public render(): TemplateResult {
+ return html`
+ ${this._uploading
+ ? html``
+ : html`
+ ${this._error ? html`${this._error}
` : ""}
+
+ `}
+ `;
+ }
+
+ private _handleDrop(ev: DragEvent) {
+ ev.preventDefault();
+ ev.stopPropagation();
+ if (ev.dataTransfer?.files) {
+ if (this.crop) {
+ this._cropFile(ev.dataTransfer.files[0]);
+ } else {
+ this._uploadFile(ev.dataTransfer.files[0]);
+ }
+ }
+ this._drag = false;
+ }
+
+ private _handleDragStart(ev: DragEvent) {
+ ev.preventDefault();
+ ev.stopPropagation();
+ this._drag = true;
+ }
+
+ private _handleDragEnd(ev: DragEvent) {
+ ev.preventDefault();
+ ev.stopPropagation();
+ this._drag = false;
+ }
+
+ private async _handleFilePicked(ev) {
+ if (this.crop) {
+ this._cropFile(ev.target.files[0]);
+ } else {
+ this._uploadFile(ev.target.files[0]);
+ }
+ }
+
+ private async _cropFile(file: File) {
+ if (!["image/png", "image/jpeg", "image/gif"].includes(file.type)) {
+ this._error = this.hass.localize(
+ "ui.components.picture-upload.unsupported_format"
+ );
+ return;
+ }
+ showImageCropperDialog(this, {
+ file,
+ options: this.cropOptions || {
+ round: false,
+ aspectRatio: NaN,
+ },
+ croppedCallback: (croppedFile) => {
+ this._uploadFile(croppedFile);
+ },
+ });
+ }
+
+ private async _uploadFile(file: File) {
+ if (!["image/png", "image/jpeg", "image/gif"].includes(file.type)) {
+ this._error = this.hass.localize(
+ "ui.components.picture-upload.unsupported_format"
+ );
+ return;
+ }
+ this._uploading = true;
+ this._error = "";
+ try {
+ const media = await createImage(this.hass, file);
+ this.value = generateImageThumbnailUrl(media.id, this.size);
+ fireEvent(this, "change");
+ } catch (err) {
+ this._error = err.toString();
+ } finally {
+ this._uploading = false;
+ }
+ }
+
+ private _clearPicture(ev: Event) {
+ ev.preventDefault();
+ this.value = null;
+ this._error = "";
+ fireEvent(this, "change");
+ }
+
+ static get styles() {
+ return css`
+ .error {
+ color: var(--error-color);
+ }
+ paper-input-container {
+ position: relative;
+ padding: 8px;
+ margin: 0 -8px;
+ }
+ paper-input-container.dragged:before {
+ position: var(--layout-fit_-_position);
+ top: var(--layout-fit_-_top);
+ right: var(--layout-fit_-_right);
+ bottom: var(--layout-fit_-_bottom);
+ left: var(--layout-fit_-_left);
+ background: currentColor;
+ content: "";
+ opacity: var(--dark-divider-opacity);
+ pointer-events: none;
+ border-radius: 4px;
+ }
+ img {
+ max-width: 125px;
+ max-height: 125px;
+ }
+ input.file {
+ display: none;
+ }
+ mwc-icon-button {
+ --mdc-icon-button-size: 24px;
+ --mdc-icon-size: 20px;
+ }
+ `;
+ }
+}
+
+declare global {
+ interface HTMLElementTagNameMap {
+ "ha-picture-upload": HaPictureUpload;
+ }
+}
diff --git a/src/components/ha-settings-row.ts b/src/components/ha-settings-row.ts
new file mode 100644
index 0000000000..43c886ab99
--- /dev/null
+++ b/src/components/ha-settings-row.ts
@@ -0,0 +1,59 @@
+import {
+ css,
+ CSSResult,
+ customElement,
+ html,
+ LitElement,
+ property,
+ SVGTemplateResult,
+} from "lit-element";
+import "@polymer/paper-item/paper-item-body";
+
+@customElement("ha-settings-row")
+export class HaSettingsRow extends LitElement {
+ @property({ type: Boolean, reflect: true }) public narrow!: boolean;
+
+ @property({ type: Boolean, attribute: "three-line" })
+ public threeLine = false;
+
+ protected render(): SVGTemplateResult {
+ return html`
+
+
+
+
+
+
+ `;
+ }
+
+ static get styles(): CSSResult {
+ return css`
+ :host {
+ display: flex;
+ padding: 0 16px;
+ align-content: normal;
+ align-self: auto;
+ align-items: center;
+ }
+ :host([narrow]) {
+ align-items: normal;
+ flex-direction: column;
+ border-top: 1px solid var(--divider-color);
+ padding-bottom: 8px;
+ }
+ `;
+ }
+}
+declare global {
+ interface HTMLElementTagNameMap {
+ "ha-settings-row": HaSettingsRow;
+ }
+}
diff --git a/src/data/automation.ts b/src/data/automation.ts
index b3e8d0b0f8..d4eaa843c7 100644
--- a/src/data/automation.ts
+++ b/src/data/automation.ts
@@ -90,6 +90,12 @@ export interface ZoneTrigger {
event: "enter" | "leave";
}
+export interface TagTrigger {
+ platform: "tag";
+ tag_id: string;
+ device_id?: string;
+}
+
export interface TimeTrigger {
platform: "time";
at: string;
@@ -116,6 +122,7 @@ export type Trigger =
| TimePatternTrigger
| WebhookTrigger
| ZoneTrigger
+ | TagTrigger
| TimeTrigger
| TemplateTrigger
| EventTrigger
diff --git a/src/data/hassio/supervisor.ts b/src/data/hassio/supervisor.ts
index e32e13ad2f..f6a7d2811c 100644
--- a/src/data/hassio/supervisor.ts
+++ b/src/data/hassio/supervisor.ts
@@ -31,6 +31,7 @@ export interface CreateSessionResponse {
export interface SupervisorOptions {
channel?: "beta" | "dev" | "stable";
+ diagnostics?: boolean;
addons_repositories?: string[];
}
@@ -70,7 +71,7 @@ export const createHassioSession = async (hass: HomeAssistant) => {
"POST",
"hassio/ingress/session"
);
- document.cookie = `ingress_session=${response.data.session};path=/api/hassio_ingress/`;
+ document.cookie = `ingress_session=${response.data.session};path=/api/hassio_ingress/;SameSite=Strict`;
};
export const setSupervisorOption = async (
diff --git a/src/data/image.ts b/src/data/image.ts
new file mode 100644
index 0000000000..b81499f002
--- /dev/null
+++ b/src/data/image.ts
@@ -0,0 +1,54 @@
+import { HomeAssistant } from "../types";
+
+interface Image {
+ filesize: number;
+ name: string;
+ uploaded_at: string; // isoformat date
+ content_type: string;
+ id: string;
+}
+
+export interface ImageMutableParams {
+ name: string;
+}
+
+export const generateImageThumbnailUrl = (mediaId: string, size: number) =>
+ `/api/image/serve/${mediaId}/${size}x${size}`;
+
+export const fetchImages = (hass: HomeAssistant) =>
+ hass.callWS({ type: "image/list" });
+
+export const createImage = async (
+ hass: HomeAssistant,
+ file: File
+): Promise => {
+ const fd = new FormData();
+ fd.append("file", file);
+ const resp = await hass.fetchWithAuth("/api/image/upload", {
+ method: "POST",
+ body: fd,
+ });
+ if (resp.status === 413) {
+ throw new Error("Uploaded image is too large");
+ } else if (resp.status !== 200) {
+ throw new Error("Unknown error");
+ }
+ return await resp.json();
+};
+
+export const updateImage = (
+ hass: HomeAssistant,
+ id: string,
+ updates: Partial
+) =>
+ hass.callWS({
+ type: "image/update",
+ media_id: id,
+ ...updates,
+ });
+
+export const deleteImage = (hass: HomeAssistant, id: string) =>
+ hass.callWS({
+ type: "image/delete",
+ media_id: id,
+ });
diff --git a/src/data/media-player.ts b/src/data/media-player.ts
index 1f716cbee1..71a2939716 100644
--- a/src/data/media-player.ts
+++ b/src/data/media-player.ts
@@ -21,6 +21,11 @@ export interface MediaPlayerThumbnail {
content: string;
}
+export interface ControlButton {
+ icon: string;
+ action: string;
+}
+
export const getCurrentProgress = (stateObj: HassEntity): number => {
let progress = stateObj.attributes.media_position;
diff --git a/src/data/ozw.ts b/src/data/ozw.ts
index 491b0168a2..cbef2a8dbc 100644
--- a/src/data/ozw.ts
+++ b/src/data/ozw.ts
@@ -1,4 +1,10 @@
import { HomeAssistant } from "../types";
+import { DeviceRegistryEntry } from "./device_registry";
+
+export interface OZWNodeIdentifiers {
+ ozw_instance: number;
+ node_id: number;
+}
export interface OZWDevice {
node_id: number;
@@ -7,15 +13,102 @@ export interface OZWDevice {
is_failed: boolean;
is_zwave_plus: boolean;
ozw_instance: number;
+ event: string;
}
+export interface OZWDeviceMetaDataResponse {
+ node_id: number;
+ ozw_instance: number;
+ metadata: OZWDeviceMetaData;
+}
+
+export interface OZWDeviceMetaData {
+ OZWInfoURL: string;
+ ZWAProductURL: string;
+ ProductPic: string;
+ Description: string;
+ ProductManualURL: string;
+ ProductPageURL: string;
+ InclusionHelp: string;
+ ExclusionHelp: string;
+ ResetHelp: string;
+ WakeupHelp: string;
+ ProductSupportURL: string;
+ Frequency: string;
+ Name: string;
+ ProductPicBase64: string;
+}
+
+export const nodeQueryStages = [
+ "ProtocolInfo",
+ "Probe",
+ "WakeUp",
+ "ManufacturerSpecific1",
+ "NodeInfo",
+ "NodePlusInfo",
+ "ManufacturerSpecific2",
+ "Versions",
+ "Instances",
+ "Static",
+ "CacheLoad",
+ "Associations",
+ "Neighbors",
+ "Session",
+ "Dynamic",
+ "Configuration",
+ "Complete",
+];
+
+export const getIdentifiersFromDevice = function (
+ device: DeviceRegistryEntry
+): OZWNodeIdentifiers | undefined {
+ if (!device) {
+ return undefined;
+ }
+
+ const ozwIdentifier = device.identifiers.find(
+ (identifier) => identifier[0] === "ozw"
+ );
+ if (!ozwIdentifier) {
+ return undefined;
+ }
+
+ const identifiers = ozwIdentifier[1].split(".");
+ return {
+ node_id: parseInt(identifiers[1]),
+ ozw_instance: parseInt(identifiers[0]),
+ };
+};
+
export const fetchOZWNodeStatus = (
hass: HomeAssistant,
- ozw_instance: string,
- node_id: string
+ ozw_instance: number,
+ node_id: number
): Promise =>
hass.callWS({
type: "ozw/node_status",
ozw_instance: ozw_instance,
node_id: node_id,
});
+
+export const fetchOZWNodeMetadata = (
+ hass: HomeAssistant,
+ ozw_instance: number,
+ node_id: number
+): Promise =>
+ hass.callWS({
+ type: "ozw/node_metadata",
+ ozw_instance: ozw_instance,
+ node_id: node_id,
+ });
+
+export const refreshNodeInfo = (
+ hass: HomeAssistant,
+ ozw_instance: number,
+ node_id: number
+): Promise =>
+ hass.callWS({
+ type: "ozw/refresh_node_info",
+ ozw_instance: ozw_instance,
+ node_id: node_id,
+ });
diff --git a/src/data/person.ts b/src/data/person.ts
index 00e77a838f..eb3b358729 100644
--- a/src/data/person.ts
+++ b/src/data/person.ts
@@ -5,12 +5,14 @@ export interface Person {
name: string;
user_id?: string;
device_trackers?: string[];
+ picture?: string;
}
export interface PersonMutableParams {
name: string;
user_id: string | null;
device_trackers: string[];
+ picture: string | null;
}
export const fetchPersons = (hass: HomeAssistant) =>
diff --git a/src/data/tag.ts b/src/data/tag.ts
new file mode 100644
index 0000000000..870777f7cb
--- /dev/null
+++ b/src/data/tag.ts
@@ -0,0 +1,57 @@
+import { HomeAssistant } from "../types";
+import { HassEventBase } from "home-assistant-js-websocket";
+
+export const EVENT_TAG_SCANNED = "tag_scanned";
+
+export interface TagScannedEvent extends HassEventBase {
+ event_type: "tag_scanned";
+ data: {
+ tag_id: string;
+ device_id?: string;
+ };
+}
+
+export interface Tag {
+ id: string;
+ name?: string;
+ description?: string;
+ last_scanned?: string;
+}
+
+export interface UpdateTagParams {
+ name?: Tag["name"];
+ description?: Tag["description"];
+}
+
+export const fetchTags = async (hass: HomeAssistant) =>
+ hass.callWS({
+ type: "tag/list",
+ });
+
+export const createTag = async (
+ hass: HomeAssistant,
+ params: UpdateTagParams,
+ tagId?: string
+) =>
+ hass.callWS({
+ type: "tag/create",
+ tag_id: tagId,
+ ...params,
+ });
+
+export const updateTag = async (
+ hass: HomeAssistant,
+ tagId: string,
+ params: UpdateTagParams
+) =>
+ hass.callWS({
+ ...params,
+ type: "tag/update",
+ tag_id: tagId,
+ });
+
+export const deleteTag = async (hass: HomeAssistant, tagId: string) =>
+ hass.callWS({
+ type: "tag/delete",
+ tag_id: tagId,
+ });
diff --git a/src/dialogs/image-cropper-dialog/image-cropper-dialog.ts b/src/dialogs/image-cropper-dialog/image-cropper-dialog.ts
new file mode 100644
index 0000000000..ed9e3a05aa
--- /dev/null
+++ b/src/dialogs/image-cropper-dialog/image-cropper-dialog.ts
@@ -0,0 +1,136 @@
+import "@material/mwc-button/mwc-button";
+import Cropper from "cropperjs";
+// @ts-ignore
+import cropperCss from "cropperjs/dist/cropper.css";
+import {
+ css,
+ CSSResult,
+ customElement,
+ html,
+ internalProperty,
+ LitElement,
+ property,
+ PropertyValues,
+ query,
+ TemplateResult,
+ unsafeCSS,
+} from "lit-element";
+import "../../components/ha-dialog";
+import { haStyleDialog } from "../../resources/styles";
+import type { HomeAssistant } from "../../types";
+import { HaImageCropperDialogParams } from "./show-image-cropper-dialog";
+import { classMap } from "lit-html/directives/class-map";
+
+@customElement("image-cropper-dialog")
+export class HaImagecropperDialog extends LitElement {
+ @property({ attribute: false }) public hass!: HomeAssistant;
+
+ @internalProperty() private _params?: HaImageCropperDialogParams;
+
+ @internalProperty() private _open = false;
+
+ @query("img") private _image!: HTMLImageElement;
+
+ private _cropper?: Cropper;
+
+ public showDialog(params: HaImageCropperDialogParams): void {
+ this._params = params;
+ this._open = true;
+ }
+
+ public closeDialog() {
+ this._open = false;
+ this._params = undefined;
+ this._cropper?.destroy();
+ }
+
+ protected updated(changedProperties: PropertyValues) {
+ if (!changedProperties.has("_params") || !this._params) {
+ return;
+ }
+ if (!this._cropper) {
+ this._image.src = URL.createObjectURL(this._params.file);
+ this._cropper = new Cropper(this._image, {
+ aspectRatio: this._params.options.aspectRatio,
+ viewMode: 1,
+ dragMode: "move",
+ minCropBoxWidth: 50,
+ ready: () => {
+ URL.revokeObjectURL(this._image!.src);
+ },
+ });
+ } else {
+ this._cropper.replace(URL.createObjectURL(this._params.file));
+ }
+ }
+
+ protected render(): TemplateResult {
+ return html`
+
+
![]()
+
+
+ ${this.hass.localize("ui.common.cancel")}
+
+
+ ${this.hass.localize("ui.dialogs.image_cropper.crop")}
+
+ `;
+ }
+
+ private _cropImage() {
+ this._cropper!.getCroppedCanvas().toBlob(
+ (blob) => {
+ if (!blob) {
+ return;
+ }
+ const file = new File([blob], this._params!.file.name, {
+ type: this._params!.options.type || this._params!.file.type,
+ });
+ this._params!.croppedCallback(file);
+ this.closeDialog();
+ },
+ this._params!.options.type || this._params!.file.type,
+ this._params!.options.quality
+ );
+ }
+
+ static get styles(): CSSResult[] {
+ return [
+ haStyleDialog,
+ css`
+ ${unsafeCSS(cropperCss)}
+ .container {
+ max-width: 640px;
+ }
+ img {
+ max-width: 100%;
+ }
+ .container.round .cropper-view-box,
+ .container.round .cropper-face {
+ border-radius: 50%;
+ }
+ .cropper-line,
+ .cropper-point,
+ .cropper-point.point-se::before {
+ background-color: var(--primary-color);
+ }
+ `,
+ ];
+ }
+}
+
+declare global {
+ interface HTMLElementTagNameMap {
+ "image-cropper-dialog": HaImagecropperDialog;
+ }
+}
diff --git a/src/dialogs/image-cropper-dialog/show-image-cropper-dialog.ts b/src/dialogs/image-cropper-dialog/show-image-cropper-dialog.ts
new file mode 100644
index 0000000000..3ca19001c8
--- /dev/null
+++ b/src/dialogs/image-cropper-dialog/show-image-cropper-dialog.ts
@@ -0,0 +1,30 @@
+import { fireEvent } from "../../common/dom/fire_event";
+
+export interface CropOptions {
+ round: boolean;
+ type?: "image/jpeg" | "image/png";
+ quality?: number;
+ aspectRatio: number;
+}
+
+export interface HaImageCropperDialogParams {
+ file: File;
+ options: CropOptions;
+ croppedCallback: (file: File) => void;
+}
+
+const loadImageCropperDialog = () =>
+ import(
+ /* webpackChunkName: "image-cropper-dialog" */ "./image-cropper-dialog"
+ );
+
+export const showImageCropperDialog = (
+ element: HTMLElement,
+ dialogParams: HaImageCropperDialogParams
+): void => {
+ fireEvent(element, "show-dialog", {
+ dialogTag: "image-cropper-dialog",
+ dialogImport: loadImageCropperDialog,
+ dialogParams,
+ });
+};
diff --git a/src/dialogs/more-info/controls/more-info-light.js b/src/dialogs/more-info/controls/more-info-light.js
deleted file mode 100644
index 0362e44822..0000000000
--- a/src/dialogs/more-info/controls/more-info-light.js
+++ /dev/null
@@ -1,361 +0,0 @@
-import "@polymer/iron-flex-layout/iron-flex-layout-classes";
-import "@polymer/paper-item/paper-item";
-import "@polymer/paper-listbox/paper-listbox";
-import { html } from "@polymer/polymer/lib/utils/html-tag";
-/* eslint-plugin-disable lit */
-import { PolymerElement } from "@polymer/polymer/polymer-element";
-import { featureClassNames } from "../../../common/entity/feature_class_names";
-import "../../../components/ha-attributes";
-import "../../../components/ha-color-picker";
-import "../../../components/ha-labeled-slider";
-import "../../../components/ha-paper-dropdown-menu";
-import { EventsMixin } from "../../../mixins/events-mixin";
-import LocalizeMixin from "../../../mixins/localize-mixin";
-import "../../../components/ha-icon-button";
-
-const FEATURE_CLASS_NAMES = {
- 1: "has-brightness",
- 2: "has-color_temp",
- 4: "has-effect_list",
- 16: "has-color",
- 128: "has-white_value",
-};
-/*
- * @appliesMixin EventsMixin
- */
-class MoreInfoLight extends LocalizeMixin(EventsMixin(PolymerElement)) {
- static get template() {
- return html`
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- `;
- }
-
- static get properties() {
- return {
- hass: {
- type: Object,
- },
-
- stateObj: {
- type: Object,
- observer: "stateObjChanged",
- },
-
- brightnessSliderValue: {
- type: Number,
- value: 0,
- },
-
- ctSliderValue: {
- type: Number,
- value: 0,
- },
-
- wvSliderValue: {
- type: Number,
- value: 0,
- },
-
- hueSegments: {
- type: Number,
- value: 24,
- },
-
- saturationSegments: {
- type: Number,
- value: 8,
- },
-
- colorPickerColor: {
- type: Object,
- },
- };
- }
-
- stateObjChanged(newVal, oldVal) {
- const props = {
- brightnessSliderValue: 0,
- };
-
- if (newVal && newVal.state === "on") {
- props.brightnessSliderValue = newVal.attributes.brightness;
- props.ctSliderValue = newVal.attributes.color_temp;
- props.wvSliderValue = newVal.attributes.white_value;
- if (newVal.attributes.hs_color) {
- props.colorPickerColor = {
- h: newVal.attributes.hs_color[0],
- s: newVal.attributes.hs_color[1] / 100,
- };
- }
- }
-
- this.setProperties(props);
-
- if (oldVal) {
- setTimeout(() => {
- this.fire("iron-resize");
- }, 500);
- }
- }
-
- computeClassNames(stateObj) {
- const classes = [
- "content",
- featureClassNames(stateObj, FEATURE_CLASS_NAMES),
- ];
- if (stateObj && stateObj.state === "on") {
- classes.push("is-on");
- }
- if (stateObj && stateObj.state === "unavailable") {
- classes.push("is-unavailable");
- }
- return classes.join(" ");
- }
-
- effectChanged(ev) {
- const oldVal = this.stateObj.attributes.effect;
- const newVal = ev.detail.value;
-
- if (!newVal || oldVal === newVal) return;
-
- this.hass.callService("light", "turn_on", {
- entity_id: this.stateObj.entity_id,
- effect: newVal,
- });
- }
-
- brightnessSliderChanged(ev) {
- const bri = parseInt(ev.target.value, 10);
-
- if (isNaN(bri)) return;
-
- this.hass.callService("light", "turn_on", {
- entity_id: this.stateObj.entity_id,
- brightness: bri,
- });
- }
-
- ctSliderChanged(ev) {
- const ct = parseInt(ev.target.value, 10);
-
- if (isNaN(ct)) return;
-
- this.hass.callService("light", "turn_on", {
- entity_id: this.stateObj.entity_id,
- color_temp: ct,
- });
- }
-
- wvSliderChanged(ev) {
- const wv = parseInt(ev.target.value, 10);
-
- if (isNaN(wv)) return;
-
- this.hass.callService("light", "turn_on", {
- entity_id: this.stateObj.entity_id,
- white_value: wv,
- });
- }
-
- segmentClick() {
- if (this.hueSegments === 24 && this.saturationSegments === 8) {
- this.setProperties({ hueSegments: 0, saturationSegments: 0 });
- } else {
- this.setProperties({ hueSegments: 24, saturationSegments: 8 });
- }
- }
-
- serviceChangeColor(hass, entityId, color) {
- hass.callService("light", "turn_on", {
- entity_id: entityId,
- hs_color: [color.h, color.s * 100],
- });
- }
-
- /**
- * Called when a new color has been picked.
- * should be throttled with the 'throttle=' attribute of the color picker
- */
- colorPicked(ev) {
- this.serviceChangeColor(this.hass, this.stateObj.entity_id, ev.detail.hs);
- }
-}
-
-customElements.define("more-info-light", MoreInfoLight);
diff --git a/src/dialogs/more-info/controls/more-info-light.ts b/src/dialogs/more-info/controls/more-info-light.ts
new file mode 100644
index 0000000000..3abab21a99
--- /dev/null
+++ b/src/dialogs/more-info/controls/more-info-light.ts
@@ -0,0 +1,307 @@
+import "@polymer/paper-item/paper-item";
+import "@polymer/paper-listbox/paper-listbox";
+import {
+ css,
+ CSSResult,
+ customElement,
+ html,
+ LitElement,
+ property,
+ TemplateResult,
+ internalProperty,
+ PropertyValues,
+} from "lit-element";
+import { classMap } from "lit-html/directives/class-map";
+
+import {
+ SUPPORT_BRIGHTNESS,
+ SUPPORT_COLOR_TEMP,
+ SUPPORT_WHITE_VALUE,
+ SUPPORT_COLOR,
+ SUPPORT_EFFECT,
+} from "../../../data/light";
+import { supportsFeature } from "../../../common/entity/supports-feature";
+import type { HomeAssistant, LightEntity } from "../../../types";
+
+import "../../../components/ha-attributes";
+import "../../../components/ha-color-picker";
+import "../../../components/ha-labeled-slider";
+import "../../../components/ha-icon-button";
+import "../../../components/ha-paper-dropdown-menu";
+
+interface HueSatColor {
+ h: number;
+ s: number;
+}
+
+@customElement("more-info-light")
+class MoreInfoLight extends LitElement {
+ @property({ attribute: false }) public hass!: HomeAssistant;
+
+ @property({ attribute: false }) public stateObj?: LightEntity;
+
+ @internalProperty() private _brightnessSliderValue = 0;
+
+ @internalProperty() private _ctSliderValue = 0;
+
+ @internalProperty() private _wvSliderValue = 0;
+
+ @internalProperty() private _hueSegments = 24;
+
+ @internalProperty() private _saturationSegments = 8;
+
+ @internalProperty() private _colorPickerColor?: HueSatColor;
+
+ protected render(): TemplateResult {
+ if (!this.hass || !this.stateObj) {
+ return html``;
+ }
+
+ return html`
+
+ ${this.stateObj.state === "on"
+ ? html`
+ ${supportsFeature(this.stateObj!, SUPPORT_BRIGHTNESS)
+ ? html`
+
+ `
+ : ""}
+ ${supportsFeature(this.stateObj, SUPPORT_COLOR_TEMP)
+ ? html`
+
+ `
+ : ""}
+ ${supportsFeature(this.stateObj, SUPPORT_WHITE_VALUE)
+ ? html`
+
+ `
+ : ""}
+ ${supportsFeature(this.stateObj, SUPPORT_COLOR)
+ ? html`
+
+
+
+
+
+ `
+ : ""}
+ ${supportsFeature(this.stateObj, SUPPORT_EFFECT) &&
+ this.stateObj!.attributes.effect_list?.length
+ ? html`
+
+ ${this.stateObj.attributes.effect_list.map(
+ (effect: string) => html`
+ ${effect}
+ `
+ )}
+
+
+ `
+ : ""}
+ `
+ : ""}
+
+
+ `;
+ }
+
+ protected updated(changedProps: PropertyValues): void {
+ const stateObj = this.stateObj! as LightEntity;
+ if (changedProps.has("stateObj") && stateObj.state === "on") {
+ this._brightnessSliderValue = stateObj.attributes.brightness;
+ this._ctSliderValue = stateObj.attributes.color_temp;
+ this._wvSliderValue = stateObj.attributes.white_value;
+
+ if (stateObj.attributes.hs_color) {
+ this._colorPickerColor = {
+ h: stateObj.attributes.hs_color[0],
+ s: stateObj.attributes.hs_color[1] / 100,
+ };
+ }
+ }
+ }
+
+ private _effectChanged(ev: CustomEvent) {
+ const newVal = ev.detail.value;
+
+ if (!newVal || this.stateObj!.attributes.effect === newVal) {
+ return;
+ }
+
+ this.hass.callService("light", "turn_on", {
+ entity_id: this.stateObj!.entity_id,
+ effect: newVal,
+ });
+ }
+
+ private _brightnessSliderChanged(ev: CustomEvent) {
+ const bri = parseInt((ev.target as any).value, 10);
+
+ if (isNaN(bri)) {
+ return;
+ }
+
+ this.hass.callService("light", "turn_on", {
+ entity_id: this.stateObj!.entity_id,
+ brightness: bri,
+ });
+ }
+
+ private _ctSliderChanged(ev: CustomEvent) {
+ const ct = parseInt((ev.target as any).value, 10);
+
+ if (isNaN(ct)) {
+ return;
+ }
+
+ this.hass.callService("light", "turn_on", {
+ entity_id: this.stateObj!.entity_id,
+ color_temp: ct,
+ });
+ }
+
+ private _wvSliderChanged(ev: CustomEvent) {
+ const wv = parseInt((ev.target as any).value, 10);
+
+ if (isNaN(wv)) {
+ return;
+ }
+
+ this.hass.callService("light", "turn_on", {
+ entity_id: this.stateObj!.entity_id,
+ white_value: wv,
+ });
+ }
+
+ private _segmentClick() {
+ if (this._hueSegments === 24 && this._saturationSegments === 8) {
+ this._hueSegments = 0;
+ this._saturationSegments = 0;
+ } else {
+ this._hueSegments = 24;
+ this._saturationSegments = 8;
+ }
+ }
+
+ /**
+ * Called when a new color has been picked.
+ * should be throttled with the 'throttle=' attribute of the color picker
+ */
+ private _colorPicked(ev: CustomEvent) {
+ this.hass.callService("light", "turn_on", {
+ entity_id: this.stateObj!.entity_id,
+ hs_color: [ev.detail.hs.h, ev.detail.hs.s * 100],
+ });
+ }
+
+ static get styles(): CSSResult {
+ return css`
+ .content {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ }
+
+ .content.is-on {
+ margin-top: -16px;
+ }
+
+ .content > * {
+ width: 100%;
+ max-height: 84px;
+ overflow: hidden;
+ padding-top: 16px;
+ }
+
+ .color_temp {
+ --ha-slider-background: -webkit-linear-gradient(
+ right,
+ rgb(255, 160, 0) 0%,
+ white 50%,
+ rgb(166, 209, 255) 100%
+ );
+ /* The color temp minimum value shouldn't be rendered differently. It's not "off". */
+ --paper-slider-knob-start-border-color: var(--primary-color);
+ }
+
+ .segmentationContainer {
+ position: relative;
+ max-height: 500px;
+ }
+
+ ha-color-picker {
+ --ha-color-picker-wheel-borderwidth: 5;
+ --ha-color-picker-wheel-bordercolor: white;
+ --ha-color-picker-wheel-shadow: none;
+ --ha-color-picker-marker-borderwidth: 2;
+ --ha-color-picker-marker-bordercolor: white;
+ }
+
+ .segmentationButton {
+ position: absolute;
+ top: 5%;
+ color: var(--secondary-text-color);
+ }
+
+ paper-item {
+ cursor: pointer;
+ }
+ `;
+ }
+}
+
+declare global {
+ interface HTMLElementTagNameMap {
+ "more-info-light": MoreInfoLight;
+ }
+}
diff --git a/src/dialogs/more-info/controls/more-info-media_player.js b/src/dialogs/more-info/controls/more-info-media_player.js
deleted file mode 100644
index 2e45403793..0000000000
--- a/src/dialogs/more-info/controls/more-info-media_player.js
+++ /dev/null
@@ -1,421 +0,0 @@
-import "@polymer/iron-flex-layout/iron-flex-layout-classes";
-import "../../../components/ha-icon-button";
-import "@polymer/paper-item/paper-item";
-import "@polymer/paper-listbox/paper-listbox";
-import { html } from "@polymer/polymer/lib/utils/html-tag";
-/* eslint-plugin-disable lit */
-import { PolymerElement } from "@polymer/polymer/polymer-element";
-import { isComponentLoaded } from "../../../common/config/is_component_loaded";
-import { attributeClassNames } from "../../../common/entity/attribute_class_names";
-import { computeRTLDirection } from "../../../common/util/compute_rtl";
-import "../../../components/ha-paper-dropdown-menu";
-import "../../../components/ha-paper-slider";
-import "../../../components/ha-icon";
-import { EventsMixin } from "../../../mixins/events-mixin";
-import LocalizeMixin from "../../../mixins/localize-mixin";
-import HassMediaPlayerEntity from "../../../util/hass-media-player-model";
-
-/*
- * @appliesMixin LocalizeMixin
- * @appliesMixin EventsMixin
- */
-class MoreInfoMediaPlayer extends LocalizeMixin(EventsMixin(PolymerElement)) {
- static get template() {
- return html`
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- [[item]]
-
-
-
-
-
-
-
-
-
-
-
- [[item]]
-
-
-
-
-
-
-
-
- `;
- }
-
- static get properties() {
- return {
- hass: Object,
- stateObj: Object,
- playerObj: {
- type: Object,
- computed: "computePlayerObj(hass, stateObj)",
- observer: "playerObjChanged",
- },
-
- ttsLoaded: {
- type: Boolean,
- computed: "computeTTSLoaded(hass)",
- },
-
- ttsMessage: {
- type: String,
- value: "",
- },
-
- rtl: {
- type: String,
- computed: "_computeRTLDirection(hass)",
- },
- };
- }
-
- computePlayerObj(hass, stateObj) {
- return new HassMediaPlayerEntity(hass, stateObj);
- }
-
- playerObjChanged(newVal, oldVal) {
- if (oldVal) {
- setTimeout(() => {
- this.fire("iron-resize");
- }, 500);
- }
- }
-
- computeClassNames(stateObj) {
- return attributeClassNames(stateObj, ["volume_level"]);
- }
-
- computeMuteVolumeIcon(playerObj) {
- return playerObj.isMuted ? "hass:volume-off" : "hass:volume-high";
- }
-
- computeHideVolumeButtons(playerObj) {
- return !playerObj.supportsVolumeButtons || playerObj.isOff;
- }
-
- computeShowPlaybackControls(playerObj) {
- return !playerObj.isOff && playerObj.hasMediaControl;
- }
-
- computePlaybackControlIcon(playerObj) {
- if (playerObj.isPlaying) {
- return playerObj.supportsPause ? "hass:pause" : "hass:stop";
- }
- if (playerObj.hasMediaControl || playerObj.isOff || playerObj.isIdle) {
- if (
- playerObj.hasMediaControl &&
- playerObj.supportsPause &&
- !playerObj.isPaused
- ) {
- return "hass:play-pause";
- }
- return playerObj.supportsPlay ? "hass:play" : null;
- }
- return "";
- }
-
- computeHidePowerButton(playerObj) {
- return playerObj.isOff
- ? !playerObj.supportsTurnOn
- : !playerObj.supportsTurnOff;
- }
-
- computeHideSelectSource(playerObj) {
- return (
- playerObj.isOff ||
- !playerObj.supportsSelectSource ||
- !playerObj.sourceList
- );
- }
-
- computeHideSelectSoundMode(playerObj) {
- return (
- playerObj.isOff ||
- !playerObj.supportsSelectSoundMode ||
- !playerObj.soundModeList
- );
- }
-
- computeHideTTS(ttsLoaded, playerObj) {
- return !ttsLoaded || !playerObj.supportsPlayMedia;
- }
-
- computeTTSLoaded(hass) {
- return isComponentLoaded(hass, "tts");
- }
-
- handleTogglePower() {
- this.playerObj.togglePower();
- }
-
- handlePrevious() {
- this.playerObj.previousTrack();
- }
-
- handlePlaybackControl() {
- this.playerObj.mediaPlayPause();
- }
-
- handleNext() {
- this.playerObj.nextTrack();
- }
-
- handleSourceChanged(ev) {
- if (!this.playerObj) return;
-
- const oldVal = this.playerObj.source;
- const newVal = ev.detail.value;
-
- if (!newVal || oldVal === newVal) return;
-
- this.playerObj.selectSource(newVal);
- }
-
- handleSoundModeChanged(ev) {
- if (!this.playerObj) return;
-
- const oldVal = this.playerObj.soundMode;
- const newVal = ev.detail.value;
-
- if (!newVal || oldVal === newVal) return;
-
- this.playerObj.selectSoundMode(newVal);
- }
-
- handleVolumeTap() {
- if (!this.playerObj.supportsVolumeMute) {
- return;
- }
- this.playerObj.volumeMute(!this.playerObj.isMuted);
- }
-
- handleVolumeTouchEnd(ev) {
- /* when touch ends, we must prevent this from
- * becoming a mousedown, up, click by emulation */
- ev.preventDefault();
- }
-
- handleVolumeUp() {
- const obj = this.$.volumeUp;
- this.handleVolumeWorker("volume_up", obj, true);
- }
-
- handleVolumeDown() {
- const obj = this.$.volumeDown;
- this.handleVolumeWorker("volume_down", obj, true);
- }
-
- handleVolumeWorker(service, obj, force) {
- if (force || (obj !== undefined && obj.pointerDown)) {
- this.playerObj.callService(service);
- setTimeout(() => this.handleVolumeWorker(service, obj, false), 500);
- }
- }
-
- volumeSliderChanged(ev) {
- const volPercentage = parseFloat(ev.target.value);
- const volume = volPercentage > 0 ? volPercentage / 100 : 0;
- this.playerObj.setVolume(volume);
- }
-
- ttsCheckForEnter(ev) {
- if (ev.keyCode === 13) this.sendTTS();
- }
-
- sendTTS() {
- const services = this.hass.services.tts;
- const serviceKeys = Object.keys(services).sort();
- let service;
- let i;
-
- for (i = 0; i < serviceKeys.length; i++) {
- if (serviceKeys[i].indexOf("_say") !== -1) {
- service = serviceKeys[i];
- break;
- }
- }
-
- if (!service) {
- return;
- }
-
- this.hass.callService("tts", service, {
- entity_id: this.stateObj.entity_id,
- message: this.ttsMessage,
- });
- this.ttsMessage = "";
- this.$.ttsInput.focus();
- }
-
- _computeRTLDirection(hass) {
- return computeRTLDirection(hass);
- }
-}
-
-customElements.define("more-info-media_player", MoreInfoMediaPlayer);
diff --git a/src/dialogs/more-info/controls/more-info-media_player.ts b/src/dialogs/more-info/controls/more-info-media_player.ts
new file mode 100644
index 0000000000..efa96dec86
--- /dev/null
+++ b/src/dialogs/more-info/controls/more-info-media_player.ts
@@ -0,0 +1,381 @@
+import "@polymer/paper-item/paper-item";
+import "@polymer/paper-listbox/paper-listbox";
+import "@polymer/paper-input/paper-input";
+
+import {
+ css,
+ CSSResult,
+ html,
+ LitElement,
+ property,
+ TemplateResult,
+ customElement,
+ query,
+} from "lit-element";
+import { computeRTLDirection } from "../../../common/util/compute_rtl";
+import { HomeAssistant, MediaEntity } from "../../../types";
+import { supportsFeature } from "../../../common/entity/supports-feature";
+import { UNAVAILABLE_STATES, UNAVAILABLE, UNKNOWN } from "../../../data/entity";
+import {
+ SUPPORT_TURN_ON,
+ SUPPORT_TURN_OFF,
+ SUPPORTS_PLAY,
+ SUPPORT_PREVIOUS_TRACK,
+ SUPPORT_PAUSE,
+ SUPPORT_STOP,
+ SUPPORT_NEXT_TRACK,
+ SUPPORT_VOLUME_MUTE,
+ SUPPORT_VOLUME_SET,
+ SUPPORT_VOLUME_BUTTONS,
+ SUPPORT_SELECT_SOURCE,
+ SUPPORT_SELECT_SOUND_MODE,
+ SUPPORT_PLAY_MEDIA,
+ ControlButton,
+} from "../../../data/media-player";
+import { isComponentLoaded } from "../../../common/config/is_component_loaded";
+
+import "../../../components/ha-paper-dropdown-menu";
+import "../../../components/ha-icon-button";
+import "../../../components/ha-slider";
+import "../../../components/ha-icon";
+
+@customElement("more-info-media_player")
+class MoreInfoMediaPlayer extends LitElement {
+ @property({ attribute: false }) public hass!: HomeAssistant;
+
+ @property({ attribute: false }) public stateObj?: MediaEntity;
+
+ @query("#ttsInput") private _ttsInput?: HTMLInputElement;
+
+ protected render(): TemplateResult {
+ if (!this.stateObj) {
+ return html``;
+ }
+
+ const controls = this._getControls();
+ const stateObj = this.stateObj;
+
+ return html`
+ ${!controls
+ ? ""
+ : html`
+
+ ${controls!.map(
+ (control) => html`
+
+ `
+ )}
+
+ `}
+ ${(supportsFeature(stateObj, SUPPORT_VOLUME_SET) ||
+ supportsFeature(stateObj, SUPPORT_VOLUME_BUTTONS)) &&
+ ![UNAVAILABLE, UNKNOWN, "off"].includes(stateObj.state)
+ ? html`
+
+ ${supportsFeature(stateObj, SUPPORT_VOLUME_MUTE)
+ ? html`
+
+ `
+ : ""}
+ ${supportsFeature(stateObj, SUPPORT_VOLUME_SET)
+ ? html`
+
+ `
+ : supportsFeature(stateObj, SUPPORT_VOLUME_BUTTONS)
+ ? html`
+
+
+ `
+ : ""}
+
+ `
+ : ""}
+ ${stateObj.state !== "off" &&
+ supportsFeature(stateObj, SUPPORT_SELECT_SOURCE) &&
+ stateObj.attributes.source_list?.length
+ ? html`
+
+ `
+ : ""}
+ ${supportsFeature(stateObj, SUPPORT_SELECT_SOUND_MODE) &&
+ stateObj.attributes.sound_mode_list?.length
+ ? html`
+
+ `
+ : ""}
+ ${isComponentLoaded(this.hass, "tts") &&
+ supportsFeature(stateObj, SUPPORT_PLAY_MEDIA)
+ ? html`
+
+
+ `
+ : ""}
+ `;
+ }
+
+ static get styles(): CSSResult {
+ return css`
+ ha-icon-button[action="turn_off"],
+ ha-icon-button[action="turn_on"],
+ ha-slider,
+ #ttsInput {
+ flex-grow: 1;
+ }
+
+ .volume,
+ .controls,
+ .source-input,
+ .sound-input,
+ .tts {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ }
+
+ .source-input ha-icon,
+ .sound-input ha-icon {
+ padding: 7px;
+ margin-top: 24px;
+ }
+
+ .source-input ha-paper-dropdown-menu,
+ .sound-input ha-paper-dropdown-menu {
+ margin-left: 10px;
+ flex-grow: 1;
+ }
+
+ paper-item {
+ cursor: pointer;
+ }
+ `;
+ }
+
+ private _getControls(): ControlButton[] | undefined {
+ const stateObj = this.stateObj;
+
+ if (!stateObj) {
+ return undefined;
+ }
+
+ const state = stateObj.state;
+
+ if (UNAVAILABLE_STATES.includes(state)) {
+ return undefined;
+ }
+
+ if (state === "off") {
+ return supportsFeature(stateObj, SUPPORT_TURN_ON)
+ ? [
+ {
+ icon: "hass:power",
+ action: "turn_on",
+ },
+ ]
+ : undefined;
+ }
+
+ if (state === "idle") {
+ return supportsFeature(stateObj, SUPPORTS_PLAY)
+ ? [
+ {
+ icon: "hass:play",
+ action: "media_play",
+ },
+ ]
+ : undefined;
+ }
+
+ const buttons: ControlButton[] = [];
+
+ if (supportsFeature(stateObj, SUPPORT_TURN_OFF)) {
+ buttons.push({
+ icon: "hass:power",
+ action: "turn_off",
+ });
+ }
+
+ if (supportsFeature(stateObj, SUPPORT_PREVIOUS_TRACK)) {
+ buttons.push({
+ icon: "hass:skip-previous",
+ action: "media_previous_track",
+ });
+ }
+
+ if (
+ (state === "playing" &&
+ (supportsFeature(stateObj, SUPPORT_PAUSE) ||
+ supportsFeature(stateObj, SUPPORT_STOP))) ||
+ (state === "paused" && supportsFeature(stateObj, SUPPORTS_PLAY))
+ ) {
+ buttons.push({
+ icon:
+ state !== "playing"
+ ? "hass:play"
+ : supportsFeature(stateObj, SUPPORT_PAUSE)
+ ? "hass:pause"
+ : "hass:stop",
+ action: "media_play_pause",
+ });
+ }
+
+ if (supportsFeature(stateObj, SUPPORT_NEXT_TRACK)) {
+ buttons.push({
+ icon: "hass:skip-next",
+ action: "media_next_track",
+ });
+ }
+
+ return buttons.length > 0 ? buttons : undefined;
+ }
+
+ private _handleClick(e: MouseEvent): void {
+ this.hass!.callService(
+ "media_player",
+ (e.currentTarget! as HTMLElement).getAttribute("action")!,
+ {
+ entity_id: this.stateObj!.entity_id,
+ }
+ );
+ }
+
+ private _toggleMute() {
+ this.hass!.callService("media_player", "volume_mute", {
+ entity_id: this.stateObj!.entity_id,
+ is_volume_muted: !this.stateObj!.attributes.is_volume_muted,
+ });
+ }
+
+ private _selectedValueChanged(e: Event): void {
+ this.hass!.callService("media_player", "volume_set", {
+ entity_id: this.stateObj!.entity_id,
+ volume_level:
+ Number((e.currentTarget! as HTMLElement).getAttribute("value")!) / 100,
+ });
+ }
+
+ private _handleSourceChanged(e: CustomEvent) {
+ const newVal = e.detail.value;
+
+ if (!newVal || this.stateObj!.attributes.source === newVal) return;
+
+ this.hass.callService("media_player", "select_source", {
+ source: newVal,
+ });
+ }
+
+ private _handleSoundModeChanged(e: CustomEvent) {
+ const newVal = e.detail.value;
+
+ if (!newVal || this.stateObj?.attributes.sound_mode === newVal) return;
+
+ this.hass.callService("media_player", "select_sound_mode", {
+ sound_mode: newVal,
+ });
+ }
+
+ private _ttsCheckForEnter(e: KeyboardEvent) {
+ if (e.keyCode === 13) this._sendTTS();
+ }
+
+ private _sendTTS() {
+ const ttsInput = this._ttsInput;
+ if (!ttsInput) {
+ return;
+ }
+
+ const services = this.hass.services.tts;
+ const serviceKeys = Object.keys(services).sort();
+
+ const service = serviceKeys.find((key) => key.indexOf("_say") !== -1);
+
+ if (!service) {
+ return;
+ }
+
+ this.hass.callService("tts", service, {
+ entity_id: this.stateObj!.entity_id,
+ message: ttsInput.value,
+ });
+ ttsInput.value = "";
+ }
+}
+
+declare global {
+ interface HTMLElementTagNameMap {
+ "more-info-media_player": MoreInfoMediaPlayer;
+ }
+}
diff --git a/src/external_app/external_config.ts b/src/external_app/external_config.ts
index 7651b1307b..d911ea2593 100644
--- a/src/external_app/external_config.ts
+++ b/src/external_app/external_config.ts
@@ -2,6 +2,7 @@ import { ExternalMessaging } from "./external_messaging";
export interface ExternalConfig {
hasSettingsScreen: boolean;
+ canWriteTag: boolean;
}
export const getExternalConfig = (
diff --git a/src/panels/config/automation/trigger/ha-automation-trigger-row.ts b/src/panels/config/automation/trigger/ha-automation-trigger-row.ts
index c88c644683..a50373bbc8 100644
--- a/src/panels/config/automation/trigger/ha-automation-trigger-row.ts
+++ b/src/panels/config/automation/trigger/ha-automation-trigger-row.ts
@@ -34,6 +34,7 @@ import "./types/ha-automation-trigger-time";
import "./types/ha-automation-trigger-time_pattern";
import "./types/ha-automation-trigger-webhook";
import "./types/ha-automation-trigger-zone";
+import "./types/ha-automation-trigger-tag";
import { ActionDetail } from "@material/mwc-list/mwc-list-foundation";
import { haStyle } from "../../../../resources/styles";
@@ -46,6 +47,7 @@ const OPTIONS = [
"mqtt",
"numeric_state",
"sun",
+ "tag",
"template",
"time",
"time_pattern",
diff --git a/src/panels/config/automation/trigger/types/ha-automation-trigger-tag.ts b/src/panels/config/automation/trigger/types/ha-automation-trigger-tag.ts
new file mode 100644
index 0000000000..954e1ef144
--- /dev/null
+++ b/src/panels/config/automation/trigger/types/ha-automation-trigger-tag.ts
@@ -0,0 +1,72 @@
+import "@polymer/paper-input/paper-input";
+import {
+ customElement,
+ html,
+ LitElement,
+ property,
+ internalProperty,
+ PropertyValues,
+} from "lit-element";
+import { TagTrigger } from "../../../../../data/automation";
+import { HomeAssistant } from "../../../../../types";
+import { TriggerElement } from "../ha-automation-trigger-row";
+import { Tag, fetchTags } from "../../../../../data/tag";
+import { fireEvent } from "../../../../../common/dom/fire_event";
+
+@customElement("ha-automation-trigger-tag")
+export class HaTagTrigger extends LitElement implements TriggerElement {
+ @property({ attribute: false }) public hass!: HomeAssistant;
+
+ @property() public trigger!: TagTrigger;
+
+ @internalProperty() private _tags: Tag[] = [];
+
+ public static get defaultConfig() {
+ return { tag_id: "" };
+ }
+
+ protected firstUpdated(changedProperties: PropertyValues) {
+ super.firstUpdated(changedProperties);
+ this._fetchTags();
+ }
+
+ protected render() {
+ const { tag_id } = this.trigger;
+ return html`
+
+
+ ${this._tags.map(
+ (tag) => html`
+
+ ${tag.name || tag.id}
+
+ `
+ )}
+
+
+ `;
+ }
+
+ private async _fetchTags() {
+ this._tags = await fetchTags(this.hass);
+ }
+
+ private _tagChanged(ev) {
+ fireEvent(this, "value-changed", {
+ value: {
+ ...this.trigger,
+ tag_id: ev.detail.item.tag.id,
+ },
+ });
+ }
+}
diff --git a/src/panels/config/devices/device-detail/integration-elements/ozw/ha-device-info-ozw.ts b/src/panels/config/devices/device-detail/integration-elements/ozw/ha-device-info-ozw.ts
index b82ba363f0..2197fe689b 100644
--- a/src/panels/config/devices/device-detail/integration-elements/ozw/ha-device-info-ozw.ts
+++ b/src/panels/config/devices/device-detail/integration-elements/ozw/ha-device-info-ozw.ts
@@ -12,7 +12,13 @@ import {
import { DeviceRegistryEntry } from "../../../../../../data/device_registry";
import { haStyle } from "../../../../../../resources/styles";
import { HomeAssistant } from "../../../../../../types";
-import { OZWDevice, fetchOZWNodeStatus } from "../../../../../../data/ozw";
+import {
+ OZWDevice,
+ fetchOZWNodeStatus,
+ getIdentifiersFromDevice,
+ OZWNodeIdentifiers,
+} from "../../../../../../data/ozw";
+import { showOZWRefreshNodeDialog } from "../../../../integrations/integration-panels/ozw/show-dialog-ozw-refresh-node";
@customElement("ha-device-info-ozw")
export class HaDeviceInfoOzw extends LitElement {
@@ -20,26 +26,34 @@ export class HaDeviceInfoOzw extends LitElement {
@property() public device!: DeviceRegistryEntry;
+ @property()
+ private node_id = 0;
+
+ @property()
+ private ozw_instance = 1;
+
@internalProperty() private _ozwDevice?: OZWDevice;
protected updated(changedProperties: PropertyValues) {
if (changedProperties.has("device")) {
- this._fetchNodeDetails(this.device);
+ const identifiers:
+ | OZWNodeIdentifiers
+ | undefined = getIdentifiersFromDevice(this.device);
+ if (!identifiers) {
+ return;
+ }
+ this.ozw_instance = identifiers.ozw_instance;
+ this.node_id = identifiers.node_id;
+
+ this._fetchNodeDetails();
}
}
- protected async _fetchNodeDetails(device) {
- const ozwIdentifier = device.identifiers.find(
- (identifier) => identifier[0] === "ozw"
- );
- if (!ozwIdentifier) {
- return;
- }
- const identifiers = ozwIdentifier[1].split(".");
+ protected async _fetchNodeDetails() {
this._ozwDevice = await fetchOZWNodeStatus(
this.hass,
- identifiers[0],
- identifiers[1]
+ this.ozw_instance,
+ this.node_id
);
}
@@ -69,9 +83,19 @@ export class HaDeviceInfoOzw extends LitElement {
? this.hass.localize("ui.common.yes")
: this.hass.localize("ui.common.no")}
+