Merge pull request #3311 from home-assistant/dev

20190627.0
This commit is contained in:
Paulus Schoutsen 2019-06-27 17:56:22 -07:00 committed by GitHub
commit 9974510067
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
30 changed files with 682 additions and 290 deletions

View File

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

View File

@ -188,6 +188,7 @@ class HaMediaPlayerCard extends LocalizeMixin(EventsMixin(PolymerElement)) {
<div class="controls layout horizontal justified"> <div class="controls layout horizontal justified">
<paper-icon-button <paper-icon-button
aria-label="Turn off"
icon="hass:power" icon="hass:power"
on-click="handleTogglePower" on-click="handleTogglePower"
invisible$="[[computeHidePowerButton(playerObj)]]" invisible$="[[computeHidePowerButton(playerObj)]]"
@ -196,12 +197,14 @@ class HaMediaPlayerCard extends LocalizeMixin(EventsMixin(PolymerElement)) {
<div class="playback-controls"> <div class="playback-controls">
<paper-icon-button <paper-icon-button
aria-label="Previous track"
icon="hass:skip-previous" icon="hass:skip-previous"
invisible$="[[!playerObj.supportsPreviousTrack]]" invisible$="[[!playerObj.supportsPreviousTrack]]"
disabled="[[playerObj.isOff]]" disabled="[[playerObj.isOff]]"
on-click="handlePrevious" on-click="handlePrevious"
></paper-icon-button> ></paper-icon-button>
<paper-icon-button <paper-icon-button
aria-label="Play or Pause"
class="primary" class="primary"
icon="[[computePlaybackControlIcon(playerObj)]]" icon="[[computePlaybackControlIcon(playerObj)]]"
invisible$="[[!computePlaybackControlIcon(playerObj)]]" invisible$="[[!computePlaybackControlIcon(playerObj)]]"
@ -209,6 +212,7 @@ class HaMediaPlayerCard extends LocalizeMixin(EventsMixin(PolymerElement)) {
on-click="handlePlaybackControl" on-click="handlePlaybackControl"
></paper-icon-button> ></paper-icon-button>
<paper-icon-button <paper-icon-button
aria-label="Next track"
icon="hass:skip-next" icon="hass:skip-next"
invisible$="[[!playerObj.supportsNextTrack]]" invisible$="[[!playerObj.supportsNextTrack]]"
disabled="[[playerObj.isOff]]" disabled="[[playerObj.isOff]]"
@ -217,6 +221,7 @@ class HaMediaPlayerCard extends LocalizeMixin(EventsMixin(PolymerElement)) {
</div> </div>
<paper-icon-button <paper-icon-button
aria-label="More information."
icon="hass:dots-vertical" icon="hass:dots-vertical"
on-click="handleOpenMoreInfo" on-click="handleOpenMoreInfo"
class="self-center secondary" class="self-center secondary"

View File

@ -7,6 +7,7 @@ import { DEFAULT_DOMAIN_ICON } from "../const";
const fixedIcons = { const fixedIcons = {
alert: "hass:alert", alert: "hass:alert",
alexa: "hass:amazon-alexa",
automation: "hass:playlist-play", automation: "hass:playlist-play",
calendar: "hass:calendar", calendar: "hass:calendar",
camera: "hass:video", camera: "hass:video",
@ -15,6 +16,7 @@ const fixedIcons = {
conversation: "hass:text-to-speech", conversation: "hass:text-to-speech",
device_tracker: "hass:account", device_tracker: "hass:account",
fan: "hass:fan", fan: "hass:fan",
google_assistant: "hass:google-assistant",
group: "hass:google-circles-communities", group: "hass:google-circles-communities",
history_graph: "hass:chart-line", history_graph: "hass:chart-line",
homeassistant: "hass:home-assistant", homeassistant: "hass:home-assistant",

View File

@ -15,6 +15,7 @@ import {
import { HomeAssistant } from "../../types"; import { HomeAssistant } from "../../types";
import { HassEntity } from "home-assistant-js-websocket"; import { HassEntity } from "home-assistant-js-websocket";
import { forwardHaptic } from "../../data/haptics"; import { forwardHaptic } from "../../data/haptics";
import computeStateName from "../../common/entity/compute_state_name";
const isOn = (stateObj?: HassEntity) => const isOn = (stateObj?: HassEntity) =>
stateObj !== undefined && !STATES_OFF.includes(stateObj.state); stateObj !== undefined && !STATES_OFF.includes(stateObj.state);
@ -35,11 +36,13 @@ class HaEntityToggle extends LitElement {
if (this.stateObj.attributes.assumed_state) { if (this.stateObj.attributes.assumed_state) {
return html` return html`
<paper-icon-button <paper-icon-button
aria-label=${`Turn ${computeStateName(this.stateObj)} off`}
icon="hass:flash-off" icon="hass:flash-off"
@click=${this._turnOff} @click=${this._turnOff}
?state-active=${!this._isOn} ?state-active=${!this._isOn}
></paper-icon-button> ></paper-icon-button>
<paper-icon-button <paper-icon-button
aria-label=${`Turn ${computeStateName(this.stateObj)} on`}
icon="hass:flash" icon="hass:flash"
@click=${this._turnOn} @click=${this._turnOn}
?state-active=${this._isOn} ?state-active=${this._isOn}
@ -49,6 +52,9 @@ class HaEntityToggle extends LitElement {
return html` return html`
<paper-toggle-button <paper-toggle-button
aria-label=${`Toggle ${computeStateName(this.stateObj)} ${
this._isOn ? "off" : "on"
}`}
.checked=${this._isOn} .checked=${this._isOn}
@change=${this._toggleChanged} @change=${this._toggleChanged}
></paper-toggle-button> ></paper-toggle-button>

View File

@ -55,7 +55,9 @@ class HaCameraStream extends LitElement {
.src=${__DEMO__ .src=${__DEMO__
? `/api/camera_proxy_stream/${this.stateObj.entity_id}` ? `/api/camera_proxy_stream/${this.stateObj.entity_id}`
: computeMJPEGStreamUrl(this.stateObj)} : computeMJPEGStreamUrl(this.stateObj)}
.alt=${computeStateName(this.stateObj)} .alt=${`Preview of the ${computeStateName(
this.stateObj
)} camera.`}
/> />
` `
: html` : html`

View File

@ -66,11 +66,29 @@ const computePanels = (hass: HomeAssistant) => {
return result; return result;
}; };
const renderPanel = (hass, panel) => html`
<a
aria-role="option"
href="${computeUrl(panel.url_path)}"
data-panel="${panel.url_path}"
tabindex="-1"
>
<paper-icon-item>
<ha-icon slot="item-icon" .icon="${panel.icon}"></ha-icon>
<span class="item-text">
${hass.localize(`panel.${panel.title}`) || panel.title}
</span>
</paper-icon-item>
</a>
`;
/* /*
* @appliesMixin LocalizeMixin * @appliesMixin LocalizeMixin
*/ */
class HaSidebar extends LitElement { class HaSidebar extends LitElement {
@property() public hass?: HomeAssistant; @property() public hass?: HomeAssistant;
@property({ type: Boolean }) public alwaysExpand = false;
@property({ type: Boolean, reflect: true }) public expanded = false;
@property() public _defaultPage?: string = @property() public _defaultPage?: string =
localStorage.defaultPage || DEFAULT_PANEL; localStorage.defaultPage || DEFAULT_PANEL;
@property() private _externalConfig?: ExternalConfig; @property() private _externalConfig?: ExternalConfig;
@ -82,20 +100,33 @@ class HaSidebar extends LitElement {
return html``; 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];
return html` return html`
${this.expanded
? html`
<app-toolbar> <app-toolbar>
<div main-title>Home Assistant</div> <div main-title>Home Assistant</div>
${hass.user
? html`
<a href="/profile">
<ha-user-badge .user=${hass.user}></ha-user-badge>
</a>
`
: ""}
</app-toolbar> </app-toolbar>
`
: html`
<div class="logo">
<img
id="logo"
src="/static/icons/favicon-192x192.png"
alt="Home Assistant logo"
/>
</div>
`}
<paper-listbox attr-for-selected="data-panel" .selected=${hass.panelUrl}> <paper-listbox attr-for-selected="data-panel" .selected=${hass.panelUrl}>
<a <a
aria-role="option"
href="${computeUrl(this._defaultPage)}" href="${computeUrl(this._defaultPage)}"
data-panel=${this._defaultPage} data-panel=${this._defaultPage}
tabindex="-1" tabindex="-1"
@ -106,66 +137,19 @@ class HaSidebar extends LitElement {
</paper-icon-item> </paper-icon-item>
</a> </a>
${computePanels(hass).map( ${panels.map((panel) => renderPanel(hass, panel))}
(panel) => html`
<a
href="${computeUrl(panel.url_path)}"
data-panel="${panel.url_path}"
tabindex="-1"
>
<paper-icon-item>
<ha-icon slot="item-icon" .icon="${panel.icon}"></ha-icon>
<span class="item-text"
>${hass.localize(`panel.${panel.title}`) || panel.title}</span
>
</paper-icon-item>
</a>
`
)}
${this._externalConfig && this._externalConfig.hasSettingsScreen
? html`
<a
aria-label="App Configuration"
href="#external-app-configuration"
tabindex="-1"
@click=${this._handleExternalAppConfiguration}
>
<paper-icon-item>
<ha-icon
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>
`
: ""}
${!hass.user
? html`
<paper-icon-item @click=${this._handleLogOut} class="logout">
<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>
`
: html``}
</paper-listbox>
${hass.user && hass.user.is_admin <div class="spacer" disabled></div>
? html`
<div>
<div class="divider"></div>
<div class="subheader"> ${this.expanded && hass.user && hass.user.is_admin
? html`
<div class="divider" disabled></div>
<div class="subheader" disabled>
${hass.localize("ui.sidebar.developer_tools")} ${hass.localize("ui.sidebar.developer_tools")}
</div> </div>
<div class="dev-tools"> <div class="dev-tools" disabled>
<a href="/dev-service" tabindex="-1"> <a href="/dev-service" tabindex="-1">
<paper-icon-button <paper-icon-button
icon="hass:remote" icon="hass:remote"
@ -213,14 +197,76 @@ class HaSidebar extends LitElement {
></paper-icon-button> ></paper-icon-button>
</a> </a>
</div> </div>
</div> <div class="divider" disabled></div>
` `
: ""} : ""}
${this._externalConfig && this._externalConfig.hasSettingsScreen
? html`
<a
aria-role="option"
aria-label="App Configuration"
href="#external-app-configuration"
tabindex="-1"
@click=${this._handleExternalAppConfiguration}
>
<paper-icon-item>
<ha-icon
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}
</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>
`}
</paper-listbox>
`; `;
} }
protected shouldUpdate(changedProps: PropertyValues): boolean { protected shouldUpdate(changedProps: PropertyValues): boolean {
if (changedProps.has("_externalConfig")) { if (
changedProps.has("_externalConfig") ||
changedProps.has("expanded") ||
changedProps.has("alwaysExpand")
) {
return true; return true;
} }
if (!this.hass || !changedProps.has("hass")) { if (!this.hass || !changedProps.has("hass")) {
@ -247,6 +293,26 @@ class HaSidebar extends LitElement {
this._externalConfig = conf; this._externalConfig = conf;
}); });
} }
this.shadowRoot!.querySelector("paper-listbox")!.addEventListener(
"mouseenter",
() => {
this.expanded = true;
}
);
this.addEventListener("mouseleave", () => {
this._contract();
});
}
protected updated(changedProps) {
super.updated(changedProps);
if (changedProps.has("alwaysExpand")) {
this.expanded = this.alwaysExpand;
}
}
private _contract() {
this.expanded = this.alwaysExpand || false;
} }
private _handleLogOut() { private _handleLogOut() {
@ -265,7 +331,7 @@ class HaSidebar extends LitElement {
:host { :host {
height: 100%; height: 100%;
display: block; display: block;
overflow: auto; overflow: hidden auto;
-ms-user-select: none; -ms-user-select: none;
-webkit-user-select: none; -webkit-user-select: none;
-moz-user-select: none; -moz-user-select: none;
@ -274,9 +340,27 @@ class HaSidebar extends LitElement {
--sidebar-background-color, --sidebar-background-color,
var(--primary-background-color) var(--primary-background-color)
); );
width: 64px;
transition: width 0.2s ease-in;
will-change: width;
contain: strict;
}
:host([expanded]) {
width: 256px;
}
.logo {
height: 65px;
box-sizing: border-box;
padding: 8px;
border-bottom: 1px solid transparent;
}
.logo img {
width: 48px;
} }
app-toolbar { app-toolbar {
white-space: nowrap;
font-weight: 400; font-weight: 400;
color: var(--primary-text-color); color: var(--primary-text-color);
border-bottom: 1px solid var(--divider-color); border-bottom: 1px solid var(--divider-color);
@ -288,7 +372,11 @@ class HaSidebar extends LitElement {
} }
paper-listbox { paper-listbox {
padding: 0; padding: 4px 0;
height: calc(100% - 65px);
display: flex;
flex-direction: column;
box-sizing: border-box;
} }
paper-listbox > a { paper-listbox > a {
@ -299,10 +387,15 @@ class HaSidebar extends LitElement {
} }
paper-icon-item { paper-icon-item {
margin: 8px; box-sizing: border-box;
padding-left: 9px; margin: 4px 8px;
padding-left: 12px;
border-radius: 4px; border-radius: 4px;
--paper-item-min-height: 40px; --paper-item-min-height: 40px;
width: 48px;
}
:host([expanded]) paper-icon-item {
width: 240px;
} }
ha-icon[slot="item-icon"] { ha-icon[slot="item-icon"] {
@ -342,10 +435,29 @@ class HaSidebar extends LitElement {
color: var(--sidebar-selected-text-color); color: var(--sidebar-selected-text-color);
} }
a .item-text {
display: none;
}
:host([expanded]) a .item-text {
display: block;
}
paper-icon-item.logout { paper-icon-item.logout {
margin-top: 16px; margin-top: 16px;
} }
paper-icon-item.profile {
padding-left: 4px;
}
.profile .item-text {
margin-left: 8px;
}
.spacer {
flex: 1;
pointer-events: none;
}
.divider { .divider {
height: 1px; height: 1px;
background-color: var(--divider-color); background-color: var(--divider-color);
@ -357,6 +469,7 @@ class HaSidebar extends LitElement {
font-weight: 500; font-weight: 500;
font-size: 14px; font-size: 14px;
padding: 16px; padding: 16px;
white-space: nowrap;
} }
.dev-tools { .dev-tools {
@ -364,6 +477,8 @@ class HaSidebar extends LitElement {
flex-direction: row; flex-direction: row;
justify-content: space-between; justify-content: space-between;
padding: 0 8px; padding: 0 8px;
width: 256px;
box-sizing: border-box;
} }
.dev-tools a { .dev-tools a {

View File

@ -14,6 +14,7 @@ class HaStartVoiceButton extends EventsMixin(PolymerElement) {
static get template() { static get template() {
return html` return html`
<paper-icon-button <paper-icon-button
aria-label="Start conversation"
icon="hass:microphone" icon="hass:microphone"
hidden$="[[!canListen]]" hidden$="[[!canListen]]"
on-click="handleListenClick" on-click="handleListenClick"

View File

@ -7,7 +7,6 @@ import {
property, property,
customElement, customElement,
} from "lit-element"; } from "lit-element";
import { classMap } from "lit-html/directives/class-map";
import { User } from "../../data/user"; import { User } from "../../data/user";
import { CurrentUser } from "../../types"; import { CurrentUser } from "../../types";
@ -33,24 +32,23 @@ class StateBadge extends LitElement {
protected render(): TemplateResult | void { protected render(): TemplateResult | void {
const user = this.user; const user = this.user;
const initials = user ? computeInitials(user.name) : "?"; const initials = user ? computeInitials(user.name) : "?";
return html` return html`
<div
class="${classMap({
"profile-badge": true,
long: initials.length > 2,
})}"
>
${initials} ${initials}
</div>
`; `;
} }
protected updated(changedProps) {
super.updated(changedProps);
this.toggleAttribute(
"long",
(this.user ? computeInitials(this.user.name) : "?").length > 2
);
}
static get styles(): CSSResult { static get styles(): CSSResult {
return css` return css`
.profile-badge { :host {
display: inline-block; display: inline-block;
box-sizing: border-box; box-sizing: border-box;
width: 40px; width: 40px;
@ -63,7 +61,7 @@ class StateBadge extends LitElement {
overflow: hidden; overflow: hidden;
} }
.profile-badge.long { :host([long]) {
font-size: 80%; font-size: 80%;
} }
`; `;

74
src/data/collection.ts Normal file
View File

@ -0,0 +1,74 @@
import {
Collection,
Connection,
getCollection,
UnsubscribeFunc,
} from "home-assistant-js-websocket";
import { Store } from "home-assistant-js-websocket/dist/store";
interface OptimisticCollection<T> extends Collection<T> {
save(data: T): Promise<unknown>;
}
/**
* Create an optimistic collection that includes a save function.
* When the collection is saved, the collection is optimistically updated.
* The update is reversed when the update failed.
*/
export const getOptimisticCollection = <StateType>(
saveCollection: (conn: Connection, data: StateType) => Promise<unknown>,
conn: Connection,
key: string,
fetchCollection: (conn: Connection) => Promise<StateType>,
subscribeUpdates?: (
conn: Connection,
store: Store<StateType>
) => Promise<UnsubscribeFunc>
): OptimisticCollection<StateType> => {
const updateKey = `${key}-optimistic`;
const collection = getCollection<StateType>(
conn,
key,
fetchCollection,
async (_conn, store) => {
// Subscribe to original updates
const subUpResult = subscribeUpdates
? subscribeUpdates(conn, store)
: undefined;
// Store the store
conn[updateKey] = store;
// Unsub function to undo both
return () => {
if (subUpResult) {
subUpResult.then((unsub) => unsub());
}
conn[updateKey] = undefined;
};
}
);
return {
...collection,
async save(data: StateType) {
const store: Store<StateType> | undefined = conn[updateKey];
let current;
// Can be undefined if currently no subscribers
if (store) {
current = store.state;
store.setState(data, true);
}
try {
return await saveCollection(conn, data);
} catch (err) {
if (store) {
store.setState(current as any, true);
}
throw err;
}
},
};
};

View File

@ -1,8 +1,14 @@
import { HomeAssistant } from "../types"; import { Connection } from "home-assistant-js-websocket";
import { getOptimisticCollection } from "./collection";
export interface CoreFrontendUserData {
showAdvanced?: boolean;
}
declare global { declare global {
// tslint:disable-next-line interface FrontendUserData {
interface FrontendUserData {} core: CoreFrontendUserData;
}
} }
export type ValidUserDataKey = keyof FrontendUserData; export type ValidUserDataKey = keyof FrontendUserData;
@ -10,10 +16,10 @@ export type ValidUserDataKey = keyof FrontendUserData;
export const fetchFrontendUserData = async < export const fetchFrontendUserData = async <
UserDataKey extends ValidUserDataKey UserDataKey extends ValidUserDataKey
>( >(
hass: HomeAssistant, conn: Connection,
key: UserDataKey key: UserDataKey
): Promise<FrontendUserData[UserDataKey] | null> => { ): Promise<FrontendUserData[UserDataKey] | null> => {
const result = await hass.callWS<{ const result = await conn.sendMessagePromise<{
value: FrontendUserData[UserDataKey] | null; value: FrontendUserData[UserDataKey] | null;
}>({ }>({
type: "frontend/get_user_data", type: "frontend/get_user_data",
@ -25,12 +31,31 @@ export const fetchFrontendUserData = async <
export const saveFrontendUserData = async < export const saveFrontendUserData = async <
UserDataKey extends ValidUserDataKey UserDataKey extends ValidUserDataKey
>( >(
hass: HomeAssistant, conn: Connection,
key: UserDataKey, key: UserDataKey,
value: FrontendUserData[UserDataKey] value: FrontendUserData[UserDataKey]
): Promise<void> => ): Promise<void> =>
hass.callWS<void>({ conn.sendMessagePromise<void>({
type: "frontend/set_user_data", type: "frontend/set_user_data",
key, key,
value, value,
}); });
export const getOptimisticFrontendUserDataCollection = <
UserDataKey extends ValidUserDataKey
>(
conn: Connection,
userDataKey: UserDataKey
) =>
getOptimisticCollection(
(_conn, data) =>
saveFrontendUserData(
conn,
userDataKey,
// @ts-ignore
data
),
conn,
`_frontendUserData-${userDataKey}`,
() => fetchFrontendUserData(conn, userDataKey)
);

View File

@ -12,12 +12,12 @@ declare global {
} }
export const fetchTranslationPreferences = (hass: HomeAssistant) => export const fetchTranslationPreferences = (hass: HomeAssistant) =>
fetchFrontendUserData(hass, "language"); fetchFrontendUserData(hass.connection, "language");
export const saveTranslationPreferences = ( export const saveTranslationPreferences = (
hass: HomeAssistant, hass: HomeAssistant,
data: FrontendTranslationData data: FrontendTranslationData
) => saveFrontendUserData(hass, "language", data); ) => saveFrontendUserData(hass.connection, "language", data);
export const getHassTranslations = async ( export const getHassTranslations = async (
hass: HomeAssistant, hass: HomeAssistant,

View File

@ -18,6 +18,8 @@ import "./partial-panel-resolver";
import { HomeAssistant, Route } from "../types"; import { HomeAssistant, Route } from "../types";
import { fireEvent } from "../common/dom/fire_event"; import { fireEvent } from "../common/dom/fire_event";
import { PolymerChangedEvent } from "../polymer-types"; import { PolymerChangedEvent } from "../polymer-types";
// tslint:disable-next-line: no-duplicate-imports
import { AppDrawerLayoutElement } from "@polymer/app-layout/app-drawer-layout/app-drawer-layout";
const NON_SWIPABLE_PANELS = ["kiosk", "map"]; const NON_SWIPABLE_PANELS = ["kiosk", "map"];
@ -29,9 +31,9 @@ declare global {
} }
class HomeAssistantMain extends LitElement { class HomeAssistantMain extends LitElement {
@property() public hass?: HomeAssistant; @property() public hass!: HomeAssistant;
@property() public route?: Route; @property() public route?: Route;
@property() private _narrow?: boolean; @property({ type: Boolean }) private narrow?: boolean;
protected render(): TemplateResult | void { protected render(): TemplateResult | void {
const hass = this.hass; const hass = this.hass;
@ -40,7 +42,8 @@ class HomeAssistantMain extends LitElement {
return; return;
} }
const disableSwipe = NON_SWIPABLE_PANELS.indexOf(hass.panelUrl) !== -1; const disableSwipe =
!this.narrow || NON_SWIPABLE_PANELS.indexOf(hass.panelUrl) !== -1;
return html` return html`
<iron-media-query <iron-media-query
@ -50,7 +53,7 @@ class HomeAssistantMain extends LitElement {
<app-drawer-layout <app-drawer-layout
fullbleed fullbleed
.forceNarrow=${this._narrow || !hass.dockedSidebar} .forceNarrow=${this.narrow}
responsive-width="0" responsive-width="0"
> >
<app-drawer <app-drawer
@ -59,13 +62,16 @@ class HomeAssistantMain extends LitElement {
slot="drawer" slot="drawer"
.disableSwipe=${disableSwipe} .disableSwipe=${disableSwipe}
.swipeOpen=${!disableSwipe} .swipeOpen=${!disableSwipe}
.persistent=${hass.dockedSidebar} .persistent=${!this.narrow}
> >
<ha-sidebar .hass=${hass}></ha-sidebar> <ha-sidebar
.hass=${hass}
.alwaysExpand=${this.narrow || hass.dockedSidebar}
></ha-sidebar>
</app-drawer> </app-drawer>
<partial-panel-resolver <partial-panel-resolver
.narrow=${this._narrow} .narrow=${this.narrow}
.hass=${hass} .hass=${hass}
.route=${this.route} .route=${this.route}
></partial-panel-resolver> ></partial-panel-resolver>
@ -77,19 +83,17 @@ class HomeAssistantMain extends LitElement {
import(/* webpackChunkName: "ha-sidebar" */ "../components/ha-sidebar"); import(/* webpackChunkName: "ha-sidebar" */ "../components/ha-sidebar");
this.addEventListener("hass-toggle-menu", () => { this.addEventListener("hass-toggle-menu", () => {
const shouldOpen = !this.drawer.opened; if (this.narrow) {
if (this.drawer.opened) {
if (shouldOpen) {
if (this._narrow) {
this.drawer.open();
} else {
fireEvent(this, "hass-dock-sidebar", { dock: true });
}
} else {
this.drawer.close(); this.drawer.close();
if (this.hass!.dockedSidebar) { } else {
fireEvent(this, "hass-dock-sidebar", { dock: false }); this.drawer.open();
} }
} else {
fireEvent(this, "hass-dock-sidebar", {
dock: !this.hass.dockedSidebar,
});
setTimeout(() => this.appLayout.resetLayout());
} }
}); });
} }
@ -97,7 +101,9 @@ class HomeAssistantMain extends LitElement {
protected updated(changedProps: PropertyValues) { protected updated(changedProps: PropertyValues) {
super.updated(changedProps); super.updated(changedProps);
if (changedProps.has("route") && this._narrow) { this.toggleAttribute("expanded", this.narrow || this.hass.dockedSidebar);
if (changedProps.has("route") && this.narrow) {
this.drawer.close(); this.drawer.close();
} }
@ -110,19 +116,27 @@ class HomeAssistantMain extends LitElement {
} }
private _narrowChanged(ev: PolymerChangedEvent<boolean>) { private _narrowChanged(ev: PolymerChangedEvent<boolean>) {
this._narrow = ev.detail.value; this.narrow = ev.detail.value;
} }
private get drawer(): AppDrawerElement { private get drawer(): AppDrawerElement {
return this.shadowRoot!.querySelector("app-drawer")!; return this.shadowRoot!.querySelector("app-drawer")!;
} }
private get appLayout(): AppDrawerLayoutElement {
return this.shadowRoot!.querySelector("app-drawer-layout")!;
}
static get styles(): CSSResult { static get styles(): CSSResult {
return css` return css`
:host { :host {
color: var(--primary-text-color); color: var(--primary-text-color);
/* remove the grey tap highlights in iOS on the fullscreen touch targets */ /* remove the grey tap highlights in iOS on the fullscreen touch targets */
-webkit-tap-highlight-color: rgba(0, 0, 0, 0); -webkit-tap-highlight-color: rgba(0, 0, 0, 0);
--app-drawer-width: 64px;
}
:host([expanded]) {
--app-drawer-width: 256px;
} }
partial-panel-resolver, partial-panel-resolver,
ha-sidebar { ha-sidebar {

View File

@ -37,6 +37,7 @@ class HaConfigCore extends LocalizeMixin(PolymerElement) {
<div class$="[[computeClasses(isWide)]]"> <div class$="[[computeClasses(isWide)]]">
<ha-config-section-core <ha-config-section-core
is-wide="[[isWide]]" is-wide="[[isWide]]"
show-advanced="[[showAdvanced]]"
hass="[[hass]]" hass="[[hass]]"
></ha-config-section-core> ></ha-config-section-core>
</div> </div>
@ -48,6 +49,7 @@ class HaConfigCore extends LocalizeMixin(PolymerElement) {
return { return {
hass: Object, hass: Object,
isWide: Boolean, isWide: Boolean,
showAdvanced: Boolean,
}; };
} }

View File

@ -63,6 +63,7 @@ class HaConfigSectionCore extends LocalizeMixin(PolymerElement) {
<ha-config-name-form hass="[[hass]]"></ha-config-name-form> <ha-config-name-form hass="[[hass]]"></ha-config-name-form>
<ha-config-core-form hass="[[hass]]"></ha-config-core-form> <ha-config-core-form hass="[[hass]]"></ha-config-core-form>
<template is="dom-if" if="[[showAdvanced]]">
<ha-card <ha-card
header="[[localize('ui.panel.config.core.section.core.validation.heading')]]" header="[[localize('ui.panel.config.core.section.core.validation.heading')]]"
> >
@ -135,7 +136,7 @@ class HaConfigSectionCore extends LocalizeMixin(PolymerElement) {
</ha-call-service-button> </ha-call-service-button>
</div> </div>
</ha-card> </ha-card>
</template>
<ha-card <ha-card
header="[[localize('ui.panel.config.core.section.core.server_management.heading')]]" header="[[localize('ui.panel.config.core.section.core.server_management.heading')]]"
> >
@ -188,6 +189,8 @@ class HaConfigSectionCore extends LocalizeMixin(PolymerElement) {
type: String, type: String,
value: "", value: "",
}, },
showAdvanced: Boolean,
}; };
} }

View File

@ -31,10 +31,17 @@ class HaConfigDashboard extends NavigateMixin(LocalizeMixin(PolymerElement)) {
.content { .content {
padding-bottom: 32px; padding-bottom: 32px;
} }
a { ha-card a {
text-decoration: none; text-decoration: none;
color: var(--primary-text-color); color: var(--primary-text-color);
} }
.promo-advanced {
text-align: center;
color: var(--secondary-text-color);
}
.promo-advanced a {
color: var(--secondary-text-color);
}
</style> </style>
<app-header-layout has-scrolling-region=""> <app-header-layout has-scrolling-region="">
@ -99,7 +106,16 @@ class HaConfigDashboard extends NavigateMixin(LocalizeMixin(PolymerElement)) {
</a> </a>
</ha-card> </ha-card>
<ha-config-navigation hass="[[hass]]"></ha-config-navigation> <ha-config-navigation
hass="[[hass]]"
show-advanced="[[showAdvanced]]"
></ha-config-navigation>
<template is='dom-if' if='[[!showAdvanced]]'>
<div class='promo-advanced'>
Missing config options? Enable advanced mode on <a href="/profile">your profile page.</a>
</div>
</template>
</ha-config-section> </ha-config-section>
</div> </div>
</app-header-layout> </app-header-layout>
@ -111,6 +127,7 @@ class HaConfigDashboard extends NavigateMixin(LocalizeMixin(PolymerElement)) {
hass: Object, hass: Object,
isWide: Boolean, isWide: Boolean,
cloudStatus: Object, cloudStatus: Object,
showAdvanced: Boolean,
}; };
} }

View File

@ -1,87 +0,0 @@
import "@polymer/iron-icon/iron-icon";
import "@polymer/paper-item/paper-item-body";
import "@polymer/paper-item/paper-item";
import { html } from "@polymer/polymer/lib/utils/html-tag";
import { PolymerElement } from "@polymer/polymer/polymer-element";
import NavigateMixin from "../../../mixins/navigate-mixin";
import LocalizeMixin from "../../../mixins/localize-mixin";
import isComponentLoaded from "../../../common/config/is_component_loaded";
import "../../../components/ha-card";
import "../../../components/ha-icon-next";
const CORE_PAGES = ["core", "customize", "entity_registry", "area_registry"];
/*
* @appliesMixin LocalizeMixin
* @appliesMixin NavigateMixin
*/
class HaConfigNavigation extends LocalizeMixin(NavigateMixin(PolymerElement)) {
static get template() {
return html`
<style include="iron-flex">
ha-card {
overflow: hidden;
}
paper-item {
cursor: pointer;
}
</style>
<ha-card>
<template is="dom-repeat" items="[[pages]]">
<template is="dom-if" if="[[_computeLoaded(hass, item)]]">
<paper-item on-click="_navigate">
<paper-item-body two-line="">
[[_computeCaption(item, localize)]]
<div secondary="">[[_computeDescription(item, localize)]]</div>
</paper-item-body>
<ha-icon-next></ha-icon-next>
</paper-item>
</template>
</template>
</ha-card>
`;
}
static get properties() {
return {
hass: {
type: Object,
},
pages: {
type: Array,
value: [
"core",
"person",
"entity_registry",
"area_registry",
"automation",
"script",
"zha",
"zwave",
"customize",
],
},
};
}
_computeLoaded(hass, page) {
return CORE_PAGES.includes(page) || isComponentLoaded(hass, page);
}
_computeCaption(page, localize) {
return localize(`ui.panel.config.${page}.caption`);
}
_computeDescription(page, localize) {
return localize(`ui.panel.config.${page}.description`);
}
_navigate(ev) {
this.navigate("/config/" + ev.model.item);
}
}
customElements.define("ha-config-navigation", HaConfigNavigation);

View File

@ -0,0 +1,82 @@
import "@polymer/iron-icon/iron-icon";
import "@polymer/paper-item/paper-item-body";
import "@polymer/paper-item/paper-item";
import isComponentLoaded from "../../../common/config/is_component_loaded";
import "../../../components/ha-card";
import "../../../components/ha-icon-next";
import {
LitElement,
html,
TemplateResult,
property,
customElement,
CSSResult,
css,
} from "lit-element";
import { HomeAssistant } from "../../../types";
const PAGES: Array<{
page: string;
core?: boolean;
advanced?: boolean;
}> = [
{ page: "core", core: true },
{ page: "person" },
{ page: "entity_registry", core: true },
{ page: "area_registry", core: true },
{ page: "automation" },
{ page: "script" },
{ page: "zha" },
{ page: "zwave" },
{ page: "customize", core: true, advanced: true },
];
@customElement("ha-config-navigation")
class HaConfigNavigation extends LitElement {
@property() public hass!: HomeAssistant;
@property() public showAdvanced!: boolean;
protected render(): TemplateResult | void {
return html`
<ha-card>
${PAGES.map(({ page, core, advanced }) =>
(core || isComponentLoaded(this.hass, page)) &&
(!advanced || this.showAdvanced)
? html`
<a href=${`/config/${page}`}>
<paper-item>
<paper-item-body two-line="">
${this.hass.localize(`ui.panel.config.${page}.caption`)}
<div secondary>
${this.hass.localize(
`ui.panel.config.${page}.description`
)}
</div>
</paper-item-body>
<ha-icon-next></ha-icon-next>
</paper-item>
</a>
`
: ""
)}
</ha-card>
`;
}
static get styles(): CSSResult {
return css`
a {
text-decoration: none;
color: var(--primary-text-color);
}
`;
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-config-navigation": HaConfigNavigation;
}
}

View File

@ -5,6 +5,10 @@ import { HomeAssistant } from "../../types";
import { CloudStatus, fetchCloudStatus } from "../../data/cloud"; import { CloudStatus, fetchCloudStatus } from "../../data/cloud";
import { listenMediaQuery } from "../../common/dom/media_query"; import { listenMediaQuery } from "../../common/dom/media_query";
import { HassRouterPage, RouterOptions } from "../../layouts/hass-router-page"; import { HassRouterPage, RouterOptions } from "../../layouts/hass-router-page";
import {
CoreFrontendUserData,
getOptimisticFrontendUserDataCollection,
} from "../../data/frontend";
declare global { declare global {
// for fire event // for fire event
@ -17,8 +21,6 @@ declare global {
class HaPanelConfig extends HassRouterPage { class HaPanelConfig extends HassRouterPage {
@property() public hass!: HomeAssistant; @property() public hass!: HomeAssistant;
@property() public narrow!: boolean; @property() public narrow!: boolean;
@property() public _wideSidebar: boolean = false;
@property() public _wide: boolean = false;
protected routerOptions: RouterOptions = { protected routerOptions: RouterOptions = {
defaultPage: "dashboard", defaultPage: "dashboard",
@ -93,6 +95,9 @@ class HaPanelConfig extends HassRouterPage {
}, },
}; };
@property() private _wideSidebar: boolean = false;
@property() private _wide: boolean = false;
@property() private _coreUserData?: CoreFrontendUserData;
@property() private _cloudStatus?: CloudStatus; @property() private _cloudStatus?: CloudStatus;
private _listeners: Array<() => void> = []; private _listeners: Array<() => void> = [];
@ -109,6 +114,14 @@ class HaPanelConfig extends HassRouterPage {
this._wideSidebar = matches; this._wideSidebar = matches;
}) })
); );
this._listeners.push(
getOptimisticFrontendUserDataCollection(
this.hass.connection,
"core"
).subscribe((coreUserData) => {
this._coreUserData = coreUserData || {};
})
);
} }
public disconnectedCallback() { public disconnectedCallback() {
@ -131,6 +144,7 @@ class HaPanelConfig extends HassRouterPage {
protected updatePageEl(el) { protected updatePageEl(el) {
el.route = this.routeTail; el.route = this.routeTail;
el.hass = this.hass; el.hass = this.hass;
el.showAdvanced = !!(this._coreUserData && this._coreUserData.showAdvanced);
el.isWide = this.hass.dockedSidebar ? this._wideSidebar : this._wide; el.isWide = this.hass.dockedSidebar ? this._wideSidebar : this._wide;
el.narrow = this.narrow; el.narrow = this.narrow;
el.cloudStatus = this._cloudStatus; el.cloudStatus = this._cloudStatus;

View File

@ -64,9 +64,14 @@ class HaPanelDevInfo extends LitElement {
<div class="content"> <div class="content">
<div class="about"> <div class="about">
<p class="version"> <p class="version">
<a href="https://www.home-assistant.io" target="_blank" <a href="https://www.home-assistant.io" target="_blank">
><img src="/static/icons/favicon-192x192.png" height="192"/></a <img
><br /> src="/static/icons/favicon-192x192.png"
height="192"
alt="Home Assistant logo"
/>
</a>
<br />
Home Assistant<br /> Home Assistant<br />
${hass.config.version} ${hass.config.version}
</p> </p>

View File

@ -41,6 +41,7 @@ class HuiEntitiesToggle extends LitElement {
return html` return html`
<paper-toggle-button <paper-toggle-button
aria-label="Toggle entities."
?checked="${this._toggleEntities!.some((entityId) => { ?checked="${this._toggleEntities!.some((entityId) => {
const stateObj = this.hass!.states[entityId]; const stateObj = this.hass!.states[entityId];
return stateObj && stateObj.state === "on"; return stateObj && stateObj.state === "on";

View File

@ -23,6 +23,7 @@ class HuiNotificationsButton extends LitElement {
protected render(): TemplateResult | void { protected render(): TemplateResult | void {
return html` return html`
<paper-icon-button <paper-icon-button
aria-label="Show Notifications"
icon="hass:bell" icon="hass:bell"
@click="${this._clicked}" @click="${this._clicked}"
></paper-icon-button> ></paper-icon-button>

View File

@ -144,6 +144,7 @@ class HUIRoot extends LitElement {
horizontal-offset="-5" horizontal-offset="-5"
> >
<paper-icon-button <paper-icon-button
aria-label="Open Lovelace menu"
icon="hass:dots-vertical" icon="hass:dots-vertical"
slot="dropdown-trigger" slot="dropdown-trigger"
></paper-icon-button> ></paper-icon-button>

View File

@ -0,0 +1,65 @@
import {
LitElement,
property,
TemplateResult,
html,
customElement,
CSSResult,
css,
} from "lit-element";
import "../../components/ha-card";
import { HomeAssistant } from "../../types";
import {
CoreFrontendUserData,
getOptimisticFrontendUserDataCollection,
} from "../../data/frontend";
@customElement("ha-advanced-mode-card")
class AdvancedModeCard extends LitElement {
@property() public hass!: HomeAssistant;
@property() public coreUserData?: CoreFrontendUserData;
protected render(): TemplateResult | void {
return html`
<ha-card>
<div class="card-header">
<div class="title">Advanced mode</div>
<paper-toggle-button
.checked=${this.coreUserData && this.coreUserData.showAdvanced}
.disabled=${this.coreUserData === undefined}
@change=${this._advancedToggled}
></paper-toggle-button>
</div>
<div class="card-content">
Home Assistant hides advanced features and options by default. You can
make these features accessible by checking this toggle. This is a
user-specific setting and does not impact other users using Home
Assistant.
</div>
</ha-card>
`;
}
private async _advancedToggled(ev) {
getOptimisticFrontendUserDataCollection(this.hass.connection, "core").save({
showAdvanced: ev.currentTarget.checked,
});
}
static get styles(): CSSResult {
return css`
.card-header {
display: flex;
}
.title {
flex: 1;
}
`;
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-advanced-mode-card": AdvancedModeCard;
}
}

View File

@ -11,11 +11,14 @@ import "../../components/ha-card";
import "../../components/ha-menu-button"; import "../../components/ha-menu-button";
import "../../resources/ha-style"; import "../../resources/ha-style";
import { getOptimisticFrontendUserDataCollection } from "../../data/frontend";
import { EventsMixin } from "../../mixins/events-mixin"; import { EventsMixin } from "../../mixins/events-mixin";
import LocalizeMixin from "../../mixins/localize-mixin"; import LocalizeMixin from "../../mixins/localize-mixin";
import "./ha-change-password-card"; import "./ha-change-password-card";
import "./ha-mfa-modules-card"; import "./ha-mfa-modules-card";
import "./ha-advanced-mode-card";
import "./ha-refresh-tokens-card"; import "./ha-refresh-tokens-card";
import "./ha-long-lived-access-tokens-card"; import "./ha-long-lived-access-tokens-card";
@ -98,6 +101,11 @@ class HaPanelProfile extends EventsMixin(LocalizeMixin(PolymerElement)) {
mfa-modules="[[hass.user.mfa_modules]]" mfa-modules="[[hass.user.mfa_modules]]"
></ha-mfa-modules-card> ></ha-mfa-modules-card>
<ha-advanced-mode-card
hass="[[hass]]"
core-user-data="[[_coreUserData]]"
></ha-mfa-modules-card>
<ha-refresh-tokens-card <ha-refresh-tokens-card
hass="[[hass]]" hass="[[hass]]"
refresh-tokens="[[_refreshTokens]]" refresh-tokens="[[_refreshTokens]]"
@ -119,12 +127,27 @@ class HaPanelProfile extends EventsMixin(LocalizeMixin(PolymerElement)) {
hass: Object, hass: Object,
narrow: Boolean, narrow: Boolean,
_refreshTokens: Array, _refreshTokens: Array,
_coreUserData: Object,
}; };
} }
connectedCallback() { connectedCallback() {
super.connectedCallback(); super.connectedCallback();
this._refreshRefreshTokens(); this._refreshRefreshTokens();
this._unsubCoreData = getOptimisticFrontendUserDataCollection(
this.hass.connection,
"core"
).subscribe((coreUserData) => {
this._coreUserData = coreUserData;
});
}
disconnectedCallback() {
super.disconnectedCallback();
if (this._unsubCoreData) {
this._unsubCoreData();
this._unsubCoreData = undefined;
}
} }
async _refreshRefreshTokens() { async _refreshRefreshTokens() {

View File

@ -44,7 +44,7 @@ export const connectionMixin = (
localize: () => "", localize: () => "",
translationMetadata, translationMetadata,
dockedSidebar: false, dockedSidebar: true,
moreInfoEntityId: null, moreInfoEntityId: null,
callService: async (domain, service, serviceData = {}) => { callService: async (domain, service, serviceData = {}) => {
if (__DEV__) { if (__DEV__) {

View File

@ -9,9 +9,11 @@ import { HassBaseEl } from "./hass-base-mixin";
import { computeLocalize } from "../common/translations/localize"; import { computeLocalize } from "../common/translations/localize";
import { computeRTL } from "../common/util/compute_rtl"; import { computeRTL } from "../common/util/compute_rtl";
import { HomeAssistant } from "../types"; import { HomeAssistant } from "../types";
import { saveFrontendUserData } from "../data/frontend";
import { storeState } from "../util/ha-pref-storage"; import { storeState } from "../util/ha-pref-storage";
import { getHassTranslations } from "../data/translation"; import {
getHassTranslations,
saveTranslationPreferences,
} from "../data/translation";
/* /*
* superClass needs to contain `this.hass` and `this._updateHass`. * superClass needs to contain `this.hass` and `this._updateHass`.
@ -65,7 +67,7 @@ export default (superClass: Constructor<LitElement & HassBaseEl>) =>
this._updateHass({ language, selectedLanguage: language }); this._updateHass({ language, selectedLanguage: language });
storeState(this.hass); storeState(this.hass);
if (saveToBackend) { if (saveToBackend) {
saveFrontendUserData(this.hass, "language", { language }); saveTranslationPreferences(this.hass, { language });
} }
this._applyTranslations(this.hass); this._applyTranslations(this.hass);

View File

@ -1,6 +1,6 @@
import { translationMetadata } from "../resources/translations-metadata"; import { translationMetadata } from "../resources/translations-metadata";
import { fetchFrontendUserData } from "../data/frontend";
import { HomeAssistant } from "../types"; import { HomeAssistant } from "../types";
import { fetchTranslationPreferences } from "../data/translation";
const STORAGE = window.localStorage || {}; const STORAGE = window.localStorage || {};
@ -43,7 +43,7 @@ function findAvailableLanguage(language: string) {
* Get user selected language from backend * Get user selected language from backend
*/ */
export async function getUserLanguage(hass: HomeAssistant) { export async function getUserLanguage(hass: HomeAssistant) {
const result = await fetchFrontendUserData(hass, "language"); const result = await fetchTranslationPreferences(hass);
const language = result ? result.language : null; const language = result ? result.language : null;
if (language) { if (language) {
const availableLanguage = findAvailableLanguage(language); const availableLanguage = findAvailableLanguage(language);

View File

@ -338,7 +338,7 @@
"validation": { "validation": {
"heading": "Validació de la configuració", "heading": "Validació de la configuració",
"introduction": "Valida la configuració si recentment has fet algun canvi a la configuració i vols assegurar-te de que no té problemes.", "introduction": "Valida la configuració si recentment has fet algun canvi a la configuració i vols assegurar-te de que no té problemes.",
"check_config": "Comprovar la configuració", "check_config": "Comprova la configuració",
"valid": "La configuració és vàlida!", "valid": "La configuració és vàlida!",
"invalid": "La configuració és invàlida" "invalid": "La configuració és invàlida"
}, },
@ -353,8 +353,8 @@
"server_management": { "server_management": {
"heading": "Gestió del servidor", "heading": "Gestió del servidor",
"introduction": "Controla el servidor de Home Assistant... des de Home Assistant.", "introduction": "Controla el servidor de Home Assistant... des de Home Assistant.",
"restart": "Reiniciar", "restart": "Reinicia",
"stop": "Aturar" "stop": "Atura"
}, },
"core_config": { "core_config": {
"edit_requires_storage": "L'editor està desactivat ja que la configuració es troba a configuration.yaml.", "edit_requires_storage": "L'editor està desactivat ja que la configuració es troba a configuration.yaml.",

View File

@ -3,13 +3,14 @@
"config": "Stillingar", "config": "Stillingar",
"states": "Yfirlit", "states": "Yfirlit",
"map": "Kort", "map": "Kort",
"logbook": "Logbók", "logbook": "Breytingarsaga",
"history": "Saga", "history": "Saga",
"mailbox": "Pósthólf", "mailbox": "Pósthólf",
"shopping_list": "Innkaupalisti", "shopping_list": "Innkaupalisti",
"dev-services": "Þjónustur", "dev-services": "Þjónustur",
"dev-states": "Stöður", "dev-states": "Stöður",
"dev-events": "Viðburðir", "dev-events": "Viðburðir",
"dev-templates": "Skapalón",
"dev-mqtt": "MQTT", "dev-mqtt": "MQTT",
"dev-info": "Upplýsingar", "dev-info": "Upplýsingar",
"calendar": "Dagatal", "calendar": "Dagatal",
@ -75,6 +76,10 @@
"off": "Aftengdur", "off": "Aftengdur",
"on": "Tengdur" "on": "Tengdur"
}, },
"cold": {
"off": "Venjulegt",
"on": "Kalt"
},
"door": { "door": {
"off": "Lokuð", "off": "Lokuð",
"on": "Opin" "on": "Opin"
@ -83,6 +88,10 @@
"off": "Lokuð", "off": "Lokuð",
"on": "Opin" "on": "Opin"
}, },
"heat": {
"off": "Venjulegt",
"on": "Heitt"
},
"window": { "window": {
"off": "Loka", "off": "Loka",
"on": "Opna" "on": "Opna"
@ -301,6 +310,7 @@
"stop": "Stöðva" "stop": "Stöðva"
}, },
"core_config": { "core_config": {
"edit_requires_storage": "Ritill er óvirkur af því að stillingar eru vistaðar í configuration.yaml.",
"location_name": "Nafnið á Home Assistant uppsetningunni", "location_name": "Nafnið á Home Assistant uppsetningunni",
"latitude": "Breiddargráða", "latitude": "Breiddargráða",
"longitude": "Lengdargráða", "longitude": "Lengdargráða",
@ -475,7 +485,9 @@
} }
}, },
"learn_more": "Læra meira um aðgerðir" "learn_more": "Læra meira um aðgerðir"
} },
"load_error_not_editable": "Eingöngu er hægt að breyta sjálfvirkni í automations.yaml",
"load_error_unknown": "Villa kom upp við að hlaða inn sjálfvirkni ({err_no})."
} }
}, },
"script": { "script": {
@ -537,6 +549,7 @@
}, },
"config_flow": { "config_flow": {
"external_step": { "external_step": {
"description": "Þetta skref krefst þess að þú heimsækir ytri vefsíðu svo hægt sé að ljúka þessu skrefi.",
"open_site": "Opna vefsíðu" "open_site": "Opna vefsíðu"
} }
} }
@ -785,6 +798,7 @@
"finish": "Ljúka" "finish": "Ljúka"
}, },
"core-config": { "core-config": {
"intro": "Hæ {name}, velkomin(n) í Home Assistant. Hvað á heimilið þitt að heita?",
"location_name_default": "Heima", "location_name_default": "Heima",
"button_detect": "Uppgötva", "button_detect": "Uppgötva",
"finish": "Næsta" "finish": "Næsta"
@ -869,12 +883,14 @@
"cards": { "cards": {
"demo": { "demo": {
"demo_by": "eftir {name}", "demo_by": "eftir {name}",
"next_demo": "Næsta sýnidæmi",
"learn_more": "Læra meira um Home Assistant" "learn_more": "Læra meira um Home Assistant"
} }
}, },
"config": { "config": {
"arsaboo": { "arsaboo": {
"names": { "names": {
"family_room": "Fjölskyldurými",
"kitchen": "Eldhús", "kitchen": "Eldhús",
"left": "Vinstri", "left": "Vinstri",
"right": "Hægri", "right": "Hægri",
@ -885,7 +901,10 @@
"information": "Upplýsingar", "information": "Upplýsingar",
"entertainment": "Skemmtun", "entertainment": "Skemmtun",
"activity": "Virkni", "activity": "Virkni",
"hdmi_input": "HDMI inntak",
"hdmi_switcher": "HDMI rofi",
"volume": "Hljóðstyrkur", "volume": "Hljóðstyrkur",
"total_tv_time": "Heildar sjónvarpstími",
"turn_tv_off": "Slökkva á sjónvarpi" "turn_tv_off": "Slökkva á sjónvarpi"
}, },
"unit": { "unit": {
@ -985,6 +1004,7 @@
"currently": "Er núna", "currently": "Er núna",
"on_off": "Kveikt \/ slökkt", "on_off": "Kveikt \/ slökkt",
"operation": "Aðgerð", "operation": "Aðgerð",
"fan_mode": "Viftuhamur",
"swing_mode": "Sveifluhamur" "swing_mode": "Sveifluhamur"
}, },
"lock": { "lock": {
@ -1104,6 +1124,7 @@
"updater": "Uppfærsluálfur", "updater": "Uppfærsluálfur",
"weblink": "Vefslóð", "weblink": "Vefslóð",
"zwave": "Z-Wave", "zwave": "Z-Wave",
"vacuum": "Ryksuga",
"zha": "ZHA", "zha": "ZHA",
"hassio": "Hass.io", "hassio": "Hass.io",
"homeassistant": "Home Assistant", "homeassistant": "Home Assistant",

View File

@ -849,7 +849,7 @@
"data": { "data": {
"code": "Kod uwierzytelniania dwuskładnikowego" "code": "Kod uwierzytelniania dwuskładnikowego"
}, },
"description": "Otwórz **{mfa_module_name}** na swoim urządzeniu by zobaczyć kod dwuskładnikowego uwierzytelniania i zweryfikować swoją toższamość:" "description": "Otwórz **{mfa_module_name}** na swoim urządzeniu by zobaczyć kod dwuskładnikowego uwierzytelniania i zweryfikować swoją tożsamość:"
} }
}, },
"error": { "error": {