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:
Bram Kragten 2019-09-04 14:21:03 +02:00 committed by GitHub
parent b022128031
commit abb9190c98
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
43 changed files with 565 additions and 137 deletions

3
.gitignore vendored
View File

@ -25,6 +25,9 @@ dist
.vscode/*
!.vscode/extensions.json
# Cast dev settings
src/cast/dev_const.ts
# Secrets
.lokalise_token
yarn-error.log

View File

@ -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

View File

@ -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");
}

View File

@ -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

View File

@ -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
);
}
}
}
}

View File

@ -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",

View File

@ -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",

View File

@ -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,

View File

@ -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
View 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";

View File

@ -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";

View File

@ -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:
'&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a>, &copy; <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];
);
};

View File

@ -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;
}

View File

@ -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>
`;
}

View File

@ -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);

View File

@ -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",

View File

@ -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
View 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,
});
};

View File

@ -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",

View File

@ -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>

View File

@ -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;
}
}

View File

@ -49,4 +49,6 @@ export default class DeviceTrigger extends Component {
DeviceTrigger.defaultConfig = {
device_id: "",
domain: "",
entity_id: "",
};

View File

@ -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}`);

View File

@ -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

View File

@ -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";

View File

@ -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);

View 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;
}
}

View 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;
}
}

View File

@ -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 = "";
}

View File

@ -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);
}
}

View File

@ -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"

View File

@ -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;

View File

@ -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;

View File

@ -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");
};

View File

@ -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
>

View 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;
}
}

View File

@ -5,6 +5,7 @@ export interface EntityConfig {
type?: string;
name?: string;
icon?: string;
image?: string;
}
export interface DividerConfig {
type: "divider";

View File

@ -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);
}
}

View File

@ -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;

View File

@ -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);
}

View File

@ -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;
}
`;
}
}

View File

@ -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": {

View 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"),
});