mirror of
https://github.com/home-assistant/frontend.git
synced 2025-08-19 22:29:27 +00:00
Compare commits
34 Commits
Move-Devel
...
entity-fil
Author | SHA1 | Date | |
---|---|---|---|
![]() |
3459d0bb8c | ||
![]() |
8a9a93ef20 | ||
![]() |
94b561301f | ||
![]() |
86f5fe51c4 | ||
![]() |
c4cad5bccd | ||
![]() |
e4085fe1f6 | ||
![]() |
8bfef92c86 | ||
![]() |
0c07178c0a | ||
![]() |
1010777139 | ||
![]() |
e57477c16a | ||
![]() |
30fa92c120 | ||
![]() |
b32438dc18 | ||
![]() |
614bd2f451 | ||
![]() |
6c12a5a4b1 | ||
![]() |
bbcec38450 | ||
![]() |
416e2e26c0 | ||
![]() |
1a7164b466 | ||
![]() |
3ddcd2d0f6 | ||
![]() |
648c02e622 | ||
![]() |
b0b953bfac | ||
![]() |
abeaa63005 | ||
![]() |
9cd23374f4 | ||
![]() |
72bd5f84d6 | ||
![]() |
22b4550fdf | ||
![]() |
87c22229e0 | ||
![]() |
971fd8dc60 | ||
![]() |
049c3caadd | ||
![]() |
fb2a24d11e | ||
![]() |
d4646bac01 | ||
![]() |
14e5b2a7a5 | ||
![]() |
734a733a4c | ||
![]() |
8f31c182f6 | ||
![]() |
e51a819bfd | ||
![]() |
05d7e85aa3 |
@@ -1,6 +1,6 @@
|
||||
#!/bin/bash
|
||||
|
||||
TARGET_LABEL="Needs design preview"
|
||||
TARGET_LABEL="needs design preview"
|
||||
|
||||
if [[ "$NETLIFY" != "true" ]]; then
|
||||
echo "This script can only be run on Netlify"
|
||||
|
@@ -20,6 +20,7 @@ module.exports = [
|
||||
"editor-trigger",
|
||||
"editor-condition",
|
||||
"editor-action",
|
||||
"selectors",
|
||||
"trace",
|
||||
"trace-timeline",
|
||||
],
|
||||
|
3
gallery/src/pages/automation/selectors.markdown
Normal file
3
gallery/src/pages/automation/selectors.markdown
Normal file
@@ -0,0 +1,3 @@
|
||||
---
|
||||
title: Selectors
|
||||
---
|
102
gallery/src/pages/automation/selectors.ts
Normal file
102
gallery/src/pages/automation/selectors.ts
Normal file
@@ -0,0 +1,102 @@
|
||||
/* eslint-disable lit/no-template-arrow */
|
||||
import { LitElement, TemplateResult, html } from "lit";
|
||||
import { customElement, state } from "lit/decorators";
|
||||
import { provideHass } from "../../../../src/fake_data/provide_hass";
|
||||
import type { HomeAssistant } from "../../../../src/types";
|
||||
import "../../components/demo-black-white-row";
|
||||
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 "../../../../src/panels/config/automation/trigger/ha-automation-trigger";
|
||||
import { Selector } from "../../../../src/data/selector";
|
||||
import "../../../../src/components/ha-selector/ha-selector";
|
||||
|
||||
const SCHEMAS: { name: string; selector: Selector }[] = [
|
||||
{ name: "Addon", selector: { addon: {} } },
|
||||
|
||||
{ name: "Entity", selector: { entity: {} } },
|
||||
{ name: "Device", selector: { device: {} } },
|
||||
{ name: "Area", selector: { area: {} } },
|
||||
{ name: "Target", selector: { target: {} } },
|
||||
{
|
||||
name: "Number",
|
||||
selector: {
|
||||
number: {
|
||||
min: 0,
|
||||
max: 10,
|
||||
},
|
||||
},
|
||||
},
|
||||
{ name: "Boolean", selector: { boolean: {} } },
|
||||
{ name: "Time", selector: { time: {} } },
|
||||
{ name: "Action", selector: { action: {} } },
|
||||
{ name: "Text", selector: { text: { multiline: false } } },
|
||||
{ name: "Text Multiline", selector: { text: { multiline: true } } },
|
||||
{ name: "Object", selector: { object: {} } },
|
||||
{
|
||||
name: "Select",
|
||||
selector: {
|
||||
select: {
|
||||
options: ["Everyone Home", "Some Home", "All gone"],
|
||||
},
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
@customElement("demo-automation-selectors")
|
||||
class DemoHaSelector extends LitElement {
|
||||
@state() private hass!: HomeAssistant;
|
||||
|
||||
private data: any = SCHEMAS.map(() => undefined);
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
const hass = provideHass(this);
|
||||
hass.updateTranslations(null, "en");
|
||||
hass.updateTranslations("config", "en");
|
||||
mockEntityRegistry(hass);
|
||||
mockDeviceRegistry(hass);
|
||||
mockAreaRegistry(hass);
|
||||
mockHassioSupervisor(hass);
|
||||
}
|
||||
|
||||
protected render(): TemplateResult {
|
||||
const valueChanged = (ev) => {
|
||||
const sampleIdx = ev.target.sampleIdx;
|
||||
this.data[sampleIdx] = ev.detail.value;
|
||||
this.requestUpdate();
|
||||
};
|
||||
return html`
|
||||
${SCHEMAS.map(
|
||||
(info, sampleIdx) => html`
|
||||
<demo-black-white-row
|
||||
.title=${info.name}
|
||||
.value=${{ selector: info.selector, data: this.data[sampleIdx] }}
|
||||
>
|
||||
${["light", "dark"].map(
|
||||
(slot) =>
|
||||
html`
|
||||
<ha-selector
|
||||
slot=${slot}
|
||||
.hass=${this.hass}
|
||||
.selector=${info.selector}
|
||||
.label=${info.name}
|
||||
.value=${this.data[sampleIdx]}
|
||||
.sampleIdx=${sampleIdx}
|
||||
@value-changed=${valueChanged}
|
||||
></ha-selector>
|
||||
`
|
||||
)}
|
||||
</demo-black-white-row>
|
||||
`
|
||||
)}
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"demo-automation-selectors": DemoHaSelector;
|
||||
}
|
||||
}
|
2
setup.py
2
setup.py
@@ -2,7 +2,7 @@ from setuptools import setup, find_packages
|
||||
|
||||
setup(
|
||||
name="home-assistant-frontend",
|
||||
version="20220118.0",
|
||||
version="20220124.0",
|
||||
description="The Home Assistant frontend",
|
||||
url="https://github.com/home-assistant/frontend",
|
||||
author="The Home Assistant Authors",
|
||||
|
@@ -43,9 +43,9 @@ export const formatTimeWeekday = (dateObj: Date, locale: FrontendLocaleData) =>
|
||||
const formatTimeWeekdayMem = memoizeOne(
|
||||
(locale: FrontendLocaleData) =>
|
||||
new Intl.DateTimeFormat(locale.language, {
|
||||
weekday: "long",
|
||||
hour: useAmPm(locale) ? "numeric" : "2-digit",
|
||||
minute: "2-digit",
|
||||
second: "2-digit",
|
||||
hour12: useAmPm(locale),
|
||||
})
|
||||
);
|
||||
|
@@ -5,7 +5,10 @@ import type { ClassElement } from "../../types";
|
||||
type Callback = (oldValue: any, newValue: any) => void;
|
||||
|
||||
class Storage {
|
||||
constructor() {
|
||||
constructor(subscribe = true) {
|
||||
if (!subscribe) {
|
||||
return;
|
||||
}
|
||||
window.addEventListener("storage", (ev: StorageEvent) => {
|
||||
if (ev.key && this.hasKey(ev.key)) {
|
||||
this._storage[ev.key] = ev.newValue
|
||||
@@ -80,15 +83,18 @@ class Storage {
|
||||
}
|
||||
}
|
||||
|
||||
const storage = new Storage();
|
||||
const subscribeStorage = new Storage();
|
||||
|
||||
export const LocalStorage =
|
||||
(
|
||||
storageKey?: string,
|
||||
property?: boolean,
|
||||
subscribe = true,
|
||||
propertyOptions?: PropertyDeclaration
|
||||
): any =>
|
||||
(clsElement: ClassElement) => {
|
||||
const storage = subscribe ? subscribeStorage : new Storage(false);
|
||||
|
||||
const key = String(clsElement.key);
|
||||
storageKey = storageKey || String(clsElement.key);
|
||||
const initVal = clsElement.initializer
|
||||
@@ -97,7 +103,7 @@ export const LocalStorage =
|
||||
|
||||
storage.addFromStorage(storageKey);
|
||||
|
||||
const subscribe = (el: ReactiveElement): UnsubscribeFunc =>
|
||||
const subscribeChanges = (el: ReactiveElement): UnsubscribeFunc =>
|
||||
storage.subscribeChanges(storageKey!, (oldValue) => {
|
||||
el.requestUpdate(clsElement.key, oldValue);
|
||||
});
|
||||
@@ -131,17 +137,19 @@ export const LocalStorage =
|
||||
configurable: true,
|
||||
},
|
||||
finisher(cls: typeof ReactiveElement) {
|
||||
if (property) {
|
||||
if (property && subscribe) {
|
||||
const connectedCallback = cls.prototype.connectedCallback;
|
||||
const disconnectedCallback = cls.prototype.disconnectedCallback;
|
||||
cls.prototype.connectedCallback = function () {
|
||||
connectedCallback.call(this);
|
||||
this[`__unbsubLocalStorage${key}`] = subscribe(this);
|
||||
this[`__unbsubLocalStorage${key}`] = subscribeChanges(this);
|
||||
};
|
||||
cls.prototype.disconnectedCallback = function () {
|
||||
disconnectedCallback.call(this);
|
||||
this[`__unbsubLocalStorage${key}`]();
|
||||
};
|
||||
}
|
||||
if (property) {
|
||||
cls.createProperty(clsElement.key, {
|
||||
noAccessor: true,
|
||||
...propertyOptions,
|
||||
|
@@ -43,7 +43,7 @@ export const computeStateDisplay = (
|
||||
|
||||
if (domain === "input_datetime") {
|
||||
if (state !== undefined) {
|
||||
// If trying to display an explicit state, need to parse the explict state to `Date` then format.
|
||||
// If trying to display an explicit state, need to parse the explicit state to `Date` then format.
|
||||
// Attributes aren't available, we have to use `state`.
|
||||
try {
|
||||
const components = state.split(" ");
|
||||
|
@@ -1,5 +1,5 @@
|
||||
import type { HassEntity } from "home-assistant-js-websocket";
|
||||
import { html, LitElement, TemplateResult } from "lit";
|
||||
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import { isValidEntityId } from "../../common/entity/valid_entity_id";
|
||||
@@ -51,6 +51,8 @@ class HaEntitiesPickerLight extends LitElement {
|
||||
|
||||
@property({ attribute: "pick-entity-label" }) public pickEntityLabel?: string;
|
||||
|
||||
@property() public label?: string;
|
||||
|
||||
protected render(): TemplateResult {
|
||||
if (!this.hass) {
|
||||
return html``;
|
||||
@@ -58,6 +60,7 @@ class HaEntitiesPickerLight extends LitElement {
|
||||
|
||||
const currentEntities = this._currentEntities;
|
||||
return html`
|
||||
<h3>${this.label}</h3>
|
||||
${currentEntities.map(
|
||||
(entityId) => html`
|
||||
<div>
|
||||
@@ -145,6 +148,14 @@ class HaEntitiesPickerLight extends LitElement {
|
||||
|
||||
this._updateEntities([...currentEntities, toAdd]);
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return css`
|
||||
:host {
|
||||
display: var(--entity-picker-display);
|
||||
}
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
|
@@ -147,7 +147,7 @@ export class HaStateLabelBadge extends LitElement {
|
||||
default:
|
||||
return entityState.state === UNKNOWN ||
|
||||
entityState.state === UNAVAILABLE
|
||||
? "-"
|
||||
? "—"
|
||||
: isNumericState(entityState)
|
||||
? formatNumber(entityState.state, this.hass!.locale)
|
||||
: computeStateDisplay(
|
||||
|
@@ -296,6 +296,10 @@ export class HaStatisticPicker extends LitElement {
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return css`
|
||||
:host {
|
||||
display: var(--entity-picker-display);
|
||||
}
|
||||
|
||||
paper-input > ha-icon-button {
|
||||
--mdc-icon-button-size: 24px;
|
||||
padding: 2px;
|
||||
|
@@ -120,7 +120,7 @@ class HaAttributes extends LitElement {
|
||||
|
||||
private formatAttribute(attribute: string): string | TemplateResult {
|
||||
if (!this.stateObj) {
|
||||
return "-";
|
||||
return "—";
|
||||
}
|
||||
const value = this.stateObj.attributes[attribute];
|
||||
return formatAttributeValue(this.hass, value);
|
||||
|
@@ -68,7 +68,6 @@ export class HaFormString extends LitElement implements HaFormElement {
|
||||
toggles
|
||||
.label=${`${this._unmaskedPassword ? "Hide" : "Show"} password`}
|
||||
@click=${this._toggleUnmaskedPassword}
|
||||
tabindex="-1"
|
||||
.path=${this._unmaskedPassword ? mdiEyeOff : mdiEye}
|
||||
></ha-icon-button>`
|
||||
: ""}
|
||||
|
@@ -1,16 +1,10 @@
|
||||
import { css, LitElement, PropertyValues, svg, TemplateResult } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { ifDefined } from "lit/directives/if-defined";
|
||||
import { styleMap } from "lit/directives/style-map";
|
||||
import { formatNumber } from "../common/number/format_number";
|
||||
import { afterNextRender } from "../common/util/render-status";
|
||||
import { FrontendLocaleData } from "../data/translation";
|
||||
import { getValueInPercentage, normalize } from "../util/calculate";
|
||||
import { isSafari } from "../util/is_safari";
|
||||
|
||||
// Safari version 15.2 and up behaves differently than other Safari versions.
|
||||
// https://github.com/home-assistant/frontend/issues/10766
|
||||
const isSafari152 = isSafari && /Version\/15\.[^0-1]/.test(navigator.userAgent);
|
||||
|
||||
const getAngle = (value: number, min: number, max: number) => {
|
||||
const percentage = getValueInPercentage(normalize(value, min, max), min, max);
|
||||
@@ -65,12 +59,12 @@ export class Gauge extends LitElement {
|
||||
|
||||
protected render() {
|
||||
return svg`
|
||||
<svg viewBox="0 0 100 50" class="gauge">
|
||||
<svg viewBox="-50 -50 100 50" class="gauge">
|
||||
${
|
||||
!this.needle || !this.levels
|
||||
? svg`<path
|
||||
class="dial"
|
||||
d="M 10 50 A 40 40 0 0 1 90 50"
|
||||
d="M -40 0 A 40 40 0 0 1 40 0"
|
||||
></path>`
|
||||
: ""
|
||||
}
|
||||
@@ -87,9 +81,9 @@ export class Gauge extends LitElement {
|
||||
stroke="var(--info-color)"
|
||||
class="level"
|
||||
d="M
|
||||
${50 - 40 * Math.cos((angle * Math.PI) / 180)}
|
||||
${50 - 40 * Math.sin((angle * Math.PI) / 180)}
|
||||
A 40 40 0 0 1 90 50
|
||||
${0 - 40 * Math.cos((angle * Math.PI) / 180)}
|
||||
${0 - 40 * Math.sin((angle * Math.PI) / 180)}
|
||||
A 40 40 0 0 1 40 0
|
||||
"
|
||||
></path>`;
|
||||
}
|
||||
@@ -98,9 +92,9 @@ export class Gauge extends LitElement {
|
||||
stroke="${level.stroke}"
|
||||
class="level"
|
||||
d="M
|
||||
${50 - 40 * Math.cos((angle * Math.PI) / 180)}
|
||||
${50 - 40 * Math.sin((angle * Math.PI) / 180)}
|
||||
A 40 40 0 0 1 90 50
|
||||
${0 - 40 * Math.cos((angle * Math.PI) / 180)}
|
||||
${0 - 40 * Math.sin((angle * Math.PI) / 180)}
|
||||
A 40 40 0 0 1 40 0
|
||||
"
|
||||
></path>`;
|
||||
})
|
||||
@@ -110,46 +104,16 @@ export class Gauge extends LitElement {
|
||||
this.needle
|
||||
? svg`<path
|
||||
class="needle"
|
||||
d="M 25 47.5 L 2.5 50 L 25 52.5 z"
|
||||
style=${ifDefined(
|
||||
!isSafari
|
||||
? styleMap({ transform: `rotate(${this._angle}deg)` })
|
||||
: undefined
|
||||
)}
|
||||
transform=${ifDefined(
|
||||
isSafari
|
||||
? `rotate(${this._angle}${isSafari152 ? "" : " 50 50"})`
|
||||
: undefined
|
||||
)}
|
||||
d="M -25 -2.5 L -47.5 0 L -25 2.5 z"
|
||||
style=${styleMap({ transform: `rotate(${this._angle}deg)` })}
|
||||
>
|
||||
`
|
||||
: svg`<path
|
||||
class="value"
|
||||
d="M 90 50.001 A 40 40 0 0 1 10 50"
|
||||
style=${ifDefined(
|
||||
!isSafari
|
||||
? styleMap({ transform: `rotate(${this._angle}deg)` })
|
||||
: undefined
|
||||
)}
|
||||
transform=${ifDefined(
|
||||
isSafari
|
||||
? `rotate(${this._angle}${isSafari152 ? "" : " 50 50"})`
|
||||
: undefined
|
||||
)}
|
||||
d="M -40 0 A 40 40 0 1 0 40 0"
|
||||
style=${styleMap({ transform: `rotate(${this._angle}deg)` })}
|
||||
>`
|
||||
}
|
||||
${
|
||||
// Workaround for https://github.com/home-assistant/frontend/issues/6467
|
||||
isSafari
|
||||
? svg`<animateTransform
|
||||
attributeName="transform"
|
||||
type="rotate"
|
||||
from="0 50 50"
|
||||
to="${this._angle} 50 50"
|
||||
dur="1s"
|
||||
/>`
|
||||
: ""
|
||||
}
|
||||
</path>
|
||||
</svg>
|
||||
<svg class="text">
|
||||
@@ -187,12 +151,10 @@ export class Gauge extends LitElement {
|
||||
fill: none;
|
||||
stroke-width: 15;
|
||||
stroke: var(--gauge-color);
|
||||
transform-origin: 50% 100%;
|
||||
transition: all 1s ease 0s;
|
||||
}
|
||||
.needle {
|
||||
fill: var(--primary-text-color);
|
||||
transform-origin: 50% 100%;
|
||||
transition: all 1s ease 0s;
|
||||
}
|
||||
.level {
|
||||
|
@@ -9,7 +9,6 @@ import {
|
||||
} from "lit";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import { nextRender } from "../common/util/render-status";
|
||||
import { getExternalConfig } from "../external_app/external_config";
|
||||
import type { HomeAssistant } from "../types";
|
||||
import "./ha-alert";
|
||||
|
||||
@@ -91,18 +90,9 @@ class HaHLSPlayer extends LitElement {
|
||||
this._startHls();
|
||||
}
|
||||
|
||||
private async _getUseExoPlayer(): Promise<boolean> {
|
||||
if (!this.hass!.auth.external || !this.allowExoPlayer) {
|
||||
return false;
|
||||
}
|
||||
const externalConfig = await getExternalConfig(this.hass!.auth.external);
|
||||
return externalConfig && externalConfig.hasExoPlayer;
|
||||
}
|
||||
|
||||
private async _startHls(): Promise<void> {
|
||||
this._error = undefined;
|
||||
|
||||
const useExoPlayerPromise = this._getUseExoPlayer();
|
||||
const masterPlaylistPromise = fetch(this.url);
|
||||
|
||||
const Hls: typeof HlsType = (await import("hls.js/dist/hls.light.min"))
|
||||
@@ -126,7 +116,8 @@ class HaHLSPlayer extends LitElement {
|
||||
return;
|
||||
}
|
||||
|
||||
const useExoPlayer = await useExoPlayerPromise;
|
||||
const useExoPlayer =
|
||||
this.allowExoPlayer && this.hass.auth.external?.config.hasExoPlayer;
|
||||
const masterPlaylist = await (await masterPlaylistPromise).text();
|
||||
|
||||
if (!this.isConnected) {
|
||||
|
@@ -130,6 +130,33 @@ export class HaServiceControl extends LitElement {
|
||||
this._value = this.value;
|
||||
}
|
||||
|
||||
if (oldValue?.service !== this.value?.service) {
|
||||
let updatedDefaultValue = false;
|
||||
if (this._value && serviceData) {
|
||||
// Set mandatory bools without a default value to false
|
||||
this._value.data ??= {};
|
||||
serviceData.fields.forEach((field) => {
|
||||
if (
|
||||
field.selector &&
|
||||
field.required &&
|
||||
field.default === undefined &&
|
||||
"boolean" in field.selector &&
|
||||
this._value!.data![field.key] === undefined
|
||||
) {
|
||||
updatedDefaultValue = true;
|
||||
this._value!.data![field.key] = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
if (updatedDefaultValue) {
|
||||
fireEvent(this, "value-changed", {
|
||||
value: {
|
||||
...this._value,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (this._value?.data) {
|
||||
const yamlEditor = this._yamlEditor;
|
||||
if (yamlEditor && yamlEditor.value !== this._value.data) {
|
||||
|
@@ -8,6 +8,7 @@ import {
|
||||
mdiClose,
|
||||
mdiCog,
|
||||
mdiFormatListBulletedType,
|
||||
mdiHammer,
|
||||
mdiLightningBolt,
|
||||
mdiMenu,
|
||||
mdiMenuOpen,
|
||||
@@ -43,10 +44,6 @@ import {
|
||||
PersistentNotification,
|
||||
subscribeNotifications,
|
||||
} from "../data/persistent_notification";
|
||||
import {
|
||||
ExternalConfig,
|
||||
getExternalConfig,
|
||||
} from "../external_app/external_config";
|
||||
import { actionHandler } from "../panels/lovelace/common/directives/action-handler-directive";
|
||||
import { haStyleScrollbar } from "../resources/styles";
|
||||
import type { HomeAssistant, PanelInfo, Route } from "../types";
|
||||
@@ -56,7 +53,7 @@ import "./ha-menu-button";
|
||||
import "./ha-svg-icon";
|
||||
import "./user/ha-user-badge";
|
||||
|
||||
const SHOW_AFTER_SPACER = ["config"];
|
||||
const SHOW_AFTER_SPACER = ["config", "developer-tools"];
|
||||
|
||||
const SUPPORT_SCROLL_IF_NEEDED = "scrollIntoViewIfNeeded" in document.body;
|
||||
|
||||
@@ -65,12 +62,14 @@ const SORT_VALUE_URL_PATHS = {
|
||||
map: 2,
|
||||
logbook: 3,
|
||||
history: 4,
|
||||
"developer-tools": 9,
|
||||
config: 11,
|
||||
};
|
||||
|
||||
const PANEL_ICONS = {
|
||||
calendar: mdiCalendar,
|
||||
config: mdiCog,
|
||||
"developer-tools": mdiHammer,
|
||||
energy: mdiLightningBolt,
|
||||
history: mdiChartBox,
|
||||
logbook: mdiFormatListBulletedType,
|
||||
@@ -189,8 +188,6 @@ class HaSidebar extends LitElement {
|
||||
|
||||
@property({ type: Boolean }) public editMode = false;
|
||||
|
||||
@state() private _externalConfig?: ExternalConfig;
|
||||
|
||||
@state() private _notifications?: PersistentNotification[];
|
||||
|
||||
@state() private _renderEmptySortable = false;
|
||||
@@ -267,13 +264,6 @@ class HaSidebar extends LitElement {
|
||||
|
||||
protected firstUpdated(changedProps: PropertyValues) {
|
||||
super.firstUpdated(changedProps);
|
||||
|
||||
if (this.hass && this.hass.auth.external) {
|
||||
getExternalConfig(this.hass.auth.external).then((conf) => {
|
||||
this._externalConfig = conf;
|
||||
});
|
||||
}
|
||||
|
||||
subscribeNotifications(this.hass.connection, (notifications) => {
|
||||
this._notifications = notifications;
|
||||
});
|
||||
@@ -556,8 +546,7 @@ class HaSidebar extends LitElement {
|
||||
|
||||
private _renderExternalConfiguration() {
|
||||
return html`${!this.hass.user?.is_admin &&
|
||||
this._externalConfig &&
|
||||
this._externalConfig.hasSettingsScreen
|
||||
this.hass.auth.external?.config.hasSettingsScreen
|
||||
? html`
|
||||
<a
|
||||
role="option"
|
||||
@@ -1030,6 +1019,19 @@ class HaSidebar extends LitElement {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.dev-tools {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
padding: 0 8px;
|
||||
width: 256px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.dev-tools a {
|
||||
color: var(--sidebar-icon-color);
|
||||
}
|
||||
|
||||
.tooltip {
|
||||
display: none;
|
||||
position: absolute;
|
||||
|
@@ -12,7 +12,10 @@ export class HaSvgIcon extends LitElement {
|
||||
<svg
|
||||
viewBox=${this.viewBox || "0 0 24 24"}
|
||||
preserveAspectRatio="xMidYMid meet"
|
||||
focusable="false">
|
||||
focusable="false"
|
||||
role="img"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<g>
|
||||
${this.path ? svg`<path d=${this.path}></path>` : ""}
|
||||
</g>
|
||||
|
25
src/components/ha-textfield.ts
Normal file
25
src/components/ha-textfield.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import { TextField } from "@material/mwc-textfield";
|
||||
import { TemplateResult, html } from "lit";
|
||||
import { customElement } from "lit/decorators";
|
||||
|
||||
@customElement("ha-textfield")
|
||||
export class HaTextField extends TextField {
|
||||
override renderIcon(_icon: string, isTrailingIcon = false): TemplateResult {
|
||||
const type = isTrailingIcon ? "trailing" : "leading";
|
||||
|
||||
return html`
|
||||
<span
|
||||
class="mdc-text-field__icon mdc-text-field__icon--${type}"
|
||||
tabindex=${isTrailingIcon ? 1 : -1}
|
||||
>
|
||||
<slot name="${type}Icon"></slot>
|
||||
</span>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-textfield": HaTextField;
|
||||
}
|
||||
}
|
@@ -64,7 +64,7 @@ class HaWaterHeaterState extends LocalizeMixin(PolymerElement) {
|
||||
return `${formatNumber(
|
||||
stateObj.attributes.target_temp_low,
|
||||
this.hass.locale
|
||||
)} - ${formatNumber(
|
||||
)} – ${formatNumber(
|
||||
stateObj.attributes.target_temp_high,
|
||||
this.hass.locale
|
||||
)} ${hass.config.unit_system.temperature}`;
|
||||
|
@@ -60,6 +60,7 @@ export class HaYamlEditor extends LitElement {
|
||||
mode="yaml"
|
||||
.error=${this.isValid === false}
|
||||
@value-changed=${this._onChange}
|
||||
dir="ltr"
|
||||
></ha-code-editor>
|
||||
`;
|
||||
}
|
||||
|
@@ -340,7 +340,7 @@ export class HaMediaPlayerBrowse extends LitElement {
|
||||
</mwc-list>
|
||||
`
|
||||
: html`
|
||||
<div class="container">
|
||||
<div class="container no-items">
|
||||
${this.hass.localize("ui.components.media-browser.no_items")}
|
||||
<br />
|
||||
${currentItem.media_content_id ===
|
||||
@@ -696,6 +696,10 @@ export class HaMediaPlayerBrowse extends LitElement {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.no-items {
|
||||
padding-left: 32px;
|
||||
}
|
||||
|
||||
.content {
|
||||
overflow-y: auto;
|
||||
padding-bottom: 20px;
|
||||
|
@@ -17,6 +17,7 @@ export class HaTraceBlueprintConfig extends LitElement {
|
||||
<ha-code-editor
|
||||
.value=${dump(this.trace.blueprint_inputs || "").trimRight()}
|
||||
readOnly
|
||||
dir="ltr"
|
||||
></ha-code-editor>
|
||||
`;
|
||||
}
|
||||
|
@@ -17,6 +17,7 @@ export class HaTraceConfig extends LitElement {
|
||||
<ha-code-editor
|
||||
.value=${dump(this.trace.config).trimRight()}
|
||||
readOnly
|
||||
dir="ltr"
|
||||
></ha-code-editor>
|
||||
`;
|
||||
}
|
||||
|
@@ -150,6 +150,7 @@ export class HaTracePathDetails extends LitElement {
|
||||
? html`<ha-code-editor
|
||||
.value=${dump(config).trimRight()}
|
||||
readOnly
|
||||
dir="ltr"
|
||||
></ha-code-editor>`
|
||||
: "Unable to find config";
|
||||
}
|
||||
|
@@ -333,3 +333,12 @@ export const formatMediaTime = (seconds: number): string => {
|
||||
: secondsString.substring(14, 19);
|
||||
return secondsString.replace(/^0+/, "").padStart(4, "0");
|
||||
};
|
||||
|
||||
export const cleanupMediaTitle = (title?: string): string | undefined => {
|
||||
if (!title) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const index = title.indexOf("?authSig=");
|
||||
return index > 0 ? title.slice(0, index) : title;
|
||||
};
|
||||
|
15
src/data/media_source.ts
Normal file
15
src/data/media_source.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import { HomeAssistant } from "../types";
|
||||
|
||||
export interface ResolvedMediaSource {
|
||||
url: string;
|
||||
mime_type: string;
|
||||
}
|
||||
|
||||
export const resolveMediaSource = (
|
||||
hass: HomeAssistant,
|
||||
media_content_id: string
|
||||
) =>
|
||||
hass.callWS<ResolvedMediaSource>({
|
||||
type: "media_source/resolve_media",
|
||||
media_content_id,
|
||||
});
|
58
src/data/supervisor/root.ts
Normal file
58
src/data/supervisor/root.ts
Normal file
@@ -0,0 +1,58 @@
|
||||
import { HomeAssistant } from "../../types";
|
||||
|
||||
interface SupervisorBaseAvailableUpdates {
|
||||
panel_path?: string;
|
||||
update_type?: string;
|
||||
version_latest?: string;
|
||||
}
|
||||
|
||||
interface SupervisorAddonAvailableUpdates
|
||||
extends SupervisorBaseAvailableUpdates {
|
||||
update_type?: "addon";
|
||||
icon?: string;
|
||||
name?: string;
|
||||
}
|
||||
|
||||
interface SupervisorCoreAvailableUpdates
|
||||
extends SupervisorBaseAvailableUpdates {
|
||||
update_type?: "core";
|
||||
}
|
||||
|
||||
interface SupervisorOsAvailableUpdates extends SupervisorBaseAvailableUpdates {
|
||||
update_type?: "os";
|
||||
}
|
||||
|
||||
interface SupervisorSupervisorAvailableUpdates
|
||||
extends SupervisorBaseAvailableUpdates {
|
||||
update_type?: "supervisor";
|
||||
}
|
||||
|
||||
export type SupervisorAvailableUpdates =
|
||||
| SupervisorAddonAvailableUpdates
|
||||
| SupervisorCoreAvailableUpdates
|
||||
| SupervisorOsAvailableUpdates
|
||||
| SupervisorSupervisorAvailableUpdates;
|
||||
|
||||
export interface SupervisorAvailableUpdatesResponse {
|
||||
available_updates: SupervisorAvailableUpdates[];
|
||||
}
|
||||
|
||||
export const fetchSupervisorAvailableUpdates = async (
|
||||
hass: HomeAssistant
|
||||
): Promise<SupervisorAvailableUpdates[]> =>
|
||||
(
|
||||
await hass.callWS<SupervisorAvailableUpdatesResponse>({
|
||||
type: "supervisor/api",
|
||||
endpoint: "/available_updates",
|
||||
method: "get",
|
||||
})
|
||||
).available_updates;
|
||||
|
||||
export const refreshSupervisorAvailableUpdates = async (
|
||||
hass: HomeAssistant
|
||||
): Promise<void> =>
|
||||
hass.callWS<void>({
|
||||
type: "supervisor/api",
|
||||
endpoint: "/refresh_updates",
|
||||
method: "post",
|
||||
});
|
@@ -70,42 +70,6 @@ export interface Supervisor {
|
||||
localize: LocalizeFunc;
|
||||
}
|
||||
|
||||
interface SupervisorBaseAvailableUpdates {
|
||||
panel_path?: string;
|
||||
update_type?: string;
|
||||
version_latest?: string;
|
||||
}
|
||||
|
||||
interface SupervisorAddonAvailableUpdates
|
||||
extends SupervisorBaseAvailableUpdates {
|
||||
update_type?: "addon";
|
||||
icon?: string;
|
||||
name?: string;
|
||||
}
|
||||
|
||||
interface SupervisorCoreAvailableUpdates
|
||||
extends SupervisorBaseAvailableUpdates {
|
||||
update_type?: "core";
|
||||
}
|
||||
|
||||
interface SupervisorOsAvailableUpdates extends SupervisorBaseAvailableUpdates {
|
||||
update_type?: "os";
|
||||
}
|
||||
|
||||
interface SupervisorSupervisorAvailableUpdates
|
||||
extends SupervisorBaseAvailableUpdates {
|
||||
update_type?: "supervisor";
|
||||
}
|
||||
|
||||
export type SupervisorAvailableUpdates =
|
||||
| SupervisorAddonAvailableUpdates
|
||||
| SupervisorCoreAvailableUpdates
|
||||
| SupervisorOsAvailableUpdates
|
||||
| SupervisorSupervisorAvailableUpdates;
|
||||
|
||||
export interface SupervisorAvailableUpdatesResponse {
|
||||
available_updates: SupervisorAvailableUpdates[];
|
||||
}
|
||||
export const supervisorApiWsRequest = <T>(
|
||||
conn: Connection,
|
||||
request: supervisorApiRequest
|
||||
@@ -175,14 +139,3 @@ export const subscribeSupervisorEvents = (
|
||||
getSupervisorEventCollection(hass.connection, key, endpoint).subscribe(
|
||||
onChange
|
||||
);
|
||||
|
||||
export const fetchSupervisorAvailableUpdates = async (
|
||||
hass: HomeAssistant
|
||||
): Promise<SupervisorAvailableUpdates[]> =>
|
||||
(
|
||||
await hass.callWS<SupervisorAvailableUpdatesResponse>({
|
||||
type: "supervisor/api",
|
||||
endpoint: "/supervisor/available_updates",
|
||||
method: "get",
|
||||
})
|
||||
).available_updates;
|
||||
|
@@ -436,3 +436,19 @@ export const getWeatherStateIcon = (
|
||||
|
||||
return undefined;
|
||||
};
|
||||
|
||||
const DAY_IN_MILLISECONDS = 86400000;
|
||||
|
||||
export const isForecastHourly = (
|
||||
forecast?: ForecastAttribute[]
|
||||
): boolean | undefined => {
|
||||
if (forecast && forecast?.length && forecast?.length > 2) {
|
||||
const date1 = new Date(forecast[1].datetime);
|
||||
const date2 = new Date(forecast[2].datetime);
|
||||
const timeDiff = date2.getTime() - date1.getTime();
|
||||
|
||||
return timeDiff < DAY_IN_MILLISECONDS;
|
||||
}
|
||||
|
||||
return undefined;
|
||||
};
|
||||
|
@@ -33,7 +33,11 @@ import { formatDateWeekday } from "../../../common/datetime/format_date";
|
||||
import { formatTimeWeekday } from "../../../common/datetime/format_time";
|
||||
import { formatNumber } from "../../../common/number/format_number";
|
||||
import "../../../components/ha-svg-icon";
|
||||
import { getWeatherUnit, getWind } from "../../../data/weather";
|
||||
import {
|
||||
getWeatherUnit,
|
||||
getWind,
|
||||
isForecastHourly,
|
||||
} from "../../../data/weather";
|
||||
import { HomeAssistant } from "../../../types";
|
||||
|
||||
const weatherIcons = {
|
||||
@@ -82,6 +86,8 @@ class MoreInfoWeather extends LitElement {
|
||||
return html``;
|
||||
}
|
||||
|
||||
const hourly = isForecastHourly(this.stateObj.attributes.forecast);
|
||||
|
||||
return html`
|
||||
<div class="flex">
|
||||
<ha-svg-icon .path=${mdiThermometer}></ha-svg-icon>
|
||||
@@ -169,48 +175,49 @@ class MoreInfoWeather extends LitElement {
|
||||
<div class="section">
|
||||
${this.hass.localize("ui.card.weather.forecast")}:
|
||||
</div>
|
||||
${this.stateObj.attributes.forecast.map(
|
||||
(item) => html`
|
||||
<div class="flex">
|
||||
${item.condition
|
||||
? html`
|
||||
<ha-svg-icon
|
||||
.path=${weatherIcons[item.condition]}
|
||||
></ha-svg-icon>
|
||||
`
|
||||
: ""}
|
||||
${!this._showValue(item.templow)
|
||||
? html`
|
||||
<div class="main">
|
||||
${formatTimeWeekday(
|
||||
new Date(item.datetime),
|
||||
this.hass.locale
|
||||
)}
|
||||
</div>
|
||||
`
|
||||
: ""}
|
||||
${this._showValue(item.templow)
|
||||
? html`
|
||||
<div class="main">
|
||||
${formatDateWeekday(
|
||||
new Date(item.datetime),
|
||||
this.hass.locale
|
||||
)}
|
||||
</div>
|
||||
<div class="templow">
|
||||
${formatNumber(item.templow, this.hass.locale)}
|
||||
${getWeatherUnit(this.hass, "temperature")}
|
||||
</div>
|
||||
`
|
||||
: ""}
|
||||
<div class="temp">
|
||||
${this._showValue(item.temperature)
|
||||
? `${formatNumber(item.temperature, this.hass.locale)}
|
||||
${getWeatherUnit(this.hass, "temperature")}`
|
||||
${this.stateObj.attributes.forecast.map((item) =>
|
||||
this._showValue(item.templow) || this._showValue(item.temperature)
|
||||
? html`<div class="flex">
|
||||
${item.condition
|
||||
? html`
|
||||
<ha-svg-icon
|
||||
.path=${weatherIcons[item.condition]}
|
||||
></ha-svg-icon>
|
||||
`
|
||||
: ""}
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
${hourly
|
||||
? html`
|
||||
<div class="main">
|
||||
${formatTimeWeekday(
|
||||
new Date(item.datetime),
|
||||
this.hass.locale
|
||||
)}
|
||||
</div>
|
||||
`
|
||||
: html`
|
||||
<div class="main">
|
||||
${formatDateWeekday(
|
||||
new Date(item.datetime),
|
||||
this.hass.locale
|
||||
)}
|
||||
</div>
|
||||
`}
|
||||
<div class="templow">
|
||||
${this._showValue(item.templow)
|
||||
? `${formatNumber(item.templow, this.hass.locale)}
|
||||
${getWeatherUnit(this.hass, "temperature")}`
|
||||
: hourly
|
||||
? ""
|
||||
: "—"}
|
||||
</div>
|
||||
<div class="temp">
|
||||
${this._showValue(item.temperature)
|
||||
? `${formatNumber(item.temperature, this.hass.locale)}
|
||||
${getWeatherUnit(this.hass, "temperature")}`
|
||||
: "—"}
|
||||
</div>
|
||||
</div>`
|
||||
: ""
|
||||
)}
|
||||
`
|
||||
: ""}
|
||||
|
@@ -1,3 +1,4 @@
|
||||
import "../../components/ha-textfield";
|
||||
import { Layout1d, scroll } from "@lit-labs/virtualizer";
|
||||
import "@material/mwc-list/mwc-list";
|
||||
import type { List } from "@material/mwc-list/mwc-list";
|
||||
@@ -33,7 +34,6 @@ import {
|
||||
import { debounce } from "../../common/util/debounce";
|
||||
import "../../components/ha-chip";
|
||||
import "../../components/ha-circular-progress";
|
||||
import "../../components/ha-dialog";
|
||||
import "../../components/ha-header-bar";
|
||||
import "../../components/ha-icon-button";
|
||||
import { domainToName } from "../../data/integration";
|
||||
@@ -95,7 +95,11 @@ export class QuickBar extends LitElement {
|
||||
|
||||
@state() private _done = false;
|
||||
|
||||
@query("paper-input", false) private _filterInputField?: HTMLElement;
|
||||
@state() private _narrow = false;
|
||||
|
||||
@state() private _hint?: string;
|
||||
|
||||
@query("ha-textfield", false) private _filterInputField?: HTMLElement;
|
||||
|
||||
private _focusSet = false;
|
||||
|
||||
@@ -103,6 +107,8 @@ export class QuickBar extends LitElement {
|
||||
|
||||
public async showDialog(params: QuickBarParams) {
|
||||
this._commandMode = params.commandMode || this._toggleIfAlreadyOpened();
|
||||
this._hint = params.hint;
|
||||
this._narrow = matchMedia("(max-width: 600px)").matches;
|
||||
this._initializeItemsIfNeeded();
|
||||
this._opened = true;
|
||||
}
|
||||
@@ -137,63 +143,90 @@ export class QuickBar extends LitElement {
|
||||
@closed=${this.closeDialog}
|
||||
hideActions
|
||||
>
|
||||
<paper-input
|
||||
dialogInitialFocus
|
||||
no-label-float
|
||||
slot="heading"
|
||||
class="heading"
|
||||
@value-changed=${this._handleSearchChange}
|
||||
.label=${this.hass.localize(
|
||||
"ui.dialogs.quick-bar.filter_placeholder"
|
||||
)}
|
||||
.value=${this._commandMode ? `>${this._search}` : this._search}
|
||||
@keydown=${this._handleInputKeyDown}
|
||||
@focus=${this._setFocusFirstListItem}
|
||||
>
|
||||
${this._commandMode
|
||||
? html`<ha-svg-icon
|
||||
slot="prefix"
|
||||
class="prefix"
|
||||
.path=${mdiConsoleLine}
|
||||
></ha-svg-icon>`
|
||||
: html`<ha-svg-icon
|
||||
slot="prefix"
|
||||
class="prefix"
|
||||
.path=${mdiMagnify}
|
||||
></ha-svg-icon>`}
|
||||
${this._search &&
|
||||
html`
|
||||
<ha-icon-button
|
||||
slot="suffix"
|
||||
@click=${this._clearSearch}
|
||||
.label=${this.hass!.localize("ui.common.clear")}
|
||||
.path=${mdiClose}
|
||||
></ha-icon-button>
|
||||
`}
|
||||
</paper-input>
|
||||
<div slot="heading" class="heading">
|
||||
<ha-textfield
|
||||
dialogInitialFocus
|
||||
.placeholder=${this.hass.localize(
|
||||
"ui.dialogs.quick-bar.filter_placeholder"
|
||||
)}
|
||||
aria-label=${this.hass.localize(
|
||||
"ui.dialogs.quick-bar.filter_placeholder"
|
||||
)}
|
||||
.value=${this._commandMode ? `>${this._search}` : this._search}
|
||||
.icon=${true}
|
||||
.iconTrailing=${this._search !== undefined}
|
||||
@input=${this._handleSearchChange}
|
||||
@keydown=${this._handleInputKeyDown}
|
||||
@focus=${this._setFocusFirstListItem}
|
||||
>
|
||||
${this._commandMode
|
||||
? html`
|
||||
<ha-svg-icon
|
||||
slot="leadingIcon"
|
||||
class="prefix"
|
||||
.path=${mdiConsoleLine}
|
||||
></ha-svg-icon>
|
||||
`
|
||||
: html`
|
||||
<ha-svg-icon
|
||||
slot="leadingIcon"
|
||||
class="prefix"
|
||||
.path=${mdiMagnify}
|
||||
></ha-svg-icon>
|
||||
`}
|
||||
${this._search &&
|
||||
html`
|
||||
<ha-icon-button
|
||||
slot="trailingIcon"
|
||||
@click=${this._clearSearch}
|
||||
.label=${this.hass!.localize("ui.common.clear")}
|
||||
.path=${mdiClose}
|
||||
></ha-icon-button>
|
||||
`}
|
||||
</ha-textfield>
|
||||
${this._narrow
|
||||
? html`
|
||||
<mwc-button
|
||||
.label=${this.hass!.localize("ui.common.close")}
|
||||
@click=${this.closeDialog}
|
||||
></mwc-button>
|
||||
`
|
||||
: ""}
|
||||
</div>
|
||||
${!items
|
||||
? html`<ha-circular-progress
|
||||
size="small"
|
||||
active
|
||||
></ha-circular-progress>`
|
||||
: html`<mwc-list
|
||||
@rangechange=${this._handleRangeChanged}
|
||||
@keydown=${this._handleListItemKeyDown}
|
||||
@selected=${this._handleSelected}
|
||||
style=${styleMap({
|
||||
height: `${Math.min(
|
||||
items.length * (this._commandMode ? 56 : 72) + 26,
|
||||
this._done ? 500 : 0
|
||||
)}px`,
|
||||
})}
|
||||
>
|
||||
${scroll({
|
||||
items,
|
||||
layout: Layout1d,
|
||||
renderItem: (item: QuickBarItem, index) =>
|
||||
this._renderItem(item, index),
|
||||
})}
|
||||
</mwc-list>`}
|
||||
: items.length === 0
|
||||
? html`
|
||||
<div class="nothing-found">
|
||||
${this.hass.localize("ui.dialogs.quick-bar.nothing_found")}
|
||||
</div>
|
||||
`
|
||||
: html`
|
||||
<mwc-list
|
||||
@rangechange=${this._handleRangeChanged}
|
||||
@keydown=${this._handleListItemKeyDown}
|
||||
@selected=${this._handleSelected}
|
||||
style=${styleMap({
|
||||
height: `${Math.min(
|
||||
items.length * (this._commandMode ? 56 : 72) + 26,
|
||||
this._done ? 500 : 0
|
||||
)}px`,
|
||||
})}
|
||||
>
|
||||
${scroll({
|
||||
items,
|
||||
layout: Layout1d,
|
||||
renderItem: (item: QuickBarItem, index) =>
|
||||
this._renderItem(item, index),
|
||||
})}
|
||||
</mwc-list>
|
||||
`}
|
||||
${!this._narrow && this._hint
|
||||
? html`<div class="hint">${this._hint}</div>`
|
||||
: ""}
|
||||
</ha-dialog>
|
||||
`;
|
||||
}
|
||||
@@ -337,15 +370,29 @@ export class QuickBar extends LitElement {
|
||||
}
|
||||
|
||||
private _handleSearchChange(ev: CustomEvent): void {
|
||||
const newFilter = ev.detail.value;
|
||||
const newFilter = (ev.currentTarget as any).value;
|
||||
const oldCommandMode = this._commandMode;
|
||||
const oldSearch = this._search;
|
||||
let newCommandMode: boolean;
|
||||
let newSearch: string;
|
||||
|
||||
if (newFilter.startsWith(">")) {
|
||||
this._commandMode = true;
|
||||
this._search = newFilter.substring(1);
|
||||
newCommandMode = true;
|
||||
newSearch = newFilter.substring(1);
|
||||
} else {
|
||||
this._commandMode = false;
|
||||
this._search = newFilter;
|
||||
newCommandMode = false;
|
||||
newSearch = newFilter;
|
||||
}
|
||||
|
||||
if (oldCommandMode === newCommandMode && oldSearch === newSearch) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._commandMode = newCommandMode;
|
||||
this._search = newSearch;
|
||||
|
||||
if (this._hint) {
|
||||
this._hint = undefined;
|
||||
}
|
||||
|
||||
if (oldCommandMode !== this._commandMode) {
|
||||
@@ -539,21 +586,27 @@ export class QuickBar extends LitElement {
|
||||
|
||||
for (const sectionKey of Object.keys(configSections)) {
|
||||
for (const page of configSections[sectionKey]) {
|
||||
if (canShowPage(this.hass, page)) {
|
||||
if (page.component) {
|
||||
const info = this._getNavigationInfoFromConfig(page);
|
||||
|
||||
// Add to list, but only if we do not already have an entry for the same path and component
|
||||
if (
|
||||
info &&
|
||||
!items.some(
|
||||
(e) => e.path === info.path && e.component === info.component
|
||||
)
|
||||
) {
|
||||
items.push(info);
|
||||
}
|
||||
}
|
||||
if (!canShowPage(this.hass, page)) {
|
||||
continue;
|
||||
}
|
||||
if (!page.component) {
|
||||
continue;
|
||||
}
|
||||
const info = this._getNavigationInfoFromConfig(page);
|
||||
|
||||
if (!info) {
|
||||
continue;
|
||||
}
|
||||
// Add to list, but only if we do not already have an entry for the same path and component
|
||||
if (
|
||||
items.some(
|
||||
(e) => e.path === info.path && e.component === info.component
|
||||
)
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
items.push(info);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -563,14 +616,15 @@ export class QuickBar extends LitElement {
|
||||
private _getNavigationInfoFromConfig(
|
||||
page: PageNavigation
|
||||
): NavigationInfo | undefined {
|
||||
if (page.component) {
|
||||
const caption = this.hass.localize(
|
||||
`ui.dialogs.quick-bar.commands.navigation.${page.component}`
|
||||
);
|
||||
if (!page.component) {
|
||||
return undefined;
|
||||
}
|
||||
const caption = this.hass.localize(
|
||||
`ui.dialogs.quick-bar.commands.navigation.${page.component}`
|
||||
);
|
||||
|
||||
if (page.translationKey && caption) {
|
||||
return { ...page, primaryText: caption };
|
||||
}
|
||||
if (page.translationKey && caption) {
|
||||
return { ...page, primaryText: caption };
|
||||
}
|
||||
|
||||
return undefined;
|
||||
@@ -627,7 +681,13 @@ export class QuickBar extends LitElement {
|
||||
haStyleDialog,
|
||||
css`
|
||||
.heading {
|
||||
padding: 8px 20px 0px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
--mdc-theme-primary: var(--primary-text-color);
|
||||
}
|
||||
|
||||
.heading ha-textfield {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
ha-dialog {
|
||||
@@ -651,11 +711,10 @@ export class QuickBar extends LitElement {
|
||||
}
|
||||
|
||||
ha-svg-icon.prefix {
|
||||
margin: 8px;
|
||||
color: var(--primary-text-color);
|
||||
}
|
||||
|
||||
paper-input ha-icon-button {
|
||||
ha-textfield ha-icon-button {
|
||||
--mdc-icon-button-size: 24px;
|
||||
color: var(--primary-text-color);
|
||||
}
|
||||
@@ -688,6 +747,17 @@ export class QuickBar extends LitElement {
|
||||
mwc-list-item.command-item {
|
||||
text-transform: capitalize;
|
||||
}
|
||||
|
||||
.hint {
|
||||
padding: 20px;
|
||||
font-style: italic;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.nothing-found {
|
||||
padding: 16px 0px;
|
||||
text-align: center;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
@@ -3,6 +3,7 @@ import { fireEvent } from "../../common/dom/fire_event";
|
||||
export interface QuickBarParams {
|
||||
entityFilter?: string;
|
||||
commandMode?: boolean;
|
||||
hint?: string;
|
||||
}
|
||||
|
||||
export const loadQuickBar = () => import("./ha-quick-bar");
|
||||
|
52
src/external_app/external_app_entrypoint.ts
Normal file
52
src/external_app/external_app_entrypoint.ts
Normal file
@@ -0,0 +1,52 @@
|
||||
/*
|
||||
All commands that do UI stuff need to be loaded from the app bundle as UI stuff
|
||||
in core bundle slows things down and causes duplicate registration.
|
||||
|
||||
This is the entry point for providing external app stuff from app entrypoint.
|
||||
*/
|
||||
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import { HomeAssistantMain } from "../layouts/home-assistant-main";
|
||||
import type { EMExternalMessageCommands } from "./external_messaging";
|
||||
|
||||
export const attachExternalToApp = (hassMainEl: HomeAssistantMain) => {
|
||||
window.addEventListener("haptic", (ev) =>
|
||||
hassMainEl.hass.auth.external!.fireMessage({
|
||||
type: "haptic",
|
||||
payload: { hapticType: ev.detail },
|
||||
})
|
||||
);
|
||||
|
||||
hassMainEl.hass.auth.external!.addCommandHandler((msg) =>
|
||||
handleExternalMessage(hassMainEl, msg)
|
||||
);
|
||||
};
|
||||
|
||||
const handleExternalMessage = (
|
||||
hassMainEl: HomeAssistantMain,
|
||||
msg: EMExternalMessageCommands
|
||||
): boolean => {
|
||||
const bus = hassMainEl.hass.auth.external!;
|
||||
|
||||
if (msg.command === "restart") {
|
||||
hassMainEl.hass.connection.reconnect(true);
|
||||
bus.fireMessage({
|
||||
id: msg.id,
|
||||
type: "result",
|
||||
success: true,
|
||||
result: null,
|
||||
});
|
||||
} else if (msg.command === "notifications/show") {
|
||||
fireEvent(hassMainEl, "hass-show-notifications");
|
||||
bus.fireMessage({
|
||||
id: msg.id,
|
||||
type: "result",
|
||||
success: true,
|
||||
result: null,
|
||||
});
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
@@ -128,14 +128,14 @@ export class ExternalAuth extends Auth {
|
||||
}
|
||||
}
|
||||
|
||||
export const createExternalAuth = (hassUrl: string) => {
|
||||
export const createExternalAuth = async (hassUrl: string) => {
|
||||
const auth = new ExternalAuth(hassUrl);
|
||||
if (
|
||||
(window.externalApp && window.externalApp.externalBus) ||
|
||||
(window.webkit && window.webkit.messageHandlers.externalBus)
|
||||
) {
|
||||
auth.external = new ExternalMessaging();
|
||||
auth.external.attach();
|
||||
await auth.external.attach();
|
||||
}
|
||||
return auth;
|
||||
};
|
||||
|
@@ -1,18 +0,0 @@
|
||||
import { ExternalMessaging } from "./external_messaging";
|
||||
|
||||
export interface ExternalConfig {
|
||||
hasSettingsScreen: boolean;
|
||||
canWriteTag: boolean;
|
||||
hasExoPlayer: boolean;
|
||||
}
|
||||
|
||||
export const getExternalConfig = (
|
||||
bus: ExternalMessaging
|
||||
): Promise<ExternalConfig> => {
|
||||
if (!bus.cache.cfg) {
|
||||
bus.cache.cfg = bus.sendMessage<ExternalConfig>({
|
||||
type: "config/get",
|
||||
});
|
||||
}
|
||||
return bus.cache.cfg;
|
||||
};
|
@@ -1,15 +0,0 @@
|
||||
import { ExternalMessaging } from "./external_messaging";
|
||||
|
||||
export const externalForwardConnectionEvents = (bus: ExternalMessaging) => {
|
||||
window.addEventListener("connection-status", (ev) =>
|
||||
bus.fireMessage({
|
||||
type: "connection-status",
|
||||
payload: { event: ev.detail },
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
export const externalForwardHaptics = (bus: ExternalMessaging) =>
|
||||
window.addEventListener("haptic", (ev) =>
|
||||
bus.fireMessage({ type: "haptic", payload: { hapticType: ev.detail } })
|
||||
);
|
@@ -1,9 +1,3 @@
|
||||
import { Connection } from "home-assistant-js-websocket";
|
||||
import {
|
||||
externalForwardConnectionEvents,
|
||||
externalForwardHaptics,
|
||||
} from "./external_events_forwarder";
|
||||
|
||||
const CALLBACK_EXTERNAL_BUS = "externalBus";
|
||||
|
||||
interface CommandInFlight {
|
||||
@@ -42,24 +36,54 @@ interface EMExternalMessageRestart {
|
||||
command: "restart";
|
||||
}
|
||||
|
||||
interface EMExternMessageShowNotifications {
|
||||
id: number;
|
||||
type: "command";
|
||||
command: "notifications/show";
|
||||
}
|
||||
|
||||
export type EMExternalMessageCommands =
|
||||
| EMExternalMessageRestart
|
||||
| EMExternMessageShowNotifications;
|
||||
|
||||
type ExternalMessage =
|
||||
| EMMessageResultSuccess
|
||||
| EMMessageResultError
|
||||
| EMExternalMessageRestart;
|
||||
| EMExternalMessageCommands;
|
||||
|
||||
type ExternalMessageHandler = (msg: EMExternalMessageCommands) => boolean;
|
||||
|
||||
export interface ExternalConfig {
|
||||
hasSettingsScreen: boolean;
|
||||
hasSidebar: boolean;
|
||||
canWriteTag: boolean;
|
||||
hasExoPlayer: boolean;
|
||||
}
|
||||
|
||||
export class ExternalMessaging {
|
||||
public config!: ExternalConfig;
|
||||
|
||||
public commands: { [msgId: number]: CommandInFlight } = {};
|
||||
|
||||
public connection?: Connection;
|
||||
|
||||
public cache: Record<string, any> = {};
|
||||
|
||||
public msgId = 0;
|
||||
|
||||
public attach() {
|
||||
externalForwardConnectionEvents(this);
|
||||
externalForwardHaptics(this);
|
||||
private _commandHandler?: ExternalMessageHandler;
|
||||
|
||||
public async attach() {
|
||||
window[CALLBACK_EXTERNAL_BUS] = (msg) => this.receiveMessage(msg);
|
||||
window.addEventListener("connection-status", (ev) =>
|
||||
this.fireMessage({
|
||||
type: "connection-status",
|
||||
payload: { event: ev.detail },
|
||||
})
|
||||
);
|
||||
this.config = await this.sendMessage<ExternalConfig>({
|
||||
type: "config/get",
|
||||
});
|
||||
}
|
||||
|
||||
public addCommandHandler(handler: ExternalMessageHandler) {
|
||||
this._commandHandler = handler;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -97,36 +121,25 @@ export class ExternalMessaging {
|
||||
}
|
||||
|
||||
if (msg.type === "command") {
|
||||
if (!this.connection) {
|
||||
if (!this._commandHandler || !this._commandHandler(msg)) {
|
||||
let code: string;
|
||||
let message: string;
|
||||
if (this._commandHandler) {
|
||||
code = "not_ready";
|
||||
message = "Command handler not ready";
|
||||
} else {
|
||||
code = "unknown_command";
|
||||
message = `Unknown command ${msg.command}`;
|
||||
}
|
||||
// eslint-disable-next-line no-console
|
||||
console.warn("Received command without having connection set", msg);
|
||||
console.warn(message, msg);
|
||||
this.fireMessage({
|
||||
id: msg.id,
|
||||
type: "result",
|
||||
success: false,
|
||||
error: {
|
||||
code: "commands_not_init",
|
||||
message: `Commands connection not set`,
|
||||
},
|
||||
});
|
||||
} else if (msg.command === "restart") {
|
||||
this.connection.reconnect(true);
|
||||
this.fireMessage({
|
||||
id: msg.id,
|
||||
type: "result",
|
||||
success: true,
|
||||
result: null,
|
||||
});
|
||||
} else {
|
||||
// eslint-disable-next-line no-console
|
||||
console.warn("Received unknown command", msg.command, msg);
|
||||
this.fireMessage({
|
||||
id: msg.id,
|
||||
type: "result",
|
||||
success: false,
|
||||
error: {
|
||||
code: "unknown_command",
|
||||
message: `Unknown command ${msg.command}`,
|
||||
code,
|
||||
message,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
@@ -38,7 +38,7 @@ interface EditSideBarEvent {
|
||||
}
|
||||
|
||||
@customElement("home-assistant-main")
|
||||
class HomeAssistantMain extends LitElement {
|
||||
export class HomeAssistantMain extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property() public route?: Route;
|
||||
@@ -47,6 +47,8 @@ class HomeAssistantMain extends LitElement {
|
||||
|
||||
@state() private _sidebarEditMode = false;
|
||||
|
||||
@state() private _externalSidebar = false;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
listenMediaQuery("(max-width: 870px)", (matches) => {
|
||||
@@ -56,11 +58,12 @@ class HomeAssistantMain extends LitElement {
|
||||
|
||||
protected render(): TemplateResult {
|
||||
const hass = this.hass;
|
||||
const sidebarNarrow = this._sidebarNarrow;
|
||||
const sidebarNarrow = this._sidebarNarrow || this._externalSidebar;
|
||||
const disableSwipe =
|
||||
this._sidebarEditMode ||
|
||||
!sidebarNarrow ||
|
||||
NON_SWIPABLE_PANELS.indexOf(hass.panelUrl) !== -1;
|
||||
NON_SWIPABLE_PANELS.indexOf(hass.panelUrl) !== -1 ||
|
||||
this._externalSidebar;
|
||||
|
||||
// Style block in render because of the mixin that is not supported
|
||||
return html`
|
||||
@@ -107,6 +110,14 @@ class HomeAssistantMain extends LitElement {
|
||||
protected firstUpdated() {
|
||||
import(/* webpackPreload: true */ "../components/ha-sidebar");
|
||||
|
||||
if (this.hass.auth.external) {
|
||||
this._externalSidebar =
|
||||
this.hass.auth.external.config.hasSidebar === true;
|
||||
import("../external_app/external_app_entrypoint").then((mod) =>
|
||||
mod.attachExternalToApp(this)
|
||||
);
|
||||
}
|
||||
|
||||
this.addEventListener(
|
||||
"hass-edit-sidebar",
|
||||
(ev: HASSDomEvent<EditSideBarEvent>) => {
|
||||
@@ -129,6 +140,12 @@ class HomeAssistantMain extends LitElement {
|
||||
if (this._sidebarEditMode) {
|
||||
return;
|
||||
}
|
||||
if (this._externalSidebar) {
|
||||
this.hass.auth.external!.fireMessage({
|
||||
type: "sidebar/show",
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (this._sidebarNarrow) {
|
||||
if (this.drawer.opened) {
|
||||
this.drawer.close();
|
||||
|
@@ -31,9 +31,9 @@ export class DialogTryTts extends LitElement {
|
||||
|
||||
@query("#message") private _messageInput?: PaperTextareaElement;
|
||||
|
||||
@LocalStorage("cloudTtsTryMessage") private _message!: string;
|
||||
@LocalStorage("cloudTtsTryMessage", false, false) private _message!: string;
|
||||
|
||||
@LocalStorage("cloudTtsTryTarget") private _target!: string;
|
||||
@LocalStorage("cloudTtsTryTarget", false, false) private _target!: string;
|
||||
|
||||
public showDialog(params: TryTtsDialogParams) {
|
||||
this._params = params;
|
||||
|
@@ -1,25 +1,22 @@
|
||||
import { mdiCloudLock } from "@mdi/js";
|
||||
import { mdiCloudLock, mdiDotsVertical, mdiMagnify } from "@mdi/js";
|
||||
import "@material/mwc-list/mwc-list-item";
|
||||
import type { ActionDetail } from "@material/mwc-list";
|
||||
import "@polymer/app-layout/app-header/app-header";
|
||||
import "@polymer/app-layout/app-toolbar/app-toolbar";
|
||||
import {
|
||||
css,
|
||||
CSSResultGroup,
|
||||
html,
|
||||
LitElement,
|
||||
PropertyValues,
|
||||
TemplateResult,
|
||||
} from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { isComponentLoaded } from "../../../common/config/is_component_loaded";
|
||||
import "../../../components/ha-card";
|
||||
import "../../../components/ha-icon-next";
|
||||
import "../../../components/ha-icon-button";
|
||||
import "../../../components/ha-menu-button";
|
||||
import "../../../components/ha-button-menu";
|
||||
import { CloudStatus } from "../../../data/cloud";
|
||||
import { SupervisorAvailableUpdates } from "../../../data/supervisor/supervisor";
|
||||
import {
|
||||
ExternalConfig,
|
||||
getExternalConfig,
|
||||
} from "../../../external_app/external_config";
|
||||
refreshSupervisorAvailableUpdates,
|
||||
SupervisorAvailableUpdates,
|
||||
} from "../../../data/supervisor/root";
|
||||
import { showQuickBar } from "../../../dialogs/quick-bar/show-dialog-quick-bar";
|
||||
import "../../../layouts/ha-app-layout";
|
||||
import { haStyle } from "../../../resources/styles";
|
||||
import { HomeAssistant } from "../../../types";
|
||||
@@ -27,6 +24,8 @@ import "../ha-config-section";
|
||||
import { configSections } from "../ha-panel-config";
|
||||
import "./ha-config-navigation";
|
||||
import "./ha-config-updates";
|
||||
import { fireEvent } from "../../../common/dom/fire_event";
|
||||
import { showAlertDialog } from "../../../dialogs/generic/show-dialog-box";
|
||||
|
||||
@customElement("ha-config-dashboard")
|
||||
class HaConfigDashboard extends LitElement {
|
||||
@@ -39,22 +38,11 @@ class HaConfigDashboard extends LitElement {
|
||||
|
||||
@property() public cloudStatus?: CloudStatus;
|
||||
|
||||
// null means not available
|
||||
@property() public supervisorUpdates?: SupervisorAvailableUpdates[] | null;
|
||||
|
||||
@property() public showAdvanced!: boolean;
|
||||
|
||||
@state() private _externalConfig?: ExternalConfig;
|
||||
|
||||
protected firstUpdated(changedProps: PropertyValues) {
|
||||
super.firstUpdated(changedProps);
|
||||
|
||||
if (this.hass && this.hass.auth.external) {
|
||||
getExternalConfig(this.hass.auth.external).then((conf) => {
|
||||
this._externalConfig = conf;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
protected render(): TemplateResult {
|
||||
return html`
|
||||
<ha-app-layout>
|
||||
@@ -65,6 +53,25 @@ class HaConfigDashboard extends LitElement {
|
||||
.narrow=${this.narrow}
|
||||
></ha-menu-button>
|
||||
<div main-title>${this.hass.localize("panel.config")}</div>
|
||||
<ha-icon-button
|
||||
.path=${mdiMagnify}
|
||||
@click=${this._showQuickBar}
|
||||
></ha-icon-button>
|
||||
<ha-button-menu
|
||||
corner="BOTTOM_START"
|
||||
@action=${this._handleMenuAction}
|
||||
activatable
|
||||
>
|
||||
<ha-icon-button
|
||||
slot="trigger"
|
||||
.label=${this.hass.localize("ui.common.menu")}
|
||||
.path=${mdiDotsVertical}
|
||||
></ha-icon-button>
|
||||
|
||||
<mwc-list-item>
|
||||
${this.hass.localize("ui.panel.config.updates.check_updates")}
|
||||
</mwc-list-item>
|
||||
</ha-button-menu>
|
||||
</app-toolbar>
|
||||
</app-header>
|
||||
|
||||
@@ -73,9 +80,9 @@ class HaConfigDashboard extends LitElement {
|
||||
.isWide=${this.isWide}
|
||||
full-width
|
||||
>
|
||||
${isComponentLoaded(this.hass, "hassio") &&
|
||||
this.supervisorUpdates === undefined
|
||||
? html``
|
||||
${this.supervisorUpdates === undefined
|
||||
? // Hide everything until updates loaded
|
||||
html``
|
||||
: html`${this.supervisorUpdates?.length
|
||||
? html`<ha-card>
|
||||
<ha-config-updates
|
||||
@@ -113,7 +120,6 @@ class HaConfigDashboard extends LitElement {
|
||||
<ha-config-navigation
|
||||
.hass=${this.hass}
|
||||
.narrow=${this.narrow}
|
||||
.externalConfig=${this._externalConfig}
|
||||
.showAdvanced=${this.showAdvanced}
|
||||
.pages=${configSections.dashboard}
|
||||
></ha-config-navigation>
|
||||
@@ -123,6 +129,34 @@ class HaConfigDashboard extends LitElement {
|
||||
`;
|
||||
}
|
||||
|
||||
private _showQuickBar(): void {
|
||||
showQuickBar(this, {
|
||||
commandMode: true,
|
||||
hint: this.hass.localize("ui.dialogs.quick-bar.key_c_hint"),
|
||||
});
|
||||
}
|
||||
|
||||
private async _handleMenuAction(ev: CustomEvent<ActionDetail>) {
|
||||
switch (ev.detail.index) {
|
||||
case 0:
|
||||
if (isComponentLoaded(this.hass, "hassio")) {
|
||||
await refreshSupervisorAvailableUpdates(this.hass);
|
||||
fireEvent(this, "ha-refresh-supervisor");
|
||||
return;
|
||||
}
|
||||
showAlertDialog(this, {
|
||||
title: this.hass.localize(
|
||||
"ui.panel.config.updates.check_unavailable.title"
|
||||
),
|
||||
text: this.hass.localize(
|
||||
"ui.panel.config.updates.check_unavailable.description"
|
||||
),
|
||||
warning: true,
|
||||
});
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return [
|
||||
haStyle,
|
||||
|
@@ -6,7 +6,6 @@ import { canShowPage } from "../../../common/config/can_show_page";
|
||||
import "../../../components/ha-card";
|
||||
import "../../../components/ha-icon-next";
|
||||
import { CloudStatus, CloudStatusLoggedIn } from "../../../data/cloud";
|
||||
import { ExternalConfig } from "../../../external_app/external_config";
|
||||
import { PageNavigation } from "../../../layouts/hass-tabs-subpage";
|
||||
import { HomeAssistant } from "../../../types";
|
||||
|
||||
@@ -20,14 +19,12 @@ class HaConfigNavigation extends LitElement {
|
||||
|
||||
@property() public pages!: PageNavigation[];
|
||||
|
||||
@property() public externalConfig?: ExternalConfig;
|
||||
|
||||
protected render(): TemplateResult {
|
||||
return html`
|
||||
${this.pages.map((page) =>
|
||||
(
|
||||
page.path === "#external-app-configuration"
|
||||
? this.externalConfig?.hasSettingsScreen
|
||||
? this.hass.auth.external?.config.hasSettingsScreen
|
||||
: canShowPage(this.hass, page)
|
||||
)
|
||||
? html`
|
||||
|
@@ -7,9 +7,10 @@ import { customElement, property, state } from "lit/decorators";
|
||||
import "../../../components/ha-alert";
|
||||
import "../../../components/ha-logo-svg";
|
||||
import "../../../components/ha-svg-icon";
|
||||
import { SupervisorAvailableUpdates } from "../../../data/supervisor/supervisor";
|
||||
import { SupervisorAvailableUpdates } from "../../../data/supervisor/root";
|
||||
import { buttonLinkStyle } from "../../../resources/styles";
|
||||
import { HomeAssistant } from "../../../types";
|
||||
import "../../../components/ha-icon-next";
|
||||
|
||||
export const SUPERVISOR_UPDATE_NAMES = {
|
||||
core: "Home Assistant Core",
|
||||
@@ -46,34 +47,33 @@ class HaConfigUpdates extends LitElement {
|
||||
</div>
|
||||
${updates.map(
|
||||
(update) => html`
|
||||
<paper-icon-item>
|
||||
<span slot="item-icon" class="icon">
|
||||
${update.update_type === "addon"
|
||||
? update.icon
|
||||
? html`<img src="/api/hassio${update.icon}" />`
|
||||
: html`<ha-svg-icon .path=${mdiPackageVariant}></ha-svg-icon>`
|
||||
: html`<ha-logo-svg></ha-logo-svg>`}
|
||||
</span>
|
||||
<paper-item-body two-line>
|
||||
${update.update_type === "addon"
|
||||
? update.name
|
||||
: SUPERVISOR_UPDATE_NAMES[update.update_type!]}
|
||||
<div secondary>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.updates.version_available",
|
||||
{
|
||||
version_available: update.version_latest,
|
||||
}
|
||||
)}
|
||||
</div>
|
||||
</paper-item-body>
|
||||
<a href="/hassio${update.panel_path}">
|
||||
<mwc-button
|
||||
.label=${this.hass.localize("ui.panel.config.updates.show")}
|
||||
>
|
||||
</mwc-button>
|
||||
</a>
|
||||
</paper-icon-item>
|
||||
<a href="/hassio${update.panel_path}">
|
||||
<paper-icon-item>
|
||||
<span slot="item-icon" class="icon">
|
||||
${update.update_type === "addon"
|
||||
? update.icon
|
||||
? html`<img src="/api/hassio${update.icon}" />`
|
||||
: html`<ha-svg-icon
|
||||
.path=${mdiPackageVariant}
|
||||
></ha-svg-icon>`
|
||||
: html`<ha-logo-svg></ha-logo-svg>`}
|
||||
</span>
|
||||
<paper-item-body two-line>
|
||||
${update.update_type === "addon"
|
||||
? update.name
|
||||
: SUPERVISOR_UPDATE_NAMES[update.update_type!]}
|
||||
<div secondary>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.updates.version_available",
|
||||
{
|
||||
version_available: update.version_latest,
|
||||
}
|
||||
)}
|
||||
</div>
|
||||
</paper-item-body>
|
||||
${!this.narrow ? html`<ha-icon-next></ha-icon-next>` : ""}
|
||||
</paper-icon-item>
|
||||
</a>
|
||||
`
|
||||
)}
|
||||
${!this._showAll && this.supervisorUpdates.length >= 4
|
||||
@@ -120,10 +120,10 @@ class HaConfigUpdates extends LitElement {
|
||||
ha-logo-svg {
|
||||
color: var(--secondary-text-color);
|
||||
}
|
||||
button.show-all {
|
||||
color: var(--primary-color);
|
||||
text-decoration: none;
|
||||
margin: 16px;
|
||||
ha-icon-next {
|
||||
color: var(--secondary-text-color);
|
||||
height: 24px;
|
||||
width: 24px;
|
||||
}
|
||||
`,
|
||||
];
|
||||
|
@@ -3,7 +3,6 @@ import "@polymer/paper-tooltip/paper-tooltip";
|
||||
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { ifDefined } from "lit/directives/if-defined";
|
||||
import { until } from "lit/directives/until";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { isComponentLoaded } from "../../../common/config/is_component_loaded";
|
||||
import { computeDomain } from "../../../common/entity/compute_domain";
|
||||
@@ -90,9 +89,10 @@ export class HaConfigDevicePage extends LitElement {
|
||||
|
||||
@state() private _related?: RelatedResult;
|
||||
|
||||
@state() private _diagnosticDownloadLinks?: Promise<
|
||||
(TemplateResult | string)[]
|
||||
>;
|
||||
// If a number, it's the request ID so we make sure we don't show older info
|
||||
@state() private _diagnosticDownloadLinks?:
|
||||
| number
|
||||
| (TemplateResult | string)[];
|
||||
|
||||
private _device = memoizeOne(
|
||||
(
|
||||
@@ -196,20 +196,18 @@ export class HaConfigDevicePage extends LitElement {
|
||||
return;
|
||||
}
|
||||
|
||||
this._diagnosticDownloadLinks = this._renderDiagnosticButtons();
|
||||
this._diagnosticDownloadLinks = Math.random();
|
||||
this._renderDiagnosticButtons(this._diagnosticDownloadLinks);
|
||||
}
|
||||
|
||||
private async _renderDiagnosticButtons(): Promise<
|
||||
(TemplateResult | string)[]
|
||||
> {
|
||||
const result: TemplateResult[] = [];
|
||||
private async _renderDiagnosticButtons(requestId: number): Promise<void> {
|
||||
const device = this._device(this.deviceId, this.devices);
|
||||
|
||||
if (!device) {
|
||||
return result;
|
||||
return;
|
||||
}
|
||||
|
||||
return Promise.all(
|
||||
let links = await Promise.all(
|
||||
this._integrations(device, this.entries)
|
||||
.filter((entry) => entry.state === "loaded")
|
||||
.map(async (entry) => {
|
||||
@@ -232,6 +230,13 @@ export class HaConfigDevicePage extends LitElement {
|
||||
`;
|
||||
})
|
||||
);
|
||||
if (this._diagnosticDownloadLinks !== requestId) {
|
||||
return;
|
||||
}
|
||||
links = links.filter(Boolean);
|
||||
if (links.length > 0) {
|
||||
this._diagnosticDownloadLinks = links;
|
||||
}
|
||||
}
|
||||
|
||||
protected firstUpdated(changedProps) {
|
||||
@@ -308,7 +313,7 @@ export class HaConfigDevicePage extends LitElement {
|
||||
);
|
||||
}
|
||||
|
||||
const deviceActions: TemplateResult[] = [];
|
||||
const deviceActions: (TemplateResult | string)[] = [];
|
||||
|
||||
if (configurationUrl) {
|
||||
deviceActions.push(html`
|
||||
@@ -339,8 +344,8 @@ export class HaConfigDevicePage extends LitElement {
|
||||
deviceActions
|
||||
);
|
||||
|
||||
if (this._diagnosticDownloadLinks) {
|
||||
deviceActions.push(html`${until(this._diagnosticDownloadLinks)}`);
|
||||
if (Array.isArray(this._diagnosticDownloadLinks)) {
|
||||
deviceActions.push(...this._diagnosticDownloadLinks);
|
||||
}
|
||||
|
||||
return html`
|
||||
@@ -739,7 +744,7 @@ export class HaConfigDevicePage extends LitElement {
|
||||
device,
|
||||
integrations: ConfigEntry[],
|
||||
deviceInfo: TemplateResult[],
|
||||
deviceActions: TemplateResult[]
|
||||
deviceActions: (string | TemplateResult)[]
|
||||
): TemplateResult[] {
|
||||
const domains = integrations.map((int) => int.domain);
|
||||
const templates: TemplateResult[] = [];
|
||||
|
@@ -197,7 +197,7 @@ export class HaConfigDeviceDashboard extends LitElement {
|
||||
),
|
||||
model: device.model || "<unknown>",
|
||||
manufacturer: device.manufacturer || "<unknown>",
|
||||
area: device.area_id ? areaLookup[device.area_id].name : undefined,
|
||||
area: device.area_id ? areaLookup[device.area_id].name : "—",
|
||||
integration: device.config_entries.length
|
||||
? device.config_entries
|
||||
.filter((entId) => entId in entryLookup)
|
||||
@@ -320,7 +320,7 @@ export class HaConfigDeviceDashboard extends LitElement {
|
||||
.batteryChargingStateObj=${batteryCharging}
|
||||
></ha-battery-icon>
|
||||
`
|
||||
: html` - `;
|
||||
: html`—`;
|
||||
},
|
||||
};
|
||||
if (showDisabled) {
|
||||
|
@@ -73,7 +73,7 @@ class HaConfigEnergy extends LitElement {
|
||||
.narrow=${this.narrow}
|
||||
.backPath=${this._searchParms.has("historyBack")
|
||||
? undefined
|
||||
: "/config"}
|
||||
: "/config/lovelace/dashboards"}
|
||||
.header=${this.hass.localize("ui.panel.config.energy.caption")}
|
||||
>
|
||||
<ha-alert>
|
||||
|
@@ -171,6 +171,7 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) {
|
||||
type: "icon",
|
||||
template: (_, entry: any) => html`
|
||||
<ha-state-icon
|
||||
.title=${entry.entity.state}
|
||||
slot="item-icon"
|
||||
.state=${entry.entity}
|
||||
></ha-state-icon>
|
||||
@@ -284,7 +285,7 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) {
|
||||
</paper-tooltip>
|
||||
</div>
|
||||
`
|
||||
: "",
|
||||
: "—",
|
||||
},
|
||||
})
|
||||
);
|
||||
@@ -377,7 +378,7 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) {
|
||||
name: computeEntityRegistryName(this.hass!, entry),
|
||||
unavailable,
|
||||
restored,
|
||||
area: area ? area.name : undefined,
|
||||
area: area ? area.name : "—",
|
||||
status: restored
|
||||
? this.hass.localize(
|
||||
"ui.panel.config.entities.picker.status.restored"
|
||||
|
@@ -4,7 +4,6 @@ import {
|
||||
mdiCellphoneCog,
|
||||
mdiCog,
|
||||
mdiDevices,
|
||||
mdiHammer,
|
||||
mdiHomeAssistant,
|
||||
mdiInformation,
|
||||
mdiLightningBolt,
|
||||
@@ -33,7 +32,7 @@ import { CloudStatus, fetchCloudStatus } from "../../data/cloud";
|
||||
import {
|
||||
fetchSupervisorAvailableUpdates,
|
||||
SupervisorAvailableUpdates,
|
||||
} from "../../data/supervisor/supervisor";
|
||||
} from "../../data/supervisor/root";
|
||||
import "../../layouts/hass-loading-screen";
|
||||
import { HassRouterPage, RouterOptions } from "../../layouts/hass-router-page";
|
||||
import { PageNavigation } from "../../layouts/hass-tabs-subpage";
|
||||
@@ -43,6 +42,7 @@ declare global {
|
||||
// for fire event
|
||||
interface HASSDomEvents {
|
||||
"ha-refresh-cloud-status": undefined;
|
||||
"ha-refresh-supervisor": undefined;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -73,7 +73,7 @@ export const configSections: { [name: string]: PageNavigation[] } = {
|
||||
path: "/hassio",
|
||||
translationKey: "supervisor",
|
||||
iconPath: mdiHomeAssistant,
|
||||
iconColor: "#F1C447",
|
||||
iconColor: "#4084CD",
|
||||
component: "hassio",
|
||||
},
|
||||
{
|
||||
@@ -83,13 +83,6 @@ export const configSections: { [name: string]: PageNavigation[] } = {
|
||||
iconColor: "#B1345C",
|
||||
component: "lovelace",
|
||||
},
|
||||
{
|
||||
path: "/config/energy",
|
||||
translationKey: "energy",
|
||||
iconPath: mdiLightningBolt,
|
||||
iconColor: "#F1C447",
|
||||
component: "energy",
|
||||
},
|
||||
{
|
||||
path: "/config/tags",
|
||||
translationKey: "tags",
|
||||
@@ -117,12 +110,6 @@ export const configSections: { [name: string]: PageNavigation[] } = {
|
||||
iconColor: "#4A5963",
|
||||
core: true,
|
||||
},
|
||||
{
|
||||
path: "/developer-tools",
|
||||
translationKey: "developer_tools",
|
||||
iconPath: mdiHammer,
|
||||
iconColor: "#4084CD",
|
||||
},
|
||||
],
|
||||
devices: [
|
||||
{
|
||||
@@ -207,6 +194,7 @@ export const configSections: { [name: string]: PageNavigation[] } = {
|
||||
iconColor: "#616161",
|
||||
},
|
||||
],
|
||||
// Not used as a tab, but this way it will stay in the quick bar
|
||||
energy: [
|
||||
{
|
||||
component: "energy",
|
||||
@@ -459,6 +447,9 @@ class HaPanelConfig extends HassRouterPage {
|
||||
}
|
||||
if (isComponentLoaded(this.hass, "hassio")) {
|
||||
this._loadSupervisorUpdates();
|
||||
this.addEventListener("ha-refresh-supervisor", () => {
|
||||
this._loadSupervisorUpdates();
|
||||
});
|
||||
this.addEventListener("connection-status", (ev) => {
|
||||
if (ev.detail === "connected") {
|
||||
this._loadSupervisorUpdates();
|
||||
|
@@ -13,6 +13,7 @@ import "@polymer/paper-tooltip/paper-tooltip";
|
||||
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { classMap } from "lit/directives/class-map";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { fireEvent } from "../../../common/dom/fire_event";
|
||||
import { shouldHandleRequestSelectedEvent } from "../../../common/mwc/handle-request-selected-event";
|
||||
import "../../../components/ha-button-menu";
|
||||
@@ -175,9 +176,9 @@ export class HaIntegrationCard extends LitElement {
|
||||
}
|
||||
|
||||
private _renderSingleEntry(item: ConfigEntryExtended): TemplateResult {
|
||||
const devices = this._getDevices(item);
|
||||
const services = this._getServices(item);
|
||||
const entities = this._getEntities(item);
|
||||
const devices = this._getDevices(item, this.deviceRegistryEntries);
|
||||
const services = this._getServices(item, this.deviceRegistryEntries);
|
||||
const entities = this._getEntities(item, this.entityRegistryEntries);
|
||||
|
||||
let stateText: [string, ...unknown[]] | undefined;
|
||||
let stateTextExtra: TemplateResult | string | undefined;
|
||||
@@ -220,6 +221,61 @@ export class HaIntegrationCard extends LitElement {
|
||||
}
|
||||
}
|
||||
|
||||
let devicesLine: (TemplateResult | string)[] = [];
|
||||
|
||||
for (const [items, localizeKey] of [
|
||||
[devices, "devices"],
|
||||
[services, "services"],
|
||||
] as [DeviceRegistryEntry[], string][]) {
|
||||
if (items.length === 0) {
|
||||
continue;
|
||||
}
|
||||
const url =
|
||||
items.length === 1
|
||||
? `/config/devices/device/${items[0].id}`
|
||||
: `/config/devices/dashboard?historyBack=1&config_entry=${item.entry_id}`;
|
||||
devicesLine.push(
|
||||
// no white space before/after template on purpose
|
||||
html`<a href=${url}
|
||||
>${this.hass.localize(
|
||||
`ui.panel.config.integrations.config_entry.${localizeKey}`,
|
||||
"count",
|
||||
items.length
|
||||
)}</a
|
||||
>`
|
||||
);
|
||||
}
|
||||
|
||||
if (entities.length) {
|
||||
devicesLine.push(
|
||||
// no white space before/after template on purpose
|
||||
html`<a
|
||||
href=${`/config/entities?historyBack=1&config_entry=${item.entry_id}`}
|
||||
>${this.hass.localize(
|
||||
"ui.panel.config.integrations.config_entry.entities",
|
||||
"count",
|
||||
entities.length
|
||||
)}</a
|
||||
>`
|
||||
);
|
||||
}
|
||||
|
||||
if (devicesLine.length === 2) {
|
||||
devicesLine = [
|
||||
devicesLine[0],
|
||||
` ${this.hass.localize("ui.common.and")} `,
|
||||
devicesLine[1],
|
||||
];
|
||||
} else if (devicesLine.length === 3) {
|
||||
devicesLine = [
|
||||
devicesLine[0],
|
||||
", ",
|
||||
devicesLine[1],
|
||||
` ${this.hass.localize("ui.common.and")} `,
|
||||
devicesLine[2],
|
||||
];
|
||||
}
|
||||
|
||||
return html`
|
||||
${stateText
|
||||
? html`
|
||||
@@ -229,53 +285,7 @@ export class HaIntegrationCard extends LitElement {
|
||||
</div>
|
||||
`
|
||||
: ""}
|
||||
<div class="content">
|
||||
${devices.length || services.length || entities.length
|
||||
? html`
|
||||
<div>
|
||||
${devices.length
|
||||
? html`
|
||||
<a
|
||||
href=${`/config/devices/dashboard?historyBack=1&config_entry=${item.entry_id}`}
|
||||
>${this.hass.localize(
|
||||
"ui.panel.config.integrations.config_entry.devices",
|
||||
"count",
|
||||
devices.length
|
||||
)}</a
|
||||
>${services.length ? "," : ""}
|
||||
`
|
||||
: ""}
|
||||
${services.length
|
||||
? html`
|
||||
<a
|
||||
href=${`/config/devices/dashboard?historyBack=1&config_entry=${item.entry_id}`}
|
||||
>${this.hass.localize(
|
||||
"ui.panel.config.integrations.config_entry.services",
|
||||
"count",
|
||||
services.length
|
||||
)}</a
|
||||
>
|
||||
`
|
||||
: ""}
|
||||
${(devices.length || services.length) && entities.length
|
||||
? this.hass.localize("ui.common.and")
|
||||
: ""}
|
||||
${entities.length
|
||||
? html`
|
||||
<a
|
||||
href=${`/config/entities?historyBack=1&config_entry=${item.entry_id}`}
|
||||
>${this.hass.localize(
|
||||
"ui.panel.config.integrations.config_entry.entities",
|
||||
"count",
|
||||
entities.length
|
||||
)}</a
|
||||
>
|
||||
`
|
||||
: ""}
|
||||
</div>
|
||||
`
|
||||
: ""}
|
||||
</div>
|
||||
<div class="content">${devicesLine}</div>
|
||||
<div class="actions">
|
||||
<div>
|
||||
${item.disabled_by === "user"
|
||||
@@ -421,36 +431,51 @@ export class HaIntegrationCard extends LitElement {
|
||||
this.classList.remove("highlight");
|
||||
}
|
||||
|
||||
private _getEntities(configEntry: ConfigEntry): EntityRegistryEntry[] {
|
||||
if (!this.entityRegistryEntries) {
|
||||
return [];
|
||||
private _getEntities = memoizeOne(
|
||||
(
|
||||
configEntry: ConfigEntry,
|
||||
entityRegistryEntries: EntityRegistryEntry[]
|
||||
): EntityRegistryEntry[] => {
|
||||
if (!entityRegistryEntries) {
|
||||
return [];
|
||||
}
|
||||
return entityRegistryEntries.filter(
|
||||
(entity) => entity.config_entry_id === configEntry.entry_id
|
||||
);
|
||||
}
|
||||
return this.entityRegistryEntries.filter(
|
||||
(entity) => entity.config_entry_id === configEntry.entry_id
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
private _getDevices(configEntry: ConfigEntry): DeviceRegistryEntry[] {
|
||||
if (!this.deviceRegistryEntries) {
|
||||
return [];
|
||||
private _getDevices = memoizeOne(
|
||||
(
|
||||
configEntry: ConfigEntry,
|
||||
deviceRegistryEntries: DeviceRegistryEntry[]
|
||||
): DeviceRegistryEntry[] => {
|
||||
if (!deviceRegistryEntries) {
|
||||
return [];
|
||||
}
|
||||
return deviceRegistryEntries.filter(
|
||||
(device) =>
|
||||
device.config_entries.includes(configEntry.entry_id) &&
|
||||
device.entry_type !== "service"
|
||||
);
|
||||
}
|
||||
return this.deviceRegistryEntries.filter(
|
||||
(device) =>
|
||||
device.config_entries.includes(configEntry.entry_id) &&
|
||||
device.entry_type !== "service"
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
private _getServices(configEntry: ConfigEntry): DeviceRegistryEntry[] {
|
||||
if (!this.deviceRegistryEntries) {
|
||||
return [];
|
||||
private _getServices = memoizeOne(
|
||||
(
|
||||
configEntry: ConfigEntry,
|
||||
deviceRegistryEntries: DeviceRegistryEntry[]
|
||||
): DeviceRegistryEntry[] => {
|
||||
if (!deviceRegistryEntries) {
|
||||
return [];
|
||||
}
|
||||
return deviceRegistryEntries.filter(
|
||||
(device) =>
|
||||
device.config_entries.includes(configEntry.entry_id) &&
|
||||
device.entry_type === "service"
|
||||
);
|
||||
}
|
||||
return this.deviceRegistryEntries.filter(
|
||||
(device) =>
|
||||
device.config_entries.includes(configEntry.entry_id) &&
|
||||
device.entry_type === "service"
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
private _showOptions(ev) {
|
||||
showOptionsFlowDialog(this, ev.target.closest("ha-card").configEntry);
|
||||
|
@@ -61,6 +61,7 @@ class HaPanelDevMqtt extends LitElement {
|
||||
mode="jinja2"
|
||||
.value=${this.payload}
|
||||
@value-changed=${this._handlePayload}
|
||||
dir="ltr"
|
||||
></ha-code-editor>
|
||||
</div>
|
||||
<div class="card-actions">
|
||||
|
@@ -42,7 +42,12 @@ class DialogZHADeviceZigbeeInfo extends LitElement {
|
||||
this.hass.localize(`ui.dialogs.zha_device_info.device_signature`)
|
||||
)}
|
||||
>
|
||||
<ha-code-editor mode="yaml" readonly .value=${this._signature}>
|
||||
<ha-code-editor
|
||||
mode="yaml"
|
||||
readonly
|
||||
.value=${this._signature}
|
||||
dir="ltr"
|
||||
>
|
||||
</ha-code-editor>
|
||||
</ha-dialog>
|
||||
`;
|
||||
|
@@ -89,7 +89,7 @@ export class HaConfigLovelaceDashboards extends LitElement {
|
||||
${this.hass.localize(
|
||||
`ui.panel.config.lovelace.dashboards.conf_mode.${dashboard.mode}`
|
||||
)}${dashboard.filename
|
||||
? html` - ${dashboard.filename} `
|
||||
? html` – ${dashboard.filename} `
|
||||
: ""}
|
||||
</div>
|
||||
`
|
||||
@@ -132,8 +132,8 @@ export class HaConfigLovelaceDashboards extends LitElement {
|
||||
width: "100px",
|
||||
template: (requireAdmin: boolean) =>
|
||||
requireAdmin
|
||||
? html` <ha-svg-icon .path=${mdiCheck}></ha-svg-icon> `
|
||||
: html` - `,
|
||||
? html`<ha-svg-icon .path=${mdiCheck}></ha-svg-icon>`
|
||||
: html`—`,
|
||||
};
|
||||
columns.show_in_sidebar = {
|
||||
title: this.hass.localize(
|
||||
@@ -143,8 +143,8 @@ export class HaConfigLovelaceDashboards extends LitElement {
|
||||
width: "121px",
|
||||
template: (sidebar) =>
|
||||
sidebar
|
||||
? html` <ha-svg-icon .path=${mdiCheck}></ha-svg-icon> `
|
||||
: html` - `,
|
||||
? html`<ha-svg-icon .path=${mdiCheck}></ha-svg-icon>`
|
||||
: html`—`,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -194,6 +194,12 @@ export class HaConfigLovelaceDashboards extends LitElement {
|
||||
mode: defaultMode,
|
||||
filename: defaultMode === "yaml" ? "ui-lovelace.yaml" : "",
|
||||
},
|
||||
{
|
||||
icon: "hass:lightning-bolt",
|
||||
title: this.hass.localize(`ui.panel.config.dashboard.energy.title`),
|
||||
url_path: "energy",
|
||||
filename: "",
|
||||
},
|
||||
...dashboards.map((dashboard) => ({
|
||||
filename: "",
|
||||
...dashboard,
|
||||
@@ -255,6 +261,11 @@ export class HaConfigLovelaceDashboards extends LitElement {
|
||||
|
||||
private _editDashboard(ev: CustomEvent) {
|
||||
const urlPath = (ev.detail as RowClickedEvent).id;
|
||||
|
||||
if (urlPath === "energy") {
|
||||
navigate("/config/energy");
|
||||
return;
|
||||
}
|
||||
const dashboard = this._dashboards.find((res) => res.url_path === urlPath);
|
||||
this._openDialog(dashboard, urlPath);
|
||||
}
|
||||
|
@@ -28,7 +28,6 @@ import {
|
||||
showAlertDialog,
|
||||
showConfirmationDialog,
|
||||
} from "../../../dialogs/generic/show-dialog-box";
|
||||
import { getExternalConfig } from "../../../external_app/external_config";
|
||||
import "../../../layouts/hass-tabs-subpage-data-table";
|
||||
import { SubscribeMixin } from "../../../mixins/subscribe-mixin";
|
||||
import { HomeAssistant, Route } from "../../../types";
|
||||
@@ -53,14 +52,12 @@ export class HaConfigTags extends SubscribeMixin(LitElement) {
|
||||
|
||||
@state() private _tags: Tag[] = [];
|
||||
|
||||
@state() private _canWriteTags = false;
|
||||
private get _canWriteTags() {
|
||||
return this.hass.auth.external?.config.canWriteTag;
|
||||
}
|
||||
|
||||
private _columns = memoizeOne(
|
||||
(
|
||||
narrow: boolean,
|
||||
canWriteTags: boolean,
|
||||
_language
|
||||
): DataTableColumnContainer => {
|
||||
(narrow: boolean, _language): DataTableColumnContainer => {
|
||||
const columns: DataTableColumnContainer = {
|
||||
icon: {
|
||||
title: "",
|
||||
@@ -103,7 +100,7 @@ export class HaConfigTags extends SubscribeMixin(LitElement) {
|
||||
`,
|
||||
};
|
||||
}
|
||||
if (canWriteTags) {
|
||||
if (this._canWriteTags) {
|
||||
columns.write = {
|
||||
title: "",
|
||||
type: "icon-button",
|
||||
@@ -152,11 +149,6 @@ export class HaConfigTags extends SubscribeMixin(LitElement) {
|
||||
protected firstUpdated(changedProperties: PropertyValues) {
|
||||
super.firstUpdated(changedProperties);
|
||||
this._fetchTags();
|
||||
if (this.hass && this.hass.auth.external) {
|
||||
getExternalConfig(this.hass.auth.external).then((conf) => {
|
||||
this._canWriteTags = conf.canWriteTag;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
protected hassSubscribe() {
|
||||
@@ -181,11 +173,7 @@ export class HaConfigTags extends SubscribeMixin(LitElement) {
|
||||
back-path="/config"
|
||||
.route=${this.route}
|
||||
.tabs=${configSections.tags}
|
||||
.columns=${this._columns(
|
||||
this.narrow,
|
||||
this._canWriteTags,
|
||||
this.hass.language
|
||||
)}
|
||||
.columns=${this._columns(this.narrow, this.hass.language)}
|
||||
.data=${this._data(this._tags)}
|
||||
.noDataText=${this.hass.localize("ui.panel.config.tag.no_tags")}
|
||||
hasFab
|
||||
|
@@ -67,7 +67,7 @@ export class HaConfigUsers extends LitElement {
|
||||
width: "20%",
|
||||
direction: "asc",
|
||||
hidden: narrow,
|
||||
template: (username) => html` ${username || "-"} `,
|
||||
template: (username) => html`${username || "—"}`,
|
||||
},
|
||||
group_ids: {
|
||||
title: localize("ui.panel.config.users.picker.headers.group"),
|
||||
|
@@ -94,6 +94,7 @@ class HaPanelDevEvent extends EventsMixin(LocalizeMixin(PolymerElement)) {
|
||||
value="[[eventData]]"
|
||||
error="[[!validJSON]]"
|
||||
on-value-changed="_yamlChanged"
|
||||
dir="ltr"
|
||||
></ha-code-editor>
|
||||
</div>
|
||||
<mwc-button on-click="fireEvent" raised disabled="[[!validJSON]]"
|
||||
|
@@ -38,10 +38,10 @@ class HaPanelDevService extends LitElement {
|
||||
|
||||
@state() private _uiAvailable = true;
|
||||
|
||||
@LocalStorage("panel-dev-service-state-service-data", true)
|
||||
@LocalStorage("panel-dev-service-state-service-data", true, false)
|
||||
private _serviceData?: ServiceAction = { service: "", target: {}, data: {} };
|
||||
|
||||
@LocalStorage("panel-dev-service-state-yaml-mode", true)
|
||||
@LocalStorage("panel-dev-service-state-yaml-mode", true, false)
|
||||
private _yamlMode = false;
|
||||
|
||||
@query("ha-yaml-editor") private _yamlEditor?: HaYamlEditor;
|
||||
|
@@ -85,6 +85,7 @@ class HaPanelDevState extends EventsMixin(LocalizeMixin(PolymerElement)) {
|
||||
|
||||
:host([rtl]) .entities th {
|
||||
text-align: right;
|
||||
direction: rtl;
|
||||
}
|
||||
|
||||
.entities tr {
|
||||
@@ -145,7 +146,7 @@ class HaPanelDevState extends EventsMixin(LocalizeMixin(PolymerElement)) {
|
||||
[[localize('ui.panel.developer-tools.tabs.states.current_entities')]]
|
||||
</h1>
|
||||
<ha-expansion-panel
|
||||
header="Set state"
|
||||
header="[[localize('ui.panel.developer-tools.tabs.states.set_state')]]"
|
||||
outlined
|
||||
expanded="[[_expanded]]"
|
||||
on-expanded-changed="expandedChanged"
|
||||
@@ -181,6 +182,7 @@ class HaPanelDevState extends EventsMixin(LocalizeMixin(PolymerElement)) {
|
||||
value="[[_stateAttributes]]"
|
||||
error="[[!validJSON]]"
|
||||
on-value-changed="_yamlChanged"
|
||||
dir="ltr"
|
||||
></ha-code-editor>
|
||||
<div class="button-row">
|
||||
<mwc-button
|
||||
|
@@ -132,6 +132,7 @@ class HaPanelDevTemplate extends LitElement {
|
||||
.error=${this._error}
|
||||
autofocus
|
||||
@value-changed=${this._templateChanged}
|
||||
dir="ltr"
|
||||
></ha-code-editor>
|
||||
<mwc-button @click=${this._restoreDemo}>
|
||||
${this.hass.localize(
|
||||
|
@@ -15,6 +15,7 @@ import { formatTimeWithSeconds } from "../../common/datetime/format_time";
|
||||
import { restoreScroll } from "../../common/decorators/restore-scroll";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import { computeDomain } from "../../common/entity/compute_domain";
|
||||
import { domainIcon } from "../../common/entity/domain_icon";
|
||||
import { computeRTL, emitRTLDirection } from "../../common/util/compute_rtl";
|
||||
import "../../components/entity/state-badge";
|
||||
import "../../components/ha-circular-progress";
|
||||
@@ -150,7 +151,10 @@ class HaLogbook extends LitElement {
|
||||
html`
|
||||
<state-badge
|
||||
.hass=${this.hass}
|
||||
.overrideIcon=${item.icon}
|
||||
.overrideIcon=${item.icon ||
|
||||
(item.domain && !stateObj
|
||||
? domainIcon(item.domain!)
|
||||
: undefined)}
|
||||
.overrideImage=${DOMAINS_WITH_DYNAMIC_PICTURE.has(domain)
|
||||
? ""
|
||||
: stateObj?.attributes.entity_picture_local ||
|
||||
|
@@ -274,7 +274,7 @@ class HuiEnergyDistrubutionCard
|
||||
? formatNumber(lowCarbonEnergy, this.hass.locale, {
|
||||
maximumFractionDigits: 1,
|
||||
})
|
||||
: "-"}
|
||||
: "—"}
|
||||
kWh
|
||||
</a>
|
||||
<svg width="80" height="30">
|
||||
|
@@ -191,7 +191,7 @@ export class HuiEnergyGasGraphCard
|
||||
return datasets[0].label;
|
||||
}
|
||||
const date = new Date(datasets[0].parsed.x);
|
||||
return `${formatTime(date, locale)} - ${formatTime(
|
||||
return `${formatTime(date, locale)} – ${formatTime(
|
||||
addHours(date, 1),
|
||||
locale
|
||||
)}`;
|
||||
|
@@ -184,7 +184,7 @@ export class HuiEnergySolarGraphCard
|
||||
return datasets[0].label;
|
||||
}
|
||||
const date = new Date(datasets[0].parsed.x);
|
||||
return `${formatTime(date, locale)} - ${formatTime(
|
||||
return `${formatTime(date, locale)} – ${formatTime(
|
||||
addHours(date, 1),
|
||||
locale
|
||||
)}`;
|
||||
|
@@ -177,7 +177,7 @@ export class HuiEnergyUsageGraphCard
|
||||
return datasets[0].label;
|
||||
}
|
||||
const date = new Date(datasets[0].parsed.x);
|
||||
return `${formatTime(date, locale)} - ${formatTime(
|
||||
return `${formatTime(date, locale)} – ${formatTime(
|
||||
addHours(date, 1),
|
||||
locale
|
||||
)}`;
|
||||
|
@@ -8,10 +8,15 @@ import { findEntities } from "../common/find-entities";
|
||||
import { processConfigEntities } from "../common/process-config-entities";
|
||||
import { createCardElement } from "../create-element/create-card-element";
|
||||
import { EntityFilterEntityConfig } from "../entity-rows/types";
|
||||
import { LovelaceCard } from "../types";
|
||||
import { LovelaceCard, LovelaceCardEditor } from "../types";
|
||||
import { EntityFilterCardConfig } from "./types";
|
||||
|
||||
class EntityFilterCard extends ReactiveElement implements LovelaceCard {
|
||||
public static async getConfigElement(): Promise<LovelaceCardEditor> {
|
||||
await import("../editor/config-elements/hui-entity-filter-card-editor");
|
||||
return document.createElement("hui-entity-filter-card-editor");
|
||||
}
|
||||
|
||||
public static getStubConfig(
|
||||
hass: HomeAssistant,
|
||||
entities: string[],
|
||||
@@ -57,7 +62,7 @@ class EntityFilterCard extends ReactiveElement implements LovelaceCard {
|
||||
}
|
||||
|
||||
public setConfig(config: EntityFilterCardConfig): void {
|
||||
if (!config.entities.length || !Array.isArray(config.entities)) {
|
||||
if (!config.entities || !Array.isArray(config.entities)) {
|
||||
throw new Error("Entities must be specified");
|
||||
}
|
||||
|
||||
|
@@ -24,6 +24,7 @@ import "../../../components/ha-state-icon";
|
||||
import { showMediaBrowserDialog } from "../../../components/media-player/show-media-browser-dialog";
|
||||
import { UNAVAILABLE_STATES } from "../../../data/entity";
|
||||
import {
|
||||
cleanupMediaTitle,
|
||||
computeMediaControls,
|
||||
computeMediaDescription,
|
||||
getCurrentProgress,
|
||||
@@ -182,6 +183,7 @@ export class HuiMediaControlCard extends LitElement implements LovelaceCard {
|
||||
entityState === "on");
|
||||
|
||||
const mediaDescription = computeMediaDescription(stateObj);
|
||||
const mediaTitleClean = cleanupMediaTitle(stateObj.attributes.media_title);
|
||||
|
||||
return html`
|
||||
<ha-card>
|
||||
@@ -244,24 +246,21 @@ export class HuiMediaControlCard extends LitElement implements LovelaceCard {
|
||||
</div>
|
||||
</div>
|
||||
${!isUnavailable &&
|
||||
(mediaDescription || stateObj.attributes.media_title || showControls)
|
||||
(mediaDescription || mediaTitleClean || showControls)
|
||||
? html`
|
||||
<div>
|
||||
<div class="title-controls">
|
||||
${!mediaDescription && !stateObj.attributes.media_title
|
||||
${!mediaDescription && !mediaTitleClean
|
||||
? ""
|
||||
: html`
|
||||
<div class="media-info">
|
||||
<hui-marquee
|
||||
.text=${stateObj.attributes.media_title ||
|
||||
mediaDescription}
|
||||
.text=${mediaTitleClean || mediaDescription}
|
||||
.active=${this._marqueeActive}
|
||||
@mouseover=${this._marqueeMouseOver}
|
||||
@mouseleave=${this._marqueeMouseLeave}
|
||||
></hui-marquee>
|
||||
${!stateObj.attributes.media_title
|
||||
? ""
|
||||
: mediaDescription}
|
||||
${!mediaTitleClean ? "" : mediaDescription}
|
||||
</div>
|
||||
`}
|
||||
${!showControls
|
||||
|
@@ -24,6 +24,7 @@ import {
|
||||
getWeatherStateIcon,
|
||||
getWeatherUnit,
|
||||
getWind,
|
||||
isForecastHourly,
|
||||
weatherAttrIcons,
|
||||
WeatherEntity,
|
||||
weatherSVGStyles,
|
||||
@@ -177,23 +178,15 @@ class HuiWeatherForecastCard extends LitElement implements LovelaceCard {
|
||||
: undefined;
|
||||
const weather = !forecast || this._config?.show_current !== false;
|
||||
|
||||
let hourly: boolean | undefined;
|
||||
const hourly = isForecastHourly(forecast);
|
||||
let dayNight: boolean | undefined;
|
||||
|
||||
if (forecast?.length && forecast?.length > 2) {
|
||||
const date1 = new Date(forecast[1].datetime);
|
||||
const date2 = new Date(forecast[2].datetime);
|
||||
const timeDiff = date2.getTime() - date1.getTime();
|
||||
if (hourly) {
|
||||
const dateFirst = new Date(forecast![0].datetime);
|
||||
const datelast = new Date(forecast![forecast!.length - 1].datetime);
|
||||
const dayDiff = datelast.getTime() - dateFirst.getTime();
|
||||
|
||||
hourly = timeDiff < DAY_IN_MILLISECONDS;
|
||||
|
||||
if (hourly) {
|
||||
const dateFirst = new Date(forecast[0].datetime);
|
||||
const datelast = new Date(forecast[forecast.length - 1].datetime);
|
||||
const dayDiff = datelast.getTime() - dateFirst.getTime();
|
||||
|
||||
dayNight = dayDiff > DAY_IN_MILLISECONDS;
|
||||
}
|
||||
dayNight = dayDiff > DAY_IN_MILLISECONDS;
|
||||
}
|
||||
|
||||
const weatherStateIcon = getWeatherStateIcon(stateObj.state, this);
|
||||
@@ -288,69 +281,76 @@ class HuiWeatherForecastCard extends LitElement implements LovelaceCard {
|
||||
${forecast
|
||||
? html`
|
||||
<div class="forecast">
|
||||
${forecast.map(
|
||||
(item) => html`
|
||||
<div>
|
||||
<div>
|
||||
${dayNight
|
||||
? html`
|
||||
${new Date(item.datetime).toLocaleDateString(
|
||||
this.hass!.language,
|
||||
{ weekday: "short" }
|
||||
)}
|
||||
<div class="daynight">
|
||||
${item.daytime === undefined || item.daytime
|
||||
? this.hass!.localize("ui.card.weather.day")
|
||||
: this.hass!.localize(
|
||||
"ui.card.weather.night"
|
||||
)}<br />
|
||||
</div>
|
||||
`
|
||||
: hourly
|
||||
? html`
|
||||
${formatTime(
|
||||
new Date(item.datetime),
|
||||
this.hass!.locale
|
||||
)}
|
||||
`
|
||||
: html`
|
||||
${new Date(item.datetime).toLocaleDateString(
|
||||
this.hass!.language,
|
||||
{ weekday: "short" }
|
||||
)}
|
||||
`}
|
||||
</div>
|
||||
${item.condition !== undefined && item.condition !== null
|
||||
? html`
|
||||
<div class="forecast-image-icon">
|
||||
${getWeatherStateIcon(
|
||||
item.condition,
|
||||
this,
|
||||
!(item.daytime || item.daytime === undefined)
|
||||
)}
|
||||
</div>
|
||||
`
|
||||
: ""}
|
||||
${item.temperature !== undefined &&
|
||||
item.temperature !== null
|
||||
? html`
|
||||
<div class="temp">
|
||||
${formatNumber(
|
||||
item.temperature,
|
||||
this.hass!.locale
|
||||
)}°
|
||||
</div>
|
||||
`
|
||||
: ""}
|
||||
${item.templow !== undefined && item.templow !== null
|
||||
? html`
|
||||
<div class="templow">
|
||||
${formatNumber(item.templow, this.hass!.locale)}°
|
||||
</div>
|
||||
`
|
||||
: ""}
|
||||
</div>
|
||||
`
|
||||
${forecast.map((item) =>
|
||||
this._showValue(item.templow) ||
|
||||
this._showValue(item.temperature)
|
||||
? html`
|
||||
<div>
|
||||
<div>
|
||||
${dayNight
|
||||
? html`
|
||||
${new Date(item.datetime).toLocaleDateString(
|
||||
this.hass!.language,
|
||||
{ weekday: "short" }
|
||||
)}
|
||||
<div class="daynight">
|
||||
${item.daytime === undefined || item.daytime
|
||||
? this.hass!.localize(
|
||||
"ui.card.weather.day"
|
||||
)
|
||||
: this.hass!.localize(
|
||||
"ui.card.weather.night"
|
||||
)}<br />
|
||||
</div>
|
||||
`
|
||||
: hourly
|
||||
? html`
|
||||
${formatTime(
|
||||
new Date(item.datetime),
|
||||
this.hass!.locale
|
||||
)}
|
||||
`
|
||||
: html`
|
||||
${new Date(item.datetime).toLocaleDateString(
|
||||
this.hass!.language,
|
||||
{ weekday: "short" }
|
||||
)}
|
||||
`}
|
||||
</div>
|
||||
${this._showValue(item.condition)
|
||||
? html`
|
||||
<div class="forecast-image-icon">
|
||||
${getWeatherStateIcon(
|
||||
item.condition!,
|
||||
this,
|
||||
!(
|
||||
item.daytime || item.daytime === undefined
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
`
|
||||
: ""}
|
||||
<div class="temp">
|
||||
${this._showValue(item.temperature)
|
||||
? html`${formatNumber(
|
||||
item.temperature,
|
||||
this.hass!.locale
|
||||
)}°`
|
||||
: "—"}
|
||||
</div>
|
||||
<div class="templow">
|
||||
${this._showValue(item.templow)
|
||||
? html`${formatNumber(
|
||||
item.templow!,
|
||||
this.hass!.locale
|
||||
)}°`
|
||||
: hourly
|
||||
? ""
|
||||
: "—"}
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
: ""
|
||||
)}
|
||||
</div>
|
||||
`
|
||||
@@ -402,6 +402,10 @@ class HuiWeatherForecastCard extends LitElement implements LovelaceCard {
|
||||
this._veryVeryNarrow = card.offsetWidth < 245;
|
||||
}
|
||||
|
||||
private _showValue(item?: any): boolean {
|
||||
return typeof item !== "undefined" && item !== null;
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return [
|
||||
weatherSVGStyles,
|
||||
|
@@ -103,7 +103,7 @@ export class HuiEnergyPeriodSelector extends SubscribeMixin(LitElement) {
|
||||
: `${formatDateShort(
|
||||
this._startDate,
|
||||
this.hass.locale
|
||||
)} - ${formatDateShort(
|
||||
)} – ${formatDateShort(
|
||||
this._endDate || new Date(),
|
||||
this.hass.locale
|
||||
)}`}
|
||||
|
@@ -186,6 +186,10 @@ export class HuiEntityEditor extends LitElement {
|
||||
return [
|
||||
sortableStyles,
|
||||
css`
|
||||
:host {
|
||||
display: var(--entity-picker-display);
|
||||
}
|
||||
|
||||
.entity {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
@@ -245,7 +245,7 @@ export class HuiDialogEditCard
|
||||
<mwc-button @click=${this._cancel}>
|
||||
${this.hass!.localize("ui.common.cancel")}
|
||||
</mwc-button>
|
||||
${this._cardConfig !== undefined
|
||||
${this._cardConfig !== undefined && this._dirty
|
||||
? html`
|
||||
<mwc-button
|
||||
?disabled=${!this._canSave || this._saving}
|
||||
@@ -259,9 +259,7 @@ export class HuiDialogEditCard
|
||||
size="small"
|
||||
></ha-circular-progress>
|
||||
`
|
||||
: this._dirty
|
||||
? this.hass!.localize("ui.common.save")
|
||||
: this.hass!.localize("ui.common.close")}
|
||||
: this.hass!.localize("ui.common.save")}
|
||||
</mwc-button>
|
||||
`
|
||||
: ``}
|
||||
|
@@ -111,15 +111,12 @@ export class HuiCalendarCardEditor
|
||||
@value-changed=${this._valueChanged}
|
||||
></hui-theme-select-editor>
|
||||
</div>
|
||||
<h3>
|
||||
${this.hass.localize(
|
||||
"ui.panel.lovelace.editor.card.calendar.calendar_entities"
|
||||
) +
|
||||
" (" +
|
||||
this.hass!.localize("ui.panel.lovelace.editor.card.config.required") +
|
||||
")"}
|
||||
</h3>
|
||||
<ha-entities-picker
|
||||
.label=${`${this.hass.localize(
|
||||
"ui.panel.lovelace.editor.card.calendar.calendar_entities"
|
||||
)} (${this.hass!.localize(
|
||||
"ui.panel.lovelace.editor.card.config.required"
|
||||
)})`}
|
||||
.hass=${this.hass!}
|
||||
.value=${this._configEntities}
|
||||
.includeDomains=${["calendar"]}
|
||||
|
@@ -0,0 +1,393 @@
|
||||
import "@material/mwc-tab-bar/mwc-tab-bar";
|
||||
import "@material/mwc-tab/mwc-tab";
|
||||
import type { MDCTabBarActivatedEvent } from "@material/tab-bar";
|
||||
import { mdiClose } from "@mdi/js";
|
||||
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import {
|
||||
any,
|
||||
array,
|
||||
assert,
|
||||
assign,
|
||||
boolean,
|
||||
object,
|
||||
optional,
|
||||
string,
|
||||
} from "superstruct";
|
||||
import { fireEvent, HASSDomEvent } from "../../../../common/dom/fire_event";
|
||||
import "../../../../components/entity/ha-entity-picker";
|
||||
import { LovelaceCardConfig, LovelaceConfig } from "../../../../data/lovelace";
|
||||
import { HomeAssistant } from "../../../../types";
|
||||
import { EntityFilterCardConfig } from "../../cards/types";
|
||||
import {
|
||||
EntityFilterEntityConfig,
|
||||
LovelaceRowConfig,
|
||||
} from "../../entity-rows/types";
|
||||
import { LovelaceCardEditor } from "../../types";
|
||||
import "../card-editor/hui-card-element-editor";
|
||||
import type { HuiCardElementEditor } from "../card-editor/hui-card-element-editor";
|
||||
import "../card-editor/hui-card-picker";
|
||||
import "../hui-element-editor";
|
||||
import type { ConfigChangedEvent } from "../hui-element-editor";
|
||||
import { baseLovelaceCardConfig } from "../structs/base-card-struct";
|
||||
import { entitiesConfigStruct } from "../structs/entities-struct";
|
||||
import { EntitiesEditorEvent, GUIModeChangedEvent } from "../types";
|
||||
import { configElementStyle } from "./config-elements-style";
|
||||
import { processEditorEntities } from "../process-editor-entities";
|
||||
import { computeRTLDirection } from "../../../../common/util/compute_rtl";
|
||||
|
||||
const cardConfigStruct = assign(
|
||||
baseLovelaceCardConfig,
|
||||
object({
|
||||
card: optional(any()),
|
||||
entities: array(entitiesConfigStruct),
|
||||
state_filter: array(string()),
|
||||
show_empty: optional(boolean()),
|
||||
})
|
||||
);
|
||||
|
||||
@customElement("hui-entity-filter-card-editor")
|
||||
export class HuiEntityFilterCardEditor
|
||||
extends LitElement
|
||||
implements LovelaceCardEditor
|
||||
{
|
||||
@property({ attribute: false }) public hass?: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public lovelace?: LovelaceConfig;
|
||||
|
||||
@state() protected _config?: EntityFilterCardConfig;
|
||||
|
||||
@state() private _configEntities?: LovelaceRowConfig[];
|
||||
|
||||
@state() private _GUImode = true;
|
||||
|
||||
@state() private _guiModeAvailable? = true;
|
||||
|
||||
@state() private _cardTab = false;
|
||||
|
||||
@query("hui-card-element-editor")
|
||||
private _cardEditorEl?: HuiCardElementEditor;
|
||||
|
||||
public setConfig(config: EntityFilterCardConfig): void {
|
||||
assert(config, cardConfigStruct);
|
||||
this._config = config;
|
||||
this._configEntities = processEditorEntities(config.entities);
|
||||
}
|
||||
|
||||
public focusYamlEditor() {
|
||||
this._cardEditorEl?.focusYamlEditor();
|
||||
}
|
||||
|
||||
protected render(): TemplateResult {
|
||||
if (!this.hass || !this._config) {
|
||||
return html``;
|
||||
}
|
||||
|
||||
return html`
|
||||
<mwc-tab-bar
|
||||
.activeIndex=${this._cardTab ? 1 : 0}
|
||||
@MDCTabBar:activated=${this._selectTab}
|
||||
>
|
||||
<mwc-tab
|
||||
.label=${this.hass!.localize(
|
||||
"ui.panel.lovelace.editor.card.entity-filter.filters"
|
||||
)}
|
||||
></mwc-tab>
|
||||
<mwc-tab
|
||||
.label=${this.hass!.localize(
|
||||
"ui.panel.lovelace.editor.card.entity-filter.card"
|
||||
)}
|
||||
></mwc-tab>
|
||||
</mwc-tab-bar>
|
||||
${this._cardTab ? this._renderCardEditor() : this._renderFilterEditor()}
|
||||
`;
|
||||
}
|
||||
|
||||
private _renderFilterEditor(): TemplateResult {
|
||||
return html`
|
||||
<div class="entities">
|
||||
<hui-entity-editor
|
||||
.hass=${this.hass}
|
||||
.entities=${this._configEntities}
|
||||
@entities-changed=${this._entitiesChanged}
|
||||
></hui-entity-editor>
|
||||
</div>
|
||||
<div class="states">
|
||||
<h3>
|
||||
${this.hass!.localize(
|
||||
"ui.panel.lovelace.editor.card.entity-filter.display_states"
|
||||
)}
|
||||
(${this.hass!.localize(
|
||||
"ui.panel.lovelace.editor.card.config.required"
|
||||
)})
|
||||
</h3>
|
||||
${this._config!.state_filter.map(
|
||||
(stte, idx) => html`<div class="state">
|
||||
<paper-input
|
||||
.label=${this.hass!.localize(
|
||||
"ui.panel.lovelace.editor.card.entity-filter.state"
|
||||
)}
|
||||
.value=${stte as string}
|
||||
.index=${idx}
|
||||
@change=${this._stateChanged}
|
||||
>
|
||||
<ha-icon-button
|
||||
.label=${this.hass!.localize(
|
||||
"ui.panel.lovelace.editor.card.entity-filter.delete_state"
|
||||
)}
|
||||
.path=${mdiClose}
|
||||
tabindex="-1"
|
||||
no-ripple
|
||||
.index=${idx}
|
||||
slot="suffix"
|
||||
@click=${this._stateDeleted}
|
||||
>
|
||||
</ha-icon-button>
|
||||
</paper-input>
|
||||
</div>`
|
||||
)}
|
||||
<paper-input
|
||||
.label=${this.hass!.localize(
|
||||
"ui.panel.lovelace.editor.card.entity-filter.state"
|
||||
)}
|
||||
@change=${this._stateAdded}
|
||||
></paper-input>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
private _renderCardEditor(): TemplateResult {
|
||||
return html`
|
||||
<div class="card">
|
||||
<ha-formfield
|
||||
.label=${this.hass!.localize(
|
||||
"ui.panel.lovelace.editor.card.entity-filter.show_empty"
|
||||
)}
|
||||
.dir=${computeRTLDirection(this.hass!)}
|
||||
>
|
||||
<ha-switch
|
||||
.checked=${this._config!.show_empty !== false}
|
||||
@change=${this._showEmptyToggle}
|
||||
></ha-switch>
|
||||
</ha-formfield>
|
||||
${this._config!.card && this._config!.card.type !== undefined
|
||||
? html`
|
||||
<div class="card-options">
|
||||
<mwc-button
|
||||
@click=${this._toggleMode}
|
||||
.disabled=${!this._guiModeAvailable}
|
||||
class="gui-mode-button"
|
||||
>
|
||||
${this.hass!.localize(
|
||||
!this._cardEditorEl || this._GUImode
|
||||
? "ui.panel.lovelace.editor.edit_card.show_code_editor"
|
||||
: "ui.panel.lovelace.editor.edit_card.show_visual_editor"
|
||||
)}
|
||||
</mwc-button>
|
||||
<mwc-button @click=${this._handleReplaceCard}
|
||||
>${this.hass!.localize(
|
||||
"ui.panel.lovelace.editor.card.conditional.change_type"
|
||||
)}</mwc-button
|
||||
>
|
||||
</div>
|
||||
<hui-card-element-editor
|
||||
.hass=${this.hass}
|
||||
.value=${this._getCardConfig()}
|
||||
.lovelace=${this.lovelace}
|
||||
@config-changed=${this._handleCardChanged}
|
||||
@GUImode-changed=${this._handleGUIModeChanged}
|
||||
></hui-card-element-editor>
|
||||
`
|
||||
: html`
|
||||
<hui-card-picker
|
||||
.hass=${this.hass}
|
||||
.lovelace=${this.lovelace}
|
||||
@config-changed=${this._handleCardPicked}
|
||||
></hui-card-picker>
|
||||
`}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
private _selectTab(ev: MDCTabBarActivatedEvent): void {
|
||||
this._cardTab = ev.detail.index === 1;
|
||||
}
|
||||
|
||||
private _toggleMode(): void {
|
||||
this._cardEditorEl?.toggleMode();
|
||||
}
|
||||
|
||||
private _setMode(value: boolean): void {
|
||||
this._GUImode = value;
|
||||
if (this._cardEditorEl) {
|
||||
this._cardEditorEl.GUImode = value;
|
||||
}
|
||||
}
|
||||
|
||||
private _showEmptyToggle(): void {
|
||||
if (!this._config || !this.hass) {
|
||||
return;
|
||||
}
|
||||
this._config = {
|
||||
...this._config,
|
||||
show_empty: this._config.show_empty === false,
|
||||
};
|
||||
fireEvent(this, "config-changed", { config: this._config });
|
||||
}
|
||||
|
||||
private _entitiesChanged(ev: EntitiesEditorEvent): void {
|
||||
if (!this._config || !this.hass) {
|
||||
return;
|
||||
}
|
||||
if (!ev.detail || !ev.detail.entities) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._config = {
|
||||
...this._config,
|
||||
entities: ev.detail.entities as EntityFilterEntityConfig[],
|
||||
};
|
||||
this._configEntities = processEditorEntities(this._config.entities);
|
||||
|
||||
fireEvent(this, "config-changed", { config: this._config });
|
||||
}
|
||||
|
||||
private _stateDeleted(ev: Event): void {
|
||||
const target = ev.target! as any;
|
||||
if (target.value === "" || !this._config) {
|
||||
return;
|
||||
}
|
||||
const state_filter = [...this._config.state_filter];
|
||||
state_filter.splice(target.index, 1);
|
||||
|
||||
this._config = { ...this._config, state_filter };
|
||||
fireEvent(this, "config-changed", { config: this._config });
|
||||
}
|
||||
|
||||
private _stateAdded(ev: Event): void {
|
||||
const target = ev.target! as any;
|
||||
if (target.value === "" || !this._config) {
|
||||
return;
|
||||
}
|
||||
const state_filter = [...this._config.state_filter];
|
||||
state_filter.push(target.value);
|
||||
|
||||
this._config = { ...this._config, state_filter };
|
||||
target.value = "";
|
||||
fireEvent(this, "config-changed", { config: this._config });
|
||||
}
|
||||
|
||||
private _stateChanged(ev: Event): void {
|
||||
const target = ev.target! as any;
|
||||
if (target.value === "" || !this._config) {
|
||||
return;
|
||||
}
|
||||
const state_filter = [...this._config.state_filter];
|
||||
state_filter[target.index] = target.value;
|
||||
|
||||
this._config = { ...this._config, state_filter };
|
||||
fireEvent(this, "config-changed", { config: this._config });
|
||||
}
|
||||
|
||||
private _handleGUIModeChanged(ev: HASSDomEvent<GUIModeChangedEvent>): void {
|
||||
ev.stopPropagation();
|
||||
this._GUImode = ev.detail.guiMode;
|
||||
this._guiModeAvailable = ev.detail.guiModeAvailable;
|
||||
}
|
||||
|
||||
private _handleCardPicked(ev: CustomEvent): void {
|
||||
ev.stopPropagation();
|
||||
if (!this._config) {
|
||||
return;
|
||||
}
|
||||
const cardConfig = { ...ev.detail.config } as LovelaceCardConfig;
|
||||
delete cardConfig.entities;
|
||||
this._setMode(true);
|
||||
this._guiModeAvailable = true;
|
||||
this._config = { ...this._config, card: cardConfig };
|
||||
fireEvent(this, "config-changed", { config: this._config });
|
||||
}
|
||||
|
||||
private _handleCardChanged(ev: HASSDomEvent<ConfigChangedEvent>): void {
|
||||
ev.stopPropagation();
|
||||
if (!this._config) {
|
||||
return;
|
||||
}
|
||||
const cardConfig = { ...ev.detail.config } as LovelaceCardConfig;
|
||||
delete cardConfig.entities;
|
||||
this._config = {
|
||||
...this._config,
|
||||
card: cardConfig,
|
||||
};
|
||||
this._guiModeAvailable = ev.detail.guiModeAvailable;
|
||||
fireEvent(this, "config-changed", { config: this._config });
|
||||
}
|
||||
|
||||
private _handleReplaceCard(): void {
|
||||
if (!this._config) {
|
||||
return;
|
||||
}
|
||||
// @ts-ignore
|
||||
this._config = { ...this._config, card: {} };
|
||||
// @ts-ignore
|
||||
fireEvent(this, "config-changed", { config: this._config });
|
||||
}
|
||||
|
||||
private _getCardConfig(): LovelaceCardConfig {
|
||||
const cardConfig = { ...this._config!.card } as LovelaceCardConfig;
|
||||
cardConfig.entities = [];
|
||||
return cardConfig;
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return [
|
||||
configElementStyle,
|
||||
css`
|
||||
mwc-tab-bar {
|
||||
border-bottom: 1px solid var(--divider-color);
|
||||
}
|
||||
|
||||
.entities,
|
||||
.states,
|
||||
.card {
|
||||
margin-top: 8px;
|
||||
padding: 12px;
|
||||
}
|
||||
@media (max-width: 450px) {
|
||||
.entities,
|
||||
.states,
|
||||
.card {
|
||||
margin: 8px -12px 0;
|
||||
}
|
||||
}
|
||||
.card {
|
||||
--entity-picker-display: none;
|
||||
}
|
||||
|
||||
.state {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
width: 100%;
|
||||
}
|
||||
.state paper-input {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.card .card-options {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
width: 100%;
|
||||
}
|
||||
.gui-mode-button {
|
||||
margin-right: auto;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"hui-entity-filter-card-editor": HuiEntityFilterCardEditor;
|
||||
}
|
||||
}
|
@@ -102,14 +102,12 @@ export class HuiLogbookCardEditor
|
||||
@value-changed=${this._valueChanged}
|
||||
></paper-input>
|
||||
</div>
|
||||
<h3>
|
||||
${`${this.hass!.localize(
|
||||
<ha-entities-picker
|
||||
.label=${`${this.hass!.localize(
|
||||
"ui.panel.lovelace.editor.card.generic.entities"
|
||||
)} (${this.hass!.localize(
|
||||
"ui.panel.lovelace.editor.card.config.required"
|
||||
)})`}
|
||||
</h3>
|
||||
<ha-entities-picker
|
||||
.hass=${this.hass}
|
||||
.value=${this._configEntities}
|
||||
@value-changed=${this._valueChanged}
|
||||
|
@@ -11,7 +11,6 @@ import {
|
||||
import { property, state, query } from "lit/decorators";
|
||||
import { fireEvent } from "../../../common/dom/fire_event";
|
||||
import { handleStructError } from "../../../common/structs/handle-errors";
|
||||
import { computeRTL } from "../../../common/util/compute_rtl";
|
||||
import { deepEqual } from "../../../common/util/deep-equal";
|
||||
import "../../../components/ha-circular-progress";
|
||||
import "../../../components/ha-code-editor";
|
||||
@@ -200,9 +199,9 @@ export abstract class HuiElementEditor<T> extends LitElement {
|
||||
autofocus
|
||||
.value=${this.yaml}
|
||||
.error=${Boolean(this._errors)}
|
||||
.rtl=${computeRTL(this.hass)}
|
||||
@value-changed=${this._handleYAMLChanged}
|
||||
@keydown=${this._ignoreKeydown}
|
||||
dir="ltr"
|
||||
></ha-code-editor>
|
||||
</div>
|
||||
`}
|
||||
|
@@ -253,6 +253,10 @@ export class HuiEntitiesCardRowEditor extends LitElement {
|
||||
return [
|
||||
sortableStyles,
|
||||
css`
|
||||
:host {
|
||||
display: var(--entity-picker-display);
|
||||
}
|
||||
|
||||
.entity {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
@@ -36,6 +36,7 @@ import {
|
||||
PANEL_VIEW_LAYOUT,
|
||||
VIEWS_NO_BADGE_SUPPORT,
|
||||
} from "../../views/const";
|
||||
import { deepEqual } from "../../../../common/util/deep-equal";
|
||||
|
||||
@customElement("hui-dialog-edit-view")
|
||||
export class HuiDialogEditView extends LitElement {
|
||||
@@ -53,6 +54,8 @@ export class HuiDialogEditView extends LitElement {
|
||||
|
||||
@state() private _curTab?: string;
|
||||
|
||||
@state() private _dirty = false;
|
||||
|
||||
private _curTabIndex = 0;
|
||||
|
||||
get _type(): string {
|
||||
@@ -71,6 +74,7 @@ export class HuiDialogEditView extends LitElement {
|
||||
this._config = {};
|
||||
this._badges = [];
|
||||
this._cards = [];
|
||||
this._dirty = false;
|
||||
} else {
|
||||
const { cards, badges, ...viewConfig } =
|
||||
this._params.lovelace!.config.views[this._params.viewIndex];
|
||||
@@ -85,6 +89,7 @@ export class HuiDialogEditView extends LitElement {
|
||||
this._params = undefined;
|
||||
this._config = {};
|
||||
this._badges = [];
|
||||
this._dirty = false;
|
||||
fireEvent(this, "dialog-closed", { dialog: this.localName });
|
||||
}
|
||||
|
||||
@@ -214,7 +219,7 @@ export class HuiDialogEditView extends LitElement {
|
||||
>
|
||||
<mwc-button
|
||||
slot="primaryAction"
|
||||
?disabled=${!this._config || this._saving}
|
||||
?disabled=${!this._config || this._saving || !this._dirty}
|
||||
@click=${this._save}
|
||||
>
|
||||
${this._saving
|
||||
@@ -316,8 +321,13 @@ export class HuiDialogEditView extends LitElement {
|
||||
}
|
||||
|
||||
private _viewConfigChanged(ev: ViewEditEvent): void {
|
||||
if (ev.detail && ev.detail.config) {
|
||||
if (
|
||||
ev.detail &&
|
||||
ev.detail.config &&
|
||||
!deepEqual(this._config, ev.detail.config)
|
||||
) {
|
||||
this._config = ev.detail.config;
|
||||
this._dirty = true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -327,6 +337,7 @@ export class HuiDialogEditView extends LitElement {
|
||||
if (ev.detail.visible && this._config) {
|
||||
this._config.visible = ev.detail.visible;
|
||||
}
|
||||
this._dirty = true;
|
||||
}
|
||||
|
||||
private _badgesChanged(ev: EntitiesEditorEvent): void {
|
||||
@@ -334,6 +345,7 @@ export class HuiDialogEditView extends LitElement {
|
||||
return;
|
||||
}
|
||||
this._badges = processEditorEntities(ev.detail.entities);
|
||||
this._dirty = true;
|
||||
}
|
||||
|
||||
private _isConfigChanged(): boolean {
|
||||
|
@@ -15,7 +15,6 @@ import {
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { classMap } from "lit/directives/class-map";
|
||||
import { array, assert, object, optional, string, type } from "superstruct";
|
||||
import { computeRTL } from "../../common/util/compute_rtl";
|
||||
import { deepEqual } from "../../common/util/deep-equal";
|
||||
import "../../components/ha-circular-progress";
|
||||
import "../../components/ha-code-editor";
|
||||
@@ -92,10 +91,10 @@ class LovelaceFullConfigEditor extends LitElement {
|
||||
<ha-code-editor
|
||||
mode="yaml"
|
||||
autofocus
|
||||
.rtl=${computeRTL(this.hass)}
|
||||
.hass=${this.hass}
|
||||
@value-changed=${this._yamlChanged}
|
||||
@editor-save=${this._handleSave}
|
||||
dir="ltr"
|
||||
>
|
||||
</ha-code-editor>
|
||||
</div>
|
||||
|
@@ -2,9 +2,7 @@ import "@material/mwc-button";
|
||||
import "@material/mwc-list/mwc-list-item";
|
||||
import type { RequestSelectedDetail } from "@material/mwc-list/mwc-list-item";
|
||||
import {
|
||||
mdiClose,
|
||||
mdiCodeBraces,
|
||||
mdiCog,
|
||||
mdiDotsVertical,
|
||||
mdiFileMultiple,
|
||||
mdiFormatListBulletedTriangle,
|
||||
@@ -119,13 +117,6 @@ class HUIRoot extends LitElement {
|
||||
${this._editMode
|
||||
? html`
|
||||
<app-toolbar class="edit-mode">
|
||||
<ha-icon-button
|
||||
.label=${this.hass!.localize(
|
||||
"ui.panel.lovelace.menu.exit_edit_mode"
|
||||
)}
|
||||
.path=${mdiClose}
|
||||
@click=${this._editModeDisable}
|
||||
></ha-icon-button>
|
||||
<div main-title>
|
||||
${this.config.title ||
|
||||
this.hass!.localize("ui.panel.lovelace.editor.header")}
|
||||
@@ -138,6 +129,13 @@ class HUIRoot extends LitElement {
|
||||
@click=${this._editLovelace}
|
||||
></ha-icon-button>
|
||||
</div>
|
||||
<mwc-button
|
||||
class="exit-edit-mode"
|
||||
.label=${this.hass!.localize(
|
||||
"ui.panel.lovelace.menu.exit_edit_mode"
|
||||
)}
|
||||
@click=${this._editModeDisable}
|
||||
></mwc-button>
|
||||
<a
|
||||
href=${documentationUrl(this.hass, "/lovelace/")}
|
||||
rel="noreferrer"
|
||||
@@ -377,30 +375,36 @@ class HUIRoot extends LitElement {
|
||||
)}
|
||||
<ha-svg-icon
|
||||
slot="graphic"
|
||||
.path=${mdiCog}
|
||||
.path=${mdiPencil}
|
||||
></ha-svg-icon>
|
||||
</mwc-list-item>
|
||||
`
|
||||
: ""}
|
||||
<a
|
||||
href=${documentationUrl(this.hass, "/lovelace/")}
|
||||
rel="noreferrer"
|
||||
class="menu-link"
|
||||
target="_blank"
|
||||
>
|
||||
<mwc-list-item
|
||||
graphic="icon"
|
||||
aria-label=${this.hass!.localize(
|
||||
"ui.panel.lovelace.menu.help"
|
||||
)}
|
||||
>
|
||||
${this.hass!.localize("ui.panel.lovelace.menu.help")}
|
||||
<ha-svg-icon
|
||||
slot="graphic"
|
||||
.path=${mdiHelp}
|
||||
></ha-svg-icon>
|
||||
</mwc-list-item>
|
||||
</a>
|
||||
${this._editMode
|
||||
? html`
|
||||
<a
|
||||
href=${documentationUrl(this.hass, "/lovelace/")}
|
||||
rel="noreferrer"
|
||||
class="menu-link"
|
||||
target="_blank"
|
||||
>
|
||||
<mwc-list-item
|
||||
graphic="icon"
|
||||
aria-label=${this.hass!.localize(
|
||||
"ui.panel.lovelace.menu.help"
|
||||
)}
|
||||
>
|
||||
${this.hass!.localize(
|
||||
"ui.panel.lovelace.menu.help"
|
||||
)}
|
||||
<ha-svg-icon
|
||||
slot="graphic"
|
||||
.path=${mdiHelp}
|
||||
></ha-svg-icon>
|
||||
</mwc-list-item>
|
||||
</a>
|
||||
`
|
||||
: ""}
|
||||
</ha-button-menu>
|
||||
</app-toolbar>
|
||||
`}
|
||||
@@ -933,6 +937,10 @@ class HUIRoot extends LitElement {
|
||||
var(--primary-background-color)
|
||||
);
|
||||
}
|
||||
.exit-edit-mode {
|
||||
--mdc-theme-primary: var(--primary-text-color);
|
||||
--mdc-typography-button-font-size: 14px;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
@@ -75,7 +75,7 @@ class HuiAttributeRow extends LitElement implements LovelaceRow {
|
||||
? formatNumber(attribute, this.hass.locale)
|
||||
: attribute !== undefined
|
||||
? formatAttributeValue(this.hass, attribute)
|
||||
: "-"}
|
||||
: "—"}
|
||||
${this._config.suffix}
|
||||
</hui-generic-entity-row>
|
||||
`;
|
||||
|
106
src/panels/media-browser/browser-media-player.ts
Normal file
106
src/panels/media-browser/browser-media-player.ts
Normal file
@@ -0,0 +1,106 @@
|
||||
import {
|
||||
BROWSER_PLAYER,
|
||||
MediaPlayerEntity,
|
||||
MediaPlayerItem,
|
||||
SUPPORT_PAUSE,
|
||||
SUPPORT_PLAY,
|
||||
} from "../../data/media-player";
|
||||
import { resolveMediaSource } from "../../data/media_source";
|
||||
import { HomeAssistant } from "../../types";
|
||||
|
||||
export class BrowserMediaPlayer {
|
||||
private player?: HTMLAudioElement;
|
||||
|
||||
private stopped = false;
|
||||
|
||||
constructor(
|
||||
public hass: HomeAssistant,
|
||||
private item: MediaPlayerItem,
|
||||
private onChange: () => void
|
||||
) {}
|
||||
|
||||
public async initialize() {
|
||||
const resolvedUrl: any = await resolveMediaSource(
|
||||
this.hass,
|
||||
this.item.media_content_id
|
||||
);
|
||||
|
||||
const player = new Audio(resolvedUrl.url);
|
||||
player.addEventListener("play", this._handleChange);
|
||||
player.addEventListener("playing", this._handleChange);
|
||||
player.addEventListener("pause", this._handleChange);
|
||||
player.addEventListener("ended", this._handleChange);
|
||||
player.addEventListener("canplaythrough", () => {
|
||||
if (this.stopped) {
|
||||
return;
|
||||
}
|
||||
this.player = player;
|
||||
player.play();
|
||||
this.onChange();
|
||||
});
|
||||
}
|
||||
|
||||
private _handleChange = () => {
|
||||
if (!this.stopped) {
|
||||
this.onChange();
|
||||
}
|
||||
};
|
||||
|
||||
public pause() {
|
||||
if (this.player) {
|
||||
this.player.pause();
|
||||
}
|
||||
}
|
||||
|
||||
public play() {
|
||||
if (this.player) {
|
||||
this.player.play();
|
||||
}
|
||||
}
|
||||
|
||||
public stop() {
|
||||
this.stopped = true;
|
||||
// @ts-ignore
|
||||
this.onChange = undefined;
|
||||
if (this.player) {
|
||||
this.player.pause();
|
||||
}
|
||||
}
|
||||
|
||||
public get isPlaying(): boolean {
|
||||
return (
|
||||
this.player !== undefined && !this.player.paused && !this.player.ended
|
||||
);
|
||||
}
|
||||
|
||||
static idleStateObj(): MediaPlayerEntity {
|
||||
const now = new Date().toISOString();
|
||||
return {
|
||||
state: "idle",
|
||||
entity_id: BROWSER_PLAYER,
|
||||
last_changed: now,
|
||||
last_updated: now,
|
||||
attributes: {},
|
||||
context: { id: "", user_id: null },
|
||||
};
|
||||
}
|
||||
|
||||
toStateObj(): MediaPlayerEntity {
|
||||
// https://developer.mozilla.org/en-US/docs/Web/API/HTMLMediaElement
|
||||
const base = BrowserMediaPlayer.idleStateObj();
|
||||
if (!this.player) {
|
||||
return base;
|
||||
}
|
||||
base.state = this.isPlaying ? "playing" : "paused";
|
||||
base.attributes = {
|
||||
media_title: this.item.title,
|
||||
media_duration: this.player.duration,
|
||||
media_position: this.player.currentTime,
|
||||
media_position_updated_at: base.last_updated,
|
||||
entity_picture: this.item.thumbnail,
|
||||
// eslint-disable-next-line no-bitwise
|
||||
supported_features: SUPPORT_PLAY | SUPPORT_PAUSE,
|
||||
};
|
||||
return base;
|
||||
}
|
||||
}
|
@@ -30,11 +30,13 @@ import "../../components/ha-icon-button";
|
||||
import { UNAVAILABLE_STATES } from "../../data/entity";
|
||||
import {
|
||||
BROWSER_PLAYER,
|
||||
cleanupMediaTitle,
|
||||
computeMediaControls,
|
||||
computeMediaDescription,
|
||||
formatMediaTime,
|
||||
getCurrentProgress,
|
||||
MediaPlayerEntity,
|
||||
MediaPlayerItem,
|
||||
SUPPORT_BROWSE_MEDIA,
|
||||
SUPPORT_PAUSE,
|
||||
SUPPORT_PLAY,
|
||||
@@ -42,6 +44,7 @@ import {
|
||||
} from "../../data/media-player";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import "../lovelace/components/hui-marquee";
|
||||
import { BrowserMediaPlayer } from "./browser-media-player";
|
||||
|
||||
@customElement("ha-bar-media-player")
|
||||
class BarMediaPlayer extends LitElement {
|
||||
@@ -58,6 +61,8 @@ class BarMediaPlayer extends LitElement {
|
||||
|
||||
@state() private _marqueeActive = false;
|
||||
|
||||
@state() private _browserPlayer?: BrowserMediaPlayer;
|
||||
|
||||
private _progressInterval?: number;
|
||||
|
||||
public connectedCallback(): void {
|
||||
@@ -86,73 +91,28 @@ class BarMediaPlayer extends LitElement {
|
||||
clearInterval(this._progressInterval);
|
||||
this._progressInterval = undefined;
|
||||
}
|
||||
|
||||
if (this._browserPlayer) {
|
||||
this._browserPlayer.stop();
|
||||
this._browserPlayer = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
public async playItem(item: MediaPlayerItem) {
|
||||
if (this.entityId !== BROWSER_PLAYER) {
|
||||
throw Error("Only browser supported");
|
||||
}
|
||||
if (this._browserPlayer) {
|
||||
this._browserPlayer.stop();
|
||||
}
|
||||
this._browserPlayer = new BrowserMediaPlayer(this.hass, item, () =>
|
||||
this.requestUpdate("_browserPlayer")
|
||||
);
|
||||
await this._browserPlayer.initialize();
|
||||
}
|
||||
|
||||
protected render(): TemplateResult {
|
||||
const choosePlayerElement = html`
|
||||
<div
|
||||
class="choose-player ${this.entityId === BROWSER_PLAYER
|
||||
? "browser"
|
||||
: ""}"
|
||||
>
|
||||
<ha-button-menu corner="BOTTOM_START">
|
||||
${this.narrow
|
||||
? html`
|
||||
<ha-icon-button
|
||||
slot="trigger"
|
||||
.path=${this._stateObj
|
||||
? domainIcon(computeDomain(this.entityId), this._stateObj)
|
||||
: mdiMonitor}
|
||||
></ha-icon-button>
|
||||
`
|
||||
: html`
|
||||
<mwc-button
|
||||
slot="trigger"
|
||||
.label=${this.narrow
|
||||
? ""
|
||||
: `${
|
||||
this._stateObj
|
||||
? computeStateName(this._stateObj)
|
||||
: BROWSER_PLAYER
|
||||
}
|
||||
`}
|
||||
>
|
||||
<ha-svg-icon
|
||||
slot="icon"
|
||||
.path=${this._stateObj
|
||||
? domainIcon(computeDomain(this.entityId), this._stateObj)
|
||||
: mdiMonitor}
|
||||
></ha-svg-icon>
|
||||
<ha-svg-icon
|
||||
slot="trailingIcon"
|
||||
.path=${mdiChevronDown}
|
||||
></ha-svg-icon>
|
||||
</mwc-button>
|
||||
`}
|
||||
<mwc-list-item .player=${BROWSER_PLAYER} @click=${this._selectPlayer}
|
||||
>${this.hass.localize(
|
||||
"ui.components.media-browser.web-browser"
|
||||
)}</mwc-list-item
|
||||
>
|
||||
${this._mediaPlayerEntities.map(
|
||||
(source) => html`
|
||||
<mwc-list-item
|
||||
?selected=${source.entity_id === this.entityId}
|
||||
.disabled=${UNAVAILABLE_STATES.includes(source.state)}
|
||||
.player=${source.entity_id}
|
||||
@click=${this._selectPlayer}
|
||||
>${computeStateName(source)}</mwc-list-item
|
||||
>
|
||||
`
|
||||
)}
|
||||
</ha-button-menu>
|
||||
</div>
|
||||
`;
|
||||
|
||||
if (!this._stateObj) {
|
||||
return choosePlayerElement;
|
||||
}
|
||||
|
||||
const isBrowser = this.entityId === BROWSER_PLAYER;
|
||||
const stateObj = this._stateObj;
|
||||
const controls = !this.narrow
|
||||
? computeMediaControls(stateObj)
|
||||
@@ -185,17 +145,18 @@ class BarMediaPlayer extends LitElement {
|
||||
: [{}];
|
||||
const mediaDescription = computeMediaDescription(stateObj);
|
||||
const mediaDuration = formatMediaTime(stateObj!.attributes.media_duration!);
|
||||
const mediaTitleClean = cleanupMediaTitle(stateObj.attributes.media_title);
|
||||
|
||||
const mediaArt =
|
||||
stateObj.attributes.entity_picture_local ||
|
||||
stateObj.attributes.entity_picture;
|
||||
|
||||
return html`
|
||||
<div class="info">
|
||||
${this._image
|
||||
? html`<img src=${this.hass.hassUrl(this._image)} />`
|
||||
: stateObj.state === "off" || stateObj.state !== "playing"
|
||||
? html`<div class="blank-image"></div>`
|
||||
: ""}
|
||||
${mediaArt ? html`<img src=${this.hass.hassUrl(mediaArt)} />` : ""}
|
||||
<div class="media-info">
|
||||
<hui-marquee
|
||||
.text=${stateObj.attributes.media_title ||
|
||||
.text=${mediaTitleClean ||
|
||||
mediaDescription ||
|
||||
this.hass.localize(`ui.card.media_player.nothing_playing`)}
|
||||
.active=${this._marqueeActive}
|
||||
@@ -203,25 +164,27 @@ class BarMediaPlayer extends LitElement {
|
||||
@mouseleave=${this._marqueeMouseLeave}
|
||||
></hui-marquee>
|
||||
<span class="secondary">
|
||||
${stateObj.attributes.media_title ? mediaDescription : ""}
|
||||
${mediaTitleClean ? mediaDescription : ""}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="controls-progress">
|
||||
<div class="controls">
|
||||
${controls!.map(
|
||||
(control) => html`
|
||||
<ha-icon-button
|
||||
.label=${this.hass.localize(
|
||||
`ui.card.media_player.${control.action}`
|
||||
)}
|
||||
.path=${control.icon}
|
||||
action=${control.action}
|
||||
@click=${this._handleClick}
|
||||
>
|
||||
</ha-icon-button>
|
||||
`
|
||||
)}
|
||||
${controls === undefined
|
||||
? ""
|
||||
: controls.map(
|
||||
(control) => html`
|
||||
<ha-icon-button
|
||||
.label=${this.hass.localize(
|
||||
`ui.card.media_player.${control.action}`
|
||||
)}
|
||||
.path=${control.icon}
|
||||
action=${control.action}
|
||||
@click=${this._handleClick}
|
||||
>
|
||||
</ha-icon-button>
|
||||
`
|
||||
)}
|
||||
</div>
|
||||
${this.narrow
|
||||
? html`<mwc-linear-progress></mwc-linear-progress>`
|
||||
@@ -233,13 +196,85 @@ class BarMediaPlayer extends LitElement {
|
||||
</div>
|
||||
`}
|
||||
</div>
|
||||
${choosePlayerElement}
|
||||
<div class="choose-player ${isBrowser ? "browser" : ""}">
|
||||
<ha-button-menu corner="BOTTOM_START">
|
||||
${this.narrow
|
||||
? html`
|
||||
<ha-icon-button
|
||||
slot="trigger"
|
||||
.path=${isBrowser
|
||||
? mdiMonitor
|
||||
: domainIcon(computeDomain(this.entityId), stateObj)}
|
||||
></ha-icon-button>
|
||||
`
|
||||
: html`
|
||||
<mwc-button
|
||||
slot="trigger"
|
||||
.label=${this.narrow
|
||||
? ""
|
||||
: `${computeStateName(stateObj)}
|
||||
`}
|
||||
>
|
||||
<ha-svg-icon
|
||||
slot="icon"
|
||||
.path=${isBrowser
|
||||
? mdiMonitor
|
||||
: domainIcon(computeDomain(this.entityId), stateObj)}
|
||||
></ha-svg-icon>
|
||||
<ha-svg-icon
|
||||
slot="trailingIcon"
|
||||
.path=${mdiChevronDown}
|
||||
></ha-svg-icon>
|
||||
</mwc-button>
|
||||
`}
|
||||
<mwc-list-item
|
||||
.player=${BROWSER_PLAYER}
|
||||
?selected=${isBrowser}
|
||||
@click=${this._selectPlayer}
|
||||
>
|
||||
${this.hass.localize("ui.components.media-browser.web-browser")}
|
||||
</mwc-list-item>
|
||||
${this._mediaPlayerEntities.map(
|
||||
(source) => html`
|
||||
<mwc-list-item
|
||||
?selected=${source.entity_id === this.entityId}
|
||||
.disabled=${UNAVAILABLE_STATES.includes(source.state)}
|
||||
.player=${source.entity_id}
|
||||
@click=${this._selectPlayer}
|
||||
>
|
||||
${computeStateName(source)}
|
||||
</mwc-list-item>
|
||||
`
|
||||
)}
|
||||
</ha-button-menu>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
public willUpdate(changedProps: PropertyValues) {
|
||||
super.willUpdate(changedProps);
|
||||
if (
|
||||
changedProps.has("entityId") &&
|
||||
this.entityId !== BROWSER_PLAYER &&
|
||||
this._browserPlayer
|
||||
) {
|
||||
this._browserPlayer?.stop();
|
||||
this._browserPlayer = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
protected updated(changedProps: PropertyValues) {
|
||||
if (!this.hass || !this._stateObj || !changedProps.has("hass")) {
|
||||
return;
|
||||
super.updated(changedProps);
|
||||
|
||||
if (this.entityId === BROWSER_PLAYER) {
|
||||
if (!changedProps.has("_browserPlayer")) {
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
const oldHass = changedProps.get("hass") as HomeAssistant | undefined;
|
||||
if (oldHass && oldHass.states[this.entityId] === this._stateObj) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const stateObj = this._stateObj;
|
||||
@@ -264,8 +299,14 @@ class BarMediaPlayer extends LitElement {
|
||||
}
|
||||
}
|
||||
|
||||
private get _stateObj(): MediaPlayerEntity | undefined {
|
||||
return this.hass!.states[this.entityId] as MediaPlayerEntity;
|
||||
private get _stateObj(): MediaPlayerEntity {
|
||||
if (this._browserPlayer) {
|
||||
return this._browserPlayer.toStateObj();
|
||||
}
|
||||
return (
|
||||
(this.hass!.states[this.entityId] as MediaPlayerEntity | undefined) ||
|
||||
BrowserMediaPlayer.idleStateObj()
|
||||
);
|
||||
}
|
||||
|
||||
private get _showProgressBar() {
|
||||
@@ -275,10 +316,6 @@ class BarMediaPlayer extends LitElement {
|
||||
|
||||
const stateObj = this._stateObj;
|
||||
|
||||
if (!stateObj) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return (
|
||||
(stateObj.state === "playing" || stateObj.state === "paused") &&
|
||||
"media_duration" in stateObj.attributes &&
|
||||
@@ -286,53 +323,48 @@ class BarMediaPlayer extends LitElement {
|
||||
);
|
||||
}
|
||||
|
||||
private get _image() {
|
||||
if (!this.hass) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const stateObj = this._stateObj;
|
||||
|
||||
if (!stateObj) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return (
|
||||
stateObj.attributes.entity_picture_local ||
|
||||
stateObj.attributes.entity_picture
|
||||
private get _mediaPlayerEntities() {
|
||||
return Object.values(this.hass!.states).filter(
|
||||
(entity) =>
|
||||
computeStateDomain(entity) === "media_player" &&
|
||||
supportsFeature(entity, SUPPORT_BROWSE_MEDIA)
|
||||
);
|
||||
}
|
||||
|
||||
private get _mediaPlayerEntities() {
|
||||
return Object.values(this.hass!.states).filter((entity) => {
|
||||
if (
|
||||
computeStateDomain(entity) === "media_player" &&
|
||||
supportsFeature(entity, SUPPORT_BROWSE_MEDIA)
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
});
|
||||
}
|
||||
|
||||
private _updateProgressBar(): void {
|
||||
if (this._progressBar && this._stateObj?.attributes.media_duration) {
|
||||
const currentProgress = getCurrentProgress(this._stateObj);
|
||||
this._progressBar.progress =
|
||||
currentProgress / this._stateObj!.attributes.media_duration;
|
||||
if (!this._progressBar || !this._currentProgress) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this._currentProgress) {
|
||||
this._currentProgress.innerHTML = formatMediaTime(currentProgress);
|
||||
}
|
||||
if (!this._stateObj.attributes.media_duration) {
|
||||
this._progressBar.progress = 0;
|
||||
this._currentProgress.innerHTML = "";
|
||||
return;
|
||||
}
|
||||
|
||||
const currentProgress = getCurrentProgress(this._stateObj);
|
||||
this._progressBar.progress =
|
||||
currentProgress / this._stateObj.attributes.media_duration;
|
||||
|
||||
if (this._currentProgress) {
|
||||
this._currentProgress.innerHTML = formatMediaTime(currentProgress);
|
||||
}
|
||||
}
|
||||
|
||||
private _handleClick(e: MouseEvent): void {
|
||||
const action = (e.currentTarget! as HTMLElement).getAttribute("action")!;
|
||||
this.hass!.callService("media_player", action, {
|
||||
entity_id: this.entityId,
|
||||
});
|
||||
|
||||
if (!this._browserPlayer) {
|
||||
this.hass!.callService("media_player", action, {
|
||||
entity_id: this.entityId,
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (action === "media_pause") {
|
||||
this._browserPlayer.pause();
|
||||
} else if (action === "media_play") {
|
||||
this._browserPlayer.play();
|
||||
}
|
||||
}
|
||||
|
||||
private _marqueeMouseOver(): void {
|
||||
@@ -399,6 +431,10 @@ class BarMediaPlayer extends LitElement {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.controls {
|
||||
height: 48px;
|
||||
}
|
||||
|
||||
.controls-progress {
|
||||
flex: 2;
|
||||
display: flex;
|
||||
@@ -434,12 +470,6 @@ class BarMediaPlayer extends LitElement {
|
||||
max-height: 100px;
|
||||
}
|
||||
|
||||
.blank-image {
|
||||
height: 100px;
|
||||
width: 100px;
|
||||
background-color: var(--divider-color);
|
||||
}
|
||||
|
||||
ha-button-menu mwc-button {
|
||||
line-height: 1;
|
||||
}
|
||||
@@ -485,6 +515,10 @@ class BarMediaPlayer extends LitElement {
|
||||
top: -4px;
|
||||
left: 0;
|
||||
}
|
||||
|
||||
mwc-list-item[selected] {
|
||||
font-weight: bold;
|
||||
}
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
@@ -16,6 +16,7 @@ import "../../components/ha-menu-button";
|
||||
import "../../components/media-player/ha-media-player-browse";
|
||||
import type { MediaPlayerItemId } from "../../components/media-player/ha-media-player-browse";
|
||||
import { BROWSER_PLAYER, MediaPickedEvent } from "../../data/media-player";
|
||||
import { resolveMediaSource } from "../../data/media_source";
|
||||
import "../../layouts/ha-app-layout";
|
||||
import { haStyle } from "../../resources/styles";
|
||||
import type { HomeAssistant, Route } from "../../types";
|
||||
@@ -38,7 +39,7 @@ class PanelMediaBrowser extends LitElement {
|
||||
},
|
||||
];
|
||||
|
||||
@LocalStorage("mediaBrowseEntityId")
|
||||
@LocalStorage("mediaBrowseEntityId", true, false)
|
||||
private _entityId = BROWSER_PLAYER;
|
||||
|
||||
protected render(): TemplateResult {
|
||||
@@ -131,25 +132,28 @@ class PanelMediaBrowser extends LitElement {
|
||||
ev: HASSDomEvent<MediaPickedEvent>
|
||||
): Promise<void> {
|
||||
const item = ev.detail.item;
|
||||
if (this._entityId === BROWSER_PLAYER) {
|
||||
const resolvedUrl: any = await this.hass.callWS({
|
||||
type: "media_source/resolve_media",
|
||||
if (this._entityId !== BROWSER_PLAYER) {
|
||||
this.hass!.callService("media_player", "play_media", {
|
||||
entity_id: this._entityId,
|
||||
media_content_id: item.media_content_id,
|
||||
media_content_type: item.media_content_type,
|
||||
});
|
||||
} else if (item.media_content_type.startsWith("audio/")) {
|
||||
await this.shadowRoot!.querySelector("ha-bar-media-player")!.playItem(
|
||||
item
|
||||
);
|
||||
} else {
|
||||
const resolvedUrl: any = await resolveMediaSource(
|
||||
this.hass,
|
||||
item.media_content_id
|
||||
);
|
||||
|
||||
showWebBrowserPlayMediaDialog(this, {
|
||||
sourceUrl: resolvedUrl.url,
|
||||
sourceType: resolvedUrl.mime_type,
|
||||
title: item.title,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
this.hass!.callService("media_player", "play_media", {
|
||||
entity_id: this._entityId,
|
||||
media_content_id: item.media_content_id,
|
||||
media_content_type: item.media_content_type,
|
||||
});
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
|
@@ -29,7 +29,6 @@ class StateCardMediaPlayer extends LocalizeMixin(PolymerElement) {
|
||||
.main-text {
|
||||
@apply --paper-font-common-nowrap;
|
||||
color: var(--primary-text-color);
|
||||
text-transform: capitalize;
|
||||
}
|
||||
|
||||
.main-text[take-height] {
|
||||
|
@@ -1,15 +0,0 @@
|
||||
import { Constructor } from "../types";
|
||||
import { HassBaseEl } from "./hass-base-mixin";
|
||||
|
||||
export const ExternalMixin = <T extends Constructor<HassBaseEl>>(
|
||||
superClass: T
|
||||
) =>
|
||||
class extends superClass {
|
||||
protected hassConnected() {
|
||||
super.hassConnected();
|
||||
|
||||
if (this.hass!.auth.external) {
|
||||
this.hass!.auth.external.connection = this.hass!.connection;
|
||||
}
|
||||
}
|
||||
};
|
@@ -6,7 +6,6 @@ import DisconnectToastMixin from "./disconnect-toast-mixin";
|
||||
import { hapticMixin } from "./haptic-mixin";
|
||||
import { HassBaseEl } from "./hass-base-mixin";
|
||||
import { loggingMixin } from "./logging-mixin";
|
||||
import { ExternalMixin } from "./external-mixin";
|
||||
import MoreInfoMixin from "./more-info-mixin";
|
||||
import NotificationMixin from "./notification-mixin";
|
||||
import { panelTitleMixin } from "./panel-title-mixin";
|
||||
@@ -32,5 +31,4 @@ export class HassElement extends ext(HassBaseEl, [
|
||||
hapticMixin,
|
||||
panelTitleMixin,
|
||||
loggingMixin,
|
||||
ExternalMixin,
|
||||
]) {}
|
||||
|
@@ -3,7 +3,7 @@ import { Constructor, HomeAssistant } from "../types";
|
||||
import { HassBaseEl } from "./hass-base-mixin";
|
||||
|
||||
const setTitle = (title: string | undefined) => {
|
||||
document.title = title ? `${title} - Home Assistant` : "Home Assistant";
|
||||
document.title = title ? `${title} – Home Assistant` : "Home Assistant";
|
||||
};
|
||||
|
||||
export const panelTitleMixin = <T extends Constructor<HassBaseEl>>(
|
||||
|
@@ -10,7 +10,7 @@
|
||||
"mailbox": "Mailbox",
|
||||
"shopping_list": "Shopping List",
|
||||
"developer_tools": "Developer Tools",
|
||||
"media_browser": "Media Browser",
|
||||
"media_browser": "Media",
|
||||
"profile": "Profile"
|
||||
},
|
||||
"state": {
|
||||
@@ -506,7 +506,7 @@
|
||||
"pick-media": "Pick Media",
|
||||
"no_items": "No items",
|
||||
"choose_player": "Choose Player",
|
||||
"media-player-browser": "Media Player Browser",
|
||||
"media-player-browser": "Media",
|
||||
"web-browser": "Web Browser",
|
||||
"media_player": "Media Player",
|
||||
"audio_not_supported": "Your browser does not support the audio element.",
|
||||
@@ -616,7 +616,7 @@
|
||||
"person": "[%key:ui::panel::config::person::caption%]",
|
||||
"devices": "[%key:ui::panel::config::devices::caption%]",
|
||||
"entities": "[%key:ui::panel::config::entities::caption%]",
|
||||
"energy": "[%key:ui::panel::config::energy::caption%]",
|
||||
"energy": "Energy Configuration",
|
||||
"lovelace": "[%key:ui::panel::config::lovelace::caption%]",
|
||||
"core": "[%key:ui::panel::config::core::caption%]",
|
||||
"zone": "[%key:ui::panel::config::zone::caption%]",
|
||||
@@ -626,7 +626,10 @@
|
||||
"server_control": "[%key:ui::panel::config::server_control::caption%]"
|
||||
}
|
||||
},
|
||||
"filter_placeholder": "Entity Filter"
|
||||
"filter_placeholder": "Entity Filter",
|
||||
"title": "Quick Search",
|
||||
"key_c_hint": "Press 'c' on any page to open this search bar",
|
||||
"nothing_found": "Nothing found!"
|
||||
},
|
||||
"voice_command": {
|
||||
"did_not_hear": "Home Assistant did not hear anything",
|
||||
@@ -987,10 +990,6 @@
|
||||
"settings": {
|
||||
"title": "Settings",
|
||||
"description": "Basic settings, server controls, logs and info"
|
||||
},
|
||||
"developer_tools": {
|
||||
"title": "Developer Tools",
|
||||
"description": "Tools to help create automations and scripts"
|
||||
}
|
||||
},
|
||||
"common": {
|
||||
@@ -1000,6 +999,11 @@
|
||||
"learn_more": "Learn more"
|
||||
},
|
||||
"updates": {
|
||||
"check_unavailable": {
|
||||
"title": "Unable to check for updates",
|
||||
"description": "You need to run the Home Assistant operating system to be able to check and install updates from the Home Assistant user interface."
|
||||
},
|
||||
"check_updates": "Check for updates",
|
||||
"title": "{count} {count, plural,\n one {update}\n other {updates}\n}",
|
||||
"unable_to_fetch": "Unable to load updates",
|
||||
"version_available": "Version {version_available} is available",
|
||||
@@ -3186,7 +3190,7 @@
|
||||
"help": "Help",
|
||||
"start_conversation": "Start conversation",
|
||||
"reload_resources": "Reload resources",
|
||||
"exit_edit_mode": "Exit UI edit mode",
|
||||
"exit_edit_mode": "Done",
|
||||
"close": "Close"
|
||||
},
|
||||
"reload_resources": {
|
||||
@@ -3398,7 +3402,13 @@
|
||||
},
|
||||
"entity-filter": {
|
||||
"name": "Entity Filter",
|
||||
"description": "The Entity Filter card allows you to define a list of entities that you want to track only when in a certain state."
|
||||
"description": "The Entity Filter card allows you to define a list of entities that you want to track only when in a certain state.",
|
||||
"filters": "Filters",
|
||||
"card": "Card",
|
||||
"display_states": "States to show",
|
||||
"show_empty": "Show when empty",
|
||||
"state": "state",
|
||||
"delete_state": "Delete state"
|
||||
},
|
||||
"gauge": {
|
||||
"name": "Gauge",
|
||||
|
@@ -176,7 +176,7 @@ export function formatAttributeValue(
|
||||
value: any
|
||||
): string | TemplateResult {
|
||||
if (value === null) {
|
||||
return "-";
|
||||
return "—";
|
||||
}
|
||||
|
||||
// YAML handling
|
||||
|
@@ -1,4 +1,5 @@
|
||||
import { supportsFeature } from "../common/entity/supports-feature";
|
||||
import { cleanupMediaTitle } from "../data/media-player";
|
||||
|
||||
export default class MediaPlayerEntity {
|
||||
constructor(hass, stateObj) {
|
||||
@@ -115,7 +116,7 @@ export default class MediaPlayerEntity {
|
||||
}
|
||||
|
||||
get primaryTitle() {
|
||||
return this._attr.media_title;
|
||||
return cleanupMediaTitle(this._attr.media_title);
|
||||
}
|
||||
|
||||
get secondaryTitle() {
|
||||
|
@@ -3,6 +3,7 @@ import { assert } from "chai";
|
||||
import {
|
||||
formatTime,
|
||||
formatTimeWithSeconds,
|
||||
formatTimeWeekday,
|
||||
} from "../../../src/common/datetime/format_time";
|
||||
import { NumberFormat, TimeFormat } from "../../../src/data/translation";
|
||||
|
||||
@@ -51,3 +52,26 @@ describe("formatTimeWithSeconds", () => {
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("formatTimeWeekday", () => {
|
||||
const dateObj = new Date(2017, 10, 18, 23, 12, 13, 1400);
|
||||
|
||||
it("Formats English times", () => {
|
||||
assert.strictEqual(
|
||||
formatTimeWeekday(dateObj, {
|
||||
language: "en",
|
||||
number_format: NumberFormat.language,
|
||||
time_format: TimeFormat.am_pm,
|
||||
}),
|
||||
"Wednesday 11:12 PM"
|
||||
);
|
||||
assert.strictEqual(
|
||||
formatTimeWeekday(dateObj, {
|
||||
language: "en",
|
||||
number_format: NumberFormat.language,
|
||||
time_format: TimeFormat.twenty_four,
|
||||
}),
|
||||
"Wednesday 23:12"
|
||||
);
|
||||
});
|
||||
});
|
||||
|
Reference in New Issue
Block a user