Merge pull request #8448 from home-assistant/dev

Co-authored-by: Bram Kragten <mail@bramkragten.nl>
Co-authored-by: Philip Allgaier <mail@spacegaier.de>
Co-authored-by: J. Nick Koston <nick@koston.org>
Co-authored-by: GitHub Action <github-action@users.noreply.github.com>
Co-authored-by: Joakim Sørensen <joasoe@gmail.com>
Co-authored-by: Álvaro Fernández Rojas <noltari@gmail.com>
Co-authored-by: Marc Mueller <30130371+cdce8p@users.noreply.github.com>
Co-authored-by: Kendell R <KTibow@users.noreply.github.com>
Co-authored-by: larena1 <60823161+larena1@users.noreply.github.com>
This commit is contained in:
Bram Kragten 2021-02-24 20:36:51 +01:00 committed by GitHub
commit c85f69c9ee
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
75 changed files with 1816 additions and 677 deletions

View File

@ -23,7 +23,7 @@ class HassioRouter extends HassRouterPage {
protected routerOptions: RouterOptions = {
// Hass.io has a page with tabs, so we route all non-matching routes to it.
defaultPage: "dashboard",
initialLoad: () => this._fetchData(),
initialLoad: () => this._redirectIngress(),
showLoading: true,
routes: {
dashboard: {
@ -50,7 +50,13 @@ class HassioRouter extends HassRouterPage {
protected updatePageEl(el) {
// the tabs page does its own routing so needs full route.
const route = el.nodeName === "HASSIO-PANEL" ? this.route : this.routeTail;
const hassioPanel = el.nodeName === "HASSIO-PANEL";
const route = hassioPanel ? this.route : this.routeTail;
if (hassioPanel && this.panel.config?.ingress) {
this._redirectIngress();
return;
}
el.hass = this.hass;
el.narrow = this.narrow;
@ -63,15 +69,14 @@ class HassioRouter extends HassRouterPage {
}
}
private async _fetchData() {
private async _redirectIngress() {
if (this.panel.config && this.panel.config.ingress) {
this._redirectIngress(this.panel.config.ingress);
this.route = {
prefix: "/hassio",
path: `/ingress/${this.panel.config.ingress}`,
};
}
}
private _redirectIngress(addonSlug: string) {
this.route = { prefix: "/hassio", path: `/ingress/${addonSlug}` };
}
}
declare global {

View File

@ -23,6 +23,14 @@
"license": "Apache-2.0",
"dependencies": {
"@braintree/sanitize-url": "^5.0.0",
"@codemirror/commands": "^0.17.2",
"@codemirror/gutter": "^0.17.2",
"@codemirror/highlight": "^0.17.2",
"@codemirror/legacy-modes": "^0.17.1",
"@codemirror/state": "^0.17.1",
"@codemirror/stream-parser": "^0.17.1",
"@codemirror/text": "^0.17.2",
"@codemirror/view": "^0.17.7",
"@formatjs/intl-getcanonicallocales": "^1.4.6",
"@formatjs/intl-pluralrules": "^3.4.10",
"@fullcalendar/common": "5.1.0",
@ -177,7 +185,7 @@
"eslint": "^6.8.0",
"eslint-config-airbnb-typescript": "^7.2.1",
"eslint-config-prettier": "^6.10.1",
"eslint-import-resolver-webpack": "^0.12.2",
"eslint-import-resolver-webpack": "^0.13.0",
"eslint-plugin-disable": "^2.0.1",
"eslint-plugin-import": "^2.20.2",
"eslint-plugin-lit": "^1.2.0",
@ -213,16 +221,16 @@
"sinon": "^7.3.1",
"source-map-url": "^0.4.0",
"systemjs": "^6.3.2",
"terser-webpack-plugin": "^5.0.0",
"terser-webpack-plugin": "^5.1.1",
"ts-lit-plugin": "^1.2.1",
"ts-mocha": "^7.0.0",
"typescript": "^4.0.3",
"vinyl-buffer": "^1.0.1",
"vinyl-source-stream": "^2.0.0",
"webpack": "5.1.3",
"webpack-cli": "4.1.0",
"webpack-dev-server": "^3.11.0",
"webpack-manifest-plugin": "~3.0.0",
"webpack": "^5.24.1",
"webpack-cli": "^4.5.0",
"webpack-dev-server": "^3.11.2",
"webpack-manifest-plugin": "^3.0.0",
"workbox-build": "^5.1.3"
},
"_comment": "Polymer fixed to 3.1 because 3.2 throws on logbook page",

View File

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

View File

@ -5,11 +5,15 @@ export const atLeastVersion = (
patch?: number
): boolean => {
const [haMajor, haMinor, haPatch] = version.split(".", 3);
return (
Number(haMajor) > major ||
(Number(haMajor) === major && Number(haMinor) >= minor) ||
(Number(haMajor) === major && (patch === undefined
? Number(haMinor) >= minor
: Number(haMinor) > minor)) ||
(patch !== undefined &&
Number(haMajor) === major && Number(haMinor) === minor &&
Number(haMajor) === major &&
Number(haMinor) === minor &&
Number(haPatch) >= patch)
);
};

View File

@ -8,12 +8,19 @@ export const batteryIcon = (
const battery = Number(batteryState.state);
const battery_charging =
batteryChargingState && batteryChargingState.state === "on";
let icon = "hass:battery";
if (isNaN(battery)) {
return "hass:battery-unknown";
if (batteryState.state === "off") {
icon += "-full";
} else if (batteryState.state === "on") {
icon += "-alert";
} else {
icon += "-unknown";
}
return icon;
}
let icon = "hass:battery";
const batteryRound = Math.round(battery / 10) * 10;
if (battery_charging && battery > 10) {
icon += `-charging-${batteryRound}`;

View File

@ -15,7 +15,7 @@ export const iconColorCSS = css`
ha-icon[data-domain="media_player"][data-state="on"],
ha-icon[data-domain="media_player"][data-state="paused"],
ha-icon[data-domain="media_player"][data-state="playing"],
ha-icon[data-domain="script"][data-state="running"],
ha-icon[data-domain="script"][data-state="on"],
ha-icon[data-domain="sun"][data-state="above_horizon"],
ha-icon[data-domain="switch"][data-state="on"],
ha-icon[data-domain="timer"][data-state="active"],

View File

@ -115,7 +115,7 @@ export class StateBadge extends LitElement {
// eslint-disable-next-line
console.warn(errorMessage);
}
// lowest brighntess will be around 50% (that's pretty dark)
// lowest brightness will be around 50% (that's pretty dark)
iconStyle.filter = `brightness(${(brightness + 245) / 5}%)`;
}
}

View File

@ -0,0 +1,148 @@
import {
customElement,
html,
internalProperty,
LitElement,
property,
query,
TemplateResult,
} from "lit-element";
import { isComponentLoaded } from "../common/config/is_component_loaded";
import { fireEvent } from "../common/dom/fire_event";
import { compare } from "../common/string/compare";
import { HassioAddonInfo } from "../data/hassio/addon";
import { fetchHassioSupervisorInfo } from "../data/hassio/supervisor";
import { showAlertDialog } from "../dialogs/generic/show-dialog-box";
import { PolymerChangedEvent } from "../polymer-types";
import { HomeAssistant } from "../types";
import { HaComboBox } from "./ha-combo-box";
const rowRenderer = (
root: HTMLElement,
_owner,
model: { item: HassioAddonInfo }
) => {
if (!root.firstElementChild) {
root.innerHTML = `
<style>
paper-item {
margin: -10px 0;
padding: 0;
}
</style>
<paper-item>
<paper-item-body two-line="">
<div class='name'>[[item.name]]</div>
<div secondary>[[item.slug]]</div>
</paper-item-body>
</paper-item>
`;
}
root.querySelector(".name")!.textContent = model.item.name;
root.querySelector("[secondary]")!.textContent = model.item.slug;
};
@customElement("ha-addon-picker")
class HaAddonPicker extends LitElement {
public hass!: HomeAssistant;
@property() public label?: string;
@property() public value = "";
@internalProperty() private _addons?: HassioAddonInfo[];
@property({ type: Boolean }) public disabled = false;
@query("ha-combo-box") private _comboBox!: HaComboBox;
public open() {
this._comboBox?.open();
}
public focus() {
this._comboBox?.focus();
}
protected firstUpdated() {
this._getAddons();
}
protected render(): TemplateResult {
if (!this._addons) {
return html``;
}
return html`
<ha-combo-box
.hass=${this.hass}
.label=${this.label === undefined && this.hass
? this.hass.localize("ui.components.addon-picker.addon")
: this.label}
.value=${this._value}
.renderer=${rowRenderer}
.items=${this._addons}
item-value-path="slug"
item-id-path="slug"
item-label-path="name"
@value-changed=${this._addonChanged}
></ha-combo-box>
`;
}
private async _getAddons() {
try {
if (isComponentLoaded(this.hass, "hassio")) {
const supervisorInfo = await fetchHassioSupervisorInfo(this.hass);
this._addons = supervisorInfo.addons.sort((a, b) =>
compare(a.name, b.name)
);
} else {
showAlertDialog(this, {
title: this.hass.localize(
"ui.componencts.addon-picker.error.no_supervisor.title"
),
text: this.hass.localize(
"ui.componencts.addon-picker.error.no_supervisor.description"
),
});
}
} catch (error) {
showAlertDialog(this, {
title: this.hass.localize(
"ui.componencts.addon-picker.error.fetch_addons.title"
),
text: this.hass.localize(
"ui.componencts.addon-picker.error.fetch_addons.description"
),
});
}
}
private get _value() {
return this.value || "";
}
private _addonChanged(ev: PolymerChangedEvent<string>) {
ev.stopPropagation();
const newValue = ev.detail.value;
if (newValue !== this._value) {
this._setValue(newValue);
}
}
private _setValue(value: string) {
this.value = value;
setTimeout(() => {
fireEvent(this, "value-changed", { value });
fireEvent(this, "change");
}, 0);
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-addon-picker": HaAddonPicker;
}
}

View File

@ -140,7 +140,7 @@ export class HaAreaPicker extends SubscribeMixin(LitElement) {
this._devices = devices;
}),
subscribeEntityRegistry(this.hass.connection!, (entities) => {
this._entities = entities;
this._entities = entities.filter((entity) => entity.area_id);
}),
];
}
@ -193,11 +193,14 @@ export class HaAreaPicker extends SubscribeMixin(LitElement) {
deviceEntityLookup[entity.device_id].push(entity);
}
inputDevices = devices;
inputEntities = entities.filter((entity) => entity.area_id);
} else if (deviceFilter) {
inputDevices = devices;
} else if (entityFilter) {
inputEntities = entities.filter((entity) => entity.area_id);
inputEntities = entities;
} else {
if (deviceFilter) {
inputDevices = devices;
}
if (entityFilter) {
inputEntities = entities;
}
}
if (includeDomains) {

View File

@ -1,4 +1,5 @@
import { Editor } from "codemirror";
import type { StreamLanguage } from "@codemirror/stream-parser";
import type { EditorView, KeyBinding, ViewUpdate } from "@codemirror/view";
import {
customElement,
internalProperty,
@ -15,32 +16,40 @@ declare global {
}
}
const modeTag = Symbol("mode");
const readOnlyTag = Symbol("readOnly");
const saveKeyBinding: KeyBinding = {
key: "Mod-s",
run: (view: EditorView) => {
fireEvent(view.dom, "editor-save");
return true;
},
};
@customElement("ha-code-editor")
export class HaCodeEditor extends UpdatingElement {
public codemirror?: Editor;
public codemirror?: EditorView;
@property() public mode?: string;
@property() public mode = "yaml";
@property({ type: Boolean }) public autofocus = false;
@property({ type: Boolean }) public readOnly = false;
@property() public rtl = false;
@property() public error = false;
@internalProperty() private _value = "";
@internalProperty() private _langs?: Record<string, StreamLanguage<unknown>>;
public set value(value: string) {
this._value = value;
}
public get value(): string {
return this.codemirror ? this.codemirror.getValue() : this._value;
}
public get hasComments(): boolean {
return !!this.shadowRoot!.querySelector("span.cm-comment");
return this.codemirror ? this.codemirror.state.doc.toString() : this._value;
}
public connectedCallback() {
@ -48,7 +57,6 @@ export class HaCodeEditor extends UpdatingElement {
if (!this.codemirror) {
return;
}
this.codemirror.refresh();
if (this.autofocus !== false) {
this.codemirror.focus();
}
@ -62,17 +70,27 @@ export class HaCodeEditor extends UpdatingElement {
}
if (changedProps.has("mode")) {
this.codemirror.setOption("mode", this.mode);
this.codemirror.dispatch({
reconfigure: {
[modeTag]: this._mode,
},
});
}
if (changedProps.has("autofocus")) {
this.codemirror.setOption("autofocus", this.autofocus !== false);
if (changedProps.has("readOnly")) {
this.codemirror.dispatch({
reconfigure: {
[readOnlyTag]: !this.readOnly,
},
});
}
if (changedProps.has("_value") && this._value !== this.value) {
this.codemirror.setValue(this._value);
}
if (changedProps.has("rtl")) {
this.codemirror.setOption("gutters", this._calcGutters());
this._setScrollBarDirection();
this.codemirror.dispatch({
changes: {
from: 0,
to: this.codemirror.state.doc.length,
insert: this._value,
},
});
}
if (changedProps.has("error")) {
this.classList.toggle("error-state", this.error);
@ -85,159 +103,62 @@ export class HaCodeEditor extends UpdatingElement {
this._load();
}
private get _mode() {
return this._langs![this.mode];
}
private async _load(): Promise<void> {
const loaded = await loadCodeMirror();
const codeMirror = loaded.codeMirror;
this._langs = loaded.langs;
const shadowRoot = this.attachShadow({ mode: "open" });
shadowRoot!.innerHTML = `
<style>
${loaded.codeMirrorCss}
.CodeMirror {
height: var(--code-mirror-height, auto);
direction: var(--code-mirror-direction, ltr);
font-family: var(--code-font-family, monospace);
}
.CodeMirror-scroll {
max-height: var(--code-mirror-max-height, --code-mirror-height);
}
:host(.error-state) .CodeMirror-gutters {
shadowRoot!.innerHTML = `<style>
:host(.error-state) div.cm-wrap .cm-gutters {
border-color: var(--error-state-color, red);
}
.CodeMirror-focused .CodeMirror-gutters {
border-right: 2px solid var(--paper-input-container-focus-color, var(--primary-color));
}
.CodeMirror-linenumber {
color: var(--paper-dialog-color, var(--secondary-text-color));
}
.rtl .CodeMirror-vscrollbar {
right: auto;
left: 0px;
}
.rtl-gutter {
width: 20px;
}
.CodeMirror-gutters {
border-right: 1px solid var(--paper-input-container-color, var(--secondary-text-color));
background-color: var(--paper-dialog-background-color, var(--primary-background-color));
transition: 0.2s ease border-right;
}
.cm-s-default.CodeMirror {
background-color: var(--code-editor-background-color, var(--card-background-color));
color: var(--primary-text-color);
}
.cm-s-default .CodeMirror-cursor {
border-left: 1px solid var(--secondary-text-color);
}
.cm-s-default div.CodeMirror-selected, .cm-s-default.CodeMirror-focused div.CodeMirror-selected {
background: rgba(var(--rgb-primary-color), 0.2);
}
.cm-s-default .CodeMirror-line::selection,
.cm-s-default .CodeMirror-line>span::selection,
.cm-s-default .CodeMirror-line>span>span::selection {
background: rgba(var(--rgb-primary-color), 0.2);
}
.cm-s-default .cm-keyword {
color: var(--codemirror-keyword, #6262FF);
}
.cm-s-default .cm-operator {
color: var(--codemirror-operator, #cda869);
}
.cm-s-default .cm-variable-2 {
color: var(--codemirror-variable-2, #690);
}
.cm-s-default .cm-builtin {
color: var(--codemirror-builtin, #9B7536);
}
.cm-s-default .cm-atom {
color: var(--codemirror-atom, #F90);
}
.cm-s-default .cm-number {
color: var(--codemirror-number, #ca7841);
}
.cm-s-default .cm-def {
color: var(--codemirror-def, #8DA6CE);
}
.cm-s-default .cm-string {
color: var(--codemirror-string, #07a);
}
.cm-s-default .cm-string-2 {
color: var(--codemirror-string-2, #bd6b18);
}
.cm-s-default .cm-comment {
color: var(--codemirror-comment, #777);
}
.cm-s-default .cm-variable {
color: var(--codemirror-variable, #07a);
}
.cm-s-default .cm-tag {
color: var(--codemirror-tag, #997643);
}
.cm-s-default .cm-meta {
color: var(--codemirror-meta, var(--primary-text-color));
}
.cm-s-default .cm-attribute {
color: var(--codemirror-attribute, #d6bb6d);
}
.cm-s-default .cm-property {
color: var(--codemirror-property, #905);
}
.cm-s-default .cm-qualifier {
color: var(--codemirror-qualifier, #690);
}
.cm-s-default .cm-variable-3 {
color: var(--codemirror-variable-3, #07a);
}
.cm-s-default .cm-type {
color: var(--codemirror-type, #07a);
}
</style>`;
this.codemirror = codeMirror(shadowRoot, {
value: this._value,
lineNumbers: true,
tabSize: 2,
mode: this.mode,
autofocus: this.autofocus !== false,
viewportMargin: Infinity,
readOnly: this.readOnly,
extraKeys: {
Tab: "indentMore",
"Shift-Tab": "indentLess",
},
gutters: this._calcGutters(),
const container = document.createElement("span");
shadowRoot.appendChild(container);
this.codemirror = new loaded.EditorView({
state: loaded.EditorState.create({
doc: this._value,
extensions: [
loaded.lineNumbers(),
loaded.keymap.of([
...loaded.defaultKeymap,
loaded.defaultTabBinding,
saveKeyBinding,
]),
loaded.tagExtension(modeTag, this._mode),
loaded.theme,
loaded.Prec.fallback(loaded.highlightStyle),
loaded.EditorView.updateListener.of((update) =>
this._onUpdate(update)
),
loaded.tagExtension(
readOnlyTag,
loaded.EditorView.editable.of(!this.readOnly)
),
],
}),
root: shadowRoot,
parent: container,
});
this._setScrollBarDirection();
this.codemirror!.on("changes", () => this._onChange());
}
private _blockKeyboardShortcuts() {
this.addEventListener("keydown", (ev) => ev.stopPropagation());
}
private _onChange(): void {
private _onUpdate(update: ViewUpdate): void {
if (!update.docChanged) {
return;
}
const newValue = this.value;
if (newValue === this._value) {
return;
@ -245,16 +166,6 @@ export class HaCodeEditor extends UpdatingElement {
this._value = newValue;
fireEvent(this, "value-changed", { value: this._value });
}
private _calcGutters(): string[] {
return this.rtl ? ["rtl-gutter", "CodeMirror-linenumbers"] : [];
}
private _setScrollBarDirection(): void {
if (this.codemirror) {
this.codemirror.getWrapperElement().classList.toggle("rtl", this.rtl);
}
}
}
declare global {

View File

@ -0,0 +1,30 @@
import { customElement, html, LitElement, property } from "lit-element";
import { AddonSelector } from "../../data/selector";
import { HomeAssistant } from "../../types";
import "../ha-addon-picker";
@customElement("ha-selector-addon")
export class HaAddonSelector extends LitElement {
@property() public hass!: HomeAssistant;
@property() public selector!: AddonSelector;
@property() public value?: any;
@property() public label?: string;
protected render() {
return html`<ha-addon-picker
.hass=${this.hass}
.value=${this.value}
.label=${this.label}
allow-custom-entity
></ha-addon-picker>`;
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-selector-addon": HaAddonSelector;
}
}

View File

@ -3,6 +3,7 @@ import { dynamicElement } from "../../common/dom/dynamic-element-directive";
import { Selector } from "../../data/selector";
import { HomeAssistant } from "../../types";
import "./ha-selector-action";
import "./ha-selector-addon";
import "./ha-selector-area";
import "./ha-selector-boolean";
import "./ha-selector-device";

View File

@ -79,6 +79,10 @@ class HaSlider extends PaperSliderClass {
return subTemplate;
}
_setImmediateValue(newImmediateValue) {
super._setImmediateValue(Math.round(newImmediateValue));
}
_calcStep(value) {
if (!this.step) {
return parseFloat(value);

View File

@ -5,20 +5,10 @@ import {
internalProperty,
LitElement,
property,
query,
TemplateResult,
} from "lit-element";
import { fireEvent } from "../common/dom/fire_event";
import { afterNextRender } from "../common/util/render-status";
import "./ha-code-editor";
import type { HaCodeEditor } from "./ha-code-editor";
declare global {
// for fire event
interface HASSDomEvents {
"editor-refreshed": undefined;
}
}
const isEmpty = (obj: Record<string, unknown>): boolean => {
if (typeof obj !== "object") {
@ -44,8 +34,6 @@ export class HaYamlEditor extends LitElement {
@internalProperty() private _yaml = "";
@query("ha-code-editor") private _editor?: HaCodeEditor;
public setValue(value): void {
try {
this._yaml = value && !isEmpty(value) ? safeDump(value) : "";
@ -54,12 +42,6 @@ export class HaYamlEditor extends LitElement {
console.error(err, value);
alert(`There was an error converting to YAML: ${err}`);
}
afterNextRender(() => {
if (this._editor?.codemirror) {
this._editor.codemirror.refresh();
}
afterNextRender(() => fireEvent(this, "editor-refreshed"));
});
}
protected firstUpdated(): void {

View File

@ -6,6 +6,7 @@ import {
html,
LitElement,
property,
PropertyValues,
TemplateResult,
} from "lit-element";
import "./state-history-chart-line";
@ -83,6 +84,10 @@ class StateHistoryCharts extends LitElement {
`;
}
protected shouldUpdate(changedProps: PropertyValues): boolean {
return !(changedProps.size === 1 && changedProps.has("hass"));
}
private _isHistoryEmpty(): boolean {
const historyDataEmpty =
!this.historyData ||

View File

@ -9,6 +9,7 @@ export interface ConfigEntry {
connection_class: string;
supports_options: boolean;
supports_unload: boolean;
disabled_by: string | null;
}
export interface ConfigEntryMutableParams {
@ -43,6 +44,27 @@ export const reloadConfigEntry = (hass: HomeAssistant, configEntryId: string) =>
require_restart: boolean;
}>("POST", `config/config_entries/entry/${configEntryId}/reload`);
export const disableConfigEntry = (
hass: HomeAssistant,
configEntryId: string
) =>
hass.callWS<{
require_restart: boolean;
}>({
type: "config_entries/disable",
entry_id: configEntryId,
disabled_by: "user",
});
export const enableConfigEntry = (hass: HomeAssistant, configEntryId: string) =>
hass.callWS<{
require_restart: boolean;
}>({
type: "config_entries/disable",
entry_id: configEntryId,
disabled_by: null,
});
export const getConfigEntrySystemOptions = (
hass: HomeAssistant,
configEntryId: string

View File

@ -1,4 +1,5 @@
export type Selector =
| AddonSelector
| EntitySelector
| DeviceSelector
| AreaSelector
@ -30,6 +31,13 @@ export interface DeviceSelector {
};
}
export interface AddonSelector {
addon: {
name?: string;
slug?: string;
};
}
export interface AreaSelector {
area: {
entity?: {

View File

@ -52,6 +52,7 @@ class MoreInfoFan extends LocalizeMixin(EventsMixin(PolymerElement)) {
caption="[[localize('ui.card.fan.speed')]]"
min="0"
max="100"
step="[[computePercentageStepSize(stateObj)]]"
value="{{percentageSliderValue}}"
on-change="percentageChanged"
pin=""
@ -113,7 +114,7 @@ class MoreInfoFan extends LocalizeMixin(EventsMixin(PolymerElement)) {
<ha-attributes
state-obj="[[stateObj]]"
extra-filters="speed,preset_mode,preset_modes,speed_list,percentage,oscillating,direction"
extra-filters="percentage_step,speed,preset_mode,preset_modes,speed_list,percentage,oscillating,direction"
></ha-attributes>
`;
}
@ -154,6 +155,13 @@ class MoreInfoFan extends LocalizeMixin(EventsMixin(PolymerElement)) {
}
}
computePercentageStepSize(stateObj) {
if (stateObj.attributes.percentage_step) {
return stateObj.attributes.percentage_step;
}
return 1;
}
computeClassNames(stateObj) {
return (
"more-info-fan " +

View File

@ -12,13 +12,14 @@ import {
import { ifDefined } from "lit-html/directives/if-defined";
import memoizeOne from "memoize-one";
import { isComponentLoaded } from "../../../common/config/is_component_loaded";
import { computeStateDomain } from "../../../common/entity/compute_state_domain";
import { computeStateName } from "../../../common/entity/compute_state_name";
import { compare } from "../../../common/string/compare";
import { slugify } from "../../../common/string/slugify";
import "../../../components/entity/ha-battery-icon";
import "../../../components/ha-icon-next";
import { AreaRegistryEntry } from "../../../data/area_registry";
import { ConfigEntry } from "../../../data/config_entries";
import { ConfigEntry, disableConfigEntry } from "../../../data/config_entries";
import {
computeDeviceName,
DeviceRegistryEntry,
@ -160,6 +161,8 @@ export class HaConfigDevicePage extends LitElement {
const batteryState = batteryEntity
? this.hass.states[batteryEntity.entity_id]
: undefined;
const batteryIsBinary = batteryState
&& computeStateDomain(batteryState) === "binary_sensor";
const batteryChargingState = batteryChargingEntity
? this.hass.states[batteryChargingEntity.entity_id]
: undefined;
@ -215,7 +218,7 @@ export class HaConfigDevicePage extends LitElement {
batteryState
? html`
<div class="battery">
${batteryState.state}%
${batteryIsBinary ? "" : batteryState.state + "%"}
<ha-battery-icon
.hass=${this.hass!}
.batteryStateObj=${batteryState}
@ -261,11 +264,13 @@ export class HaConfigDevicePage extends LitElement {
)}
</p>
</div>
<div class="card-actions" slot="actions">
<mwc-button unelevated @click=${this._enableDevice}>
${this.hass.localize("ui.common.enable")}
</mwc-button>
</div>
${device.disabled_by === "user"
? html` <div class="card-actions" slot="actions">
<mwc-button unelevated @click=${this._enableDevice}>
${this.hass.localize("ui.common.enable")}
</mwc-button>
</div>`
: ""}
`
: html``
}
@ -626,6 +631,41 @@ export class HaConfigDevicePage extends LitElement {
updateEntry: async (updates) => {
const oldDeviceName = device.name_by_user || device.name;
const newDeviceName = updates.name_by_user;
const disabled =
updates.disabled_by === "user" && device.disabled_by !== "user";
if (disabled) {
for (const cnfg_entry of device.config_entries) {
if (
!this.devices.some(
(dvc) =>
dvc.id !== device.id &&
dvc.config_entries.includes(cnfg_entry)
)
) {
const config_entry = this.entries.find(
(entry) => entry.entry_id === cnfg_entry
);
if (
config_entry &&
!config_entry.disabled_by &&
// eslint-disable-next-line no-await-in-loop
(await showConfirmationDialog(this, {
title: this.hass.localize(
"ui.panel.config.devices.confirm_disable_config_entry",
"entry_name",
config_entry.title
),
confirmText: this.hass.localize("ui.common.yes"),
dismissText: this.hass.localize("ui.common.no"),
}))
) {
disableConfigEntry(this.hass, cnfg_entry);
delete updates.disabled_by;
}
}
}
}
await updateDeviceRegistryEntry(this.hass, this.deviceId, updates);
if (

View File

@ -15,6 +15,7 @@ import {
import { classMap } from "lit-html/directives/class-map";
import memoizeOne from "memoize-one";
import { HASSDomEvent } from "../../../common/dom/fire_event";
import { computeStateDomain } from "../../../common/entity/compute_state_domain";
import { navigate } from "../../../common/navigate";
import { LocalizeFunc } from "../../../common/translations/localize";
import { computeRTL } from "../../../common/util/compute_rtl";
@ -293,9 +294,11 @@ export class HaConfigDeviceDashboard extends LitElement {
batteryEntityPair && batteryEntityPair[1]
? this.hass.states[batteryEntityPair[1]]
: undefined;
return battery && !isNaN(battery.state as any)
const batteryIsBinary =
battery && computeStateDomain(battery) === "binary_sensor";
return battery && (batteryIsBinary || !isNaN(battery.state as any))
? html`
${battery.state}%
${batteryIsBinary ? "" : battery.state + "%"}
<ha-battery-icon
.hass=${this.hass!}
.batteryStateObj=${battery}

View File

@ -1,4 +1,5 @@
import "@material/mwc-icon-button";
import { ActionDetail } from "@material/mwc-list";
import "@material/mwc-list/mwc-list-item";
import { mdiDotsVertical, mdiPlus } from "@mdi/js";
import "@polymer/app-route/app-route";
@ -122,6 +123,8 @@ class HaConfigIntegrations extends SubscribeMixin(LitElement) {
@internalProperty() private _showIgnored = false;
@internalProperty() private _showDisabled = false;
@internalProperty() private _searchParms = new URLSearchParams(
window.location.hash.substring(1)
);
@ -181,18 +184,30 @@ class HaConfigIntegrations extends SubscribeMixin(LitElement) {
(
configEntries: ConfigEntryExtended[],
filter?: string
): [Map<string, ConfigEntryExtended[]>, ConfigEntryExtended[]] => {
): [
Map<string, ConfigEntryExtended[]>,
ConfigEntryExtended[],
Map<string, ConfigEntryExtended[]>
] => {
const filteredConfigEnties = this._filterConfigEntries(
configEntries,
filter
);
const ignored: ConfigEntryExtended[] = [];
const disabled: ConfigEntryExtended[] = [];
for (let i = filteredConfigEnties.length - 1; i >= 0; i--) {
if (filteredConfigEnties[i].source === "ignore") {
ignored.push(filteredConfigEnties.splice(i, 1)[0]);
}
if (filteredConfigEnties[i].disabled_by !== null) {
disabled.push(filteredConfigEnties.splice(i, 1)[0]);
}
}
return [groupByIntegration(filteredConfigEnties), ignored];
return [
groupByIntegration(filteredConfigEnties),
ignored,
groupByIntegration(disabled),
];
}
);
@ -254,6 +269,7 @@ class HaConfigIntegrations extends SubscribeMixin(LitElement) {
const [
groupedConfigEntries,
ignoredConfigEntries,
disabledConfigEntries,
] = this._filterGroupConfigEntries(this._configEntries, this._filter);
const configEntriesInProgress = this._filterConfigEntriesInProgress(
this._configEntriesInProgress,
@ -289,7 +305,7 @@ class HaConfigIntegrations extends SubscribeMixin(LitElement) {
<ha-button-menu
corner="BOTTOM_START"
slot="toolbar-icon"
@action=${this._toggleShowIgnored}
@action=${this._handleMenuAction}
>
<mwc-icon-button
.title=${this.hass.localize("ui.common.menu")}
@ -305,6 +321,13 @@ class HaConfigIntegrations extends SubscribeMixin(LitElement) {
: "ui.panel.config.integrations.ignore.show_ignored"
)}
</mwc-list-item>
<mwc-list-item>
${this.hass.localize(
this._showDisabled
? "ui.panel.config.integrations.disable.hide_disabled"
: "ui.panel.config.integrations.disable.show_disabled"
)}
</mwc-list-item>
</ha-button-menu>
${!this.narrow
@ -319,6 +342,20 @@ class HaConfigIntegrations extends SubscribeMixin(LitElement) {
"ui.panel.config.integrations.search"
)}
></search-input>
${!this._showDisabled && disabledConfigEntries.size
? html`<div class="active-filters">
${this.hass.localize(
"ui.panel.config.integrations.disable.disabled_integrations",
"number",
disabledConfigEntries.size
)}
<mwc-button @click=${this._toggleShowDisabled}
>${this.hass.localize(
"ui.panel.config.filtering.show"
)}</mwc-button
>
</div>`
: ""}
</div>
`
: ""}
@ -433,6 +470,21 @@ class HaConfigIntegrations extends SubscribeMixin(LitElement) {
}
)
: ""}
${this._showDisabled
? Array.from(disabledConfigEntries.entries()).map(
([domain, items]) =>
html`<ha-integration-card
data-domain=${domain}
disabled
.hass=${this.hass}
.domain=${domain}
.items=${items}
.manifest=${this._manifests[domain]}
.entityRegistryEntries=${this._entityRegistryEntries}
.deviceRegistryEntries=${this._deviceRegistryEntries}
></ha-integration-card> `
)
: ""}
${groupedConfigEntries.size
? Array.from(groupedConfigEntries.entries()).map(
([domain, items]) =>
@ -596,10 +648,25 @@ class HaConfigIntegrations extends SubscribeMixin(LitElement) {
getConfigFlowInProgressCollection(this.hass.connection).refresh();
}
private _handleMenuAction(ev: CustomEvent<ActionDetail>) {
switch (ev.detail.index) {
case 0:
this._toggleShowIgnored();
break;
case 1:
this._toggleShowDisabled();
break;
}
}
private _toggleShowIgnored() {
this._showIgnored = !this._showIgnored;
}
private _toggleShowDisabled() {
this._showDisabled = !this._showDisabled;
}
private async _removeIgnoredIntegration(ev: Event) {
const entry = (ev.target! as any).entry;
showConfirmationDialog(this, {
@ -767,11 +834,14 @@ class HaConfigIntegrations extends SubscribeMixin(LitElement) {
margin-left: 16px;
}
.search {
display: flex;
align-items: center;
padding: 0 16px;
background: var(--sidebar-background-color);
border-bottom: 1px solid var(--divider-color);
}
.search search-input {
flex: 1;
position: relative;
top: 2px;
}
@ -796,6 +866,32 @@ class HaConfigIntegrations extends SubscribeMixin(LitElement) {
text-overflow: ellipsis;
white-space: normal;
}
.active-filters {
color: var(--primary-text-color);
position: relative;
display: flex;
align-items: center;
padding: 2px 2px 2px 8px;
margin-left: 4px;
font-size: 14px;
}
.active-filters ha-icon {
color: var(--primary-color);
}
.active-filters mwc-button {
margin-left: 8px;
}
.active-filters::before {
background-color: var(--primary-color);
opacity: 0.12;
border-radius: 4px;
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
content: "";
}
`,
];
}

View File

@ -9,12 +9,15 @@ import {
property,
TemplateResult,
} from "lit-element";
import { classMap } from "lit-html/directives/class-map";
import { fireEvent } from "../../../common/dom/fire_event";
import { shouldHandleRequestSelectedEvent } from "../../../common/mwc/handle-request-selected-event";
import "../../../components/ha-icon-next";
import {
ConfigEntry,
deleteConfigEntry,
disableConfigEntry,
enableConfigEntry,
reloadConfigEntry,
updateConfigEntry,
} from "../../../data/config_entries";
@ -88,6 +91,8 @@ export class HaIntegrationCard extends LitElement {
@property() public selectedConfigEntryId?: string;
@property({ type: Boolean }) public disabled = false;
firstUpdated(changedProps) {
super.firstUpdated(changedProps);
}
@ -109,7 +114,14 @@ export class HaIntegrationCard extends LitElement {
private _renderGroupedIntegration(): TemplateResult {
return html`
<ha-card outlined class="group">
<ha-card outlined class="group ${classMap({ disabled: this.disabled })}">
${this.disabled
? html`<div class="header">
${this.hass.localize(
"ui.panel.config.integrations.config_entry.disable.disabled"
)}
</div>`
: ""}
<div class="group-header">
<img
src=${brandsUrl(this.domain, "icon")}
@ -148,7 +160,9 @@ export class HaIntegrationCard extends LitElement {
return html`
<ha-card
outlined
class="single integration"
class="single integration ${classMap({
disabled: Boolean(item.disabled_by),
})}"
.configEntry=${item}
.id=${item.entry_id}
>
@ -159,6 +173,17 @@ export class HaIntegrationCard extends LitElement {
@click=${this._back}
></ha-icon-button>`
: ""}
${item.disabled_by
? html`<div class="header">
${this.hass.localize(
"ui.panel.config.integrations.config_entry.disable.disabled_cause",
"cause",
this.hass.localize(
`ui.panel.config.integrations.config_entry.disable.disabled_by.${item.disabled_by}`
) || item.disabled_by
)}
</div>`
: ""}
<div class="card-content">
<div class="image">
<img
@ -222,11 +247,16 @@ export class HaIntegrationCard extends LitElement {
</div>
<div class="card-actions">
<div>
<mwc-button @click=${this._editEntryName}
>${this.hass.localize(
${item.disabled_by === "user"
? html`<mwc-button unelevated @click=${this._handleEnable}>
${this.hass.localize("ui.common.enable")}
</mwc-button>`
: ""}
<mwc-button @click=${this._editEntryName}>
${this.hass.localize(
"ui.panel.config.integrations.config_entry.rename"
)}</mwc-button
>
)}
</mwc-button>
${item.domain in integrationsWithPanel
? html`<a
href=${`${
@ -279,13 +309,25 @@ export class HaIntegrationCard extends LitElement {
</mwc-list-item>
</a>
`}
${item.state === "loaded" && item.supports_unload
${!item.disabled_by &&
item.state === "loaded" &&
item.supports_unload
? html`<mwc-list-item @request-selected="${this._handleReload}">
${this.hass.localize(
"ui.panel.config.integrations.config_entry.reload"
)}
</mwc-list-item>`
: ""}
${item.disabled_by === "user"
? html`<mwc-list-item @request-selected="${this._handleEnable}">
${this.hass.localize("ui.common.enable")}
</mwc-list-item>`
: html`<mwc-list-item
class="warning"
@request-selected="${this._handleDisable}"
>
${this.hass.localize("ui.common.disable")}
</mwc-list-item>`}
<mwc-list-item
class="warning"
@request-selected="${this._handleDelete}"
@ -370,6 +412,24 @@ export class HaIntegrationCard extends LitElement {
);
}
private _handleDisable(ev: CustomEvent<RequestSelectedDetail>): void {
if (!shouldHandleRequestSelectedEvent(ev)) {
return;
}
this._disableIntegration(
((ev.target as HTMLElement).closest("ha-card") as any).configEntry
);
}
private _handleEnable(ev: CustomEvent<RequestSelectedDetail>): void {
if (ev.detail.source && !shouldHandleRequestSelectedEvent(ev)) {
return;
}
this._enableIntegration(
((ev.target as HTMLElement).closest("ha-card") as any).configEntry
);
}
private _handleSystemOptions(ev: CustomEvent<RequestSelectedDetail>): void {
if (!shouldHandleRequestSelectedEvent(ev)) {
return;
@ -385,6 +445,48 @@ export class HaIntegrationCard extends LitElement {
});
}
private async _disableIntegration(configEntry: ConfigEntry) {
const entryId = configEntry.entry_id;
const confirmed = await showConfirmationDialog(this, {
text: this.hass.localize(
"ui.panel.config.integrations.config_entry.disable.disable_confirm"
),
});
if (!confirmed) {
return;
}
const result = await disableConfigEntry(this.hass, entryId);
if (result.require_restart) {
showAlertDialog(this, {
text: this.hass.localize(
"ui.panel.config.integrations.config_entry.disable_restart_confirm"
),
});
}
fireEvent(this, "entry-updated", {
entry: { ...configEntry, disabled_by: "user" },
});
}
private async _enableIntegration(configEntry: ConfigEntry) {
const entryId = configEntry.entry_id;
const result = await enableConfigEntry(this.hass, entryId);
if (result.require_restart) {
showAlertDialog(this, {
text: this.hass.localize(
"ui.panel.config.integrations.config_entry.enable_restart_confirm"
),
});
}
fireEvent(this, "entry-updated", {
entry: { ...configEntry, disabled_by: null },
});
}
private async _removeIntegration(configEntry: ConfigEntry) {
const entryId = configEntry.entry_id;
@ -397,31 +499,29 @@ export class HaIntegrationCard extends LitElement {
if (!confirmed) {
return;
}
deleteConfigEntry(this.hass, entryId).then((result) => {
fireEvent(this, "entry-removed", { entryId });
const result = await deleteConfigEntry(this.hass, entryId);
fireEvent(this, "entry-removed", { entryId });
if (result.require_restart) {
showAlertDialog(this, {
text: this.hass.localize(
"ui.panel.config.integrations.config_entry.restart_confirm"
),
});
}
});
if (result.require_restart) {
showAlertDialog(this, {
text: this.hass.localize(
"ui.panel.config.integrations.config_entry.restart_confirm"
),
});
}
}
private async _reloadIntegration(configEntry: ConfigEntry) {
const entryId = configEntry.entry_id;
reloadConfigEntry(this.hass, entryId).then((result) => {
const locale_key = result.require_restart
? "reload_restart_confirm"
: "reload_confirm";
showAlertDialog(this, {
text: this.hass.localize(
`ui.panel.config.integrations.config_entry.${locale_key}`
),
});
const result = await reloadConfigEntry(this.hass, entryId);
const locale_key = result.require_restart
? "reload_restart_confirm"
: "reload_confirm";
showAlertDialog(this, {
text: this.hass.localize(
`ui.panel.config.integrations.config_entry.${locale_key}`
),
});
}
@ -461,6 +561,15 @@ export class HaIntegrationCard extends LitElement {
:host(.highlight) ha-card {
border: 1px solid var(--accent-color);
}
.disabled {
--ha-card-border-color: var(--warning-color);
}
.disabled .header {
background: var(--warning-color);
color: var(--text-primary-color);
padding: 8px;
text-align: center;
}
.card-content {
padding: 16px;
text-align: center;

View File

@ -11,6 +11,7 @@ import memoizeOne from "memoize-one";
import { LocalStorage } from "../../../common/decorators/local-storage";
import { computeDomain } from "../../../common/entity/compute_domain";
import { computeObjectId } from "../../../common/entity/compute_object_id";
import { extractSearchParam } from "../../../common/url/search-params";
import "../../../components/buttons/ha-progress-button";
import "../../../components/entity/ha-entity-picker";
import "../../../components/ha-card";
@ -40,7 +41,19 @@ class HaPanelDevService extends LitElement {
protected firstUpdated(params) {
super.firstUpdated(params);
if (!this._serviceData?.service) {
const serviceParam = extractSearchParam("service");
if (serviceParam) {
this._serviceData = {
service: serviceParam,
target: {},
data: {},
};
if (this._yamlMode) {
this.updateComplete.then(() =>
this._yamlEditor?.setValue(this._serviceData)
);
}
} else if (!this._serviceData?.service) {
const domain = Object.keys(this.hass.services).sort()[0];
const service = Object.keys(this.hass.services[domain]).sort()[0];
this._serviceData = {
@ -48,6 +61,11 @@ class HaPanelDevService extends LitElement {
target: {},
data: {},
};
if (this._yamlMode) {
this.updateComplete.then(() =>
this._yamlEditor?.setValue(this._serviceData)
);
}
}
}
@ -140,7 +158,10 @@ class HaPanelDevService extends LitElement {
)}
</th>
</tr>
${fields.map(
${(this._yamlMode
? fields
: this._filterSelectorFields(fields)
).map(
(field) => html` <tr>
<td><pre>${field.key}</pre></td>
<td>${field.description}</td>

View File

@ -1,5 +1,8 @@
import "@material/mwc-button";
import { mdiInformationOutline } from "@mdi/js";
import {
mdiInformationOutline,
mdiClipboardTextMultipleOutline
} from "@mdi/js";
import "@polymer/paper-checkbox/paper-checkbox";
import "@polymer/paper-input/paper-input";
import { html } from "@polymer/polymer/lib/utils/html-tag";
@ -15,6 +18,7 @@ import { showAlertDialog } from "../../../dialogs/generic/show-dialog-box";
import { EventsMixin } from "../../../mixins/events-mixin";
import LocalizeMixin from "../../../mixins/localize-mixin";
import "../../../styles/polymer-ha-style";
import { copyToClipboard } from "../../../common/util/copy-clipboard";
const ERROR_SENTINEL = {};
/*
@ -165,7 +169,7 @@ class HaPanelDevState extends EventsMixin(LocalizeMixin(PolymerElement)) {
<th>[[localize('ui.panel.developer-tools.tabs.states.state')]]</th>
<th hidden$="[[narrow]]">
[[localize('ui.panel.developer-tools.tabs.states.attributes')]]
<paper-checkbox checked="{{_showAttributes}}"></paper-checkbox>
<paper-checkbox checked="{{_showAttributes}}" on-change="{{saveAttributeCheckboxState}}"></paper-checkbox>
</th>
</tr>
<tr>
@ -205,6 +209,12 @@ class HaPanelDevState extends EventsMixin(LocalizeMixin(PolymerElement)) {
title="[[localize('ui.panel.developer-tools.tabs.states.more_info')]]"
path="[[informationOutlineIcon()]]"
></ha-svg-icon>
<ha-svg-icon
on-click="copyEntity"
alt="[[localize('ui.panel.developer-tools.tabs.states.copy_id')]]"
title="[[localize('ui.panel.developer-tools.tabs.states.copy_id')]]"
path="[[clipboardOutlineIcon()]]"
></ha-svg-icon>
<a href="#" on-click="entitySelected">[[entity.entity_id]]</a>
</td>
<td>
@ -275,7 +285,7 @@ class HaPanelDevState extends EventsMixin(LocalizeMixin(PolymerElement)) {
_showAttributes: {
type: Boolean,
value: true,
value: JSON.parse(localStorage.getItem("devToolsShowAttributes") || true),
},
_entities: {
@ -296,6 +306,11 @@ class HaPanelDevState extends EventsMixin(LocalizeMixin(PolymerElement)) {
};
}
copyEntity(ev) {
ev.preventDefault();
copyToClipboard(ev.model.entity.entity_id);
}
entitySelected(ev) {
const state = ev.model.entity;
this._entityId = state.entity_id;
@ -345,6 +360,10 @@ class HaPanelDevState extends EventsMixin(LocalizeMixin(PolymerElement)) {
return mdiInformationOutline;
}
clipboardOutlineIcon() {
return mdiClipboardTextMultipleOutline;
}
computeEntities(hass, _entityFilter, _stateFilter, _attributeFilter) {
return Object.keys(hass.states)
.map(function (key) {
@ -459,6 +478,14 @@ class HaPanelDevState extends EventsMixin(LocalizeMixin(PolymerElement)) {
return Array.isArray(value) ? value.join(", ") : value;
}
saveAttributeCheckboxState(ev) {
try {
localStorage.setItem("devToolsShowAttributes", ev.target.checked);
} catch (e) {
// Catch for Safari private mode
}
}
_computeParsedStateAttributes(stateAttributes) {
try {
return stateAttributes.trim() ? safeLoad(stateAttributes) : {};

View File

@ -49,10 +49,15 @@ export class HuiActionEditor extends LitElement {
return config.url_path || "";
}
get _service(): string {
const config = this.config as CallServiceActionConfig;
return config.service || "";
}
private _serviceAction = memoizeOne(
(config: CallServiceActionConfig): ServiceAction => {
return {
service: config.service || "",
service: this._service,
data: config.service_data,
target: config.target,
};
@ -155,8 +160,25 @@ export class HuiActionEditor extends LitElement {
}
return;
}
let data;
switch (value) {
case "url": {
data = { url_path: this._url_path };
break;
}
case "call-service": {
data = { service: this._service };
break;
}
case "navigate": {
data = { navigation_path: this._navigation_path };
break;
}
}
fireEvent(this, "value-changed", {
value: { action: value },
value: { action: value, ...data },
});
}

View File

@ -282,7 +282,7 @@ export class HuiDialogEditCard extends LitElement
}
private _opened() {
this._cardEditorEl?.refreshYamlEditor();
this._cardEditorEl?.focusYamlEditor();
}
private get _canSave(): boolean {

View File

@ -61,8 +61,8 @@ export class HuiConditionalCardEditor extends LitElement
this._config = config;
}
public refreshYamlEditor(focus) {
this._cardEditorEl?.refreshYamlEditor(focus);
public focusYamlEditor() {
this._cardEditorEl?.focusYamlEditor();
}
protected render(): TemplateResult {

View File

@ -54,8 +54,8 @@ export class HuiStackCardEditor extends LitElement
this._config = config;
}
public refreshYamlEditor(focus) {
this._cardEditorEl?.refreshYamlEditor(focus);
public focusYamlEditor() {
this._cardEditorEl?.focusYamlEditor();
}
protected render(): TemplateResult {

View File

@ -157,17 +157,14 @@ export abstract class HuiElementEditor<T> extends LitElement {
this.GUImode = !this.GUImode;
}
public refreshYamlEditor(focus = false) {
if (this._configElement?.refreshYamlEditor) {
this._configElement.refreshYamlEditor(focus);
public focusYamlEditor() {
if (this._configElement?.focusYamlEditor) {
this._configElement.focusYamlEditor();
}
if (!this._yamlEditor?.codemirror) {
return;
}
this._yamlEditor.codemirror.refresh();
if (focus) {
this._yamlEditor.codemirror.focus();
}
this._yamlEditor.codemirror.focus();
}
protected async getConfigElement(): Promise<
@ -290,7 +287,7 @@ export abstract class HuiElementEditor<T> extends LitElement {
if (this._configElementType !== this.configElementType) {
// If the type has changed, we need to load a new GUI editor
this._guiSupported = false;
this._guiSupported = undefined;
this._configElement = undefined;
if (!this.configElementType) {

View File

@ -120,13 +120,13 @@ const actionConfigStructConfirmation = union([
const actionConfigStructUrl = object({
action: literal("url"),
url_path: optional(string()),
url_path: string(),
confirmation: optional(actionConfigStructConfirmation),
});
const actionConfigStructService = object({
action: literal("call-service"),
service: optional(string()),
service: string(),
service_data: optional(object()),
target: optional(
object({
@ -140,7 +140,7 @@ const actionConfigStructService = object({
const actionConfigStructNavigate = object({
action: literal("navigate"),
navigation_path: optional(string()),
navigation_path: string(),
confirmation: optional(actionConfigStructConfirmation),
});

View File

@ -54,7 +54,8 @@ class HuiScriptEntityRow extends LitElement implements LovelaceRow {
<hui-generic-entity-row .hass=${this.hass} .config=${this._config}>
${stateObj.state === "on"
? html`<mwc-button @click=${this._cancelScript}>
${(stateObj.attributes.current || 0) > 0
${stateObj.attributes.mode !== "single" &&
(stateObj.attributes.current || 0) > 0
? this.hass.localize(
"ui.card.script.cancel_multiple",
"number",

View File

@ -29,6 +29,8 @@ export interface WeblinkConfig {
name?: string;
icon?: string;
url: string;
new_tab?: boolean;
download?: boolean;
}
export interface TextConfig {
type: "text";

View File

@ -47,8 +47,6 @@ class LovelaceFullConfigEditor extends LitElement {
@internalProperty() private _changed?: boolean;
private _generation = 1;
protected render(): TemplateResult | void {
return html`
<ha-app-layout>
@ -133,11 +131,7 @@ class LovelaceFullConfigEditor extends LitElement {
}
.content {
height: calc(100vh - 68px);
}
hui-code-editor {
height: 100%;
height: calc(100vh - var(--header-height));
}
.save-button {
@ -154,15 +148,11 @@ class LovelaceFullConfigEditor extends LitElement {
}
private _yamlChanged() {
this._changed = !this.yamlEditor
.codemirror!.getDoc()
.isClean(this._generation);
if (this._changed && !window.onbeforeunload) {
this._changed = true;
if (!window.onbeforeunload) {
window.onbeforeunload = () => {
return true;
};
} else if (!this._changed && window.onbeforeunload) {
window.onbeforeunload = null;
}
}
@ -224,7 +214,7 @@ class LovelaceFullConfigEditor extends LitElement {
return;
}
if (this.yamlEditor.hasComments) {
if (/^#|\s#/gm.test(value)) {
if (
!confirm(
this.hass.localize(
@ -281,9 +271,6 @@ class LovelaceFullConfigEditor extends LitElement {
),
});
}
this._generation = this.yamlEditor
.codemirror!.getDoc()
.changeGeneration(true);
window.onbeforeunload = null;
this._saving = false;
this._changed = false;

View File

@ -7,6 +7,7 @@ import {
LitElement,
TemplateResult,
} from "lit-element";
import { ifDefined } from "lit-html/directives/if-defined";
import "../../../components/ha-icon";
import { HomeAssistant } from "../../../types";
import { LovelaceRow, WeblinkConfig } from "../entity-rows/types";
@ -37,8 +38,9 @@ class HuiWeblinkRow extends LitElement implements LovelaceRow {
return html`
<a
href=${this._config.url}
target=${this._config.url.indexOf("://") !== -1 ? "_blank" : ""}
target=${ifDefined(this._computeTargetValue())}
rel="noreferrer"
?download=${this._config.download}
>
<ha-icon .icon="${this._config.icon}"></ha-icon>
<div>${this._config.name}</div>
@ -66,6 +68,15 @@ class HuiWeblinkRow extends LitElement implements LovelaceRow {
}
`;
}
protected _computeTargetValue(): string | undefined {
return this._config &&
(this._config.url.indexOf("://") !== -1 ||
this._config.new_tab === true ||
this._config.download === true)
? "_blank"
: undefined;
}
}
declare global {

View File

@ -86,5 +86,5 @@ export interface LovelaceGenericElementEditor extends HTMLElement {
hass?: HomeAssistant;
lovelace?: LovelaceConfig;
setConfig(config: any): void;
refreshYamlEditor?: (focus: boolean) => void;
focusYamlEditor?: () => void;
}

View File

@ -15,6 +15,7 @@ import {
import "../../layouts/hass-error-screen";
import { isComponentLoaded } from "../../common/config/is_component_loaded";
import { domainToName } from "../../data/integration";
import { documentationUrl } from "../../util/documentation-url";
const REDIRECTS: Redirects = {
developer_states: {
@ -23,12 +24,21 @@ const REDIRECTS: Redirects = {
developer_services: {
redirect: "/developer-tools/service",
},
developer_call_service: {
redirect: "/developer-tools/service",
params: {
service: "string",
},
},
developer_template: {
redirect: "/developer-tools/template",
},
developer_events: {
redirect: "/developer-tools/event",
},
config: {
redirect: "/config",
},
cloud: {
component: "cloud",
redirect: "/config/cloud",
@ -42,6 +52,18 @@ const REDIRECTS: Redirects = {
domain: "string",
},
},
config_mqtt: {
component: "mqtt",
redirect: "/config/mqtt",
},
config_zha: {
component: "zha",
redirect: "/config/zha/dashboard",
},
config_zwave_js: {
component: "zwave_js",
redirect: "/config/zwave_js/dashboard",
},
devices: {
redirect: "/config/devices/dashboard",
},
@ -52,39 +74,49 @@ const REDIRECTS: Redirects = {
redirect: "/config/areas/dashboard",
},
blueprints: {
component: "blueprint",
redirect: "/config/blueprint/dashboard",
},
blueprint_import: {
component: "blueprint",
redirect: "/config/blueprint/dashboard/import",
params: {
blueprint_url: "url",
},
},
automations: {
component: "automation",
redirect: "/config/automation/dashboard",
},
scenes: {
component: "scene",
redirect: "/config/scene/dashboard",
},
scripts: {
component: "script",
redirect: "/config/script/dashboard",
},
helpers: {
redirect: "/config/helpers",
},
tags: {
component: "tags",
redirect: "/config/tags",
},
lovelace_dashboards: {
component: "lovelace",
redirect: "/config/lovelace/dashboards",
},
lovelace_resources: {
component: "lovelace",
redirect: "/config/lovelace/resources",
},
people: {
component: "person",
redirect: "/config/person",
},
zones: {
component: "zone",
redirect: "/config/zone",
},
users: {
@ -108,6 +140,14 @@ const REDIRECTS: Redirects = {
profile: {
redirect: "/profile/dashboard",
},
logbook: {
component: "logbook",
redirect: "/logbook",
},
history: {
component: "history",
redirect: "/history",
},
};
export type ParamType = "url" | "string";
@ -201,12 +241,16 @@ class HaPanelMy extends LitElement {
) || "This redirect is not supported.";
break;
case "no_supervisor":
error =
this.hass.localize(
"ui.panel.my.component_not_loaded",
"integration",
"Home Assistant Supervisor"
) || "This redirect requires Home Assistant Supervisor.";
error = this.hass.localize(
"ui.panel.my.no_supervisor",
"docs_link",
html`<a
target="_blank"
rel="noreferrer noopener"
href="${documentationUrl(this.hass, "/installation")}"
>${this.hass.localize("ui.panel.my.documentation")}</a
>`
);
break;
default:
error = this.hass.localize("ui.panel.my.error") || "Unknown error";

View File

@ -1,11 +1,8 @@
interface LoadedCodeMirror {
codeMirror: any;
codeMirrorCss: any;
}
let loaded: Promise<typeof import("./codemirror")>;
let loaded: Promise<LoadedCodeMirror>;
export const loadCodeMirror = async (): Promise<LoadedCodeMirror> => {
export const loadCodeMirror = async (): Promise<
typeof import("./codemirror")
> => {
if (!loaded) {
loaded = import("./codemirror");
}

View File

@ -1,14 +1,122 @@
// @ts-ignore
import _CodeMirror, { Editor } from "codemirror";
// @ts-ignore
import _codeMirrorCss from "codemirror/lib/codemirror.css";
import "codemirror/mode/jinja2/jinja2";
import "codemirror/mode/yaml/yaml";
import { fireEvent } from "../common/dom/fire_event";
import { HighlightStyle, tags } from "@codemirror/highlight";
import { EditorView as CMEditorView } from "@codemirror/view";
import { StreamLanguage } from "@codemirror/stream-parser";
import { jinja2 } from "@codemirror/legacy-modes/mode/jinja2";
import { yaml } from "@codemirror/legacy-modes/mode/yaml";
// @ts-ignore
_CodeMirror.commands.save = (cm: Editor) => {
fireEvent(cm.getWrapperElement(), "editor-save");
export { keymap } from "@codemirror/view";
export { CMEditorView as EditorView };
export { EditorState, Prec, tagExtension } from "@codemirror/state";
export { defaultKeymap, defaultTabBinding } from "@codemirror/commands";
export { lineNumbers } from "@codemirror/gutter";
export const langs = {
jinja2: StreamLanguage.define(jinja2),
yaml: StreamLanguage.define(yaml),
};
export const codeMirror: any = _CodeMirror;
export const codeMirrorCss: any = _codeMirrorCss;
export const theme = CMEditorView.theme({
$: {
color: "var(--primary-text-color)",
backgroundColor:
"var(--code-editor-background-color, var(--card-background-color))",
"& ::selection": { backgroundColor: "rgba(var(--rgb-primary-color), 0.2)" },
caretColor: "var(--secondary-text-color)",
height: "var(--code-mirror-height, auto)",
},
$$focused: { outline: "none" },
"$$focused $cursor": { borderLeftColor: "#var(--secondary-text-color)" },
"$$focused $selectionBackground, $selectionBackground": {
backgroundColor: "rgba(var(--rgb-primary-color), 0.2)",
},
$gutters: {
backgroundColor:
"var(--paper-dialog-background-color, var(--primary-background-color))",
color: "var(--paper-dialog-color, var(--secondary-text-color))",
border: "none",
borderRight:
"1px solid var(--paper-input-container-color, var(--secondary-text-color))",
},
"$$focused $gutters": {
borderRight:
"2px solid var(--paper-input-container-focus-color, var(--primary-color))",
},
"$gutterElementags.lineNumber": { color: "inherit" },
});
export const highlightStyle = HighlightStyle.define(
{ tag: tags.keyword, color: "var(--codemirror-keyword, #6262FF)" },
{
tag: [
tags.name,
tags.deleted,
tags.character,
tags.propertyName,
tags.macroName,
],
color: "var(--codemirror-property, #905)",
},
{
tag: [tags.function(tags.variableName), tags.labelName],
color: "var(--codemirror-variable, #07a)",
},
{
tag: [tags.color, tags.constant(tags.name), tags.standard(tags.name)],
color: "var(--codemirror-qualifier, #690)",
},
{
tag: [tags.definition(tags.name), tags.separator],
color: "var(--codemirror-def, #8DA6CE)",
},
{
tag: [
tags.typeName,
tags.className,
tags.number,
tags.changed,
tags.annotation,
tags.modifier,
tags.self,
tags.namespace,
],
color: "var(--codemirror-number, #ca7841)",
},
{
tag: [
tags.operator,
tags.operatorKeyword,
tags.url,
tags.escape,
tags.regexp,
tags.link,
tags.special(tags.string),
],
color: "var(--codemirror-operator, #cda869)",
},
{ tag: tags.comment, color: "var(--codemirror-comment, #777)" },
{
tag: tags.meta,
color: "var(--codemirror-meta, var(--primary-text-color))",
},
{ tag: tags.strong, fontWeight: "bold" },
{ tag: tags.emphasis, fontStyle: "italic" },
{
tag: tags.link,
color: "var(--primary-color)",
textDecoration: "underline",
},
{ tag: tags.heading, fontWeight: "bold" },
{ tag: tags.atom, color: "var(--codemirror-atom, #F90)" },
{ tag: tags.bool, color: "var(--codemirror-atom, #F90)" },
{
tag: tags.special(tags.variableName),
color: "var(--codemirror-variable-2, #690)",
},
{ tag: tags.processingInstruction, color: "var(--secondary-text-color)" },
{ tag: tags.string, color: "var(--codemirror-string, #07a)" },
{ tag: tags.inserted, color: "var(--codemirror-string2, #07a)" },
{ tag: tags.invalid, color: "var(--error-color)" }
);

View File

@ -33,7 +33,8 @@ export class StateCardScript extends LitElement {
></state-info>
${stateObj.state === "on"
? html`<mwc-button @click=${this._cancelScript}>
${(stateObj.attributes.current || 0) > 0
${stateObj.attributes.mode !== "single" &&
(stateObj.attributes.current || 0) > 0
? this.hass.localize(
"ui.card.script.cancel_multiple",
"number",

View File

@ -98,7 +98,7 @@
"disabled_by": {
"user": "User",
"integration": "Integration",
"config_entry": "Config Entry",
"config_entry": "Config entry",
"device": "Device"
}
},
@ -339,8 +339,8 @@
}
},
"target-picker": {
"expand_area_id": "Expand this area in the seperate devices and entities that it contains. After expanding it will not update the devices and entities when the area changes.",
"expand_device_id": "Expand this device in seperate entities. After expanding it will not update the entities when the device changes.",
"expand_area_id": "Expand this area into the separate devices and entities that it contains. After expanding, it will not update the devices and entities when the area changes.",
"expand_device_id": "Expand this device into the separate entities that it contains. After expanding, it will not update the entities when the device changes.",
"remove_area_id": "Remove area",
"remove_device_id": "Remove device",
"remove_entity_id": "Remove entity",
@ -382,6 +382,19 @@
"failed_create_area": "Failed to create area."
}
},
"addon-picker": {
"addon": "Add-on",
"error": {
"no_supervisor": {
"title": "No Supervisor",
"description": "No Supervisor found, so add-ons could not be loaded."
},
"fetch_addons": {
"title": "Error fetching add-ons",
"description": "Fetching add-ons returned an error."
}
}
},
"picture-upload": {
"label": "Picture",
"unsupported_format": "Unsupported format, please choose a JPEG, PNG or GIF image."
@ -426,8 +439,8 @@
},
"service-control": {
"required": "This field is required",
"target": "Target",
"target_description": "What should this service call target",
"target": "Targets",
"target_description": "What should this service use as targeted areas, devices or entities.",
"service_data": "Service data"
},
"related-items": {
@ -813,6 +826,8 @@
"my": {
"not_supported": "This redirect is not supported by your Home Assistant instance. Check the {link} for the supported redirects and the version they where introduced.",
"component_not_loaded": "This redirect is not supported by your Home Assistant instance. You need the integration {integration} to use this redirect.",
"no_supervisor": "This redirect is not supported by your Home Assistant installation. It needs either the Home Assistant Operating System or Home Assistant Supervised installation method. For more information, see the {docs_link}.",
"documentation": "documentation",
"faq_link": "My Home Assistant FAQ",
"error": "An unknown error occured"
},
@ -821,7 +836,8 @@
"introduction": "In this view it is possible to configure your components and Home Assistant. Not everything is possible to configure from the UI yet, but we're working on it.",
"filtering": {
"filtering_by": "Filtering by",
"clear": "Clear"
"clear": "Clear",
"show": "Show"
},
"advanced_mode": {
"hint_enable": "Missing config options? Enable advanced mode on",
@ -1819,7 +1835,7 @@
"disabled_by": {
"user": "User",
"integration": "Integration",
"config_entry": "Config Entry"
"config_entry": "Config entry"
},
"enabled_description": "Disabled devices will not be shown and entities belonging to the device will be disabled and not added to Home Assistant.",
"automation": {
@ -1870,6 +1886,7 @@
"scenes": "Scenes",
"confirm_rename_entity_ids": "Do you also want to rename the entity IDs of your entities?",
"confirm_rename_entity_ids_warning": "This will not change any configuration (like automations, scripts, scenes, dashboards) that is currently using these entities! You will have to update them yourself to use the new entity IDs!",
"confirm_disable_config_entry": "There are no more devices for the config entry {entry_name}, do you want to instead disable the config entry?",
"disabled": "Disabled",
"data_table": {
"device": "Device",
@ -2027,6 +2044,11 @@
"rename_dialog": "Edit the name of this config entry",
"rename_input_label": "Entry name",
"search": "Search integrations",
"disable": {
"show_disabled": "Show disabled integrations",
"hide_disabled": "Hide disabled integrations",
"disabled_integrations": "{number} disabled"
},
"ignore": {
"ignore": "Ignore",
"confirm_ignore_title": "Ignore discovery of {name}?",
@ -2052,6 +2074,8 @@
"restart_confirm": "Restart Home Assistant to finish removing this integration",
"reload_confirm": "The integration was reloaded",
"reload_restart_confirm": "Restart Home Assistant to finish reloading this integration",
"disable_restart_confirm": "Restart Home Assistant to finish disabling this integration",
"enable_restart_confirm": "Restart Home Assistant to finish enabling this integration",
"manuf": "by {manufacturer}",
"hub": "Connected via",
"firmware": "Firmware: {version}",
@ -2059,7 +2083,17 @@
"device_unavailable": "Device unavailable",
"entity_unavailable": "Entity unavailable",
"area": "In {area}",
"no_area": "No Area"
"no_area": "No Area",
"disable": {
"disabled": "Disabled",
"disabled_cause": "Disabled by {cause}",
"disabled_by": {
"user": "user",
"integration": "integration",
"device": "device"
},
"disable_confirm": "Are you sure you want to disable this config entry? It's devices and entities will be disabled."
}
},
"config_flow": {
"aborted": "Aborted",
@ -2620,7 +2654,7 @@
"confirm_remove_config_title": "Are you sure you want to remove your Lovelace UI configuration?",
"confirm_remove_config_text": "We will automatically generate your Lovelace UI views with your areas and devices if you remove your Lovelace UI configuration.",
"confirm_unsaved_changes": "You have unsaved changes, are you sure you want to exit?",
"confirm_unsaved_comments": "Your configuration contains comment(s), these will not be saved. Do you want to continue?",
"confirm_unsaved_comments": "Your configuration might contains comment(s), these will not be saved. Do you want to continue?",
"error_parse_yaml": "Unable to parse YAML: {error}",
"error_invalid_config": "Your configuration is not valid: {error}",
"error_save_yaml": "Unable to save YAML: {error}",
@ -3311,7 +3345,8 @@
"more_info": "More Info",
"alert_entity_field": "Entity is a mandatory field",
"last_updated": "[%key:ui::dialogs::more_info_control::last_updated%]",
"last_changed": "[%key:ui::dialogs::more_info_control::last_changed%]"
"last_changed": "[%key:ui::dialogs::more_info_control::last_changed%]",
"copy_id": "Copy ID to clipboard"
},
"templates": {
"title": "Template",

View File

@ -0,0 +1,41 @@
import { assert } from "chai";
import { atLeastVersion } from "../../../src/common/config/version";
const testTruthyData = [
{ version: "2021.1.1", major: 2021, minor: 1, patch: 1 },
{ version: "2021.1.1", major: 2021, minor: 1 },
{ version: "2021.1.1", major: 2020, minor: 12, patch: 1 },
{ version: "2021.1.1", major: 2020, minor: 12 },
{ version: "2021.1.1", major: 2021, minor: 2, patch: 0 },
{ version: "2021.1.1", major: 2021, minor: 2 },
{ version: "2021.2.4", major: 0, minor: 113, patch: 0 },
{ version: "2021.2.4", major: 0, minor: 113 },
{ version: "0.114.0", major: 0, minor: 113, patch: 0 },
{ version: "0.114.0", major: 0, minor: 113 },
{ version: "2021.2.0dev.2323", major: 2021, minor: 2, patch: 0 },
{ version: "2021.2.0dev.2323", major: 2021, minor: 2 },
];
const testFalsyData = [
{ version: "0.114.0", major: 0, minor: 113 },
{ version: "2021.2.0dev.2323", major: 2021, minor: 2, patch: 0 },
];
describe("atLeastVersion - Truthy", () => {
testTruthyData.forEach((test) =>
it(`'${test.version}' >= ${test.major},${test.minor},${test.patch}`, () => {
assert.isTrue(atLeastVersion("2021.1.1", 2021, 1));
})
);
});
describe("atLeastVersion - Falsy", () => {
testFalsyData.forEach((test) =>
it(`'${test.version}' >= ${test.major},${test.minor},${test.patch}`, () => {
assert.isTrue(atLeastVersion("2021.1.1", 2021, 1));
})
);
});

View File

@ -754,7 +754,10 @@
"time": {
"after": "Na",
"before": "Voor",
"label": "Tyd"
"label": "Tyd",
"weekdays": {
"mon": "Maandag"
}
},
"zone": {
"entity": "Entiteit met plek",

View File

@ -717,7 +717,10 @@
"time": {
"after": "بعد",
"before": "قبل",
"label": "وقت"
"label": "وقت",
"weekdays": {
"mon": "الاثنين"
}
},
"zone": {
"entity": "كيان مع موقع",

View File

@ -186,6 +186,15 @@
"no_blueprints": "আপনার কোনও ব্লুপ্রিন্ট নেই",
"no_inputs": "এই ব্লুপ্রিন্টের কোনও ইনপুট নেই।"
},
"conditions": {
"type": {
"time": {
"weekdays": {
"mon": "সোমবার"
}
}
}
},
"triggers": {
"introduction": "ট্রিগারগুলি যা অটোমেশন নিয়মের প্রক্রিয়া শুরু করে। একই নিয়মের জন্য একাধিক ট্রিগার নির্দিষ্ট করা সম্ভব। ট্রিগার শুরু হয়ে গেলে, Home Assistant এর শর্তগুলি যথাযথভাবে প্রযোজ্য হবে, যদি থাকে তবে এবং অ্যাকশন কল করবে।",
"type": {

View File

@ -242,6 +242,15 @@
"config": {
"automation": {
"editor": {
"conditions": {
"type": {
"time": {
"weekdays": {
"mon": "Ponedeljak"
}
}
}
},
"triggers": {
"type": {
"mqtt": {

View File

@ -720,6 +720,12 @@
"week": "fa {count} {count, plural,\n one {setmana}\n other {setmanes}\n}"
}
},
"service-control": {
"required": "Aquest camp és obligatori",
"service_data": "Dades del servei",
"target": "Objectiu",
"target_description": "Objectiu d'aquesta crida de servei"
},
"service-picker": {
"service": "Servei"
},
@ -1152,7 +1158,7 @@
"event": {
"event": "Esdeveniment:",
"label": "Disparar esdeveniment",
"service_data": "Dades de servei"
"service_data": "Dades del servei"
},
"repeat": {
"label": "Repeteix",
@ -1988,8 +1994,10 @@
"config_flow": {
"aborted": "Avortat",
"close": "Tanca",
"could_not_load": "El flux de dades de configuració no s'ha pogut carregar",
"created_config": "S'ha creat configuració per a {name}.",
"dismiss": "Omet el diàleg",
"error": "Error",
"error_saving_area": "Error desant àrea: {error}",
"external_step": {
"description": "Aquest pas requereix que visitis un lloc web extern per completar-lo.",
@ -1998,6 +2006,10 @@
"finish": "Finalitza",
"loading_first_time": "Espera mentre s'instal·la la integració",
"not_all_required_fields": "No s'han omplert tots els camps obligatoris.",
"pick_flow_step": {
"new_flow": "No, configura una altra instància de {integration}",
"title": "N'hem descobert aquests, vols configurar-los?"
},
"submit": "Envia"
},
"configure": "Configurar",
@ -2857,7 +2869,9 @@
"type": "Tipus d'esdeveniment"
},
"services": {
"accepts_target": "Aquest servei accepta un objectiu, per exemple: `entity_id: light.bed_light`",
"alert_parsing_yaml": "S'ha produït un error analitzant el codi YAML: {data}",
"all_parameters": "Tots els paràmetres disponibles",
"call_service": "Crida servei",
"column_description": "Descripció",
"column_example": "Exemple",
@ -2868,7 +2882,10 @@
"no_description": "No hi ha cap descripció disponible",
"no_parameters": "Aquest servei no té paràmetres.",
"select_service": "Selecciona un servei per veure'n la descripció",
"title": "Serveis"
"title": "Serveis",
"ui_mode": "Vés al mode d'interfície d'usuari",
"yaml_mode": "Vés al mode YAML",
"yaml_parameters": "Paràmetres només disponibles en mode YAML"
},
"states": {
"alert_entity_field": "L'entitat és un camp obligatori",

View File

@ -611,7 +611,10 @@
"time": {
"after": "Ar ôl",
"before": "Cyn",
"label": "Amser"
"label": "Amser",
"weekdays": {
"mon": "Dydd Llun"
}
},
"zone": {
"entity": "Endid gyda lleoliad",

View File

@ -720,6 +720,12 @@
"week": "{count} {count, plural,\n one {week}\n other {weeks}\n} ago"
}
},
"service-control": {
"required": "This field is required",
"service_data": "Service data",
"target": "Targets",
"target_description": "What should this service use as targeted areas, devices or entities."
},
"service-picker": {
"service": "Service"
},
@ -727,8 +733,8 @@
"add_area_id": "Pick area",
"add_device_id": "Pick device",
"add_entity_id": "Pick entity",
"expand_area_id": "Expand this area in the seperate devices and entities that it contains. After expanding it will not update the devices and entities when the area changes.",
"expand_device_id": "Expand this device in seperate entities. After expanding it will not update the entities when the device changes.",
"expand_area_id": "Expand this area into the separate devices and entities that it contains. After expanding, it will not update the devices and entities when the area changes.",
"expand_device_id": "Expand this device into the separate entities that it contains. After expanding, it will not update the entities when the device changes.",
"remove_area_id": "Remove area",
"remove_device_id": "Remove device",
"remove_entity_id": "Remove entity"
@ -1988,8 +1994,10 @@
"config_flow": {
"aborted": "Aborted",
"close": "Close",
"could_not_load": "Config flow could not be loaded",
"created_config": "Created configuration for {name}.",
"dismiss": "Dismiss dialog",
"error": "Error",
"error_saving_area": "Error saving area: {error}",
"external_step": {
"description": "This step requires you to visit an external website to be completed.",
@ -1998,6 +2006,10 @@
"finish": "Finish",
"loading_first_time": "Please wait while the integration is being installed",
"not_all_required_fields": "Not all required fields are filled in.",
"pick_flow_step": {
"new_flow": "No, set up an other instance of {integration}",
"title": "We discovered these, want to set them up?"
},
"submit": "Submit"
},
"configure": "Configure",
@ -2857,7 +2869,9 @@
"type": "Event Type"
},
"services": {
"accepts_target": "This service accepts a target, for example: `entity_id: light.bed_light`",
"alert_parsing_yaml": "Error parsing YAML: {data}",
"all_parameters": "All available parameters",
"call_service": "Call Service",
"column_description": "Description",
"column_example": "Example",
@ -2868,7 +2882,10 @@
"no_description": "No description is available",
"no_parameters": "This service takes no parameters.",
"select_service": "Select a service to see the description",
"title": "Services"
"title": "Services",
"ui_mode": "Go to UI mode",
"yaml_mode": "Go to YAML mode",
"yaml_parameters": "Parameters only available in YAML mode"
},
"states": {
"alert_entity_field": "Entity is a mandatory field",

View File

@ -7,6 +7,19 @@
},
"panel": {
"config": {
"automation": {
"editor": {
"conditions": {
"type": {
"time": {
"weekdays": {
"mon": "Monday"
}
}
}
}
}
},
"integrations": {
"caption": "Integrations",
"configure": "Configure",

View File

@ -74,7 +74,14 @@
}
},
"conditions": {
"name": "Kondiĉo"
"name": "Kondiĉo",
"type": {
"time": {
"weekdays": {
"mon": "Lundo"
}
}
}
},
"edit_ui": "Redakti kun UI",
"edit_yaml": "Redakti kiel YAML",

View File

@ -720,6 +720,12 @@
"week": "hace {count} {count, plural,\none {semana}\nother {semanas}\n}"
}
},
"service-control": {
"required": "Este campo es obligatorio",
"service_data": "Datos de servicio",
"target": "Objetivo",
"target_description": "¿A qué debería dirigirse esta llamada de servicio?"
},
"service-picker": {
"service": "Servicio"
},
@ -1988,8 +1994,10 @@
"config_flow": {
"aborted": "Abortado",
"close": "Cerrar",
"could_not_load": "No se pudo cargar el flujo de configuración",
"created_config": "Configuración creada para {name}.",
"dismiss": "Descartar diálogo",
"error": "Error",
"error_saving_area": "Error al guardar el área: {error}",
"external_step": {
"description": "Este paso requiere que visites una web externa para ser completado.",
@ -1998,6 +2006,10 @@
"finish": "Terminar",
"loading_first_time": "Por favor, espera mientras la integración está siendo instalada",
"not_all_required_fields": "No se han completado todos los campos requeridos.",
"pick_flow_step": {
"new_flow": "No, configura otra instancia de {integration}",
"title": "Hemos descubierto éstas, ¿quieres configurarlas?"
},
"submit": "Enviar"
},
"configure": "Configurar",
@ -2857,7 +2869,9 @@
"type": "Tipo de evento"
},
"services": {
"accepts_target": "Este servicio acepta un objetivo, por ejemplo: `entity_id: light.luz_dormitorio`",
"alert_parsing_yaml": "Error al analizar YAML: {data}",
"all_parameters": "Todos los parámetros disponibles",
"call_service": "Llamar servicio",
"column_description": "Descripción",
"column_example": "Ejemplo",
@ -2868,7 +2882,10 @@
"no_description": "No hay descripción disponible.",
"no_parameters": "Este servicio no toma parámetros.",
"select_service": "Seleccione un servicio para ver la descripción.",
"title": "Servicios"
"title": "Servicios",
"ui_mode": "Ir al modo IU",
"yaml_mode": "Ir al modo YAML",
"yaml_parameters": "Parámetros solo disponibles en modo YAML"
},
"states": {
"alert_entity_field": "Entidad es un campo obligatorio",

View File

@ -720,6 +720,12 @@
"week": "{count} {count, plural,\n one {nädala}\n other {nädala}\n} eest"
}
},
"service-control": {
"required": "See väli on nõutav",
"service_data": "Teenuse andmed",
"target": "Sihtmärk",
"target_description": "Mida peaks see teenus välja kutsuma"
},
"service-picker": {
"service": "Teenus"
},
@ -1988,8 +1994,10 @@
"config_flow": {
"aborted": "Katkestatud",
"close": "Sulge",
"could_not_load": "Konfiguratsioonivoogu ei saanud laadida",
"created_config": "Üksuse {name} jaoks on loodud konfiguratsioon.",
"dismiss": "Loobumisdialoog",
"error": "Viga",
"error_saving_area": "Viga ala salvestamisel: {error}",
"external_step": {
"description": "Selle etapi lõpetamine nõuab välise veebisaidi külastamist.",
@ -1998,6 +2006,10 @@
"finish": "Lõpeta",
"loading_first_time": "Palun oodake kuni sidumist paigaldatakse",
"not_all_required_fields": "Kõik nõutavad väljad pole täidetud.",
"pick_flow_step": {
"new_flow": "Ei, seadista teine {integration} eksemplar",
"title": "Avastasime need, kas soovid neid seadistada?"
},
"submit": "Esita"
},
"configure": "Seadista",
@ -2857,7 +2869,9 @@
"type": "Sündmuse tüüp"
},
"services": {
"accepts_target": "See teenus aktsepteerib sihtmärki, näiteks: \"entity_id: light.elutuba\"",
"alert_parsing_yaml": "Viga YAML'i parsimisel: {data}",
"all_parameters": "Kõik saadaolevad parameetrid",
"call_service": "Kutsu teenus",
"column_description": "Kirjeldus",
"column_example": "Näide",
@ -2868,7 +2882,10 @@
"no_description": "Kirjeldus pole saadaval",
"no_parameters": "Sellel teenusel pole parameetreid.",
"select_service": "Kirjelduse kuvamiseks vali teenus",
"title": "Teenused"
"title": "Teenused",
"ui_mode": "Ava kasutajaliidese režiim",
"yaml_mode": "Redigeeri YAML-is",
"yaml_parameters": "Parameetrid on saadaval ainult YAML režiimis"
},
"states": {
"alert_entity_field": "Olem on kohustuslik väli",

View File

@ -442,7 +442,10 @@
"value_template": "Valio txantiloia"
},
"time": {
"label": "Denbora"
"label": "Denbora",
"weekdays": {
"mon": "Astelehena"
}
}
}
},

View File

@ -895,7 +895,10 @@
"time": {
"after": "بعد از",
"before": "قبل از",
"label": "زمان"
"label": "زمان",
"weekdays": {
"mon": "دوشنبه"
}
},
"zone": {
"entity": "نهاد به همراخ موقعیت",

View File

@ -4,10 +4,12 @@
"config": {
"automation": {
"editor": {
"triggers": {
"conditions": {
"type": {
"mqtt": {
"label": ""
"time": {
"weekdays": {
"mon": "Luns"
}
}
}
}

View File

@ -233,7 +233,10 @@
},
"time": {
"after": "बाद",
"before": "पहले"
"before": "पहले",
"weekdays": {
"mon": "सोमवार"
}
}
}
},

View File

@ -731,7 +731,10 @@
"time": {
"after": "Nakon",
"before": "Prije",
"label": "Vrijeme"
"label": "Vrijeme",
"weekdays": {
"mon": "ponedjeljak"
}
},
"zone": {
"entity": "Entitet sa lokacijom",

View File

@ -654,7 +654,10 @@
"time": {
"after": "Հետո",
"before": "Նախքան",
"label": "ժամանակը"
"label": "ժամանակը",
"weekdays": {
"mon": "Երկուշաբթի"
}
},
"zone": {
"entity": "կազմակերպության գտնվելու վայրը",

View File

@ -5,6 +5,19 @@
"ui": {
"panel": {
"config": {
"automation": {
"editor": {
"conditions": {
"type": {
"time": {
"weekdays": {
"mon": "ორშაბათს"
}
}
}
}
}
},
"blueprint": {
"add": {
"file_name": "ლოკალური გეგმა-ფაილის სახელი",

View File

@ -544,7 +544,10 @@
"time": {
"after": "Po",
"before": "Prieš",
"label": "Laikas"
"label": "Laikas",
"weekdays": {
"mon": "Pirmadienis"
}
},
"zone": {
"label": "Vieta",

View File

@ -874,7 +874,10 @@
"time": {
"after": "Pēc",
"before": "Pirms",
"label": "Laiks"
"label": "Laiks",
"weekdays": {
"mon": "Pirmdiena"
}
},
"zone": {
"entity": "Vienība ar atrašanos vietu",

View File

@ -3409,6 +3409,7 @@
"playback_title": "Meldingsavspilling"
},
"my": {
"component_not_loaded": "Denne viderekoblingen støttes ikke av Home Assistant-forekomsten. Du trenger integrasjonen {integration} å bruke denne viderekoblingen.",
"error": "Det oppstod en ukjent feil",
"faq_link": "Vanlige spørsmål om Min Home Assistant",
"not_supported": "Denne viderekoblingen støttes ikke av Home Assistant-forekomsten. Se på {link} for viderekoblinger som støttes, og hvilken versjon de ble introdusert."

View File

@ -2002,7 +2002,7 @@
},
"configure": "Configureer",
"configured": "Geconfigureerd",
"confirm_new": "Wil je {integratie} instellen?",
"confirm_new": "Wil je {integration} instellen?",
"description": "Beheer integraties met services, apparaten, ...",
"details": "Integratiedetails",
"discovered": "Ontdekt",

View File

@ -766,7 +766,10 @@
"time": {
"after": "Etter",
"before": "Før",
"label": "Tid"
"label": "Tid",
"weekdays": {
"mon": "mandag"
}
},
"zone": {
"entity": "Eining med posisjon",

View File

@ -720,6 +720,12 @@
"week": "{count} {count, plural,\n one {tydzień}\n few {tygodnie}\n many {tygodni}\n other {tygodni}\n} temu"
}
},
"service-control": {
"required": "To pole jest wymagane",
"service_data": "Dane usługi",
"target": "Cel",
"target_description": "Jaki powinien być cel wywołania tej usługi"
},
"service-picker": {
"service": "Usługa"
},
@ -1988,8 +1994,10 @@
"config_flow": {
"aborted": "Przerwano",
"close": "Zamknij",
"could_not_load": "Nie można wczytać interfejsu konfiguracji",
"created_config": "Utworzono konfigurację dla {name}.",
"dismiss": "Okno dialogowe odrzucenia",
"error": "Błąd",
"error_saving_area": "Błąd podczas zapisywania obszaru: {error}",
"external_step": {
"description": "Ten krok wymaga od Ciebie odwiedzenia zewnętrznej strony.",
@ -1998,6 +2006,10 @@
"finish": "Zakończ",
"loading_first_time": "Proszę czekać, trwa instalowanie integracji...",
"not_all_required_fields": "Nie wszystkie wymagane pola są wypełnione.",
"pick_flow_step": {
"new_flow": "Nie, skonfiguruj inną instancję integracji {integration}",
"title": "Odkryliśmy je, chcesz je skonfigurować?"
},
"submit": "Zatwierdź"
},
"configure": "Konfiguruj",
@ -2857,7 +2869,9 @@
"type": "Typ zdarzenia"
},
"services": {
"accepts_target": "Ta usługa akceptuje cel, na przykład: `entity_id: light.bed_light`",
"alert_parsing_yaml": "Błąd parsowania YAML: {data}",
"all_parameters": "Wszystkie dostępne parametry",
"call_service": "Wywołaj usługę",
"column_description": "Opis",
"column_example": "Przykład",
@ -2868,7 +2882,10 @@
"no_description": "Opis nie jest dostępny",
"no_parameters": "Ta usługa nie przyjmuje parametrów.",
"select_service": "Wybierz usługę, aby zobaczyć opis",
"title": "Usługi"
"title": "Usługi",
"ui_mode": "Przejdź do trybu interfejsu użytkownika",
"yaml_mode": "Przejdź do trybu YAML",
"yaml_parameters": "Parametry dostępne tylko w trybie YAML"
},
"states": {
"alert_entity_field": "Encja jest polem obowiązkowym",

View File

@ -720,6 +720,12 @@
"week": "{count} {count, plural,\n one {нед.}\n other {нед.}\n} назад"
}
},
"service-control": {
"required": "Обязательное поле",
"service_data": "Данные службы",
"target": "Цель",
"target_description": "Что эта служба должна использовать в качестве цели"
},
"service-picker": {
"service": "Служба"
},
@ -1152,7 +1158,7 @@
"event": {
"event": "Событие:",
"label": "Создание события",
"service_data": "Данные"
"service_data": "Данные службы"
},
"repeat": {
"label": "Повтор",
@ -1988,8 +1994,10 @@
"config_flow": {
"aborted": "Отменено",
"close": "Закрыть",
"could_not_load": "Не удалось загрузить мастер настройки",
"created_config": "Создана конфигурация для {name}.",
"dismiss": "Закрыть",
"error": "Ошибка",
"error_saving_area": "Ошибка сохранения помещения: {error}",
"external_step": {
"description": "Для завершения этого шага требуется посетить внешний веб-сайт.",
@ -1998,6 +2006,10 @@
"finish": "Готово",
"loading_first_time": "Идет установка интеграции, пожалуйста, подождите",
"not_all_required_fields": "Не все обязательные поля заполнены.",
"pick_flow_step": {
"new_flow": "Нет, настроить другой экземпляр {integration}",
"title": "Это обнаружено автоматически, начать настройку?"
},
"submit": "Подтвердить"
},
"configure": "Настроить",
@ -2857,7 +2869,9 @@
"type": "Событие"
},
"services": {
"accepts_target": "Эта служба принимает целевой объект, например: `entity_id: light.bed_light`",
"alert_parsing_yaml": "Ошибка при разборе синтаксиса YAML: {data}",
"all_parameters": "Все доступные параметры",
"call_service": "Вызвать службу",
"column_description": "Описание",
"column_example": "Пример",
@ -2868,7 +2882,10 @@
"no_description": "Описание недоступно",
"no_parameters": "Нет параметров для этой службы.",
"select_service": "Выберите службу, чтобы увидеть описание.",
"title": "Службы"
"title": "Службы",
"ui_mode": "Перейти в режим формы ввода",
"yaml_mode": "Перейти в режим YAML",
"yaml_parameters": "Параметры доступны только в режиме YAML"
},
"states": {
"alert_entity_field": "Укажите объект.",

View File

@ -666,6 +666,9 @@
"week": "Pred {count} {count, plural,\none {týždňom}\nother {týždňami}\n}"
}
},
"service-control": {
"service_data": "Dáta služby"
},
"service-picker": {
"service": "Služba"
},
@ -1643,6 +1646,7 @@
"close": "Zavrieť",
"created_config": "Vytvorená konfigurácia pre {name}.",
"dismiss": "Zrušiť dialógové okno",
"error": "Chyba",
"error_saving_area": "Chyba pri ukladaní oblasti: {error}",
"external_step": {
"description": "Tento krok vyžaduje, aby ste navštívili externú webovú stránku, ktorá dokončí proces.",

View File

@ -73,6 +73,15 @@
"config": {
"automation": {
"editor": {
"conditions": {
"type": {
"time": {
"weekdays": {
"mon": "Понедељак"
}
}
}
},
"triggers": {
"type": {
"mqtt": {

View File

@ -56,6 +56,15 @@
"config": {
"automation": {
"editor": {
"conditions": {
"type": {
"time": {
"weekdays": {
"mon": "Понедељак"
}
}
}
},
"triggers": {
"learn_more": "Сазнајте више о окидачима",
"type": {

View File

@ -239,6 +239,15 @@
"config": {
"automation": {
"editor": {
"conditions": {
"type": {
"time": {
"weekdays": {
"mon": "திங்கட்கிழமை"
}
}
}
},
"triggers": {
"type": {
"mqtt": {

View File

@ -409,7 +409,10 @@
},
"time": {
"after": "తరువాత",
"before": "ముందు"
"before": "ముందు",
"weekdays": {
"mon": "సోమవారం"
}
}
}
},

View File

@ -670,7 +670,10 @@
"time": {
"after": "หลังจาก",
"before": "ก่อนที่จะ",
"label": "เวลา"
"label": "เวลา",
"weekdays": {
"mon": "วันจันทร์"
}
},
"zone": {
"entity": "Entity พร้อมตำแหน่ง",

View File

@ -37,6 +37,11 @@
"type": {
"device": {
"label": "آلہ"
},
"time": {
"weekdays": {
"mon": "پیر"
}
}
}
}

View File

@ -720,6 +720,12 @@
"week": "{count} {count, plural,\n one {週}\n other {週}\n}以前"
}
},
"service-control": {
"required": "必填欄位",
"service_data": "服務資料",
"target": "目標",
"target_description": "此服務呼叫的目標"
},
"service-picker": {
"service": "服務"
},
@ -1152,7 +1158,7 @@
"event": {
"event": "事件:",
"label": "執行事件",
"service_data": "資料"
"service_data": "服務資料"
},
"repeat": {
"label": "重複",
@ -1988,8 +1994,10 @@
"config_flow": {
"aborted": "已中止",
"close": "關閉",
"could_not_load": "設定流程無法載入",
"created_config": "新增 {name} 設定。",
"dismiss": "關閉對話",
"error": "錯誤",
"error_saving_area": "儲存分區錯誤:{error}",
"external_step": {
"description": "此步驟將需要開啟外部網站方能完成。",
@ -1998,6 +2006,10 @@
"finish": "完成",
"loading_first_time": "請稍候、正在安裝整合",
"not_all_required_fields": "所有所需欄位都需要填寫。",
"pick_flow_step": {
"new_flow": "不,設定另一組 {integration} 實例",
"title": "發現以下裝置,是否進行設定?"
},
"submit": "傳送"
},
"configure": "設定",
@ -2857,7 +2869,9 @@
"type": "事件類別"
},
"services": {
"accepts_target": "服務接受目標,例如:`entity_id: light.bed_light`",
"alert_parsing_yaml": "解析 YAML 錯誤:{data}",
"all_parameters": "所有可用參數",
"call_service": "執行服務",
"column_description": "說明",
"column_example": "範例",
@ -2868,7 +2882,10 @@
"no_description": "無描述可使用",
"no_parameters": "此服務未含任何參數。",
"select_service": "選擇服務以檢視其說明",
"title": "服務"
"title": "服務",
"ui_mode": "進入 UI 模式",
"yaml_mode": "進入 YAML 模式",
"yaml_parameters": "參數僅限於 YAML 模式"
},
"states": {
"alert_entity_field": "實體為必填欄位",

841
yarn.lock

File diff suppressed because it is too large Load Diff