mirror of
https://github.com/home-assistant/frontend.git
synced 2025-07-24 09:46:36 +00:00
20190904.0 (#3613)
* Alarm codes (#3566) * Handle alarm codes from keyboard input Closes https://github.com/home-assistant/home-assistant-polymer/issues/2602 * remove friendly_name changes * remove unnecessary TS check * Update azure-pipelines-release.yml for Azure Pipelines * Don't remove `hvac_action` from history attributes (#3570) So it can be used to plot a fill when active in the graph. * Update the map when making config changes (#3568) * Add haptic feedback to handle click (#3569) * Filter camera service entities (#3583) Closes https://github.com/home-assistant/home-assistant-polymer/issues/3582 * Notification drawer RTL support (#3580) * add exceptional icon (#3572) * Add options to badges (#3552) * Add options to badges name icon entity_picture * lint * lint * rename entityPicture to image * Align styling cast buttons (#3579) * Align styling cast buttons * Split dev constants * Ignore dev_const * Update README.md * Move lovelace background settings to theme (#3561) * Move lovelace background settings to theme While being backwards compatible * Also update cast * Don't allow overwrite of english lang (#3590) * Update hui-card-options.ts (#3591) * Fix display of no triggers text if no device is selected or device has no triggers (#3592) * Fix timing issue in external auth (#3587) * Fix timing issue in external auth * add await 0 * Show toast on successfull save (#3576) * Show toast on successfull save We need to make a list of places where this could benefit the user experience. * Helper method * Rename * handle unavailable lights (#3549) * handle unavailable lights * unavailable overlay * extract unavailable overlay * Option to display last changed in glance-card (#3584) * Option to display last changed in glance-card Closes https://github.com/home-assistant/ui-schema/issues/110 * move show_last_changed to entity-level * address review comments * Filter alerts in services (#3598) Closes https://github.com/home-assistant/home-assistant-polymer/issues/3597 * Add exceptional in weather to translations (#3599) * Add MQTT subscribe to dev tools (#3589) * Add mqtt subscribe to dev tools * Update mqtt-subscribe-card.ts * Comments * type * Wrap long attributes in more-info-default (#3601) Can likely be applied in many other places Closes https://github.com/home-assistant/home-assistant-polymer/issues/2811 * Bumped version to 20190904.0 (#3612)
This commit is contained in:
parent
b022128031
commit
abb9190c98
3
.gitignore
vendored
3
.gitignore
vendored
@ -25,6 +25,9 @@ dist
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
|
||||
# Cast dev settings
|
||||
src/cast/dev_const.ts
|
||||
|
||||
# Secrets
|
||||
.lokalise_token
|
||||
yarn-error.log
|
||||
|
@ -8,7 +8,7 @@ trigger:
|
||||
pr: none
|
||||
variables:
|
||||
- name: versionWheels
|
||||
value: '1.1-3.7-alpine3.10'
|
||||
value: '1.3-3.7-alpine3.10'
|
||||
- name: versionNode
|
||||
value: '12.1'
|
||||
- group: twine
|
||||
|
@ -214,6 +214,8 @@ gulp.task(
|
||||
const lang = subtags.slice(0, i).join("-");
|
||||
if (lang === "test") {
|
||||
src.push(workDir + "/test.json");
|
||||
} else if (lang === "en") {
|
||||
src.push("src/translations/en.json");
|
||||
} else {
|
||||
src.push(inDir + "/" + lang + ".json");
|
||||
}
|
||||
|
@ -25,7 +25,7 @@ Home Assistant Cast is made up of two separate applications:
|
||||
|
||||
### Setting dev variables
|
||||
|
||||
Open `src/cast/const.ts` and change `CAST_DEV` to `true` and `CAST_DEV_APP_ID` to the ID of the app you just created.
|
||||
Open `src/cast/dev_const.ts` and change `CAST_DEV_APP_ID` to the ID of the app you just created. And set the `CAST_DEV_HASS_URL` to the url of you development machine.
|
||||
|
||||
### Changing configuration
|
||||
|
||||
|
@ -57,10 +57,16 @@ class HcLovelace extends LitElement {
|
||||
const index = this._viewIndex;
|
||||
|
||||
if (index !== undefined) {
|
||||
this.shadowRoot!.querySelector("hui-view")!.style.background =
|
||||
const configBackground =
|
||||
this.lovelaceConfig.views[index].background ||
|
||||
this.lovelaceConfig.background ||
|
||||
"";
|
||||
this.lovelaceConfig.background;
|
||||
|
||||
if (configBackground) {
|
||||
this.shadowRoot!.querySelector("hui-view")!.style.setProperty(
|
||||
"--lovelace-background",
|
||||
configBackground
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
2
setup.py
2
setup.py
@ -2,7 +2,7 @@ from setuptools import setup, find_packages
|
||||
|
||||
setup(
|
||||
name="home-assistant-frontend",
|
||||
version="20190901.0",
|
||||
version="20190904.0",
|
||||
description="The Home Assistant frontend",
|
||||
url="https://github.com/home-assistant/home-assistant-polymer",
|
||||
author="The Home Assistant Authors",
|
||||
|
@ -281,6 +281,7 @@ class HaWeatherCard extends LocalizeMixin(EventsMixin(PolymerElement)) {
|
||||
this.weatherIcons = {
|
||||
"clear-night": "hass:weather-night",
|
||||
cloudy: "hass:weather-cloudy",
|
||||
exceptional: "hass:alert-circle-outline",
|
||||
fog: "hass:weather-fog",
|
||||
hail: "hass:weather-hail",
|
||||
lightning: "hass:weather-lightning",
|
||||
|
@ -1,5 +1,6 @@
|
||||
import { castApiAvailable } from "./cast_framework";
|
||||
import { CAST_APP_ID, CAST_NS, CAST_DEV_HASS_URL, CAST_DEV } from "./const";
|
||||
import { CAST_APP_ID, CAST_NS, CAST_DEV } from "./const";
|
||||
import { CAST_DEV_HASS_URL } from "./dev_const";
|
||||
import {
|
||||
castSendAuth,
|
||||
HassMessage as ReceiverMessage,
|
||||
|
@ -1,11 +1,7 @@
|
||||
import { CAST_DEV_APP_ID } from "./dev_const";
|
||||
|
||||
// Guard dev mode with `__dev__` so it can only ever be enabled in dev mode.
|
||||
export const CAST_DEV = __DEV__ && true;
|
||||
// Replace this with your own unpublished cast app that points at your local dev
|
||||
const CAST_DEV_APP_ID = "5FE44367";
|
||||
|
||||
export const CAST_APP_ID = CAST_DEV ? CAST_DEV_APP_ID : "B12CE3CA";
|
||||
export const CAST_NS = "urn:x-cast:com.nabucasa.hast";
|
||||
|
||||
// Chromecast SDK will only load on localhost and HTTPS
|
||||
// So during local development we have to send our dev IP address,
|
||||
// but then run the UI on localhost.
|
||||
export const CAST_DEV_HASS_URL = "http://192.168.1.234:8123";
|
||||
|
7
src/cast/dev_const.ts
Normal file
7
src/cast/dev_const.ts
Normal file
@ -0,0 +1,7 @@
|
||||
// Replace this with your own unpublished cast app that points at your local dev
|
||||
export const CAST_DEV_APP_ID = "5FE44367";
|
||||
|
||||
// Chromecast SDK will only load on localhost and HTTPS
|
||||
// So during local development we have to send our dev IP address,
|
||||
// but then run the UI on localhost.
|
||||
export const CAST_DEV_HASS_URL = "http://192.168.1.234:8123";
|
@ -4,7 +4,8 @@ import { Auth } from "home-assistant-js-websocket";
|
||||
import { CastManager } from "./cast_manager";
|
||||
|
||||
import { BaseCastMessage } from "./types";
|
||||
import { CAST_DEV_HASS_URL, CAST_DEV } from "./const";
|
||||
import { CAST_DEV } from "./const";
|
||||
import { CAST_DEV_HASS_URL } from "./dev_const";
|
||||
|
||||
export interface GetStatusMessage extends BaseCastMessage {
|
||||
type: "get_status";
|
||||
|
@ -20,10 +20,19 @@ export const setupLeafletMap = async (
|
||||
style.setAttribute("rel", "stylesheet");
|
||||
mapElement.parentNode.appendChild(style);
|
||||
map.setView([52.3731339, 4.8903147], 13);
|
||||
Leaflet.tileLayer(
|
||||
createTileLayer(Leaflet, darkMode).addTo(map);
|
||||
|
||||
return [map, Leaflet];
|
||||
};
|
||||
|
||||
export const createTileLayer = (
|
||||
leaflet: LeafletModuleType,
|
||||
darkMode: boolean
|
||||
) => {
|
||||
return leaflet.tileLayer(
|
||||
`https://{s}.basemaps.cartocdn.com/${
|
||||
darkMode ? "dark_all" : "light_all"
|
||||
}/{z}/{x}/{y}${Leaflet.Browser.retina ? "@2x.png" : ".png"}`,
|
||||
}/{z}/{x}/{y}${leaflet.Browser.retina ? "@2x.png" : ".png"}`,
|
||||
{
|
||||
attribution:
|
||||
'© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a>, © <a href="https://carto.com/attributions">CARTO</a>',
|
||||
@ -31,7 +40,5 @@ export const setupLeafletMap = async (
|
||||
minZoom: 0,
|
||||
maxZoom: 20,
|
||||
}
|
||||
).addTo(map);
|
||||
|
||||
return [map, Leaflet];
|
||||
);
|
||||
};
|
||||
|
@ -37,7 +37,10 @@ class HaDeviceTriggerPicker extends LitElement {
|
||||
@property() private _renderEmpty = false;
|
||||
|
||||
private get _key() {
|
||||
if (!this.value) {
|
||||
if (
|
||||
!this.value ||
|
||||
deviceAutomationTriggersEqual(this._noTrigger, this.value)
|
||||
) {
|
||||
return NO_TRIGGER_KEY;
|
||||
}
|
||||
|
||||
|
@ -29,6 +29,12 @@ export class HaStateLabelBadge extends LitElement {
|
||||
|
||||
@property() public state?: HassEntity;
|
||||
|
||||
@property() public name?: string;
|
||||
|
||||
@property() public icon?: string;
|
||||
|
||||
@property() public image?: string;
|
||||
|
||||
@property() private _timerTimeRemaining?: number;
|
||||
|
||||
private _connected?: boolean;
|
||||
@ -72,10 +78,14 @@ export class HaStateLabelBadge extends LitElement {
|
||||
"has-unit_of_measurement": "unit_of_measurement" in state.attributes,
|
||||
})}"
|
||||
.value="${this._computeValue(domain, state)}"
|
||||
.icon="${this._computeIcon(domain, state)}"
|
||||
.image="${state.attributes.entity_picture}"
|
||||
.icon="${this.icon ? this.icon : this._computeIcon(domain, state)}"
|
||||
.image="${this.icon
|
||||
? ""
|
||||
: this.image
|
||||
? this.image
|
||||
: state.attributes.entity_picture}"
|
||||
.label="${this._computeLabel(domain, state, this._timerTimeRemaining)}"
|
||||
.description="${computeStateName(state)}"
|
||||
.description="${this.name ? this.name : computeStateName(state)}"
|
||||
></ha-label-badge>
|
||||
`;
|
||||
}
|
||||
|
@ -11,6 +11,7 @@ class HaAttributes extends PolymerElement {
|
||||
<style>
|
||||
.data-entry .value {
|
||||
max-width: 200px;
|
||||
overflow-wrap: break-word;
|
||||
}
|
||||
.attribution {
|
||||
color: var(--secondary-text-color);
|
||||
|
@ -3,9 +3,11 @@ export const UNAVAILABLE = "unavailable";
|
||||
export const ENTITY_COMPONENT_DOMAINS = [
|
||||
"air_quality",
|
||||
"alarm_control_panel",
|
||||
"alert",
|
||||
"automation",
|
||||
"binary_sensor",
|
||||
"calendar",
|
||||
"camera",
|
||||
"counter",
|
||||
"cover",
|
||||
"dominos",
|
||||
|
@ -11,6 +11,7 @@ const LINE_ATTRIBUTES_TO_KEEP = [
|
||||
"current_temperature",
|
||||
"target_temp_low",
|
||||
"target_temp_high",
|
||||
"hvac_action",
|
||||
];
|
||||
|
||||
export interface LineChartState {
|
||||
|
19
src/data/mqtt.ts
Normal file
19
src/data/mqtt.ts
Normal file
@ -0,0 +1,19 @@
|
||||
import { HomeAssistant } from "../types";
|
||||
|
||||
export interface MQTTMessage {
|
||||
topic: string;
|
||||
payload: string;
|
||||
qos: number;
|
||||
retain: number;
|
||||
}
|
||||
|
||||
export const subscribeMQTTTopic = (
|
||||
hass: HomeAssistant,
|
||||
topic: string,
|
||||
callback: (message: MQTTMessage) => void
|
||||
) => {
|
||||
return hass.connection.subscribeMessage<MQTTMessage>(callback, {
|
||||
type: "mqtt/subscribe",
|
||||
topic,
|
||||
});
|
||||
};
|
@ -158,6 +158,7 @@ class MoreInfoWeather extends LocalizeMixin(PolymerElement) {
|
||||
this.weatherIcons = {
|
||||
"clear-night": "hass:weather-night",
|
||||
cloudy: "hass:weather-cloudy",
|
||||
exceptional: "hass:alert-circle-outline",
|
||||
fog: "hass:weather-fog",
|
||||
hail: "hass:weather-hail",
|
||||
lightning: "hass:weather-lightning",
|
||||
|
@ -49,7 +49,7 @@ export class HuiNotificationDrawer extends EventsMixin(
|
||||
text-align: center;
|
||||
}
|
||||
</style>
|
||||
<app-drawer id='drawer' opened="{{open}}" disable-swipe>
|
||||
<app-drawer id='drawer' opened="{{open}}" disable-swipe align="start">
|
||||
<app-toolbar>
|
||||
<div main-title>[[localize('ui.notification_drawer.title')]]</div>
|
||||
<ha-paper-icon-button-prev on-click="_closeDrawer"></paper-icon-button>
|
||||
|
@ -63,6 +63,15 @@ class ExternalAuth extends Auth {
|
||||
public async refreshAccessToken() {
|
||||
const callbackPayload = { callback: CALLBACK_SET_TOKEN };
|
||||
|
||||
const callbackPromise = new Promise<RefreshTokenResponse>(
|
||||
(resolve, reject) => {
|
||||
window[CALLBACK_SET_TOKEN] = (success, data) =>
|
||||
success ? resolve(data) : reject(data);
|
||||
}
|
||||
);
|
||||
|
||||
await 0;
|
||||
|
||||
if (window.externalApp) {
|
||||
window.externalApp.getExternalAuth(JSON.stringify(callbackPayload));
|
||||
} else {
|
||||
@ -71,12 +80,7 @@ class ExternalAuth extends Auth {
|
||||
);
|
||||
}
|
||||
|
||||
const tokens = await new Promise<RefreshTokenResponse>(
|
||||
(resolve, reject) => {
|
||||
window[CALLBACK_SET_TOKEN] = (success, data) =>
|
||||
success ? resolve(data) : reject(data);
|
||||
}
|
||||
);
|
||||
const tokens = await callbackPromise;
|
||||
|
||||
this.data.access_token = tokens.access_token;
|
||||
this.data.expires = tokens.expires_in * 1000 + Date.now();
|
||||
@ -85,6 +89,13 @@ class ExternalAuth extends Auth {
|
||||
public async revoke() {
|
||||
const callbackPayload = { callback: CALLBACK_REVOKE_TOKEN };
|
||||
|
||||
const callbackPromise = new Promise((resolve, reject) => {
|
||||
window[CALLBACK_REVOKE_TOKEN] = (success, data) =>
|
||||
success ? resolve(data) : reject(data);
|
||||
});
|
||||
|
||||
await 0;
|
||||
|
||||
if (window.externalApp) {
|
||||
window.externalApp.revokeExternalAuth(JSON.stringify(callbackPayload));
|
||||
} else {
|
||||
@ -93,10 +104,7 @@ class ExternalAuth extends Auth {
|
||||
);
|
||||
}
|
||||
|
||||
await new Promise((resolve, reject) => {
|
||||
window[CALLBACK_REVOKE_TOKEN] = (success, data) =>
|
||||
success ? resolve(data) : reject(data);
|
||||
});
|
||||
await callbackPromise;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -49,4 +49,6 @@ export default class DeviceTrigger extends Component {
|
||||
|
||||
DeviceTrigger.defaultConfig = {
|
||||
device_id: "",
|
||||
domain: "",
|
||||
entity_id: "",
|
||||
};
|
||||
|
@ -23,6 +23,7 @@ import {
|
||||
SYSTEM_GROUP_ID_USER,
|
||||
SYSTEM_GROUP_ID_ADMIN,
|
||||
} from "../../../data/user";
|
||||
import { showSaveSuccessToast } from "../../../util/toast-saved-success";
|
||||
|
||||
declare global {
|
||||
interface HASSDomEvents {
|
||||
@ -150,6 +151,7 @@ class HaUserEditor extends LitElement {
|
||||
await updateUser(this.hass!, this.user!.id, {
|
||||
group_ids: [newGroup],
|
||||
});
|
||||
showSaveSuccessToast(this, this.hass!);
|
||||
fireEvent(this, "reload-users");
|
||||
} catch (err) {
|
||||
alert(`Group update failed: ${err.message}`);
|
||||
|
@ -18,9 +18,13 @@ import format_time from "../../../common/datetime/format_time";
|
||||
@customElement("event-subscribe-card")
|
||||
class EventSubscribeCard extends LitElement {
|
||||
@property() public hass?: HomeAssistant;
|
||||
|
||||
@property() private _eventType = "";
|
||||
|
||||
@property() private _subscribed?: () => void;
|
||||
|
||||
@property() private _events: Array<{ id: number; event: HassEvent }> = [];
|
||||
|
||||
private _eventCount = 0;
|
||||
|
||||
public disconnectedCallback() {
|
||||
@ -33,7 +37,7 @@ class EventSubscribeCard extends LitElement {
|
||||
|
||||
protected render(): TemplateResult {
|
||||
return html`
|
||||
<ha-card heading="Listen to events">
|
||||
<ha-card header="Listen to events">
|
||||
<form>
|
||||
<paper-input
|
||||
.label=${this._subscribed
|
||||
|
@ -15,7 +15,6 @@ import "@polymer/paper-tabs/paper-tab";
|
||||
import "@polymer/paper-tabs/paper-tabs";
|
||||
|
||||
import "../../components/ha-menu-button";
|
||||
import "../../resources/ha-style";
|
||||
import "./developer-tools-router";
|
||||
|
||||
import scrollToTarget from "../../common/dom/scroll-to-target";
|
||||
|
@ -1,76 +0,0 @@
|
||||
import "@material/mwc-button";
|
||||
import "@polymer/paper-input/paper-input";
|
||||
import "@polymer/paper-input/paper-textarea";
|
||||
import { html } from "@polymer/polymer/lib/utils/html-tag";
|
||||
import { PolymerElement } from "@polymer/polymer/polymer-element";
|
||||
|
||||
import "../../../components/ha-card";
|
||||
import "../../../resources/ha-style";
|
||||
import "../../../util/app-localstorage-document";
|
||||
|
||||
class HaPanelDevMqtt extends PolymerElement {
|
||||
static get template() {
|
||||
return html`
|
||||
<style include="ha-style">
|
||||
:host {
|
||||
-ms-user-select: initial;
|
||||
-webkit-user-select: initial;
|
||||
-moz-user-select: initial;
|
||||
}
|
||||
|
||||
.content {
|
||||
padding: 24px 0 32px;
|
||||
max-width: 600px;
|
||||
margin: 0 auto;
|
||||
direction: ltr;
|
||||
}
|
||||
|
||||
mwc-button {
|
||||
background-color: white;
|
||||
}
|
||||
</style>
|
||||
|
||||
<app-localstorage-document key="panel-dev-mqtt-topic" data="{{topic}}">
|
||||
</app-localstorage-document>
|
||||
<app-localstorage-document
|
||||
key="panel-dev-mqtt-payload"
|
||||
data="{{payload}}"
|
||||
>
|
||||
</app-localstorage-document>
|
||||
|
||||
<div class="content">
|
||||
<ha-card header="Publish a packet">
|
||||
<div class="card-content">
|
||||
<paper-input label="topic" value="{{topic}}"></paper-input>
|
||||
|
||||
<paper-textarea
|
||||
always-float-label
|
||||
label="Payload (template allowed)"
|
||||
value="{{payload}}"
|
||||
></paper-textarea>
|
||||
</div>
|
||||
<div class="card-actions">
|
||||
<mwc-button on-click="_publish">Publish</mwc-button>
|
||||
</div>
|
||||
</ha-card>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
static get properties() {
|
||||
return {
|
||||
hass: Object,
|
||||
topic: String,
|
||||
payload: String,
|
||||
};
|
||||
}
|
||||
|
||||
_publish() {
|
||||
this.hass.callService("mqtt", "publish", {
|
||||
topic: this.topic,
|
||||
payload_template: this.payload,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define("developer-tools-mqtt", HaPanelDevMqtt);
|
126
src/panels/developer-tools/mqtt/developer-tools-mqtt.ts
Normal file
126
src/panels/developer-tools/mqtt/developer-tools-mqtt.ts
Normal file
@ -0,0 +1,126 @@
|
||||
import {
|
||||
LitElement,
|
||||
customElement,
|
||||
TemplateResult,
|
||||
html,
|
||||
property,
|
||||
CSSResultArray,
|
||||
css,
|
||||
} from "lit-element";
|
||||
import "@material/mwc-button";
|
||||
import "@polymer/paper-input/paper-input";
|
||||
import "@polymer/paper-input/paper-textarea";
|
||||
|
||||
import { HomeAssistant } from "../../../types";
|
||||
|
||||
import { haStyle } from "../../../resources/styles";
|
||||
import "../../../components/ha-card";
|
||||
import "./mqtt-subscribe-card";
|
||||
|
||||
@customElement("developer-tools-mqtt")
|
||||
class HaPanelDevMqtt extends LitElement {
|
||||
@property() public hass?: HomeAssistant;
|
||||
|
||||
@property() private topic = "";
|
||||
|
||||
@property() private payload = "";
|
||||
|
||||
private inited: boolean = false;
|
||||
|
||||
protected firstUpdated() {
|
||||
if (localStorage && localStorage["panel-dev-mqtt-topic"]) {
|
||||
this.topic = localStorage["panel-dev-mqtt-topic"];
|
||||
}
|
||||
if (localStorage && localStorage["panel-dev-mqtt-payload"]) {
|
||||
this.payload = localStorage["panel-dev-mqtt-payload"];
|
||||
}
|
||||
this.inited = true;
|
||||
}
|
||||
|
||||
protected render(): TemplateResult {
|
||||
return html`
|
||||
<div class="content">
|
||||
<ha-card header="Publish a packet">
|
||||
<div class="card-content">
|
||||
<paper-input
|
||||
label="topic"
|
||||
.value=${this.topic}
|
||||
@value-changed=${this._handleTopic}
|
||||
></paper-input>
|
||||
|
||||
<paper-textarea
|
||||
always-float-label
|
||||
label="Payload (template allowed)"
|
||||
.value="${this.payload}"
|
||||
@value-changed=${this._handlePayload}
|
||||
></paper-textarea>
|
||||
</div>
|
||||
<div class="card-actions">
|
||||
<mwc-button @click=${this._publish}>Publish</mwc-button>
|
||||
</div>
|
||||
</ha-card>
|
||||
|
||||
<mqtt-subscribe-card .hass=${this.hass}></mqtt-subscribe-card>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
private _handleTopic(ev: CustomEvent) {
|
||||
this.topic = ev.detail.value;
|
||||
if (localStorage && this.inited) {
|
||||
localStorage["panel-dev-mqtt-topic"] = this.topic;
|
||||
}
|
||||
}
|
||||
|
||||
private _handlePayload(ev: CustomEvent) {
|
||||
this.payload = ev.detail.value;
|
||||
if (localStorage && this.inited) {
|
||||
localStorage["panel-dev-mqtt-payload"] = this.payload;
|
||||
}
|
||||
}
|
||||
|
||||
private _publish(): void {
|
||||
if (!this.hass) {
|
||||
return;
|
||||
}
|
||||
this.hass.callService("mqtt", "publish", {
|
||||
topic: this.topic,
|
||||
payload_template: this.payload,
|
||||
});
|
||||
}
|
||||
|
||||
static get styles(): CSSResultArray {
|
||||
return [
|
||||
haStyle,
|
||||
css`
|
||||
:host {
|
||||
-ms-user-select: initial;
|
||||
-webkit-user-select: initial;
|
||||
-moz-user-select: initial;
|
||||
}
|
||||
|
||||
.content {
|
||||
padding: 24px 0 32px;
|
||||
max-width: 600px;
|
||||
margin: 0 auto;
|
||||
direction: ltr;
|
||||
}
|
||||
|
||||
mwc-button {
|
||||
background-color: white;
|
||||
}
|
||||
|
||||
mqtt-subscribe-card {
|
||||
display: block;
|
||||
margin: 16px auto;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"developer-tools-mqtt": HaPanelDevMqtt;
|
||||
}
|
||||
}
|
153
src/panels/developer-tools/mqtt/mqtt-subscribe-card.ts
Normal file
153
src/panels/developer-tools/mqtt/mqtt-subscribe-card.ts
Normal file
@ -0,0 +1,153 @@
|
||||
import {
|
||||
LitElement,
|
||||
customElement,
|
||||
TemplateResult,
|
||||
html,
|
||||
property,
|
||||
CSSResult,
|
||||
css,
|
||||
} from "lit-element";
|
||||
import "@material/mwc-button";
|
||||
import "@polymer/paper-input/paper-input";
|
||||
import { HomeAssistant } from "../../../types";
|
||||
import "../../../components/ha-card";
|
||||
import format_time from "../../../common/datetime/format_time";
|
||||
|
||||
import { subscribeMQTTTopic, MQTTMessage } from "../../../data/mqtt";
|
||||
|
||||
@customElement("mqtt-subscribe-card")
|
||||
class MqttSubscribeCard extends LitElement {
|
||||
@property() public hass?: HomeAssistant;
|
||||
|
||||
@property() private _topic = "";
|
||||
|
||||
@property() private _subscribed?: () => void;
|
||||
|
||||
@property() private _messages: Array<{
|
||||
id: number;
|
||||
message: MQTTMessage;
|
||||
payload: string;
|
||||
time: Date;
|
||||
}> = [];
|
||||
|
||||
private _messageCount = 0;
|
||||
|
||||
public disconnectedCallback() {
|
||||
super.disconnectedCallback();
|
||||
if (this._subscribed) {
|
||||
this._subscribed();
|
||||
this._subscribed = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
protected render(): TemplateResult {
|
||||
return html`
|
||||
<ha-card header="Listen to a topic">
|
||||
<form>
|
||||
<paper-input
|
||||
.label=${this._subscribed
|
||||
? "Listening to"
|
||||
: "Topic to subscribe to"}
|
||||
.disabled=${this._subscribed !== undefined}
|
||||
.value=${this._topic}
|
||||
@value-changed=${this._valueChanged}
|
||||
></paper-input>
|
||||
<mwc-button
|
||||
.disabled=${this._topic === ""}
|
||||
@click=${this._handleSubmit}
|
||||
type="submit"
|
||||
>
|
||||
${this._subscribed ? "Stop listening" : "Start listening"}
|
||||
</mwc-button>
|
||||
</form>
|
||||
<div class="events">
|
||||
${this._messages.map(
|
||||
(msg) => html`
|
||||
<div class="event">
|
||||
Message ${msg.id} received on <b>${msg.message.topic}</b> at
|
||||
${format_time(msg.time, this.hass!.language)}:
|
||||
<pre>${msg.payload}</pre>
|
||||
<div class="bottom">
|
||||
QoS: ${msg.message.qos} - Retain:
|
||||
${Boolean(msg.message.retain)}
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
)}
|
||||
</div>
|
||||
</ha-card>
|
||||
`;
|
||||
}
|
||||
|
||||
private _valueChanged(ev: CustomEvent): void {
|
||||
this._topic = ev.detail.value;
|
||||
}
|
||||
|
||||
private async _handleSubmit(): Promise<void> {
|
||||
if (this._subscribed) {
|
||||
this._subscribed();
|
||||
this._subscribed = undefined;
|
||||
} else {
|
||||
this._subscribed = await subscribeMQTTTopic(
|
||||
this.hass!,
|
||||
this._topic,
|
||||
(message) => this._handleMessage(message)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private _handleMessage(message: MQTTMessage) {
|
||||
const tail =
|
||||
this._messages.length > 30 ? this._messages.slice(0, 29) : this._messages;
|
||||
let payload: string;
|
||||
try {
|
||||
payload = JSON.stringify(JSON.parse(message.payload), null, 4);
|
||||
} catch (e) {
|
||||
payload = message.payload;
|
||||
}
|
||||
this._messages = [
|
||||
{
|
||||
payload,
|
||||
message,
|
||||
time: new Date(),
|
||||
id: this._messageCount++,
|
||||
},
|
||||
...tail,
|
||||
];
|
||||
}
|
||||
|
||||
static get styles(): CSSResult {
|
||||
return css`
|
||||
form {
|
||||
display: block;
|
||||
padding: 16px;
|
||||
}
|
||||
paper-input {
|
||||
display: inline-block;
|
||||
width: 200px;
|
||||
}
|
||||
.events {
|
||||
margin: -16px 0;
|
||||
padding: 0 16px;
|
||||
}
|
||||
.event {
|
||||
border-bottom: 1px solid var(--divider-color);
|
||||
padding-bottom: 16px;
|
||||
margin: 16px 0;
|
||||
}
|
||||
.event:last-child {
|
||||
border-bottom: 0;
|
||||
}
|
||||
.bottom {
|
||||
font-size: 80%;
|
||||
color: var(--secondary-text-color);
|
||||
}
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"mqtt-subscribe-card": MqttSubscribeCard;
|
||||
}
|
||||
}
|
@ -21,6 +21,7 @@ import {
|
||||
FORMAT_NUMBER,
|
||||
} from "../../../data/alarm_control_panel";
|
||||
import { AlarmPanelCardConfig } from "./types";
|
||||
import { PaperInputElement } from "@polymer/paper-input/paper-input";
|
||||
|
||||
const ICONS = {
|
||||
armed_away: "hass:shield-lock",
|
||||
@ -144,6 +145,7 @@ class HuiAlarmPanelCard extends LitElement implements LovelaceCard {
|
||||
? html``
|
||||
: html`
|
||||
<paper-input
|
||||
id="alarmCode"
|
||||
label="Alarm Code"
|
||||
type="password"
|
||||
.value="${this._code}"
|
||||
@ -198,11 +200,17 @@ class HuiAlarmPanelCard extends LitElement implements LovelaceCard {
|
||||
}
|
||||
|
||||
private _handleActionClick(e: MouseEvent): void {
|
||||
const input = this.shadowRoot!.querySelector(
|
||||
"#alarmCode"
|
||||
) as PaperInputElement;
|
||||
const code =
|
||||
this._code ||
|
||||
(input && input.value && input.value.length > 0 ? input.value : "");
|
||||
callAlarmAction(
|
||||
this.hass!,
|
||||
this._config!.entity,
|
||||
(e.currentTarget! as any).action,
|
||||
this._code!
|
||||
code
|
||||
);
|
||||
this._code = "";
|
||||
}
|
||||
|
@ -13,6 +13,7 @@ import { classMap } from "lit-html/directives/class-map";
|
||||
import computeStateDisplay from "../../../common/entity/compute_state_display";
|
||||
import computeStateName from "../../../common/entity/compute_state_name";
|
||||
import applyThemesOnElement from "../../../common/dom/apply_themes_on_element";
|
||||
import relativeTime from "../../../common/datetime/relative_time";
|
||||
|
||||
import "../../../components/entity/state-badge";
|
||||
import "../../../components/ha-card";
|
||||
@ -24,7 +25,7 @@ import { LovelaceCard, LovelaceCardEditor } from "../types";
|
||||
import { longPress } from "../common/directives/long-press-directive";
|
||||
import { processConfigEntities } from "../common/process-config-entities";
|
||||
import { handleClick } from "../common/handle-click";
|
||||
import { GlanceCardConfig, ConfigEntity } from "./types";
|
||||
import { GlanceCardConfig, GlanceConfigEntity } from "./types";
|
||||
|
||||
@customElement("hui-glance-card")
|
||||
export class HuiGlanceCard extends LitElement implements LovelaceCard {
|
||||
@ -41,7 +42,7 @@ export class HuiGlanceCard extends LitElement implements LovelaceCard {
|
||||
|
||||
@property() private _config?: GlanceCardConfig;
|
||||
|
||||
private _configEntities?: ConfigEntity[];
|
||||
private _configEntities?: GlanceConfigEntity[];
|
||||
|
||||
public getCardSize(): number {
|
||||
return (
|
||||
@ -52,7 +53,7 @@ export class HuiGlanceCard extends LitElement implements LovelaceCard {
|
||||
|
||||
public setConfig(config: GlanceCardConfig): void {
|
||||
this._config = { theme: "default", ...config };
|
||||
const entities = processConfigEntities<ConfigEntity>(config.entities);
|
||||
const entities = processConfigEntities<GlanceConfigEntity>(config.entities);
|
||||
|
||||
for (const entity of entities) {
|
||||
if (
|
||||
@ -207,11 +208,16 @@ export class HuiGlanceCard extends LitElement implements LovelaceCard {
|
||||
${this._config!.show_state !== false
|
||||
? html`
|
||||
<div>
|
||||
${computeStateDisplay(
|
||||
this.hass!.localize,
|
||||
stateObj,
|
||||
this.hass!.language
|
||||
)}
|
||||
${entityConf.show_last_changed
|
||||
? relativeTime(
|
||||
new Date(stateObj.last_changed),
|
||||
this.hass!.localize
|
||||
)
|
||||
: computeStateDisplay(
|
||||
this.hass!.localize,
|
||||
stateObj,
|
||||
this.hass!.language
|
||||
)}
|
||||
</div>
|
||||
`
|
||||
: ""}
|
||||
@ -220,12 +226,12 @@ export class HuiGlanceCard extends LitElement implements LovelaceCard {
|
||||
}
|
||||
|
||||
private _handleTap(ev: MouseEvent): void {
|
||||
const config = (ev.currentTarget as any).entityConf as ConfigEntity;
|
||||
const config = (ev.currentTarget as any).entityConf as GlanceConfigEntity;
|
||||
handleClick(this, this.hass!, config, false);
|
||||
}
|
||||
|
||||
private _handleHold(ev: MouseEvent): void {
|
||||
const config = (ev.currentTarget as any).entityConf as ConfigEntity;
|
||||
const config = (ev.currentTarget as any).entityConf as GlanceConfigEntity;
|
||||
handleClick(this, this.hass!, config, true);
|
||||
}
|
||||
}
|
||||
|
@ -15,6 +15,7 @@ import applyThemesOnElement from "../../../common/dom/apply_themes_on_element";
|
||||
import "../../../components/ha-card";
|
||||
import "../../../components/ha-icon";
|
||||
import "../components/hui-warning";
|
||||
import "../components/hui-unavailable";
|
||||
|
||||
import { fireEvent } from "../../../common/dom/fire_event";
|
||||
import { styleMap } from "lit-html/directives/style-map";
|
||||
@ -94,6 +95,13 @@ export class HuiLightCard extends LitElement implements LovelaceCard {
|
||||
return html`
|
||||
${this.renderStyle()}
|
||||
<ha-card>
|
||||
${stateObj.state === "unavailable"
|
||||
? html`
|
||||
<hui-unavailable
|
||||
.text="${this.hass.localize("state.default.unavailable")}"
|
||||
></hui-unavailable>
|
||||
`
|
||||
: ""}
|
||||
<paper-icon-button
|
||||
icon="hass:dots-vertical"
|
||||
class="more-info"
|
||||
|
@ -15,6 +15,7 @@ import "../../map/ha-entity-marker";
|
||||
|
||||
import {
|
||||
setupLeafletMap,
|
||||
createTileLayer,
|
||||
LeafletModuleType,
|
||||
} from "../../../common/dom/setup-leaflet-map";
|
||||
import computeStateDomain from "../../../common/entity/compute_state_domain";
|
||||
@ -194,6 +195,12 @@ class HuiMapCard extends LitElement implements LovelaceCard {
|
||||
if (changedProps.has("hass")) {
|
||||
this._drawEntities();
|
||||
}
|
||||
if (
|
||||
changedProps.has("_config") &&
|
||||
changedProps.get("_config") !== undefined
|
||||
) {
|
||||
this.updateMap(changedProps.get("_config") as MapCardConfig);
|
||||
}
|
||||
}
|
||||
|
||||
private get _mapEl(): HTMLDivElement {
|
||||
@ -210,6 +217,26 @@ class HuiMapCard extends LitElement implements LovelaceCard {
|
||||
this._fitMap();
|
||||
}
|
||||
|
||||
private updateMap(oldConfig: MapCardConfig): void {
|
||||
const map = this._leafletMap;
|
||||
const config = this._config;
|
||||
const Leaflet = this.Leaflet;
|
||||
if (!map || !config || !Leaflet) {
|
||||
return;
|
||||
}
|
||||
if (config.dark_mode !== oldConfig.dark_mode) {
|
||||
createTileLayer(Leaflet, config.dark_mode === true).addTo(map);
|
||||
}
|
||||
if (
|
||||
config.entities !== oldConfig.entities ||
|
||||
config.geo_location_sources !== oldConfig.geo_location_sources
|
||||
) {
|
||||
this._drawEntities();
|
||||
}
|
||||
map.invalidateSize();
|
||||
this._fitMap();
|
||||
}
|
||||
|
||||
private _fitMap(): void {
|
||||
if (!this._leafletMap || !this.Leaflet || !this._config || !this.hass) {
|
||||
return;
|
||||
|
@ -82,6 +82,10 @@ export interface ConfigEntity extends EntityConfig {
|
||||
hold_action?: ActionConfig;
|
||||
}
|
||||
|
||||
export interface GlanceConfigEntity extends ConfigEntity {
|
||||
show_last_changed?: boolean;
|
||||
}
|
||||
|
||||
export interface GlanceCardConfig extends LovelaceCardConfig {
|
||||
show_name?: boolean;
|
||||
show_state?: boolean;
|
||||
|
@ -3,6 +3,7 @@ import { fireEvent } from "../../../common/dom/fire_event";
|
||||
import { navigate } from "../../../common/navigate";
|
||||
import { toggleEntity } from "../../../../src/panels/lovelace/common/entity/toggle-entity";
|
||||
import { ActionConfig } from "../../../data/lovelace";
|
||||
import { forwardHaptic } from "../../../data/haptics";
|
||||
|
||||
export const handleClick = (
|
||||
node: HTMLElement,
|
||||
@ -49,10 +50,12 @@ export const handleClick = (
|
||||
break;
|
||||
case "call-service": {
|
||||
if (!actionConfig.service) {
|
||||
forwardHaptic("failure");
|
||||
return;
|
||||
}
|
||||
const [domain, service] = actionConfig.service.split(".", 2);
|
||||
hass.callService(domain, service, actionConfig.service_data);
|
||||
}
|
||||
}
|
||||
forwardHaptic("light");
|
||||
};
|
||||
|
@ -66,15 +66,16 @@ export class HuiCardOptions extends LitElement {
|
||||
<paper-icon-button
|
||||
icon="hass:dots-vertical"
|
||||
slot="dropdown-trigger"
|
||||
aria-label="More options"
|
||||
></paper-icon-button>
|
||||
<paper-listbox slot="dropdown-content">
|
||||
<paper-item @click="${this._moveCard}"
|
||||
>${this.hass!.localize(
|
||||
<paper-item @click="${this._moveCard}">
|
||||
${this.hass!.localize(
|
||||
"ui.panel.lovelace.editor.edit_card.move"
|
||||
)}</paper-item
|
||||
>
|
||||
<paper-item @click="${this._deleteCard}"
|
||||
>${this.hass!.localize(
|
||||
<paper-item @click="${this._deleteCard}">
|
||||
${this.hass!.localize(
|
||||
"ui.panel.lovelace.editor.edit_card.delete"
|
||||
)}</paper-item
|
||||
>
|
||||
|
55
src/panels/lovelace/components/hui-unavailable.ts
Normal file
55
src/panels/lovelace/components/hui-unavailable.ts
Normal file
@ -0,0 +1,55 @@
|
||||
import {
|
||||
html,
|
||||
LitElement,
|
||||
TemplateResult,
|
||||
CSSResult,
|
||||
css,
|
||||
customElement,
|
||||
property,
|
||||
} from "lit-element";
|
||||
|
||||
@customElement("hui-unavailable")
|
||||
export class HuiUnavailable extends LitElement {
|
||||
@property() public text?: string;
|
||||
|
||||
protected render(): TemplateResult | void {
|
||||
return html`
|
||||
<div class="disabled-overlay">
|
||||
<div>${this.text}</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
static get styles(): CSSResult {
|
||||
return css`
|
||||
.disabled-overlay {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-color: var(--state-icon-unavailable-color);
|
||||
opacity: 0.5;
|
||||
z-index: 50;
|
||||
}
|
||||
|
||||
.disabled-overlay div {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
font-size: 50px;
|
||||
color: var(--primary-text-color);
|
||||
transform: translate(-50%, -50%);
|
||||
-ms-transform: translate(-50%, -50%);
|
||||
}
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"hui-unavailable": HuiUnavailable;
|
||||
}
|
||||
}
|
@ -5,6 +5,7 @@ export interface EntityConfig {
|
||||
type?: string;
|
||||
name?: string;
|
||||
icon?: string;
|
||||
image?: string;
|
||||
}
|
||||
export interface DividerConfig {
|
||||
type: "divider";
|
||||
|
@ -568,7 +568,12 @@ class HUIRoot extends LitElement {
|
||||
unusedEntities.hass = this.hass!;
|
||||
}
|
||||
);
|
||||
root.style.background = this.config.background || "";
|
||||
if (this.config.background) {
|
||||
unusedEntities.style.setProperty(
|
||||
"--lovelace-background",
|
||||
this.config.background
|
||||
);
|
||||
}
|
||||
root.append(unusedEntities);
|
||||
return;
|
||||
}
|
||||
@ -597,8 +602,13 @@ class HUIRoot extends LitElement {
|
||||
}
|
||||
|
||||
view.hass = this.hass;
|
||||
root.style.background =
|
||||
viewConfig.background || this.config.background || "";
|
||||
|
||||
const configBackground = viewConfig.background || this.config.background;
|
||||
|
||||
if (configBackground) {
|
||||
view.style.setProperty("--lovelace-background", configBackground);
|
||||
}
|
||||
|
||||
root.append(view);
|
||||
}
|
||||
}
|
||||
|
@ -56,6 +56,9 @@ export class HuiUnusedEntities extends LitElement {
|
||||
private renderStyle(): TemplateResult {
|
||||
return html`
|
||||
<style>
|
||||
:host {
|
||||
background: var(--lovelace-background);
|
||||
}
|
||||
#root {
|
||||
padding: 4px;
|
||||
display: flex;
|
||||
|
@ -24,6 +24,7 @@ import { showEditCardDialog } from "./editor/card-editor/show-edit-card-dialog";
|
||||
import { HuiErrorCard } from "./cards/hui-error-card";
|
||||
|
||||
import { computeRTL } from "../../common/util/compute_rtl";
|
||||
import { processConfigEntities } from "./common/process-config-entities";
|
||||
|
||||
let editCodeLoaded = false;
|
||||
|
||||
@ -120,6 +121,7 @@ export class HUIView extends LitElement {
|
||||
padding: 4px 4px 0;
|
||||
transform: translateZ(0);
|
||||
position: relative;
|
||||
background: var(--lovelace-background);
|
||||
}
|
||||
|
||||
#badges {
|
||||
@ -262,10 +264,15 @@ export class HUIView extends LitElement {
|
||||
}
|
||||
|
||||
const elements: HUIView["_badges"] = [];
|
||||
for (const entityId of config.badges) {
|
||||
const badges = processConfigEntities(config.badges);
|
||||
for (const badge of badges) {
|
||||
const element = document.createElement("ha-state-label-badge");
|
||||
const entityId = badge.entity;
|
||||
element.hass = this.hass;
|
||||
element.state = this.hass!.states[entityId];
|
||||
element.name = badge.name;
|
||||
element.icon = badge.icon;
|
||||
element.image = badge.image;
|
||||
elements.push({ element, entityId });
|
||||
root.appendChild(element);
|
||||
}
|
||||
|
@ -7,6 +7,7 @@ import {
|
||||
css,
|
||||
CSSResult,
|
||||
} from "lit-element";
|
||||
import { classMap } from "lit-html/directives/class-map";
|
||||
|
||||
import { EntityRow, CastConfig } from "../entity-rows/types";
|
||||
import { HomeAssistant } from "../../../types";
|
||||
@ -45,6 +46,11 @@ class HuiCastRow extends LitElement implements EntityRow {
|
||||
return html``;
|
||||
}
|
||||
|
||||
const active =
|
||||
this._castManager &&
|
||||
this._castManager.status &&
|
||||
this._config.view === this._castManager.status.lovelacePath;
|
||||
|
||||
return html`
|
||||
<ha-icon .icon="${this._config.icon}"></ha-icon>
|
||||
<div class="flex">
|
||||
@ -68,8 +74,8 @@ class HuiCastRow extends LitElement implements EntityRow {
|
||||
<google-cast-launcher></google-cast-launcher>
|
||||
<mwc-button
|
||||
@click=${this._sendLovelace}
|
||||
.unelevated=${this._castManager.status &&
|
||||
this._config.view === this._castManager.status.lovelacePath}
|
||||
class=${classMap({ inactive: !Boolean(active) })}
|
||||
.unelevated=${active}
|
||||
.disabled=${!this._castManager.status}
|
||||
>
|
||||
SHOW
|
||||
@ -124,7 +130,6 @@ class HuiCastRow extends LitElement implements EntityRow {
|
||||
:host {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
overflow: visible;
|
||||
}
|
||||
ha-icon {
|
||||
padding: 8px;
|
||||
@ -143,7 +148,6 @@ class HuiCastRow extends LitElement implements EntityRow {
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
.controls {
|
||||
margin-right: -0.57em;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
@ -154,6 +158,9 @@ class HuiCastRow extends LitElement implements EntityRow {
|
||||
height: 24px;
|
||||
width: 24px;
|
||||
}
|
||||
.inactive {
|
||||
padding: 0 4px;
|
||||
}
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
@ -281,6 +281,7 @@
|
||||
"weather": {
|
||||
"clear-night": "Clear, night",
|
||||
"cloudy": "Cloudy",
|
||||
"exceptional": "Exceptional",
|
||||
"fog": "Fog",
|
||||
"hail": "Hail",
|
||||
"lightning": "Lightning",
|
||||
@ -493,7 +494,8 @@
|
||||
"common": {
|
||||
"loading": "Loading",
|
||||
"cancel": "Cancel",
|
||||
"save": "Save"
|
||||
"save": "Save",
|
||||
"successfully_saved": "Successfully saved"
|
||||
},
|
||||
"components": {
|
||||
"entity": {
|
||||
|
7
src/util/toast-saved-success.ts
Normal file
7
src/util/toast-saved-success.ts
Normal file
@ -0,0 +1,7 @@
|
||||
import { showToast } from "./toast";
|
||||
import { HomeAssistant } from "../types";
|
||||
|
||||
export const showSaveSuccessToast = (el: HTMLElement, hass: HomeAssistant) =>
|
||||
showToast(el, {
|
||||
message: hass!.localize("ui.common.successfully_saved"),
|
||||
});
|
Loading…
x
Reference in New Issue
Block a user