Merge pull request #3324 from home-assistant/dev

20190630.0
This commit is contained in:
Paulus Schoutsen 2019-06-30 15:20:33 -07:00 committed by GitHub
commit 8daeaab40b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
57 changed files with 1491 additions and 1216 deletions

View File

@ -22,6 +22,7 @@ const BUILT_IN_PANEL_ICONS = [
"mailbox", // Mailbox
"tooltip-account", // Map
"cart", // Shopping List
"hammer", // developer-tools
];
// Given an icon name, load the SVG file

View File

@ -36,6 +36,7 @@ customElements.get("paper-icon-button").prototype._keyBindings = {};
class HassioMain extends ProvideHassLitMixin(HassRouterPage) {
@property() public hass!: HomeAssistant;
@property() public panel!: HassioPanelInfo;
@property() public narrow!: boolean;
protected routerOptions: RouterOptions = {
// Hass.io has a page with tabs, so we route all non-matching routes to it.
@ -108,6 +109,7 @@ class HassioMain extends ProvideHassLitMixin(HassRouterPage) {
// As long as we have Polymer pages
(el as PolymerElement).setProperties({
hass: this.hass,
narrow: this.narrow,
supervisorInfo: this._supervisorInfo,
hostInfo: this._hostInfo,
hassInfo: this._hassInfo,
@ -115,6 +117,7 @@ class HassioMain extends ProvideHassLitMixin(HassRouterPage) {
});
} else {
el.hass = this.hass;
el.narrow = this.narrow;
el.supervisorInfo = this._supervisorInfo;
el.hostInfo = this._hostInfo;
el.hassInfo = this._hassInfo;

View File

@ -34,6 +34,7 @@ const HAS_REFRESH_BUTTON = ["store", "snapshots"];
@customElement("hassio-pages-with-tabs")
class HassioPagesWithTabs extends LitElement {
@property() public hass!: HomeAssistant;
@property() public narrow!: boolean;
@property() public route!: Route;
@property() public supervisorInfo!: HassioSupervisorInfo;
@property() public hostInfo!: HassioHostInfo;
@ -45,7 +46,11 @@ class HassioPagesWithTabs extends LitElement {
<app-header-layout has-scrolling-region>
<app-header fixed slot="header">
<app-toolbar>
<ha-menu-button hassio></ha-menu-button>
<ha-menu-button
.hass=${this.hass}
.narrow=${this.narrow}
hassio
></ha-menu-button>
<div main-title>Hass.io</div>
${HAS_REFRESH_BUTTON.includes(page)
? html`

View File

@ -2,7 +2,7 @@ from setuptools import setup, find_packages
setup(
name="home-assistant-frontend",
version="20190627.0",
version="20190630.0",
description="The Home Assistant frontend",
url="https://github.com/home-assistant/home-assistant-polymer",
author="The Home Assistant Authors",

View File

@ -102,6 +102,7 @@ export class HaStateLabelBadge extends LitElement {
switch (domain) {
case "binary_sensor":
case "device_tracker":
case "person":
case "updater":
case "sun":
case "alarm_control_panel":

View File

@ -5,34 +5,113 @@ import {
LitElement,
html,
customElement,
CSSResult,
css,
} from "lit-element";
import { fireEvent } from "../common/dom/fire_event";
import { HomeAssistant } from "../types";
import { UnsubscribeFunc } from "home-assistant-js-websocket";
import { subscribeNotifications } from "../data/persistent_notification";
import computeDomain from "../common/entity/compute_domain";
@customElement("ha-menu-button")
class HaMenuButton extends LitElement {
@property({ type: Boolean })
public hassio = false;
@property({ type: Boolean }) public hassio = false;
@property() public narrow!: boolean;
@property() public hass!: HomeAssistant;
@property() private _hasNotifications = false;
private _attachNotifOnConnect = false;
private _unsubNotifications?: UnsubscribeFunc;
public connectedCallback() {
super.connectedCallback();
if (this._attachNotifOnConnect) {
this._attachNotifOnConnect = false;
this._subscribeNotifications();
}
}
public disconnectedCallback() {
super.disconnectedCallback();
if (this._unsubNotifications) {
this._attachNotifOnConnect = true;
this._unsubNotifications();
this._unsubNotifications = undefined;
}
}
protected render(): TemplateResult | void {
const hasNotifications =
this.narrow &&
(this._hasNotifications ||
Object.keys(this.hass.states).some(
(entityId) => computeDomain(entityId) === "configurator"
));
return html`
<paper-icon-button
aria-label="Sidebar Toggle"
.icon=${this.hassio ? "hassio:menu" : "hass:menu"}
@click=${this._toggleMenu}
></paper-icon-button>
${hasNotifications
? html`
<div class="dot"></div>
`
: ""}
`;
}
// We are not going to use ShadowDOM as we're rendering a single element
// without any CSS used.
protected createRenderRoot(): Element | ShadowRoot {
return this;
protected updated(changedProps) {
super.updated(changedProps);
if (!changedProps.has("narrow")) {
return;
}
this.style.visibility = this.narrow ? "initial" : "hidden";
if (!this.narrow) {
this._hasNotifications = false;
if (this._unsubNotifications) {
this._unsubNotifications();
this._unsubNotifications = undefined;
}
return;
}
this._subscribeNotifications();
}
private _subscribeNotifications() {
this._unsubNotifications = subscribeNotifications(
this.hass.connection,
(notifications) => {
this._hasNotifications = notifications.length > 0;
}
);
}
private _toggleMenu(): void {
fireEvent(this, "hass-toggle-menu");
}
static get styles(): CSSResult {
return css`
:host {
position: relative;
}
.dot {
position: absolute;
background-color: var(--accent-color);
width: 12px;
height: 12px;
top: 8px;
right: 5px;
border-radius: 50%;
}
`;
}
}
declare global {

View File

@ -14,7 +14,7 @@ import "@polymer/paper-listbox/paper-listbox";
import "./ha-icon";
import "../components/user/ha-user-badge";
import isComponentLoaded from "../common/config/is_component_loaded";
import "../components/ha-menu-button";
import { HomeAssistant, PanelInfo } from "../types";
import { fireEvent } from "../common/dom/fire_event";
import { DEFAULT_PANEL } from "../common/const";
@ -22,48 +22,70 @@ import {
getExternalConfig,
ExternalConfig,
} from "../external_app/external_config";
import {
PersistentNotification,
subscribeNotifications,
} from "../data/persistent_notification";
import computeDomain from "../common/entity/compute_domain";
const SHOW_AFTER_SPACER = ["config", "developer-tools"];
const computeUrl = (urlPath) => `/${urlPath}`;
const computePanels = (hass: HomeAssistant) => {
const SORT_VALUE = {
map: 1,
logbook: 2,
history: 3,
"developer-tools": 9,
configuration: 10,
};
const panelSorter = (a, b) => {
const aBuiltIn = a.component_name in SORT_VALUE;
const bBuiltIn = b.component_name in SORT_VALUE;
if (aBuiltIn && bBuiltIn) {
return SORT_VALUE[a.component_name] - SORT_VALUE[b.component_name];
}
if (aBuiltIn) {
return -1;
}
if (bBuiltIn) {
return 1;
}
// both not built in, sort by title
if (a.title! < b.title!) {
return -1;
}
if (a.title! > b.title!) {
return 1;
}
return 0;
};
const computePanels = (hass: HomeAssistant): [PanelInfo[], PanelInfo[]] => {
const panels = hass.panels;
if (!panels) {
return [];
return [[], []];
}
const sortValue = {
map: 1,
logbook: 2,
history: 3,
};
const result: PanelInfo[] = Object.values(panels).filter(
(panel) => panel.title
);
const beforeSpacer: PanelInfo[] = [];
const afterSpacer: PanelInfo[] = [];
result.sort((a, b) => {
const aBuiltIn = a.component_name in sortValue;
const bBuiltIn = b.component_name in sortValue;
if (aBuiltIn && bBuiltIn) {
return sortValue[a.component_name] - sortValue[b.component_name];
Object.values(panels).forEach((panel) => {
if (!panel.title) {
return;
}
if (aBuiltIn) {
return -1;
}
if (bBuiltIn) {
return 1;
}
// both not built in, sort by title
if (a.title! < b.title!) {
return -1;
}
if (a.title! > b.title!) {
return 1;
}
return 0;
(SHOW_AFTER_SPACER.includes(panel.component_name)
? afterSpacer
: beforeSpacer
).push(panel);
});
return result;
beforeSpacer.sort(panelSorter);
afterSpacer.sort(panelSorter);
return [beforeSpacer, afterSpacer];
};
const renderPanel = (hass, panel) => html`
@ -86,12 +108,15 @@ const renderPanel = (hass, panel) => html`
* @appliesMixin LocalizeMixin
*/
class HaSidebar extends LitElement {
@property() public hass?: HomeAssistant;
@property() public hass!: HomeAssistant;
@property() public narrow!: boolean;
@property({ type: Boolean }) public alwaysExpand = false;
@property({ type: Boolean, reflect: true }) public expanded = false;
@property() public _defaultPage?: string =
localStorage.defaultPage || DEFAULT_PANEL;
@property() private _externalConfig?: ExternalConfig;
@property() private _notifications?: PersistentNotification[];
protected render() {
const hass = this.hass;
@ -100,29 +125,30 @@ class HaSidebar extends LitElement {
return html``;
}
const panels = computePanels(hass);
const configPanelIdx = panels.findIndex(
(panel) => panel.component_name === "config"
);
const configPanel =
configPanelIdx === -1 ? undefined : panels.splice(configPanelIdx, 1)[0];
const [beforeSpacer, afterSpacer] = computePanels(hass);
let notificationCount = this._notifications
? this._notifications.length
: 0;
for (const entityId in hass.states) {
if (computeDomain(entityId) === "configurator") {
notificationCount++;
}
}
return html`
${this.expanded
? html`
<app-toolbar>
<div main-title>Home Assistant</div>
</app-toolbar>
`
: html`
<div class="logo">
<img
id="logo"
src="/static/icons/favicon-192x192.png"
alt="Home Assistant logo"
/>
</div>
`}
<div class="menu">
${!this.narrow
? html`
<paper-icon-button
aria-label="Sidebar Toggle"
.icon=${hass.dockedSidebar ? "hass:menu-open" : "hass:menu"}
@click=${this._toggleSidebar}
></paper-icon-button>
`
: ""}
<span class="title">Home Assistant</span>
</div>
<paper-listbox attr-for-selected="data-panel" .selected=${hass.panelUrl}>
<a
@ -137,69 +163,11 @@ class HaSidebar extends LitElement {
</paper-icon-item>
</a>
${panels.map((panel) => renderPanel(hass, panel))}
${beforeSpacer.map((panel) => renderPanel(hass, panel))}
<div class="spacer" disabled></div>
${this.expanded && hass.user && hass.user.is_admin
? html`
<div class="divider" disabled></div>
<div class="subheader" disabled>
${hass.localize("ui.sidebar.developer_tools")}
</div>
<div class="dev-tools" disabled>
<a href="/dev-service" tabindex="-1">
<paper-icon-button
icon="hass:remote"
alt="${hass.localize("panel.dev-services")}"
title="${hass.localize("panel.dev-services")}"
></paper-icon-button>
</a>
<a href="/dev-state" tabindex="-1">
<paper-icon-button
icon="hass:code-tags"
alt="${hass.localize("panel.dev-states")}"
title="${hass.localize("panel.dev-states")}"
></paper-icon-button>
</a>
<a href="/dev-event" tabindex="-1">
<paper-icon-button
icon="hass:radio-tower"
alt="${hass.localize("panel.dev-events")}"
title="${hass.localize("panel.dev-events")}"
></paper-icon-button>
</a>
<a href="/dev-template" tabindex="-1">
<paper-icon-button
icon="hass:file-xml"
alt="${hass.localize("panel.dev-templates")}"
title="${hass.localize("panel.dev-templates")}"
></paper-icon-button>
</a>
${isComponentLoaded(hass, "mqtt")
? html`
<a href="/dev-mqtt" tabindex="-1">
<paper-icon-button
icon="hass:altimeter"
alt="${hass.localize("panel.dev-mqtt")}"
title="${hass.localize("panel.dev-mqtt")}"
></paper-icon-button>
</a>
`
: html``}
<a href="/dev-info" tabindex="-1">
<paper-icon-button
icon="hass:information-outline"
alt="${hass.localize("panel.dev-info")}"
title="${hass.localize("panel.dev-info")}"
></paper-icon-button>
</a>
</div>
<div class="divider" disabled></div>
`
: ""}
${afterSpacer.map((panel) => renderPanel(hass, panel))}
${this._externalConfig && this._externalConfig.hasSettingsScreen
? html`
<a
@ -214,58 +182,61 @@ class HaSidebar extends LitElement {
slot="item-icon"
icon="hass:cellphone-settings-variant"
></ha-icon>
<span class="item-text"
>${hass.localize(
"ui.sidebar.external_app_configuration"
)}</span
>
</paper-icon-item>
</a>
`
: ""}
${configPanel ? renderPanel(hass, configPanel) : ""}
${hass.user
? html`
<a
href="/profile"
data-panel="panel"
tabindex="-1"
aria-role="option"
aria-label=${hass.localize("panel.profile")}
>
<paper-icon-item class="profile">
<ha-user-badge
slot="item-icon"
.user=${hass.user}
></ha-user-badge>
<span class="item-text">
${hass.user.name}
${hass.localize("ui.sidebar.external_app_configuration")}
</span>
</paper-icon-item>
</a>
`
: html`
<paper-icon-item
@click=${this._handleLogOut}
class="logout"
aria-role="option"
>
<ha-icon slot="item-icon" icon="hass:exit-to-app"></ha-icon>
<span class="item-text"
>${hass.localize("ui.sidebar.log_out")}</span
>
</paper-icon-item>
`}
: ""}
<div disabled class="divider sticky-el"></div>
<paper-icon-item
class="notifications sticky-el"
aria-role="option"
@click=${this._handleShowNotificationDrawer}
>
<ha-icon slot="item-icon" icon="hass:bell"></ha-icon>
${notificationCount > 0
? html`
<span class="notification-badge" slot="item-icon">
${notificationCount}
</span>
`
: ""}
<span class="item-text">
${hass.localize("ui.notification_drawer.title")}
</span>
</paper-icon-item>
<a
class="profile sticky-el"
href="/profile"
data-panel="panel"
tabindex="-1"
aria-role="option"
aria-label=${hass.localize("panel.profile")}
>
<paper-icon-item>
<ha-user-badge slot="item-icon" .user=${hass.user}></ha-user-badge>
<span class="item-text">
${hass.user ? hass.user.name : ""}
</span>
</paper-icon-item>
</a>
</paper-listbox>
`;
}
protected shouldUpdate(changedProps: PropertyValues): boolean {
if (
changedProps.has("_externalConfig") ||
changedProps.has("expanded") ||
changedProps.has("alwaysExpand")
changedProps.has("narrow") ||
changedProps.has("alwaysExpand") ||
changedProps.has("_externalConfig") ||
changedProps.has("_notifications")
) {
return true;
}
@ -280,9 +251,9 @@ class HaSidebar extends LitElement {
return (
hass.panels !== oldHass.panels ||
hass.panelUrl !== oldHass.panelUrl ||
hass.config.components !== oldHass.config.components ||
hass.user !== oldHass.user ||
hass.localize !== oldHass.localize
hass.localize !== oldHass.localize ||
hass.states !== oldHass.states
);
}
@ -293,15 +264,15 @@ class HaSidebar extends LitElement {
this._externalConfig = conf;
});
}
this.shadowRoot!.querySelector("paper-listbox")!.addEventListener(
"mouseenter",
() => {
this.expanded = true;
}
);
this.addEventListener("mouseenter", () => {
this.expanded = true;
});
this.addEventListener("mouseleave", () => {
this._contract();
});
subscribeNotifications(this.hass.connection, (notifications) => {
this._notifications = notifications;
});
}
protected updated(changedProps) {
@ -315,17 +286,21 @@ class HaSidebar extends LitElement {
this.expanded = this.alwaysExpand || false;
}
private _handleLogOut() {
fireEvent(this, "hass-logout");
private _handleShowNotificationDrawer() {
fireEvent(this, "hass-show-notifications");
}
private _handleExternalAppConfiguration(ev: Event) {
ev.preventDefault();
this.hass!.auth.external!.fireMessage({
this.hass.auth.external!.fireMessage({
type: "config_screen/show",
});
}
private _toggleSidebar() {
fireEvent(this, "hass-toggle-menu");
}
static get styles(): CSSResult {
return css`
:host {
@ -344,31 +319,41 @@ class HaSidebar extends LitElement {
transition: width 0.2s ease-in;
will-change: width;
contain: strict;
transition-delay: 0.2s;
}
:host([expanded]) {
width: 256px;
}
.logo {
height: 65px;
box-sizing: border-box;
padding: 8px;
.menu {
height: 64px;
display: flex;
padding: 0 12px;
border-bottom: 1px solid transparent;
}
.logo img {
width: 48px;
}
app-toolbar {
white-space: nowrap;
font-weight: 400;
color: var(--primary-text-color);
border-bottom: 1px solid var(--divider-color);
background-color: var(--primary-background-color);
font-size: 20px;
align-items: center;
}
:host([expanded]) .menu {
width: 256px;
}
app-toolbar a {
color: var(--primary-text-color);
.menu paper-icon-button {
color: var(--sidebar-icon-color);
}
:host([expanded]) .menu paper-icon-button {
margin-right: 23px;
}
.title {
display: none;
}
:host([expanded]) .title {
display: initial;
}
paper-listbox {
@ -435,35 +420,69 @@ class HaSidebar extends LitElement {
color: var(--sidebar-selected-text-color);
}
a .item-text {
paper-icon-item .item-text {
display: none;
}
:host([expanded]) a .item-text {
:host([expanded]) paper-icon-item .item-text {
display: block;
}
paper-icon-item.logout {
margin-top: 16px;
.divider {
bottom: 88px;
padding: 10px 0;
}
paper-icon-item.profile {
.divider::before {
content: " ";
display: block;
height: 1px;
background-color: var(--divider-color);
}
.notifications {
margin-top: 0;
margin-bottom: 0;
bottom: 48px;
cursor: pointer;
}
.profile {
bottom: 0;
}
.profile paper-icon-item {
padding-left: 4px;
}
.profile .item-text {
margin-left: 8px;
}
.sticky-el {
position: sticky;
background-color: var(
--sidebar-background-color,
var(--primary-background-color)
);
}
.notification-badge {
position: absolute;
font-weight: 400;
bottom: 14px;
left: 26px;
border-radius: 50%;
background-color: var(--primary-color);
height: 20px;
line-height: 20px;
text-align: center;
padding: 0px 6px;
font-size: 0.65em;
color: var(--text-primary-color);
}
.spacer {
flex: 1;
pointer-events: none;
}
.divider {
height: 1px;
background-color: var(--divider-color);
margin: 4px 0;
}
.subheader {
color: var(--sidebar-text-color);
font-weight: 500;

View File

@ -0,0 +1,43 @@
import {
createCollection,
Connection,
HassEntity,
} from "home-assistant-js-websocket";
export interface PersitentNotificationEntity extends HassEntity {
notification_id?: string;
created_at?: string;
title?: string;
message?: string;
}
export interface PersistentNotification {
created_at: string;
message: string;
notification_id: string;
title: string;
status: "read" | "unread";
}
const fetchNotifications = (conn) =>
conn.sendMessagePromise({
type: "persistent_notification/get",
});
const subscribeUpdates = (conn, store) =>
conn.subscribeEvents(
() => fetchNotifications(conn).then((ntf) => store.setState(ntf, true)),
"persistent_notifications_updated"
);
export const subscribeNotifications = (
conn: Connection,
onChange: (notifications: PersistentNotification[]) => void
) =>
createCollection<PersistentNotification[]>(
"_ntf",
fetchNotifications,
subscribeUpdates,
conn,
onChange
);

View File

@ -1,24 +0,0 @@
import { createCollection, Connection } from "home-assistant-js-websocket";
const fetchNotifications = (conn) =>
conn.sendMessagePromise({
type: "persistent_notification/get",
});
const subscribeUpdates = (conn, store) =>
conn.subscribeEvents(
() => fetchNotifications(conn).then((ntf) => store.setState(ntf, true)),
"persistent_notifications_updated"
);
export const subscribeNotifications = (
conn: Connection,
onChange: (notifications: Notification[]) => void
) =>
createCollection<Notification[]>(
"_ntf",
fetchNotifications,
subscribeUpdates,
conn,
onChange
);

View File

@ -26,9 +26,20 @@ class MoreInfoCamera extends LitElement {
@property() public hass?: HomeAssistant;
@property() public stateObj?: CameraEntity;
@property() private _cameraPrefs?: CameraPreferences;
@property() private _attached = false;
public connectedCallback() {
super.connectedCallback();
this._attached = true;
}
public disconnectedCallback() {
super.disconnectedCallback();
this._attached = false;
}
protected render(): TemplateResult | void {
if (!this.hass || !this.stateObj) {
if (!this._attached || !this.hass || !this.stateObj) {
return html``;
}

View File

@ -7,17 +7,17 @@ import {
} from "lit-element";
import "@material/mwc-button";
import "./hui-notification-item-template";
import "./notification-item-template";
import { HomeAssistant } from "../../../../types";
import { fireEvent } from "../../../../common/dom/fire_event";
import { HassNotification } from "./types";
import { HomeAssistant } from "../../types";
import { fireEvent } from "../../common/dom/fire_event";
import { PersitentNotificationEntity } from "../../data/persistent_notification";
@customElement("hui-configurator-notification-item")
@customElement("configurator-notification-item")
export class HuiConfiguratorNotificationItem extends LitElement {
@property() public hass?: HomeAssistant;
@property() public notification?: HassNotification;
@property() public notification?: PersitentNotificationEntity;
protected render(): TemplateResult | void {
if (!this.hass || !this.notification) {
@ -25,7 +25,7 @@ export class HuiConfiguratorNotificationItem extends LitElement {
}
return html`
<hui-notification-item-template>
<notification-item-template>
<span slot="header">${this.hass.localize("domain.configurator")}</span>
<div>
@ -41,7 +41,7 @@ export class HuiConfiguratorNotificationItem extends LitElement {
`state.configurator.${this.notification.state}`
)}</mwc-button
>
</hui-notification-item-template>
</notification-item-template>
`;
}
@ -54,6 +54,6 @@ export class HuiConfiguratorNotificationItem extends LitElement {
declare global {
interface HTMLElementTagNameMap {
"hui-configurator-notification-item": HuiConfiguratorNotificationItem;
"configurator-notification-item": HuiConfiguratorNotificationItem;
}
}

View File

@ -5,13 +5,14 @@ import "@polymer/app-layout/app-toolbar/app-toolbar";
import { html } from "@polymer/polymer/lib/utils/html-tag";
import { PolymerElement } from "@polymer/polymer/polymer-element";
import "./hui-notification-item";
import "../../../../components/ha-paper-icon-button-next";
import { EventsMixin } from "../../../../mixins/events-mixin";
import LocalizeMixin from "../../../../mixins/localize-mixin";
import { computeRTL } from "../../../../common/util/compute_rtl";
import "./notification-item";
import "../../components/ha-paper-icon-button-next";
import { EventsMixin } from "../../mixins/events-mixin";
import LocalizeMixin from "../../mixins/localize-mixin";
import { computeRTL } from "../../common/util/compute_rtl";
import { subscribeNotifications } from "../../data/persistent_notification";
import computeDomain from "../../common/entity/compute_domain";
/*
* @appliesMixin EventsMixin
* @appliesMixin LocalizeMixin
@ -129,7 +130,7 @@ export class HuiNotificationDrawer extends EventsMixin(
<dom-repeat items="[[notifications]]">
<template>
<div class="notification">
<hui-notification-item hass="[[hass]]" notification="[[item]]"></hui-notification-item>
<notification-item hass="[[hass]]" notification="[[item]]"></notification-item>
</div>
</template>
</dom-repeat>
@ -160,6 +161,10 @@ export class HuiNotificationDrawer extends EventsMixin(
reflectToAttribute: true,
},
notifications: {
type: Array,
computed: "_computeNotifications(open, hass, _notificationsBackend)",
},
_notificationsBackend: {
type: Array,
value: [],
},
@ -171,6 +176,16 @@ export class HuiNotificationDrawer extends EventsMixin(
};
}
ready() {
super.ready();
window.addEventListener("location-changed", () => {
// close drawer when we navigate away.
if (this.open) {
this.open = false;
}
});
}
_closeDrawer(ev) {
ev.stopPropagation();
this.open = false;
@ -188,17 +203,44 @@ export class HuiNotificationDrawer extends EventsMixin(
this._openTimer = setTimeout(() => {
this.classList.add("open");
}, 50);
this._unsubNotifications = subscribeNotifications(
this.hass.connection,
(notifications) => {
this._notificationsBackend = notifications;
}
);
} else {
// Animate closed then hide
this.classList.remove("open");
this._openTimer = setTimeout(() => {
this.hidden = true;
}, 250);
if (this._unsubNotifications) {
this._unsubNotifications();
this._unsubNotifications = undefined;
}
}
}
_computeRTL(hass) {
return computeRTL(hass);
}
_computeNotifications(open, hass, notificationsBackend) {
if (!open) {
return [];
}
const configuratorEntities = Object.keys(hass.states)
.filter((entityId) => computeDomain(entityId) === "configurator")
.map((entityId) => hass.states[entityId]);
return notificationsBackend.concat(configuratorEntities);
}
showDialog({ narrow }) {
this.open = true;
this.narrow = narrow;
}
}
customElements.define("hui-notification-drawer", HuiNotificationDrawer);
customElements.define("notification-drawer", HuiNotificationDrawer);

View File

@ -7,9 +7,9 @@ import {
CSSResult,
} from "lit-element";
import "../../../../components/ha-card";
import "../../components/ha-card";
@customElement("hui-notification-item-template")
@customElement("notification-item-template")
export class HuiNotificationItemTemplate extends LitElement {
protected render(): TemplateResult | void {
return html`
@ -60,6 +60,6 @@ export class HuiNotificationItemTemplate extends LitElement {
declare global {
interface HTMLElementTagNameMap {
"hui-notification-item-template": HuiNotificationItemTemplate;
"notification-item-template": HuiNotificationItemTemplate;
}
}

View File

@ -6,18 +6,19 @@ import {
TemplateResult,
html,
} from "lit-element";
import { HassEntity } from "home-assistant-js-websocket";
import "./hui-configurator-notification-item";
import "./hui-persistent-notification-item";
import "./configurator-notification-item";
import "./persistent-notification-item";
import { HomeAssistant } from "../../../../types";
import { HassNotification } from "./types";
import { HomeAssistant } from "../../types";
import { PersistentNotification } from "../../data/persistent_notification";
@customElement("hui-notification-item")
@customElement("notification-item")
export class HuiNotificationItem extends LitElement {
@property() public hass?: HomeAssistant;
@property() public notification?: HassNotification;
@property() public notification?: HassEntity | PersistentNotification;
protected shouldUpdate(changedProps: PropertyValues): boolean {
if (!this.hass || !this.notification || changedProps.has("notification")) {
@ -32,24 +33,24 @@ export class HuiNotificationItem extends LitElement {
return html``;
}
return this.notification.entity_id
return "entity_id" in this.notification
? html`
<hui-configurator-notification-item
<configurator-notification-item
.hass="${this.hass}"
.notification="${this.notification}"
></hui-configurator-notification-item>
></configurator-notification-item>
`
: html`
<hui-persistent-notification-item
<persistent-notification-item
.hass="${this.hass}"
.notification="${this.notification}"
></hui-persistent-notification-item>
></persistent-notification-item>
`;
}
}
declare global {
interface HTMLElementTagNameMap {
"hui-notification-item": HuiNotificationItem;
"notification-item": HuiNotificationItem;
}
}

View File

@ -10,18 +10,18 @@ import {
import "@material/mwc-button";
import "@polymer/paper-tooltip/paper-tooltip";
import "../../../../components/ha-relative-time";
import "../../../../components/ha-markdown";
import "./hui-notification-item-template";
import "../../components/ha-relative-time";
import "../../components/ha-markdown";
import "./notification-item-template";
import { HomeAssistant } from "../../../../types";
import { HassNotification } from "./types";
import { HomeAssistant } from "../../types";
import { PersistentNotification } from "../../data/persistent_notification";
@customElement("hui-persistent-notification-item")
@customElement("persistent-notification-item")
export class HuiPersistentNotificationItem extends LitElement {
@property() public hass?: HomeAssistant;
@property() public notification?: HassNotification;
@property() public notification?: PersistentNotification;
protected render(): TemplateResult | void {
if (!this.hass || !this.notification) {
@ -29,8 +29,10 @@ export class HuiPersistentNotificationItem extends LitElement {
}
return html`
<hui-notification-item-template>
<span slot="header">${this._computeTitle(this.notification)}</span>
<notification-item-template>
<span slot="header">
${this.notification.title || this.notification.notification_id}
</span>
<ha-markdown content="${this.notification.message}"></ha-markdown>
@ -54,7 +56,7 @@ export class HuiPersistentNotificationItem extends LitElement {
"ui.card.persistent_notification.dismiss"
)}</mwc-button
>
</hui-notification-item-template>
</notification-item-template>
`;
}
@ -80,13 +82,9 @@ export class HuiPersistentNotificationItem extends LitElement {
});
}
private _computeTitle(notification: HassNotification): string | undefined {
return notification.title || notification.notification_id;
}
private _computeTooltip(
hass: HomeAssistant,
notification: HassNotification
notification: PersistentNotification
): string | undefined {
if (!hass || !notification) {
return undefined;
@ -105,6 +103,6 @@ export class HuiPersistentNotificationItem extends LitElement {
declare global {
interface HTMLElementTagNameMap {
"hui-persistent-notification-item": HuiPersistentNotificationItem;
"persistent-notification-item": HuiPersistentNotificationItem;
}
}

View File

@ -0,0 +1,16 @@
import { fireEvent } from "../../common/dom/fire_event";
export interface NotificationDrawerParams {
narrow: boolean;
}
export const showNotificationDrawer = (
element: HTMLElement,
dialogParams: NotificationDrawerParams
): void => {
fireEvent(element, "show-dialog", {
dialogTag: "notification-drawer" as any, // Not in TS yet
dialogImport: () => import("./notification-drawer"),
dialogParams,
});
};

View File

@ -12,17 +12,23 @@ import {
import "../components/ha-menu-button";
import "../components/ha-paper-icon-button-arrow-prev";
import { haStyle } from "../resources/styles";
import { HomeAssistant } from "../types";
@customElement("hass-loading-screen")
class HassLoadingScreen extends LitElement {
@property({ type: Boolean }) public rootnav? = false;
@property() public hass?: HomeAssistant;
@property() public narrow?: boolean;
protected render(): TemplateResult | void {
return html`
<app-toolbar>
${this.rootnav
? html`
<ha-menu-button></ha-menu-button>
<ha-menu-button
.hass=${this.hass}
.narrow=${this.narrow}
></ha-menu-button>
`
: html`
<ha-paper-icon-button-arrow-prev

View File

@ -15,25 +15,16 @@ class HassSubpage extends LitElement {
@property()
public header?: string;
@property({ type: Boolean })
public root = false;
@property({ type: Boolean })
public hassio = false;
protected render(): TemplateResult | void {
return html`
<div class="toolbar">
${this.root
? html`
<ha-menu-button .hassio=${this.hassio}></ha-menu-button>
`
: html`
<ha-paper-icon-button-arrow-prev
.hassio=${this.hassio}
@click=${this._backTapped}
></ha-paper-icon-button-arrow-prev>
`}
<ha-paper-icon-button-arrow-prev
.hassio=${this.hassio}
@click=${this._backTapped}
></ha-paper-icon-button-arrow-prev>
<div main-title>${this.header}</div>
<slot name="toolbar-icon"></slot>

View File

@ -20,6 +20,7 @@ import { fireEvent } from "../common/dom/fire_event";
import { PolymerChangedEvent } from "../polymer-types";
// tslint:disable-next-line: no-duplicate-imports
import { AppDrawerLayoutElement } from "@polymer/app-layout/app-drawer-layout/app-drawer-layout";
import { showNotificationDrawer } from "../dialogs/notifications/show-notification-drawer";
const NON_SWIPABLE_PANELS = ["kiosk", "map"];
@ -27,6 +28,7 @@ declare global {
// for fire event
interface HASSDomEvents {
"hass-toggle-menu": undefined;
"hass-show-notifications": undefined;
}
}
@ -66,6 +68,7 @@ class HomeAssistantMain extends LitElement {
>
<ha-sidebar
.hass=${hass}
.narrow=${this.narrow}
.alwaysExpand=${this.narrow || hass.dockedSidebar}
></ha-sidebar>
</app-drawer>
@ -96,6 +99,12 @@ class HomeAssistantMain extends LitElement {
setTimeout(() => this.appLayout.resetLayout());
}
});
this.addEventListener("hass-show-notifications", () => {
showNotificationDrawer(this, {
narrow: this.narrow!,
});
});
}
protected updated(changedProps: PropertyValues) {

View File

@ -9,7 +9,7 @@ import {
} from "./hass-router-page";
import { removeInitSkeleton } from "../util/init-skeleton";
const CACHE_COMPONENTS = ["lovelace", "states"];
const CACHE_COMPONENTS = ["lovelace", "states", "developer-tools"];
const COMPONENTS = {
calendar: () =>
import(/* webpackChunkName: "panel-calendar" */ "../panels/calendar/ha-panel-calendar"),
@ -17,18 +17,8 @@ const COMPONENTS = {
import(/* webpackChunkName: "panel-config" */ "../panels/config/ha-panel-config"),
custom: () =>
import(/* webpackChunkName: "panel-custom" */ "../panels/custom/ha-panel-custom"),
"dev-event": () =>
import(/* webpackChunkName: "panel-dev-event" */ "../panels/dev-event/ha-panel-dev-event"),
"dev-info": () =>
import(/* webpackChunkName: "panel-dev-info" */ "../panels/dev-info/ha-panel-dev-info"),
"dev-mqtt": () =>
import(/* webpackChunkName: "panel-dev-mqtt" */ "../panels/dev-mqtt/ha-panel-dev-mqtt"),
"dev-service": () =>
import(/* webpackChunkName: "panel-dev-service" */ "../panels/dev-service/ha-panel-dev-service"),
"dev-state": () =>
import(/* webpackChunkName: "panel-dev-state" */ "../panels/dev-state/ha-panel-dev-state"),
"dev-template": () =>
import(/* webpackChunkName: "panel-dev-template" */ "../panels/dev-template/ha-panel-dev-template"),
"developer-tools": () =>
import(/* webpackChunkName: "panel-developer-tools" */ "../panels/developer-tools/ha-panel-developer-tools"),
lovelace: () =>
import(/* webpackChunkName: "panel-lovelace" */ "../panels/lovelace/ha-panel-lovelace"),
states: () =>
@ -52,14 +42,17 @@ const COMPONENTS = {
};
const getRoutes = (panels: Panels): RouterOptions => {
const routes: { [route: string]: RouteOptions } = {};
const routes: RouterOptions["routes"] = {};
Object.values(panels).forEach((panel) => {
routes[panel.url_path] = {
load: COMPONENTS[panel.component_name],
const data: RouteOptions = {
tag: `ha-panel-${panel.component_name}`,
cache: CACHE_COMPONENTS.includes(panel.component_name),
};
if (panel.component_name in COMPONENTS) {
data.load = COMPONENTS[panel.component_name];
}
routes[panel.url_path] = data;
});
return {
@ -93,6 +86,8 @@ class PartialPanelResolver extends HassRouterPage {
protected createLoadingScreen() {
const el = super.createLoadingScreen();
el.rootnav = true;
el.hass = this.hass;
el.narrow = this.narrow;
return el;
}

View File

@ -67,7 +67,10 @@ class HaPanelCalendar extends LocalizeMixin(PolymerElement) {
<app-header-layout has-scrolling-region>
<app-header slot="header" fixed>
<app-toolbar>
<ha-menu-button></ha-menu-button>
<ha-menu-button
hass="[[hass]]"
narrow="[[narrow]]"
></ha-menu-button>
<div main-title>[[localize('panel.calendar')]]</div>
</app-toolbar>
</app-header>

View File

@ -47,7 +47,7 @@ class HaConfigDashboard extends NavigateMixin(LocalizeMixin(PolymerElement)) {
<app-header-layout has-scrolling-region="">
<app-header slot="header" fixed="">
<app-toolbar>
<ha-menu-button></ha-menu-button>
<ha-menu-button hass='[[hass]]' narrow='[[narrow]]'></ha-menu-button>
<div main-title="">[[localize('panel.config')]]</div>
</app-toolbar>
</app-header>
@ -125,6 +125,7 @@ class HaConfigDashboard extends NavigateMixin(LocalizeMixin(PolymerElement)) {
static get properties() {
return {
hass: Object,
narrow: Boolean,
isWide: Boolean,
cloudStatus: Object,
showAdvanced: Boolean,

View File

@ -9,6 +9,7 @@ import {
CoreFrontendUserData,
getOptimisticFrontendUserDataCollection,
} from "../../data/frontend";
import { PolymerElement } from "@polymer/polymer";
declare global {
// for fire event
@ -142,12 +143,29 @@ class HaPanelConfig extends HassRouterPage {
}
protected updatePageEl(el) {
el.route = this.routeTail;
el.hass = this.hass;
el.showAdvanced = !!(this._coreUserData && this._coreUserData.showAdvanced);
el.isWide = this.hass.dockedSidebar ? this._wideSidebar : this._wide;
el.narrow = this.narrow;
el.cloudStatus = this._cloudStatus;
const showAdvanced = !!(
this._coreUserData && this._coreUserData.showAdvanced
);
const isWide = this.hass.dockedSidebar ? this._wideSidebar : this._wide;
if ("setProperties" in el) {
// As long as we have Polymer panels
(el as PolymerElement).setProperties({
route: this.routeTail,
hass: this.hass,
showAdvanced,
isWide,
narrow: this.narrow,
cloudStatus: this._cloudStatus,
});
} else {
el.route = this.routeTail;
el.hass = this.hass;
el.showAdvanced = showAdvanced;
el.isWide = isWide;
el.narrow = this.narrow;
el.cloudStatus = this._cloudStatus;
}
}
private async _updateCloudStatus() {

View File

@ -1,221 +0,0 @@
import {
LitElement,
html,
PropertyDeclarations,
CSSResult,
css,
TemplateResult,
} from "lit-element";
import "@polymer/app-layout/app-header-layout/app-header-layout";
import "@polymer/app-layout/app-header/app-header";
import "@polymer/app-layout/app-toolbar/app-toolbar";
import "../../components/ha-menu-button";
import { HomeAssistant } from "../../types";
import { haStyle } from "../../resources/styles";
import "./system-log-card";
import "./error-log-card";
import "./system-health-card";
const JS_VERSION = __BUILD__;
const OPT_IN_PANEL = "states";
class HaPanelDevInfo extends LitElement {
public hass?: HomeAssistant;
static get properties(): PropertyDeclarations {
return {
hass: {},
};
}
protected render(): TemplateResult | void {
const hass = this.hass;
if (!hass) {
return html``;
}
const customUiList: Array<{ name: string; url: string; version: string }> =
(window as any).CUSTOM_UI_LIST || [];
const nonDefaultLink =
localStorage.defaultPage === OPT_IN_PANEL && OPT_IN_PANEL === "states"
? "/lovelace"
: "/states";
const nonDefaultLinkText =
localStorage.defaultPage === OPT_IN_PANEL && OPT_IN_PANEL === "states"
? "Go to the Lovelace UI"
: "Go to the states UI";
const defaultPageText = `${
localStorage.defaultPage === OPT_IN_PANEL ? "Remove" : "Set"
} ${OPT_IN_PANEL} as default page on this device`;
return html`
<app-header-layout has-scrolling-region>
<app-header slot="header" fixed>
<app-toolbar>
<ha-menu-button></ha-menu-button>
<div main-title>About</div>
</app-toolbar>
</app-header>
<div class="content">
<div class="about">
<p class="version">
<a href="https://www.home-assistant.io" target="_blank">
<img
src="/static/icons/favicon-192x192.png"
height="192"
alt="Home Assistant logo"
/>
</a>
<br />
Home Assistant<br />
${hass.config.version}
</p>
<p>
Path to configuration.yaml: ${hass.config.config_dir}
</p>
<p class="develop">
<a
href="https://www.home-assistant.io/developers/credits/"
target="_blank"
>
Developed by a bunch of awesome people.
</a>
</p>
<p>
Published under the Apache 2.0 license<br />
Source:
<a
href="https://github.com/home-assistant/home-assistant"
target="_blank"
>server</a
>
&mdash;
<a
href="https://github.com/home-assistant/home-assistant-polymer"
target="_blank"
>frontend-ui</a
>
</p>
<p>
Built using
<a href="https://www.python.org">Python 3</a>,
<a href="https://www.polymer-project.org" target="_blank"
>Polymer</a
>, Icons by
<a href="https://www.google.com/design/icons/" target="_blank"
>Google</a
>
and
<a href="https://MaterialDesignIcons.com" target="_blank"
>MaterialDesignIcons.com</a
>.
</p>
<p>
Frontend JavaScript version: ${JS_VERSION}
${customUiList.length > 0
? html`
<div>
Custom UIs:
${customUiList.map(
(item) => html`
<div>
<a href="${item.url}" target="_blank">
${item.name}</a
>: ${item.version}
</div>
`
)}
</div>
`
: ""}
</p>
<p>
<a href="${nonDefaultLink}">${nonDefaultLinkText}</a><br />
<mwc-button @click="${this._toggleDefaultPage}" raised>
${defaultPageText}
</mwc-button>
</p>
</div>
<system-health-card .hass=${this.hass}></system-health-card>
<system-log-card .hass=${this.hass}></system-log-card>
<error-log-card .hass=${this.hass}></error-log-card>
</div>
</app-header-layout>
`;
}
protected firstUpdated(changedProps): void {
super.firstUpdated(changedProps);
// Legacy custom UI can be slow to register, give them time.
const customUI = ((window as any).CUSTOM_UI_LIST || []).length;
setTimeout(() => {
if (((window as any).CUSTOM_UI_LIST || []).length !== customUI.length) {
this.requestUpdate();
}
}, 1000);
}
protected _toggleDefaultPage(): void {
if (localStorage.defaultPage === OPT_IN_PANEL) {
delete localStorage.defaultPage;
} else {
localStorage.defaultPage = OPT_IN_PANEL;
}
this.requestUpdate();
}
static get styles(): CSSResult[] {
return [
haStyle,
css`
:host {
-ms-user-select: initial;
-webkit-user-select: initial;
-moz-user-select: initial;
}
.content {
padding: 16px 0px 16px 0;
direction: ltr;
}
.about {
text-align: center;
line-height: 2em;
}
.version {
@apply --paper-font-headline;
}
.develop {
@apply --paper-font-subhead;
}
.about a {
color: var(--dark-primary-color);
}
system-health-card {
display: block;
max-width: 600px;
margin: 0 auto;
}
`,
];
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-panel-dev-info": HaPanelDevInfo;
}
}
customElements.define("ha-panel-dev-info", HaPanelDevInfo);

View File

@ -1,89 +0,0 @@
import "@polymer/app-layout/app-header-layout/app-header-layout";
import "@polymer/app-layout/app-header/app-header";
import "@polymer/app-layout/app-toolbar/app-toolbar";
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 "../../components/ha-menu-button";
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-header-layout has-scrolling-region>
<app-header slot="header" fixed>
<app-toolbar>
<ha-menu-button></ha-menu-button>
<div main-title>MQTT</div>
</app-toolbar>
</app-header>
<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>
</app-header-layout>
`;
}
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("ha-panel-dev-mqtt", HaPanelDevMqtt);

View File

@ -0,0 +1,68 @@
import { HassRouterPage, RouterOptions } from "../../layouts/hass-router-page";
import { customElement, property } from "lit-element";
import { PolymerElement } from "@polymer/polymer";
import { HomeAssistant } from "../../types";
@customElement("developer-tools-router")
class DeveloperToolsRouter extends HassRouterPage {
@property() public hass!: HomeAssistant;
@property() public narrow!: boolean;
protected routerOptions: RouterOptions = {
// defaultPage: "info",
beforeRender: (page) => {
if (!page || page === "not_found") {
// If we can, we are going to restore the last visited page.
return this._currentPage ? this._currentPage : "info";
}
return undefined;
},
cacheAll: true,
showLoading: true,
routes: {
event: {
tag: "developer-tools-event",
load: () => import("./event/developer-tools-event"),
},
info: {
tag: "developer-tools-info",
load: () => import("./info/developer-tools-info"),
},
mqtt: {
tag: "developer-tools-mqtt",
load: () => import("./mqtt/developer-tools-mqtt"),
},
service: {
tag: "developer-tools-service",
load: () => import("./service/developer-tools-service"),
},
state: {
tag: "developer-tools-state",
load: () => import("./state/developer-tools-state"),
},
template: {
tag: "developer-tools-template",
load: () => import("./template/developer-tools-template"),
},
},
};
protected updatePageEl(el) {
if ("setProperties" in el) {
// As long as we have Polymer pages
(el as PolymerElement).setProperties({
hass: this.hass,
narrow: this.narrow,
});
} else {
el.hass = this.hass;
el.narrow = this.narrow;
}
}
}
declare global {
interface HTMLElementTagNameMap {
"developer-tools-router": DeveloperToolsRouter;
}
}

View File

@ -1,6 +1,3 @@
import "@polymer/app-layout/app-header-layout/app-header-layout";
import "@polymer/app-layout/app-header/app-header";
import "@polymer/app-layout/app-toolbar/app-toolbar";
import "@polymer/iron-flex-layout/iron-flex-layout-classes";
import "@material/mwc-button";
import "@polymer/paper-input/paper-input";
@ -8,11 +5,10 @@ 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-menu-button";
import "../../resources/ha-style";
import "../../../resources/ha-style";
import "./events-list";
import "./event-subscribe-card";
import { EventsMixin } from "../../mixins/events-mixin";
import { EventsMixin } from "../../../mixins/events-mixin";
/*
* @appliesMixin EventsMixin
@ -26,12 +22,10 @@ class HaPanelDevEvent extends EventsMixin(PolymerElement) {
-ms-user-select: initial;
-webkit-user-select: initial;
-moz-user-select: initial;
}
.content {
@apply --paper-font-body1;
padding: 16px;
direction: ltr;
display: block;
}
.ha-form {
@ -49,45 +43,34 @@ class HaPanelDevEvent extends EventsMixin(PolymerElement) {
}
</style>
<app-header-layout has-scrolling-region>
<app-header slot="header" fixed>
<app-toolbar>
<ha-menu-button></ha-menu-button>
<div main-title>Events</div>
</app-toolbar>
</app-header>
<div class$="[[computeFormClasses(narrow)]]">
<div class="flex">
<p>Fire an event on the event bus.</p>
<div class="content">
<div class$="[[computeFormClasses(narrow)]]">
<div class="flex">
<p>Fire an event on the event bus.</p>
<div class="ha-form">
<paper-input
label="Event Type"
autofocus
required
value="{{eventType}}"
></paper-input>
<paper-textarea
label="Event Data (JSON, optional)"
value="{{eventData}}"
></paper-textarea>
<mwc-button on-click="fireEvent" raised>Fire Event</mwc-button>
</div>
</div>
<div>
<div class="header">Available Events</div>
<events-list
on-event-selected="eventSelected"
hass="[[hass]]"
></events-list>
</div>
<div class="ha-form">
<paper-input
label="Event Type"
autofocus
required
value="{{eventType}}"
></paper-input>
<paper-textarea
label="Event Data (JSON, optional)"
value="{{eventData}}"
></paper-textarea>
<mwc-button on-click="fireEvent" raised>Fire Event</mwc-button>
</div>
<event-subscribe-card hass="[[hass]]"></event-subscribe-card>
</div>
</app-header-layout>
<div>
<div class="header">Available Events</div>
<events-list
on-event-selected="eventSelected"
hass="[[hass]]"
></events-list>
</div>
</div>
<event-subscribe-card hass="[[hass]]"></event-subscribe-card>
`;
}
@ -139,4 +122,4 @@ class HaPanelDevEvent extends EventsMixin(PolymerElement) {
}
}
customElements.define("ha-panel-dev-event", HaPanelDevEvent);
customElements.define("developer-tools-event", HaPanelDevEvent);

View File

@ -10,10 +10,10 @@ import {
import "@material/mwc-button";
import "@polymer/paper-input/paper-input";
import { HassEvent } from "home-assistant-js-websocket";
import { HomeAssistant } from "../../types";
import { PolymerChangedEvent } from "../../polymer-types";
import "../../components/ha-card";
import format_time from "../../common/datetime/format_time";
import { HomeAssistant } from "../../../types";
import { PolymerChangedEvent } from "../../../polymer-types";
import "../../../components/ha-card";
import format_time from "../../../common/datetime/format_time";
@customElement("event-subscribe-card")
class EventSubscribeCard extends LitElement {

View File

@ -1,7 +1,7 @@
import { html } from "@polymer/polymer/lib/utils/html-tag";
import { PolymerElement } from "@polymer/polymer/polymer-element";
import { EventsMixin } from "../../mixins/events-mixin";
import { EventsMixin } from "../../../mixins/events-mixin";
/*
* @appliesMixin EventsMixin

View File

@ -0,0 +1,124 @@
import {
LitElement,
TemplateResult,
html,
CSSResultArray,
css,
customElement,
property,
} from "lit-element";
import "@polymer/app-layout/app-header-layout/app-header-layout";
import "@polymer/app-layout/app-header/app-header";
import "@polymer/app-layout/app-toolbar/app-toolbar";
import "@polymer/paper-icon-button/paper-icon-button";
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";
import { haStyle } from "../../resources/styles";
import { HomeAssistant, Route } from "../../types";
import { navigate } from "../../common/navigate";
import isComponentLoaded from "../../common/config/is_component_loaded";
@customElement("ha-panel-developer-tools")
class PanelDeveloperTools extends LitElement {
@property() public hass!: HomeAssistant;
@property() public route!: Route;
@property() public narrow!: boolean;
protected render(): TemplateResult | void {
const page = this._page;
return html`
<app-header-layout has-scrolling-region>
<app-header fixed slot="header">
<app-toolbar>
<ha-menu-button
.hass=${this.hass}
.narrow=${this.narrow}
></ha-menu-button>
<div main-title>Developer Tools</div>
</app-toolbar>
<paper-tabs
scrollable
attr-for-selected="page-name"
.selected=${page}
@iron-activate=${this.handlePageSelected}
>
<paper-tab page-name="info">
${this.hass.localize("panel.dev-info")}
</paper-tab>
<paper-tab page-name="event">
${this.hass.localize("panel.dev-events")}
</paper-tab>
${isComponentLoaded(this.hass, "mqtt")
? html`
<paper-tab page-name="mqtt">
${this.hass.localize("panel.dev-mqtt")}
</paper-tab>
`
: ""}
<paper-tab page-name="service">
${this.hass.localize("panel.dev-services")}
</paper-tab>
<paper-tab page-name="state">
${this.hass.localize("panel.dev-states")}
</paper-tab>
<paper-tab page-name="template">
${this.hass.localize("panel.dev-templates")}
</paper-tab>
</paper-tabs>
</app-header>
<developer-tools-router
.route=${this.route}
.narrow=${this.narrow}
.hass=${this.hass}
></developer-tools-router>
</app-header-layout>
`;
}
private handlePageSelected(ev) {
const newPage = ev.detail.item.getAttribute("page-name");
if (newPage !== this._page) {
navigate(this, `/developer-tools/${newPage}`);
}
scrollToTarget(
this,
// @ts-ignore
this.shadowRoot!.querySelector("app-header-layout").header.scrollTarget
);
}
private get _page() {
return this.route.path.substr(1);
}
static get styles(): CSSResultArray {
return [
haStyle,
css`
:host {
color: var(--primary-text-color);
--paper-card-header-color: var(--primary-text-color);
}
paper-tabs {
margin-left: 12px;
--paper-tabs-selection-bar-color: #fff;
text-transform: uppercase;
}
`,
];
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-panel-developer-tools": PanelDeveloperTools;
}
}

View File

@ -0,0 +1,194 @@
import {
LitElement,
html,
CSSResult,
css,
TemplateResult,
property,
} from "lit-element";
import { HomeAssistant } from "../../../types";
import { haStyle } from "../../../resources/styles";
import "./system-log-card";
import "./error-log-card";
import "./system-health-card";
const JS_VERSION = __BUILD__;
const OPT_IN_PANEL = "states";
class HaPanelDevInfo extends LitElement {
@property() public hass!: HomeAssistant;
protected render(): TemplateResult | void {
const hass = this.hass;
const customUiList: Array<{ name: string; url: string; version: string }> =
(window as any).CUSTOM_UI_LIST || [];
const nonDefaultLink =
localStorage.defaultPage === OPT_IN_PANEL && OPT_IN_PANEL === "states"
? "/lovelace"
: "/states";
const nonDefaultLinkText =
localStorage.defaultPage === OPT_IN_PANEL && OPT_IN_PANEL === "states"
? "Go to the Lovelace UI"
: "Go to the states UI";
const defaultPageText = `${
localStorage.defaultPage === OPT_IN_PANEL ? "Remove" : "Set"
} ${OPT_IN_PANEL} as default page on this device`;
return html`
<div class="about">
<p class="version">
<a href="https://www.home-assistant.io" target="_blank"
><img
src="/static/icons/favicon-192x192.png"
height="192"
alt="Home Assistant logo"
/></a>
<br />
Home Assistant<br />
${hass.config.version}
</p>
<p>
Path to configuration.yaml: ${hass.config.config_dir}
</p>
<p class="develop">
<a
href="https://www.home-assistant.io/developers/credits/"
target="_blank"
>
Developed by a bunch of awesome people.
</a>
</p>
<p>
Published under the Apache 2.0 license<br />
Source:
<a
href="https://github.com/home-assistant/home-assistant"
target="_blank"
>server</a
>
&mdash;
<a
href="https://github.com/home-assistant/home-assistant-polymer"
target="_blank"
>frontend-ui</a
>
</p>
<p>
Built using
<a href="https://www.python.org">Python 3</a>,
<a href="https://www.polymer-project.org" target="_blank">Polymer</a>,
Icons by
<a href="https://www.google.com/design/icons/" target="_blank"
>Google</a
>
and
<a href="https://MaterialDesignIcons.com" target="_blank"
>MaterialDesignIcons.com</a
>.
</p>
<p>
Frontend JavaScript version: ${JS_VERSION}
${customUiList.length > 0
? html`
<div>
Custom UIs:
${customUiList.map(
(item) => html`
<div>
<a href="${item.url}" target="_blank"> ${item.name}</a>:
${item.version}
</div>
`
)}
</div>
`
: ""}
</p>
<p>
<a href="${nonDefaultLink}">${nonDefaultLinkText}</a><br />
<mwc-button @click="${this._toggleDefaultPage}" raised>
${defaultPageText}
</mwc-button>
</p>
</div>
<system-health-card .hass=${this.hass}></system-health-card>
<system-log-card .hass=${this.hass}></system-log-card>
<error-log-card .hass=${this.hass}></error-log-card>
`;
}
protected firstUpdated(changedProps): void {
super.firstUpdated(changedProps);
// Legacy custom UI can be slow to register, give them time.
const customUI = ((window as any).CUSTOM_UI_LIST || []).length;
setTimeout(() => {
if (((window as any).CUSTOM_UI_LIST || []).length !== customUI.length) {
this.requestUpdate();
}
}, 1000);
}
protected _toggleDefaultPage(): void {
if (localStorage.defaultPage === OPT_IN_PANEL) {
delete localStorage.defaultPage;
} else {
localStorage.defaultPage = OPT_IN_PANEL;
}
this.requestUpdate();
}
static get styles(): CSSResult[] {
return [
haStyle,
css`
:host {
-ms-user-select: initial;
-webkit-user-select: initial;
-moz-user-select: initial;
}
.content {
padding: 16px 0px 16px 0;
direction: ltr;
}
.about {
text-align: center;
line-height: 2em;
}
.version {
@apply --paper-font-headline;
}
.develop {
@apply --paper-font-subhead;
}
.about a {
color: var(--dark-primary-color);
}
system-health-card {
display: block;
max-width: 600px;
margin: 0 auto;
}
`,
];
}
}
declare global {
interface HTMLElementTagNameMap {
"developer-tools-info": HaPanelDevInfo;
}
}
customElements.define("developer-tools-info", HaPanelDevInfo);

View File

@ -8,11 +8,11 @@ import {
} from "lit-element";
import "@polymer/paper-dialog-scrollable/paper-dialog-scrollable";
import "../../components/dialog/ha-paper-dialog";
import "../../../components/dialog/ha-paper-dialog";
import { SystemLogDetailDialogParams } from "./show-dialog-system-log-detail";
import { PolymerChangedEvent } from "../../polymer-types";
import { haStyleDialog } from "../../resources/styles";
import { PolymerChangedEvent } from "../../../polymer-types";
import { haStyleDialog } from "../../../resources/styles";
class DialogSystemLogDetail extends LitElement {
private _params?: SystemLogDetailDialogParams;

View File

@ -9,8 +9,8 @@ import {
import "@polymer/paper-icon-button/paper-icon-button";
import "@material/mwc-button";
import { HomeAssistant } from "../../types";
import { fetchErrorLog } from "../../data/error_log";
import { HomeAssistant } from "../../../types";
import { fetchErrorLog } from "../../../data/error_log";
class ErrorLogCard extends LitElement {
public hass?: HomeAssistant;

View File

@ -1,5 +1,5 @@
import { fireEvent } from "../../common/dom/fire_event";
import { LoggedError } from "../../data/system_log";
import { fireEvent } from "../../../common/dom/fire_event";
import { LoggedError } from "../../../data/system_log";
declare global {
// for fire event

View File

@ -7,13 +7,13 @@ import {
TemplateResult,
} from "lit-element";
import "@polymer/paper-spinner/paper-spinner";
import "../../components/ha-card";
import "../../../components/ha-card";
import { HomeAssistant } from "../../types";
import { HomeAssistant } from "../../../types";
import {
SystemHealthInfo,
fetchSystemHealthInfo,
} from "../../data/system_health";
} from "../../../data/system_health";
const sortKeys = (a: string, b: string) => {
if (a === "homeassistant") {

View File

@ -10,13 +10,13 @@ import "@polymer/paper-icon-button/paper-icon-button";
import "@polymer/paper-item/paper-item-body";
import "@polymer/paper-item/paper-item";
import "@polymer/paper-spinner/paper-spinner";
import "../../components/ha-card";
import "../../components/buttons/ha-call-service-button";
import "../../components/buttons/ha-progress-button";
import { HomeAssistant } from "../../types";
import { LoggedError, fetchSystemLog } from "../../data/system_log";
import formatDateTime from "../../common/datetime/format_date_time";
import formatTime from "../../common/datetime/format_time";
import "../../../components/ha-card";
import "../../../components/buttons/ha-call-service-button";
import "../../../components/buttons/ha-progress-button";
import { HomeAssistant } from "../../../types";
import { LoggedError, fetchSystemLog } from "../../../data/system_log";
import formatDateTime from "../../../common/datetime/format_date_time";
import formatTime from "../../../common/datetime/format_time";
import { showSystemLogDetailDialog } from "./show-dialog-system-log-detail";
const formatLogTime = (date, language: string) => {

View File

@ -0,0 +1,74 @@
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>
<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>
`;
}
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

@ -1,17 +1,13 @@
import "@polymer/app-layout/app-header-layout/app-header-layout";
import "@polymer/app-layout/app-header/app-header";
import "@polymer/app-layout/app-toolbar/app-toolbar";
import "@material/mwc-button";
import "@polymer/paper-input/paper-textarea";
import { html } from "@polymer/polymer/lib/utils/html-tag";
import { PolymerElement } from "@polymer/polymer/polymer-element";
import { ENTITY_COMPONENT_DOMAINS } from "../../data/entity";
import "../../components/entity/ha-entity-picker";
import "../../components/ha-menu-button";
import "../../components/ha-service-picker";
import "../../resources/ha-style";
import "../../util/app-localstorage-document";
import { ENTITY_COMPONENT_DOMAINS } from "../../../data/entity";
import "../../../components/entity/ha-entity-picker";
import "../../../components/ha-service-picker";
import "../../../resources/ha-style";
import "../../../util/app-localstorage-document";
const ERROR_SENTINEL = {};
class HaPanelDevService extends PolymerElement {
@ -22,9 +18,7 @@ class HaPanelDevService extends PolymerElement {
-ms-user-select: initial;
-webkit-user-select: initial;
-moz-user-select: initial;
}
.content {
display: block;
padding: 16px;
direction: ltr;
}
@ -81,100 +75,87 @@ class HaPanelDevService extends PolymerElement {
}
</style>
<app-header-layout has-scrolling-region>
<app-header slot="header" fixed>
<app-toolbar>
<ha-menu-button></ha-menu-button>
<div main-title>Services</div>
</app-toolbar>
</app-header>
<app-localstorage-document
key="panel-dev-service-state-domain-service"
data="{{domainService}}"
>
</app-localstorage-document>
<app-localstorage-document
key="[[_computeServicedataKey(domainService)]]"
data="{{serviceData}}"
>
</app-localstorage-document>
<app-localstorage-document
key="panel-dev-service-state-domain-service"
data="{{domainService}}"
>
</app-localstorage-document>
<app-localstorage-document
key="[[_computeServicedataKey(domainService)]]"
data="{{serviceData}}"
>
</app-localstorage-document>
<div class="content">
<p>
The service dev tool allows you to call any available service in Home
Assistant.
</p>
<div class="content">
<p>
The service dev tool allows you to call any available service in
Home Assistant.
</p>
<div class="ha-form">
<ha-service-picker
<div class="ha-form">
<ha-service-picker
hass="[[hass]]"
value="{{domainService}}"
></ha-service-picker>
<template is="dom-if" if="[[_computeHasEntity(_attributes)]]">
<ha-entity-picker
hass="[[hass]]"
value="{{domainService}}"
></ha-service-picker>
<template is="dom-if" if="[[_computeHasEntity(_attributes)]]">
<ha-entity-picker
hass="[[hass]]"
value="[[_computeEntityValue(parsedJSON)]]"
on-change="_entityPicked"
disabled="[[!validJSON]]"
domain-filter="[[_computeEntityDomainFilter(_domain)]]"
allow-custom-entity
></ha-entity-picker>
</template>
<paper-textarea
always-float-label
label="Service Data (JSON, optional)"
value="{{serviceData}}"
autocapitalize="none"
autocomplete="off"
spellcheck="false"
></paper-textarea>
<mwc-button on-click="_callService" raised disabled="[[!validJSON]]"
>Call Service</mwc-button
>
<template is="dom-if" if="[[!validJSON]]">
<span class="error">Invalid JSON</span>
</template>
</div>
<template is="dom-if" if="[[!domainService]]">
<h1>Select a service to see the description</h1>
value="[[_computeEntityValue(parsedJSON)]]"
on-change="_entityPicked"
disabled="[[!validJSON]]"
domain-filter="[[_computeEntityDomainFilter(_domain)]]"
allow-custom-entity
></ha-entity-picker>
</template>
<template is="dom-if" if="[[domainService]]">
<template is="dom-if" if="[[!_description]]">
<h1>No description is available</h1>
</template>
<template is="dom-if" if="[[_description]]">
<h3>[[_description]]</h3>
<table class="attributes">
<tr>
<th>Parameter</th>
<th>Description</th>
<th>Example</th>
</tr>
<template is="dom-if" if="[[!_attributes.length]]">
<tr>
<td colspan="3">This service takes no parameters.</td>
</tr>
</template>
<template
is="dom-repeat"
items="[[_attributes]]"
as="attribute"
>
<tr>
<td><pre>[[attribute.key]]</pre></td>
<td>[[attribute.description]]</td>
<td>[[attribute.example]]</td>
</tr>
</template>
</table>
</template>
<paper-textarea
always-float-label
label="Service Data (JSON, optional)"
value="{{serviceData}}"
autocapitalize="none"
autocomplete="off"
spellcheck="false"
></paper-textarea>
<mwc-button on-click="_callService" raised disabled="[[!validJSON]]"
>Call Service</mwc-button
>
<template is="dom-if" if="[[!validJSON]]">
<span class="error">Invalid JSON</span>
</template>
</div>
</app-header-layout>
<template is="dom-if" if="[[!domainService]]">
<h1>Select a service to see the description</h1>
</template>
<template is="dom-if" if="[[domainService]]">
<template is="dom-if" if="[[!_description]]">
<h1>No description is available</h1>
</template>
<template is="dom-if" if="[[_description]]">
<h3>[[_description]]</h3>
<table class="attributes">
<tr>
<th>Parameter</th>
<th>Description</th>
<th>Example</th>
</tr>
<template is="dom-if" if="[[!_attributes.length]]">
<tr>
<td colspan="3">This service takes no parameters.</td>
</tr>
</template>
<template is="dom-repeat" items="[[_attributes]]" as="attribute">
<tr>
<td><pre>[[attribute.key]]</pre></td>
<td>[[attribute.description]]</td>
<td>[[attribute.example]]</td>
</tr>
</template>
</table>
</template>
</template>
</div>
`;
}
@ -304,4 +285,4 @@ class HaPanelDevService extends PolymerElement {
}
}
customElements.define("ha-panel-dev-service", HaPanelDevService);
customElements.define("developer-tools-service", HaPanelDevService);

View File

@ -1,6 +1,3 @@
import "@polymer/app-layout/app-header-layout/app-header-layout";
import "@polymer/app-layout/app-header/app-header";
import "@polymer/app-layout/app-toolbar/app-toolbar";
import "@material/mwc-button";
import "@polymer/paper-checkbox/paper-checkbox";
import "@polymer/paper-input/paper-input";
@ -8,10 +5,9 @@ import "@polymer/paper-input/paper-textarea";
import { html } from "@polymer/polymer/lib/utils/html-tag";
import { PolymerElement } from "@polymer/polymer/polymer-element";
import "../../components/entity/ha-entity-picker";
import "../../components/ha-menu-button";
import "../../resources/ha-style";
import { EventsMixin } from "../../mixins/events-mixin";
import "../../../components/entity/ha-entity-picker";
import "../../../resources/ha-style";
import { EventsMixin } from "../../../mixins/events-mixin";
/*
* @appliesMixin EventsMixin
@ -24,9 +20,7 @@ class HaPanelDevState extends EventsMixin(PolymerElement) {
-ms-user-select: initial;
-webkit-user-select: initial;
-moz-user-select: initial;
}
.content {
display: block;
padding: 16px;
direction: ltr;
}
@ -70,107 +64,96 @@ class HaPanelDevState extends EventsMixin(PolymerElement) {
}
</style>
<app-header-layout has-scrolling-region>
<app-header slot="header" fixed>
<app-toolbar>
<ha-menu-button></ha-menu-button>
<div main-title>States</div>
</app-toolbar>
</app-header>
<div>
<p>
Set the representation of a device within Home Assistant.<br />
This will not communicate with the actual device.
</p>
<div class="content">
<div>
<p>
Set the representation of a device within Home Assistant.<br />
This will not communicate with the actual device.
</p>
<ha-entity-picker
autofocus
hass="[[hass]]"
value="{{_entityId}}"
allow-custom-entity
></ha-entity-picker>
<paper-input
label="State"
required
autocapitalize="none"
autocomplete="off"
autocorrect="off"
spellcheck="false"
value="{{_state}}"
class="state-input"
></paper-input>
<paper-textarea
label="State attributes (JSON, optional)"
autocapitalize="none"
autocomplete="off"
spellcheck="false"
value="{{_stateAttributes}}"
></paper-textarea>
<mwc-button on-click="handleSetState" raised>Set State</mwc-button>
</div>
<ha-entity-picker
autofocus
hass="[[hass]]"
value="{{_entityId}}"
allow-custom-entity
></ha-entity-picker>
<h1>Current entities</h1>
<table class="entities">
<tr>
<th>Entity</th>
<th>State</th>
<th hidden$="[[narrow]]">
Attributes
<paper-checkbox checked="{{_showAttributes}}"></paper-checkbox>
</th>
</tr>
<tr>
<th>
<paper-input
label="State"
required
autocapitalize="none"
autocomplete="off"
autocorrect="off"
spellcheck="false"
value="{{_state}}"
class="state-input"
label="Filter entities"
type="search"
value="{{_entityFilter}}"
></paper-input>
<paper-textarea
label="State attributes (JSON, optional)"
autocapitalize="none"
autocomplete="off"
spellcheck="false"
value="{{_stateAttributes}}"
></paper-textarea>
<mwc-button on-click="handleSetState" raised>Set State</mwc-button>
</div>
<h1>Current entities</h1>
<table class="entities">
<tr>
<th>Entity</th>
<th>State</th>
<th hidden$="[[narrow]]">
Attributes
<paper-checkbox checked="{{_showAttributes}}"></paper-checkbox>
</th>
</tr>
<tr>
<th>
<paper-input
label="Filter entities"
type="search"
value="{{_entityFilter}}"
></paper-input>
</th>
<th>
<paper-input
label="Filter states"
type="search"
value="{{_stateFilter}}"
></paper-input>
</th>
<th hidden$="[[!computeShowAttributes(narrow, _showAttributes)]]">
<paper-input
label="Filter attributes"
type="search"
value="{{_attributeFilter}}"
></paper-input>
</th>
</tr>
<tr hidden$="[[!computeShowEntitiesPlaceholder(_entities)]]">
<td colspan="3">No entities</td>
</tr>
<template is="dom-repeat" items="[[_entities]]" as="entity">
<tr>
<td>
<paper-icon-button
on-click="entityMoreInfo"
icon="hass:open-in-new"
alt="More Info"
title="More Info"
>
</paper-icon-button>
<a href="#" on-click="entitySelected">[[entity.entity_id]]</a>
</td>
<td>[[entity.state]]</td>
<template
is="dom-if"
if="[[computeShowAttributes(narrow, _showAttributes)]]"
>
<td>[[attributeString(entity)]]</td>
</template>
</tr>
</th>
<th>
<paper-input
label="Filter states"
type="search"
value="{{_stateFilter}}"
></paper-input>
</th>
<th hidden$="[[!computeShowAttributes(narrow, _showAttributes)]]">
<paper-input
label="Filter attributes"
type="search"
value="{{_attributeFilter}}"
></paper-input>
</th>
</tr>
<tr hidden$="[[!computeShowEntitiesPlaceholder(_entities)]]">
<td colspan="3">No entities</td>
</tr>
<template is="dom-repeat" items="[[_entities]]" as="entity">
<tr>
<td>
<paper-icon-button
on-click="entityMoreInfo"
icon="hass:open-in-new"
alt="More Info"
title="More Info"
>
</paper-icon-button>
<a href="#" on-click="entitySelected">[[entity.entity_id]]</a>
</td>
<td>[[entity.state]]</td>
<template
is="dom-if"
if="[[computeShowAttributes(narrow, _showAttributes)]]"
>
<td>[[attributeString(entity)]]</td>
</template>
</table>
</div>
</app-header-layout>
</tr>
</template>
</table>
`;
}
@ -351,4 +334,4 @@ class HaPanelDevState extends EventsMixin(PolymerElement) {
}
}
customElements.define("ha-panel-dev-state", HaPanelDevState);
customElements.define("developer-tools-state", HaPanelDevState);

View File

@ -1,6 +1,3 @@
import "@polymer/app-layout/app-header-layout/app-header-layout";
import "@polymer/app-layout/app-header/app-header";
import "@polymer/app-layout/app-toolbar/app-toolbar";
import "@polymer/paper-input/paper-textarea";
import "@polymer/paper-spinner/paper-spinner";
import { timeOut } from "@polymer/polymer/lib/utils/async";
@ -8,8 +5,7 @@ import { Debouncer } from "@polymer/polymer/lib/utils/debounce";
import { html } from "@polymer/polymer/lib/utils/html-tag";
import { PolymerElement } from "@polymer/polymer/polymer-element";
import "../../components/ha-menu-button";
import "../../resources/ha-style";
import "../../../resources/ha-style";
class HaPanelDevTemplate extends PolymerElement {
static get template() {
@ -67,52 +63,43 @@ class HaPanelDevTemplate extends PolymerElement {
}
</style>
<app-header-layout has-scrolling-region>
<app-header slot="header" fixed>
<app-toolbar>
<ha-menu-button></ha-menu-button>
<div main-title>Templates</div>
</app-toolbar>
</app-header>
<div class$="[[computeFormClasses(narrow)]]">
<div class="edit-pane">
<p>
Templates are rendered using the Jinja2 template engine with some
Home Assistant specific extensions.
</p>
<ul>
<li>
<a
href="http://jinja.pocoo.org/docs/dev/templates/"
target="_blank"
>Jinja2 template documentation</a
>
</li>
<li>
<a
href="https://home-assistant.io/docs/configuration/templating/"
target="_blank"
>Home Assistant template extensions</a
>
</li>
</ul>
<paper-textarea
label="Template editor"
value="{{template}}"
autofocus
></paper-textarea>
</div>
<div class="render-pane">
<paper-spinner
class="render-spinner"
active="[[rendering]]"
></paper-spinner>
<pre class$="[[computeRenderedClasses(error)]]">[[processed]]</pre>
</div>
<div class$="[[computeFormClasses(narrow)]]">
<div class="edit-pane">
<p>
Templates are rendered using the Jinja2 template engine with some
Home Assistant specific extensions.
</p>
<ul>
<li>
<a
href="http://jinja.pocoo.org/docs/dev/templates/"
target="_blank"
>Jinja2 template documentation</a
>
</li>
<li>
<a
href="https://home-assistant.io/docs/configuration/templating/"
target="_blank"
>Home Assistant template extensions</a
>
</li>
</ul>
<paper-textarea
label="Template editor"
value="{{template}}"
autofocus
></paper-textarea>
</div>
</app-header-layout>
<div class="render-pane">
<paper-spinner
class="render-spinner"
active="[[rendering]]"
></paper-spinner>
<pre class$="[[computeRenderedClasses(error)]]">[[processed]]</pre>
</div>
</div>
`;
}
@ -207,4 +194,4 @@ For loop example:
}
}
customElements.define("ha-panel-dev-template", HaPanelDevTemplate);
customElements.define("developer-tools-template", HaPanelDevTemplate);

View File

@ -65,7 +65,10 @@ class HaPanelHistory extends LocalizeMixin(PolymerElement) {
<app-header-layout has-scrolling-region>
<app-header slot="header" fixed>
<app-toolbar>
<ha-menu-button></ha-menu-button>
<ha-menu-button
hass="[[hass]]"
narrow="[[narrow]]"
></ha-menu-button>
<div main-title>[[localize('panel.history')]]</div>
</app-toolbar>
</app-header>
@ -116,9 +119,8 @@ class HaPanelHistory extends LocalizeMixin(PolymerElement) {
static get properties() {
return {
hass: {
type: Object,
},
hass: Object,
narrow: Boolean,
stateHistory: {
type: Object,

View File

@ -16,7 +16,7 @@ class HaPanelIframe extends PolymerElement {
}
</style>
<app-toolbar>
<ha-menu-button></ha-menu-button>
<ha-menu-button hass="[[hass]]" narrow="[[narrow]]"></ha-menu-button>
<div main-title>[[panel.title]]</div>
</app-toolbar>
@ -32,9 +32,9 @@ class HaPanelIframe extends PolymerElement {
static get properties() {
return {
panel: {
type: Object,
},
hass: Object,
narrow: Boolean,
panel: Object,
};
}
}

View File

@ -89,7 +89,10 @@ class HaPanelLogbook extends LocalizeMixin(PolymerElement) {
<app-header-layout has-scrolling-region>
<app-header slot="header" fixed>
<app-toolbar>
<ha-menu-button></ha-menu-button>
<ha-menu-button
hass="[[hass]]"
narrow="[[narrow]]"
></ha-menu-button>
<div main-title>[[localize('panel.logbook')]]</div>
<paper-icon-button
icon="hass:refresh"
@ -157,9 +160,8 @@ class HaPanelLogbook extends LocalizeMixin(PolymerElement) {
static get properties() {
return {
hass: {
type: Object,
},
hass: Object,
narrow: Boolean,
// ISO8601 formatted date string
_currentDate: {

View File

@ -1,9 +0,0 @@
import { HassEntities, HassEntity } from "home-assistant-js-websocket";
import computeDomain from "../../../common/entity/compute_domain";
export const computeNotifications = (states: HassEntities): HassEntity[] => {
return Object.keys(states)
.filter((entityId) => computeDomain(entityId) === "configurator")
.map((entityId) => states[entityId]);
};

View File

@ -1,81 +0,0 @@
import {
html,
LitElement,
TemplateResult,
css,
CSSResult,
property,
} from "lit-element";
import "@polymer/paper-icon-button/paper-icon-button";
import { fireEvent } from "../../../../common/dom/fire_event";
declare global {
// tslint:disable-next-line
interface HASSDomEvents {
"opened-changed": { value: boolean };
}
}
class HuiNotificationsButton extends LitElement {
@property() public notifications?: string[];
@property() public opened?: boolean;
protected render(): TemplateResult | void {
return html`
<paper-icon-button
aria-label="Show Notifications"
icon="hass:bell"
@click="${this._clicked}"
></paper-icon-button>
${this.notifications && this.notifications.length > 0
? html`
<span class="indicator">
<div>${this.notifications.length}</div>
</span>
`
: ""}
`;
}
static get styles(): CSSResult[] {
return [
css`
:host {
position: relative;
}
.indicator {
position: absolute;
top: 0px;
right: -3px;
width: 20px;
height: 20px;
border-radius: 50%;
background: var(--accent-color);
pointer-events: none;
z-index: 1;
}
.indicator > div {
right: 7px;
top: 3px;
position: absolute;
font-size: 0.55em;
}
`,
];
}
private _clicked() {
this.opened = true;
fireEvent(this, "opened-changed", { value: this.opened });
}
}
declare global {
interface HTMLElementTagNameMap {
"hui-notifications-button": HuiNotificationsButton;
}
}
customElements.define("hui-notifications-button", HuiNotificationsButton);

View File

@ -1,8 +0,0 @@
import { HassEntity } from "home-assistant-js-websocket";
export declare type HassNotification = HassEntity & {
notification_id?: string;
created_at?: string;
title?: string;
message?: string;
};

View File

@ -95,7 +95,11 @@ class LovelacePanel extends LitElement {
}
return html`
<hass-loading-screen rootnav></hass-loading-screen>
<hass-loading-screen
rootnav
.hass=${this.hass}
.narrow=${this.narrow}
></hass-loading-screen>
`;
}

View File

@ -20,7 +20,6 @@ import "@polymer/paper-listbox/paper-listbox";
import "@polymer/paper-menu-button/paper-menu-button";
import "@polymer/paper-tabs/paper-tab";
import "@polymer/paper-tabs/paper-tabs";
import { HassEntities } from "home-assistant-js-websocket";
import scrollToTarget from "../../common/dom/scroll-to-target";
@ -30,17 +29,13 @@ import "../../components/ha-paper-icon-button-arrow-next";
import "../../components/ha-paper-icon-button-arrow-prev";
import "../../components/ha-icon";
import { loadModule, loadCSS, loadJS } from "../../common/dom/load_resource";
import { subscribeNotifications } from "../../data/ws-notifications";
import { debounce } from "../../common/util/debounce";
import { HomeAssistant } from "../../types";
import { LovelaceConfig } from "../../data/lovelace";
import { navigate } from "../../common/navigate";
import { fireEvent } from "../../common/dom/fire_event";
import { computeNotifications } from "./common/compute-notifications";
import { swapView } from "./editor/config-util";
import "./components/notifications/hui-notification-drawer";
import "./components/notifications/hui-notifications-button";
import "./hui-view";
// Not a duplicate import, this one is for type
// tslint:disable-next-line
@ -65,12 +60,9 @@ class HUIRoot extends LitElement {
@property() public route?: { path: string; prefix: string };
@property() private _routeData?: { view: string };
@property() private _curView?: number | "hass-unused-entities";
@property() private _notificationsOpen = false;
@property() private _persistentNotifications?: Notification[];
private _viewCache?: { [viewId: string]: HUIView };
private _debouncedConfigChanged: () => void;
private _unsubNotifications?: () => void;
constructor() {
super();
@ -83,35 +75,11 @@ class HUIRoot extends LitElement {
);
}
public connectedCallback(): void {
super.connectedCallback();
this._unsubNotifications = subscribeNotifications(
this.hass!.connection,
(notifications) => {
this._persistentNotifications = notifications;
}
);
}
public disconnectedCallback(): void {
super.disconnectedCallback();
if (this._unsubNotifications) {
this._unsubNotifications();
}
}
protected render(): TemplateResult | void {
return html`
<app-route .route="${this.route}" pattern="/:view" data="${
this._routeData
}" @data-changed="${this._routeDataChanged}"></app-route>
<hui-notification-drawer
.hass="${this.hass}"
.notifications="${this._notifications}"
.open="${this._notificationsOpen}"
@open-changed="${this._handleNotificationsOpenChanged}"
.narrow="${this.narrow}"
></hui-notification-drawer>
<ha-app-layout id="layout">
<app-header slot="header" effects="waterfall" class="${classMap({
"edit-mode": this._editMode,
@ -163,14 +131,11 @@ class HUIRoot extends LitElement {
`
: html`
<app-toolbar>
<ha-menu-button></ha-menu-button>
<ha-menu-button
.hass=${this.hass}
.narrow=${this.narrow}
></ha-menu-button>
<div main-title>${this.config.title || "Home Assistant"}</div>
<hui-notifications-button
.hass="${this.hass}"
.opened="${this._notificationsOpen}"
@opened-changed="${this._handleNotificationsOpenChanged}"
.notifications="${this._notifications}"
></hui-notifications-button>
<ha-start-voice-button
.hass="${this.hass}"
></ha-start-voice-button>
@ -449,13 +414,6 @@ class HUIRoot extends LitElement {
}
}
private get _notifications() {
return this._updateNotifications(
this.hass!.states,
this._persistentNotifications! || []
);
}
private get config(): LovelaceConfig {
return this.lovelace!.config;
}
@ -480,18 +438,6 @@ class HUIRoot extends LitElement {
this._routeData = ev.detail.value;
}
private _handleNotificationsOpenChanged(ev): void {
this._notificationsOpen = ev.detail.value;
}
private _updateNotifications(
states: HassEntities,
persistent: unknown[]
): unknown[] {
const configurator = computeNotifications(states);
return persistent.concat(configurator);
}
private _handleRefresh(): void {
fireEvent(this, "config-refresh");
}

View File

@ -80,7 +80,10 @@ class HaPanelMailbox extends EventsMixin(LocalizeMixin(PolymerElement)) {
<app-header-layout has-scrolling-region>
<app-header slot="header" fixed>
<app-toolbar>
<ha-menu-button></ha-menu-button>
<ha-menu-button
hass="[[hass]]"
narrow="[[narrow]]"
></ha-menu-button>
<div main-title>[[localize('panel.mailbox')]]</div>
</app-toolbar>
<div sticky hidden$="[[areTabsHidden(platforms)]]">
@ -128,9 +131,8 @@ class HaPanelMailbox extends EventsMixin(LocalizeMixin(PolymerElement)) {
static get properties() {
return {
hass: {
type: Object,
},
hass: Object,
narrow: Boolean,
platforms: {
type: Array,

View File

@ -27,7 +27,7 @@ class HaPanelMap extends LocalizeMixin(PolymerElement) {
</style>
<app-toolbar>
<ha-menu-button></ha-menu-button>
<ha-menu-button hass="[[hass]]" narrow="[[narrow]]"></ha-menu-button>
<div main-title>[[localize('panel.map')]]</div>
</app-toolbar>
@ -41,6 +41,7 @@ class HaPanelMap extends LocalizeMixin(PolymerElement) {
type: Object,
observer: "drawEntities",
},
narrow: Boolean,
};
}

View File

@ -55,7 +55,7 @@ class HaPanelProfile extends EventsMixin(LocalizeMixin(PolymerElement)) {
<app-header-layout has-scrolling-region>
<app-header slot="header" fixed>
<app-toolbar>
<ha-menu-button></ha-menu-button>
<ha-menu-button hass='[[hass]]' narrow='[[narrow]]'></ha-menu-button>
<div main-title>[[localize('panel.profile')]]</div>
</app-toolbar>
</app-header>

View File

@ -67,7 +67,10 @@ class HaPanelShoppingList extends LocalizeMixin(PolymerElement) {
<app-header-layout has-scrolling-region>
<app-header slot="header" fixed>
<app-toolbar>
<ha-menu-button></ha-menu-button>
<ha-menu-button
hass="[[hass]]"
narrow="[[narrow]]"
></ha-menu-button>
<div main-title>[[localize('panel.shopping_list')]]</div>
<ha-start-voice-button
hass="[[hass]]"
@ -139,6 +142,7 @@ class HaPanelShoppingList extends LocalizeMixin(PolymerElement) {
static get properties() {
return {
hass: Object,
narrow: Boolean,
canListen: Boolean,
items: {
type: Array,

View File

@ -65,7 +65,10 @@ class PartialCards extends EventsMixin(NavigateMixin(PolymerElement)) {
<ha-app-layout id="layout">
<app-header effects="waterfall" condenses="" fixed="" slot="header">
<app-toolbar>
<ha-menu-button></ha-menu-button>
<ha-menu-button
hass="[[hass]]"
narrow="[[narrow]]"
></ha-menu-button>
<div main-title="">
[[computeTitle(views, defaultView, locationName)]]
</div>

View File

@ -997,6 +997,8 @@
"labels": {
"lights": "אורות",
"information": "מידע",
"morning_commute": "נסיעת בוקר",
"commute_home": "זמן נסיעה הביתה",
"entertainment": "בידור",
"activity": "פעילות",
"hdmi_input": "כניסת HDMI",
@ -1007,6 +1009,7 @@
"air": "אוויר"
},
"unit": {
"watching": "צופה",
"minutes_abbr": "דקות"
}
}

View File

@ -327,24 +327,24 @@
},
"config": {
"header": "Home Assistant 설정",
"introduction": "여기에서 구성요소와 Home Assistant 를 설정 할 수 있습니다. 아직 여기서 모두 설정 할 수는 없지만, 곧 구현되도록 작업 중입니다",
"introduction": "여기에서 구성요소와 Home Assistant 를 설정 할 수 있습니다. 아직 여기서 모두 설정 할 수는 없지만, 모든 내용을 설정 할 수 있도록 작업 중입니다.",
"core": {
"caption": "일반",
"description": "설정 파일의 유효성을 검사하고 서버를 제어합니다",
"description": "구성 내용 파일의 유효성을 검사하고 서버를 제어합니다",
"section": {
"core": {
"header": "설정 및 서버 제어",
"introduction": "설정을 변경하는 것은 귀찮은 작업입니다. 이 곳은 설정 변경을 좀 더 쉽게 도와줍니다",
"header": "구성 내용 설정 및 서버 제어",
"introduction": "구성 내용의 설정을 변경하는 것은 때때로 난해하고 귀찮은 작업입니다. 여기서 설정 변경을 좀 더 쉽게 하실 수 있습니다.",
"validation": {
"heading": "설정 유효성 검사",
"introduction": "최근에 설정 내용을 일부 변경하고 모두 정상 작동이 되는지 확인하려는 경우 설정 내용의 유효성을 검사합니다",
"heading": "구성 내용 유효성 검사",
"introduction": "최근에 구성 내용을 추가 혹은 변경하셨다면, 설정 확인 버튼을 눌러 구성 내용이 올바른지 검사하고 Home Assistant 가 정상 작동 되는지 확인하실 수 있습니다.",
"check_config": "설정 확인",
"valid": "설정 내용이 유효합니다!",
"invalid": "설정 내용이 잘못되었습니다"
"valid": "구성 내용이 모두 올바릅니다!",
"invalid": "구성 내용이 잘못되었습니다"
},
"reloading": {
"heading": "설정 새로고침",
"introduction": "Home Assistant 의 일부 설정은 재시작 없이 다시 읽어들일 수 있습니다. 새로고침을 누르면 현재 구성을 내리고 새로운 설정 내용을 읽어들입니다",
"heading": "구성 내용 새로고침",
"introduction": "Home Assistant 의 일부 구성 내용은 재시작 없이 다시 읽어들일 수 있습니다. 새로고침을 누르면 현재 구성 내용을 내리고 새로운 구성 내용을 읽어들입니다",
"core": "코어 새로고침",
"group": "그룹 새로고침",
"automation": "자동화 새로고침",
@ -379,7 +379,7 @@
"description": "구성요소를 사용자화 합니다",
"picker": {
"header": "사용자화",
"introduction": "구성요소의 속성을 조정할 수 있습니다. 추가 및 수정 된 사용자화 정의는 즉시 적용되며, 제거 된 사용자화 정의는 구성요소가 업데이트 될 때 적용됩니다."
"introduction": "구성요소의 속성값을 입맛에 맞게 변경할 수 있습니다. 추가 혹은 수정 된 사용자 정의 내용은 즉시 적용되지만, 제거 된 내용은 구성요소가 업데이트 될 때 적용됩니다."
}
},
"automation": {
@ -634,7 +634,7 @@
"description": "Zigbee 홈 자동화 네트워크 관리",
"services": {
"reconfigure": "ZHA 기기를 다시 구성 합니다. (장치 복구). 기기에 문제가 있는 경우 사용해주세요. 기기가 배터리로 작동하는 경우, 이 서비스를 사용할 때 기기가 켜져있고 통신이 가능한 상태인지 확인해주세요.",
"updateDeviceName": "이 기기의 사용자 정의 이름을 장치 레지스트리에 설정합니다.",
"updateDeviceName": "이 기기의 사용자 정의 이름을 기기 레지스트리에 설정합니다.",
"remove": "Zigbee 네트워크에서 기기 제거"
},
"device_card": {
@ -653,7 +653,7 @@
"description": "영역을 만들고 편집합니다",
"picker": {
"header": "영역 등록",
"introduction": "영역은 기기가있는 위치를 구성하는데 사용합니다. 이 정보는 Home Assistant 의 인터페이스 정리, 권한 및 다른 시스템과의 통합 구성에 도움을 줍니다.",
"introduction": "영역은 기기가 있는 위치를 설정하는데 사용합니다. 이 정보는 Home Assistant 의 인터페이스 정리, 권한 및 다른 시스템과의 통합 구성에 도움을 줍니다.",
"introduction2": "특정 영역에 기기를 배치하려면 아래 링크를 따라 통합 구성요소 페이지로 이동 한 다음, 설정된 구성요소의 기기를 클릭하여 영역을 설정해주세요.",
"integrations_page": "통합 구성요소 페이지",
"no_areas": "등록된 영역이 없습니다. 거실, 침실과 같이 영역을 등록해보세요!",
@ -675,7 +675,7 @@
"header": "구성요소",
"unavailable": "(사용불가)",
"introduction": "Home Assistant 는 구성요소의 식별을 위해 모든 구성요소에 고유한 레지스트리를 부여합니다. 각각의 구성요소들은 자신만의 고유한 구성요소 ID 를 가집니다.",
"introduction2": "구성요소를 편집하여 이름을 대체하거나 구성요소 ID를 변경하거나 Home Assistant 에서 항목을 제거할 수 있습니다. 단, 구성요소 편집창에서 구성요소를 삭제해도 구성요소가 완전히 제거되는것은 아닙니다. 완전히 제거하려면, 아래 링크를 따라 통합 구성요소 페이지에서 제거해주세요.",
"introduction2": "구성요소를 편집하여 이름을 대체하거나 구성요소 ID를 변경하고 Home Assistant 에서 항목을 제거하실수도 있습니다. 단, 구성요소 편집창에서 구성요소를 삭제해도 구성요소가 완전히 제거되는것은 아닙니다. 완전히 제거하려면, 아래 링크를 따라 통합 구성요소 페이지에서 제거해주세요.",
"integrations_page": "통합 구성요소 페이지"
},
"editor": {
@ -927,7 +927,7 @@
"move": "이동"
},
"migrate": {
"header": "설정이 호환되지 않습니다",
"header": "구성 내용이 호환되지 않습니다",
"para_no_id": "이 구성요소에는 ID가 없습니다. 'ui-lovelace.yaml' 에 구성요소의 ID를 추가해주세요.",
"para_migrate": "Home Assistant 는 '설정 마이그레이션' 버튼을 눌러 자동으로 모든 카드와 보기에 ID를 추가 할 수 있습니다.",
"migrate": "설정 마이그레이션"

View File

@ -258,13 +258,13 @@
},
"vacuum": {
"cleaning": "A limpar",
"docked": "Ancorado",
"docked": "Encaixado",
"error": "Erro",
"idle": "Em espera",
"off": "Desligado",
"on": "Ligado",
"paused": "Em pausa",
"returning": "A regressar ao cais"
"returning": "A regressar à doca"
},
"timer": {
"active": "ativo",
@ -280,7 +280,8 @@
"default": {
"unknown": "Desc",
"unavailable": "Indisp",
"error": "Erro"
"error": "Erro",
"entity_not_found": "Entidade não encontrada"
},
"alarm_control_panel": {
"armed": "Armado",
@ -336,7 +337,7 @@
"introduction": "Alterar a configuração pode ser um processo repetitivo. Nós sabemos. Esta secção pretende tornar a sua vida um pouco mais fácil.",
"validation": {
"heading": "Validar a configuração",
"introduction": "Valide a sua configuração caso tenha alterado recentemente a mesma e quiser certificar-se de que tudo é válido.",
"introduction": "Valide a sua configuração caso a tenha alterado recentemente e quiser certificar-se de que tudo está válido",
"check_config": "Verificar a configuração",
"valid": "Configuração válida!",
"invalid": "Configuração inválida"
@ -354,6 +355,21 @@
"introduction": "Controle o seu servidor Home Assistant... a partir do Home Assistant",
"restart": "Reiniciar",
"stop": "Parar"
},
"core_config": {
"edit_requires_storage": "Editor desativado por causa da configuração existente em configuration.yaml.",
"location_name": "Nome da instalação do seu Home Assistant",
"latitude": "Latitude",
"longitude": "Longitude",
"elevation": "Elevação",
"elevation_meters": "metros",
"time_zone": "Fuso horário",
"unit_system": "Unidades do Sistema",
"unit_system_imperial": "Imperial",
"unit_system_metric": "Métrico",
"imperial_example": "Fahrenheit, libras",
"metric_example": "Celsius, quilogramas",
"save_button": "Guardar"
}
}
}
@ -363,7 +379,7 @@
"description": "Personalizar as suas entidades",
"picker": {
"header": "Personalização",
"introduction": "Ajustar atributos por entidade. Personalizações acrescentadas\/editadas terão efeitos imediatos. Remoção de personalizaçõe terão efeito quando a entidade for actualizada."
"introduction": "Ajustar atributos por entidade. Personalizações acrescentadas\/editadas terão efeitos imediatos. Remoção de personalizações terão efeito quando a entidade for atualizada."
}
},
"automation": {
@ -378,20 +394,20 @@
"learn_more": "Saber mais sobre automações"
},
"editor": {
"introduction": "Crie automatizações para tornar a sua casa viva",
"introduction": "Crie automações para tornar a sua casa viva",
"default_name": "Nova Automação",
"save": "Guardar",
"unsaved_confirm": "Existem alterações não guardas. Tem a certeza de que quer sair?",
"unsaved_confirm": "Existem alterações não guardadas. Tem a certeza de que quer sair?",
"alias": "Nome",
"triggers": {
"header": "Gatilhos",
"introduction": "Os gatilhos são o que inicia o processamento de uma regra de automação. É possível especificar vários gatilhos para a mesma regra. Uma vez iniciado um gatilho, o Home Assistant irá validar as condições, e caso as mesmas ocorram, chamar a ação.\n\n[Saiba mais sobre gatilhos.](https:\/\/home-assistant.io\/docs\/automation\/trigger\/)",
"add": "Acrescentar gatilho",
"header": "Acionadores",
"introduction": "Os acionadores são o que iniciam o processamento de uma regra de automação. É possível especificar vários acionadores para a mesma regra. Uma vez iniciado um acionador, o Home Assistant irá validar as condições, e caso as mesmas ocorram, chamar a ação.\n\n[Saiba mais sobre acionadores.](https:\/\/home-assistant.io\/docs\/automation\/trigger\/)",
"add": "Acrescentar acionador",
"duplicate": "Duplicar",
"delete": "Apagar",
"delete_confirm": "Tem certeza que deseja apagar?",
"unsupported_platform": "Plataforma não suportada: {platform}",
"type_select": "Tipo de gatilho",
"type_select": "Tipo de acionador",
"type": {
"event": {
"label": "Evento",
@ -445,7 +461,7 @@
"leave": "Sair"
},
"webhook": {
"label": "Webhook ID",
"label": "Webhook",
"webhook_id": "Webhook ID"
},
"time_pattern": {
@ -463,11 +479,11 @@
"leave": "Sair"
}
},
"learn_more": "Saber mais sobre gatilhos"
"learn_more": "Saber mais sobre acionadores"
},
"conditions": {
"header": "Condições",
"introduction": "As condições são uma parte opcional de uma regra de automação e podem ser usadas para impedir que uma ação ocorra quando um gatilho dispara. As condições embora pareçam muito semelhantes aos gatilhos, são muito diferentes. Um gatilho examinará os eventos que acontecem no sistema, enquanto uma condição apenas analisa a forma como o sistema parece no momento. Um gatilho pode observar que um interruptor está a ser ligado. Uma condição só pode ver se um interruptor está ligado ou desligado. \n\n [Saiba mais sobre as condições.] (Https:\/\/home-assistant.io\/docs\/scripts\/conditions\/)",
"introduction": "As condições são uma parte opcional de uma regra de automação e podem ser usadas para impedir que uma ação ocorra quando um acionador é despoletado. As condições embora pareçam muito semelhantes aos acionadores, são muito diferentes. Um acionador examinará os eventos que acontecem no sistema, enquanto uma condição apenas analisa a forma como o sistema parece no momento. Um acionador pode observar que um interruptor está a ser ligado. Uma condição só pode ver se um interruptor está ligado ou desligado. \n\n [Saiba mais sobre as condições.] (Https:\/\/home-assistant.io\/docs\/scripts\/conditions\/)",
"add": "Acrescentar condição",
"duplicate": "Duplicar",
"delete": "Apagar",
@ -544,7 +560,9 @@
}
},
"learn_more": "Saber mais sobre ações"
}
},
"load_error_not_editable": "Apenas as automações em automations.yaml são editáveis.",
"load_error_unknown": "Erro ao carregar a automação ({err_no})."
}
},
"script": {
@ -573,18 +591,19 @@
"caption": "Adicionar Utilizador",
"name": "Nome",
"username": "Nome de Utilizador",
"password": "Password",
"password": "Palavra-passe",
"create": "Criar"
}
},
"cloud": {
"caption": "Home Assistant Cloud",
"description_login": "Ligado como {email}",
"description_not_login": "Não está ligado"
"description_not_login": "Não está ligado",
"description_features": "Controle fora de casa, integre com a Alexa e o Google Assistant."
},
"integrations": {
"caption": "Integrações",
"description": "Gerir dispositivos e serviços ligados",
"description": "Gerir dispositivos e serviços conectados",
"discovered": "Detetados",
"configured": "Configurado",
"new": "Configurar uma nova integração",
@ -600,18 +619,33 @@
"firmware": "Firmware: {version}",
"device_unavailable": "Dispositivo indisponível",
"entity_unavailable": "Entidade indisponível",
"no_area": "Nenhuma área"
"no_area": "Nenhuma Área",
"hub": "Conectado via"
},
"config_flow": {
"external_step": {
"description": "Para ser concluída, esta etapa exige que visite um site externo.",
"open_site": "Abrir site"
}
}
},
"zha": {
"caption": "ZHA",
"description": "Gestão de rede Zigbee Home Automation",
"services": {
"reconfigure": "Reconfigure o dispositivo ZHA (curar dispositivo). Use isto se estiver a ter problemas com o dispositivo. Se o dispositivo em questão for um dispositivo alimentado por bateria, certifique-se de que ele está ativo e a aceitar comandos ao usar este serviço.",
"updateDeviceName": "Definir um nome personalizado para este dispositivo no registo do dispositivo."
"reconfigure": "Reconfigure o dispositivo ZHA (curar dispositivo). Utilize isto se estiver a ter problemas com o dispositivo. Se o dispositivo em questão for um dispositivo alimentado por uma bateria, ao utilizar este serviço certifique-se de que o equipamento está ativo e a aceitar comandos.",
"updateDeviceName": "Definir um nome personalizado para este dispositivo no registo do dispositivo.",
"remove": "Remover um dispositivo da rede ZigBee."
},
"device_card": {
"area_picker_label": "Área"
"device_name_placeholder": "Nome do utilizador",
"area_picker_label": "Área",
"update_name_button": "Atualizar Nome"
},
"add_device_page": {
"header": "Zigbee Home Automation - Adicionar Dispositivos",
"spinner": "À procura de dispositivos ZHA Zigbee...",
"discovery_text": "Os dispositivos descobertos aparecerão aqui. Siga as instruções para o(s) seu(s) dispositivo(s) e coloque o(s) dispositivo(s) em modo de emparelhamento."
}
},
"area_registry": {
@ -619,8 +653,8 @@
"description": "Visão geral de todas as áreas da sua casa.",
"picker": {
"header": "Registo de áreas",
"introduction": "Áreas são usadas para organizar os dispositivos. Essas informações serão usadas no Home Assistant para ajudá-lo a organizar sua interface, permissões e integrações com outros sistemas.",
"introduction2": "Para colocar dispositivos em uma área, use o link abaixo para navegar até a página de integrações e, em seguida, clique em uma integração configurada para acessar os cartões de dispositivos.",
"introduction": "As áreas são utilizadas para organizar os dispositivos. Essas informações serão utilizadas no Home Assistant para o ajudar a organizar o seu interface, permissões e integrações com outros sistemas.",
"introduction2": "Para colocar dispositivos numa área, use o link abaixo para navegar até a página de integrações e em seguida, clique numa integração configurada para aceder aos cartões de dispositivos.",
"integrations_page": "Página de Integrações",
"no_areas": "Parece que ainda não tem áreas!",
"create_area": "CRIAR ÁREA"
@ -630,7 +664,7 @@
"editor": {
"default_name": "Nova área",
"delete": "APAGAR",
"update": "ACTUALIZAR",
"update": "ATUALIZAR",
"create": "CRIAR"
}
},
@ -640,20 +674,20 @@
"picker": {
"header": "Registo de Entidades",
"unavailable": "(indisponível)",
"introduction": "O Home Assistant mantém um registro de todas as entidades que já viu e que podem ser identificadas exclusivamente. Cada uma dessas entidades terá um ID de entidade atribuído, que será reservado apenas para essa entidade.",
"introduction2": "Use o registo da entidade para substituir o nome, alterar o ID da entidade ou remover a entrada do Assistente Inicial. Observe que a remoção da entrada do registo de entidade não removerá a entidade. Para fazer isso, siga o link abaixo e remova-o da página de integrações.",
"introduction": "O Home Assistant mantém um registo de todas as entidades que foram alguma vez detetadas e que podem ser identificadas de uma forma única. Cada uma dessas entidades terá um ID de entidade atribuído, que será reservado apenas para essa entidade.",
"introduction2": "Use o registo da entidade para substituir o nome, alterar o ID da entidade ou remover a entrada do Home Assistant. Note que a remoção da entrada do registo de entidade não removerá a entidade. Para fazer isso, siga o link abaixo e remova-o da página de integrações.",
"integrations_page": "Página de Integrações"
},
"editor": {
"unavailable": "Esta entidade não está atualmente disponível.",
"default_name": "Nova área",
"default_name": "Nova Área",
"delete": "APAGAR",
"update": "ACTUALIZAR"
"update": "ATUALIZAR"
}
},
"person": {
"caption": "Pessoas",
"description": "Gerir as que pessoas que o Home Assistant segue.",
"description": "Gerir as pessoas que o Home Assistant segue.",
"detail": {
"name": "Nome",
"device_tracker_intro": "Selecione os dispositivos que pertencem a esta pessoa.",
@ -667,13 +701,13 @@
"header": "Notificações push",
"description": "Enviar notificações para este dispositivo.",
"error_load_platform": "Configurar notify.html5.",
"error_use_https": "Requer interface com utilizador, com SSL activo.",
"error_use_https": "Requer SSL ativo para o interface principal",
"push_notifications": "Notificações push",
"link_promo": "Saiba mais"
},
"language": {
"header": "Idioma",
"link_promo": "Ajude a traduzir",
"link_promo": "Ajude na tradução",
"dropdown_label": "Idioma"
},
"themes": {
@ -684,7 +718,7 @@
},
"refresh_tokens": {
"header": "Atualizar Tokens",
"description": "Cada \"refresh token\" representa a uma sessão de utilizador. Os \"refresh tokens\" serão automáticamente removidos quando clicar sair. Os seguintes \"refresh tokens\" estão activos para a sua conta.",
"description": "Cada \"refresh token\" representa uma sessão de utilizador. Os \"refresh tokens\" serão automaticamente removidos quando clicar em sair. Os seguintes \"refresh tokens\" estão activos para a sua conta.",
"token_title": "Atualizar o token de {clientId}",
"created_at": "Criado a {date}",
"confirm_delete": "Tem certeza de que deseja apagar o \"refresh token\" de {name} ?",
@ -695,7 +729,7 @@
},
"long_lived_access_tokens": {
"header": "Tokens de acesso de longa duração",
"description": "Crie tokens de acesso de longa duração para permitir que os seus scripts possam interagir com a sua instancia do Home Assistant. Cada token será válido durante 10 anos após criação. Os seguintes tokens de acesso de longa duração estão actualmente activos.",
"description": "Crie tokens de acesso de longa duração para permitir que os seus scripts possam interagir com a sua instância do Home Assistant. Cada token será válido durante 10 anos após criação. Os seguintes tokens de acesso de longa duração estão atualmente activos.",
"learn_auth_requests": "Aprenda a fazer pedidos autenticados.",
"created_at": "Criado a {date}",
"confirm_delete": "Tem certeza de que deseja apagar o token de acesso de {name} ?",
@ -703,26 +737,26 @@
"create": "Criar Token",
"create_failed": "Falha ao criar o token de acesso.",
"prompt_name": "Nome?",
"prompt_copy_token": "Copie seu token de acesso. Não será mostrado novamente.",
"prompt_copy_token": "Copie o seu token de acesso. Não será mostrado novamente.",
"empty_state": "Ainda não tem um token de longa duração",
"last_used": "Última utilização a {date} em {location}",
"not_used": "Nunca foi utilizado"
},
"current_user": "Esta actualmente ligado como {fullName}",
"current_user": "Esta atualmente ligado como {fullName}",
"is_owner": "Você é um proprietário.",
"logout": "Sair",
"change_password": {
"header": "Alterar palavra-passe",
"current_password": "Palavra-passe actual",
"current_password": "Palavra-passe atual",
"new_password": "Nova palavra-passe",
"confirm_new_password": "Confirmar nova palavra-passe",
"confirm_new_password": "Confirmar a nova palavra-passe",
"error_required": "Obrigatório",
"submit": "Enviar"
},
"mfa": {
"header": "Módulos de autenticação por multíplos-factores",
"disable": "Desactivar",
"enable": "Activar",
"header": "Módulos de Autenticação por Multíplos-fatores",
"disable": "Desativar",
"enable": "Ativar",
"confirm_disable": "Tem certeza de que deseja desativar {name} ?"
},
"mfa_setup": {
@ -737,11 +771,11 @@
"initializing": "A inicializar",
"authorizing_client": "Está prestes a dar acesso a {clientId} à sua instância do Home Assistant.",
"logging_in_with": "Iniciar a sessão com **{authProviderName}**.",
"pick_auth_provider": "Ou faça login com",
"abort_intro": "Login abortado",
"pick_auth_provider": "Ou entre com",
"abort_intro": "Início de sessão cancelado",
"form": {
"working": "Por favor, aguarde",
"unknown_error": "Algo de errado aconteceu",
"unknown_error": "Algo correu mal",
"providers": {
"homeassistant": {
"step": {
@ -796,7 +830,7 @@
"data": {
"user": "Utilizador"
},
"description": "Por favor seleccione o utilizador com o qual pretende entrar:"
"description": "Por favor selecione o utilizador com o qual pretende entrar:"
}
},
"abort": {
@ -813,9 +847,9 @@
},
"mfa": {
"data": {
"code": "Código de autenticações por dois factores"
"code": "Código de autenticações por dois fatores"
},
"description": "Abrir **{mfa_module_name}** no seu dispositivo para ver o código de autenticação por dois factores e verificar a sua identidade:"
"description": "Abrir o **{mfa_module_name}** no seu dispositivo para ver o código de autenticação por dois factores e validar a sua identidade:"
}
},
"error": {
@ -830,7 +864,7 @@
}
},
"page-onboarding": {
"intro": "Está pronto para despertar a sua casa, reclamar a sua privacidade e juntar a uma comunidade mundial de tecnólogos?",
"intro": "Está pronto para despertar a sua casa, reclamar a sua privacidade e juntar-se a uma comunidade mundial de tecnólogos?",
"user": {
"intro": "Vamos começar por criar um utilizador.",
"required_field": "Obrigatório",
@ -838,13 +872,26 @@
"name": "Nome",
"username": "Utilizador",
"password": "Palavra-passe",
"password_confirm": "Confirme Password"
"password_confirm": "Confirme a palavra-passe"
},
"create_account": "Criar conta",
"error": {
"required_fields": "Preencha todos os campos obrigatórios",
"password_not_match": "Password não coincide"
"password_not_match": "As palavras-passe não coincidem"
}
},
"integration": {
"intro": "Dispositivos e serviços são representados no Home Assistant como integrações. Pode configurá-los agora ou fazê-lo mais tarde no ecrã de configuração.",
"more_integrations": "Mais",
"finish": "Terminar"
},
"core-config": {
"intro": "Olá {name}, bem-vindo ao Home Assistant. Que nome quer dar à sua casa?",
"intro_location": "Gostaríamos de saber onde vive. Esta informação ajudará a exibir informações e a configurar automações baseadas no sol. Os dados nunca são partilhados fora da sua rede.",
"intro_location_detect": "Podemos ajudá-lo a preencher esta informação fazendo um pedido único a um serviço externo.",
"location_name_default": "Início",
"button_detect": "Detetar",
"finish": "Próxima"
}
},
"lovelace": {
@ -857,14 +904,22 @@
"empty_state": {
"title": "Bem-vindo a casa",
"no_devices": "Esta página permite-lhe controlar os seus dispositivos, no entanto, parece que ainda não tem dispositivos configurados. Vá para a página de integrações para começar.",
"go_to_integrations_page": "Ir para a página das integrações."
"go_to_integrations_page": "Ir para a página de integrações."
},
"picture-elements": {
"hold": "Mantenha:",
"tap": "Toque:",
"navigate_to": "Navegue até {location}",
"toggle": "Alternar {name}",
"call_service": "Chamar o serviço {name}",
"more_info": "Mostrar mais informações: {name}"
}
},
"editor": {
"edit_card": {
"header": "Configuração do cartão",
"save": "Guardar",
"toggle_editor": "Alterar para editor",
"toggle_editor": "Alternar Editor",
"pick_card": "Escolha o cartão que deseja adicionar.",
"add": "Adicionar Cartão",
"edit": "Editar",
@ -874,8 +929,8 @@
"migrate": {
"header": "Configuração Incompatível",
"para_no_id": "Este elemento não possui um ID. Por favor adicione um ID a este elemento em 'ui-lovelace.yaml'.",
"para_migrate": "O Home Assistant pode adicionar IDs a todos os seus cartões e vistas automaticamente clicando no botão 'Migrar configuração'.",
"migrate": "Migrar configuração"
"para_migrate": "Ao clicar no botão 'Migrar configuração', o Home Assistant pode adicionar IDs a todos os seus cartões e vistas automaticamente.",
"migrate": "Migrar a configuração"
},
"header": "Editar UI",
"edit_view": {
@ -886,19 +941,19 @@
},
"save_config": {
"header": "Assumir controle sobre a interface do Lovelace",
"para": "Por omissão o Home Assistant irá manter a sua interface de utilizador, actualizando sempre que uma entidade nova ou componentes Lovelace fiquem disponíveis. Se assumir o controle não será possivel fazer alterações automáticas por si.",
"para_sure": "Tem certeza que deseja assumir o controle sobre a interface de utilizador?",
"para": "Por omissão o Home Assistant irá manter a sua interface de utilizador, atualizando-a sempre que uma nova entidade ou novos componentes Lovelace fiquem disponíveis. Se assumir o controlo, não faremos mais alterações automáticas por si.",
"para_sure": "Tem certeza que deseja assumir o controlo sobre a interface de utilizador?",
"cancel": "Cancelar",
"save": "Assumir o controle"
"save": "Assumir o controlo"
},
"menu": {
"raw_editor": "Editor de configuração fonte."
"raw_editor": "Editor de configuração do código-fonte"
},
"raw_editor": {
"header": "Editar configuração",
"save": "Guardar",
"unsaved_changes": "Alterações não gravadas",
"saved": "Guardou"
"unsaved_changes": "Alterações não guardadas",
"saved": "Guardada"
}
},
"menu": {
@ -908,14 +963,63 @@
"refresh": "Atualizar"
},
"warning": {
"entity_not_found": "Entidade indisponível",
"entity_non_numeric": "Entidade é não numérica"
"entity_not_found": "Entidade não disponível: {entity}",
"entity_non_numeric": "A entidade é não numérica: {entity}"
},
"changed_toast": {
"message": "A configuração do Lovelace foi atualizada, gostaria de atualizar?",
"refresh": "Atualizar"
},
"reload_lovelace": "Recarregar Lovelace"
},
"page-demo": {
"cards": {
"demo": {
"demo_by": "por {nome}",
"next_demo": "Próxima demonstração",
"introduction": "Bem vindo a casa! Chegou à demonstração do Home Assistant, onde exibimos as melhores UIs criadas pela nossa comunidade.",
"learn_more": "Saiba mais sobre o Home Assistant"
}
},
"config": {
"arsaboo": {
"names": {
"upstairs": "Andar de cima",
"family_room": "Sala",
"kitchen": "Cozinha",
"patio": "Pátio",
"hallway": "Corredor",
"master_bedroom": "Quarto principal",
"left": "Esquerda",
"right": "Direita",
"mirror": "Espelho"
},
"labels": {
"lights": "Luzes",
"information": "Informação",
"morning_commute": "Trajeto da manhã",
"commute_home": "Deslocação até casa",
"entertainment": "Entretenimento",
"activity": "Atividade",
"hdmi_input": "Entrada HDMI",
"hdmi_switcher": "Comutador HDMI",
"volume": "Volume",
"total_tv_time": "Tempo total de TV",
"turn_tv_off": "Desligar a televisão",
"air": "Ar"
},
"unit": {
"watching": "a assistir",
"minutes_abbr": "min"
}
}
}
}
},
"sidebar": {
"log_out": "Sair",
"developer_tools": "Ferramentas de programação"
"developer_tools": "Ferramentas de programação",
"external_app_configuration": "Configuração da Aplicação"
},
"common": {
"loading": "A carregar",
@ -983,7 +1087,7 @@
"arm_away": "Armado ausente",
"arm_night": "Armado noite",
"armed_custom_bypass": "Desvio personalizado",
"arm_custom_bypass": "bypass personalizado"
"arm_custom_bypass": "Desvio personalizado"
},
"automation": {
"last_triggered": "Última ocorrência",
@ -1001,7 +1105,7 @@
"light": {
"brightness": "Brilho",
"color_temperature": "Temperatura de cor",
"white_value": "Quantidade de branco",
"white_value": "Quantidade de brancos",
"effect": "Efeito"
},
"media_player": {
@ -1022,13 +1126,13 @@
},
"lock": {
"code": "Código",
"lock": "Trancar",
"unlock": "Destrancar"
"lock": "Bloquear",
"unlock": "Desbloquear"
},
"vacuum": {
"actions": {
"resume_cleaning": "Retomar a limpeza",
"return_to_base": "Regressar ao cais",
"return_to_base": "Voltar à doca",
"start_cleaning": "Iniciar a limpeza",
"turn_on": "Ligar",
"turn_off": "Desligar"
@ -1077,13 +1181,13 @@
},
"dialogs": {
"more_info_settings": {
"save": "Salvar",
"save": "Guardar",
"name": "Nome",
"entity_id": "ID da entidade"
},
"more_info_control": {
"script": {
"last_action": "Última ação"
"last_action": "Última ocorrência"
},
"sun": {
"elevation": "Elevação",
@ -1147,7 +1251,7 @@
"hassio": "Hass.io",
"homeassistant": "Home Assistant",
"lovelace": "Lovelace",
"system_health": "Saúde do sistema",
"system_health": "Integridade do Sistema",
"person": "Pessoa"
},
"attribute": {
@ -1160,15 +1264,15 @@
"state_attributes": {
"climate": {
"fan_mode": {
"off": "Off",
"on": "On",
"auto": "Auto"
"off": "Desligar",
"on": "Ligar",
"auto": "Automático"
}
}
},
"groups": {
"system-admin": "Administradores",
"system-users": "Utilizadores",
"system-read-only": "Utilizadores somente de leitura"
"system-read-only": "Utilizadores só com permissões de leitura"
}
}

View File

@ -133,7 +133,7 @@
"climate": {
"off": "Выкл",
"on": "Вкл",
"heat": "Нагрев",
"heat": "Обогрев",
"cool": "Охлаждение",
"idle": "Бездействие",
"auto": "Авто",
@ -990,17 +990,17 @@
"patio": "Внутренний дворик",
"hallway": "Прихожая",
"master_bedroom": "Спальня",
"left": "Влево",
"right": "Вправо",
"left": "Левая сторона",
"right": "Правая сторона",
"mirror": "Зеркало"
},
"labels": {
"lights": "Свет",
"lights": "Освещение",
"information": "Информация",
"morning_commute": "Утренняя поездка",
"commute_home": "Поездка домой",
"entertainment": "Развлечения",
"activity": "Активность",
"activity": "Процесс",
"hdmi_input": "Вход HDMI",
"hdmi_switcher": "Переключатель HDMI",
"volume": "Громкость",