Merge pull request #6510 from home-assistant/dev

This commit is contained in:
Bram Kragten 2020-08-03 16:48:29 +02:00 committed by GitHub
commit bd5115f9aa
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
230 changed files with 6008 additions and 2669 deletions

View File

@ -1,7 +1,7 @@
{
"extends": [
"plugin:@typescript-eslint/recommended",
"airbnb-typescript/base",
"plugin:@typescript-eslint/recommended",
"plugin:wc/recommended",
"plugin:lit/recommended",
"prettier",
@ -45,16 +45,16 @@
"func-names": 0,
"prefer-arrow-callback": 0,
"no-underscore-dangle": 0,
"no-var": 0,
"strict": 0,
"prefer-spread": 0,
"no-plusplus": 0,
"no-bitwise": 0,
"no-bitwise": 2,
"comma-dangle": 0,
"vars-on-top": 0,
"no-continue": 0,
"no-param-reassign": 0,
"no-multi-assign": 0,
"no-console": 2,
"radix": 0,
"no-alert": 0,
"no-return-await": 0,

View File

@ -147,6 +147,10 @@
"path": "M21.11,18.5C20.97,18.5 20.83,18.44 20.71,18.36C20.37,18.13 20.28,17.68 20.5,17.34C21.18,16.34 21.54,15.16 21.54,13.93C21.54,12.71 21.18,11.53 20.5,10.5C20.28,10.18 20.37,9.73 20.71,9.5C21.04,9.28 21.5,9.37 21.72,9.7C22.56,10.95 23,12.41 23,13.93C23,15.45 22.56,16.91 21.72,18.16C21.58,18.37 21.35,18.5 21.11,18.5M19,17.29C18.88,17.29 18.74,17.25 18.61,17.17C18.28,16.94 18.19,16.5 18.42,16.15C18.86,15.5 19.1,14.73 19.1,13.93C19.1,13.14 18.86,12.37 18.42,11.71C18.19,11.37 18.28,10.92 18.61,10.69C18.95,10.47 19.4,10.55 19.63,10.89C20.24,11.79 20.56,12.84 20.56,13.93C20.56,15 20.24,16.07 19.63,16.97C19.5,17.18 19.25,17.29 19,17.29M14.9,15.73C15.89,15.73 16.7,14.92 16.7,13.93C16.7,13.17 16.22,12.5 15.55,12.25C15.5,12.55 15.43,12.85 15.34,13.14C15.23,13.44 14.95,13.64 14.64,13.64C14.57,13.64 14.5,13.62 14.41,13.6C14.03,13.47 13.82,13.06 13.95,12.67C14.09,12.24 14.17,11.78 14.17,11.32C14.17,8.93 12.22,7 9.82,7C8.1,7 6.56,8 5.87,9.5C6.54,9.7 7.16,10.04 7.66,10.54C7.95,10.83 7.95,11.29 7.66,11.58C7.38,11.86 6.91,11.86 6.63,11.58C6.17,11.12 5.56,10.86 4.9,10.86C3.56,10.86 2.46,11.96 2.46,13.3C2.46,14.64 3.56,15.73 4.9,15.73H14.9M15.6,10.75C17.06,11.07 18.17,12.37 18.17,13.93C18.17,15.73 16.7,17.19 14.9,17.19H4.9C2.75,17.19 1,15.45 1,13.3C1,11.34 2.45,9.73 4.33,9.45C5.12,7.12 7.33,5.5 9.82,5.5C12.83,5.5 15.31,7.82 15.6,10.75Z",
"name": "mixcloud"
},
{
"path": "M5.68,3.96L11.41,11.65C11.55,11.84 11.55,12.1 11.41,12.29L5.65,20L5.5,20.18C4.76,21 3.47,21.07 2.64,20.31C1.85,19.59 1.79,18.37 2.43,17.5L6.56,11.97L2.46,6.47C1.83,5.62 1.88,4.39 2.67,3.67L2.82,3.54C3.73,2.87 5,3.05 5.68,3.96M18.32,3.96C19,3.05 20.27,2.87 21.18,3.54L21.33,3.67C22.12,4.39 22.17,5.61 21.54,6.47L17.44,11.97L21.57,17.5C22.21,18.36 22.15,19.59 21.36,20.31C20.53,21.07 19.24,21 18.5,20.18L18.35,20L12.59,12.29C12.45,12.1 12.45,11.84 12.59,11.65L18.32,3.96Z",
"name": "mixer"
},
{
"path": "M3.25,4.03L19.95,20.73L18.7,22L14.86,18.13C14.77,18.12 14.68,18.09 14.59,18.05C14.26,17.89 14.14,17.62 14.11,17.38L12.18,15.45C12.14,15.53 12.09,15.6 12.05,15.66C11.62,16.26 11.19,16.26 10.86,16.04C10.54,15.83 5.5,12 5.23,11.87C4.95,11.76 4.85,12.03 5.12,13.5C5.39,15 4.95,15.39 4.57,15.45C4.2,15.5 3.06,15.18 3,12.14C2.95,9.11 3.76,8.62 4.14,8.62C4.6,8.62 7.08,10.69 8.84,12.12L2,5.28L3.25,4.03M18.38,16.56C18.75,15.4 19.12,13.8 19.1,12.03V12C19.14,8.5 17.66,5.58 17.66,5.58C17.66,5.58 17.42,4.72 18.12,4.39C18.83,4.06 19.3,4.61 19.3,4.61C21.12,8.22 21,11.64 21,12C21,12.27 21.09,14.96 19.88,18.05L18.38,16.56M15.14,13.31C15.19,12.92 15.22,12.5 15.24,12.03V12C15.14,8.5 14.13,7.21 14.13,7.21C14.13,7.21 13.89,6.34 14.59,6C15.3,5.69 15.77,6.23 15.77,6.23C17.26,8.94 17.16,11.64 17.14,12C17.15,12.2 17.2,13.38 16.82,15L15.14,13.31M10.2,8.38C10.23,7.77 10.59,7.64 10.59,7.64C10.59,7.64 11.19,7.37 11.57,7.8C11.91,8.19 12.72,9.57 12.89,11.07L10.2,8.38Z",
"name": "nfc-off"

View File

@ -22,6 +22,8 @@ class HcLovelace extends LitElement {
@property() public viewPath?: string | number;
public urlPath?: string | null;
protected render(): TemplateResult {
const index = this._viewIndex;
if (index === undefined) {
@ -35,6 +37,7 @@ class HcLovelace extends LitElement {
const lovelace: Lovelace = {
config: this.lovelaceConfig,
editMode: false,
urlPath: this.urlPath!,
enableFullEditMode: () => undefined,
mode: "storage",
language: "en",

View File

@ -87,6 +87,7 @@ export class HcMain extends HassElement {
.hass=${this.hass}
.lovelaceConfig=${this._lovelaceConfig}
.viewPath=${this._lovelacePath}
.urlPath=${this._urlPath}
@config-refresh=${this._generateLovelaceConfig}
></hc-lovelace>
`;

View File

@ -52,7 +52,6 @@ class CastDemoRow extends LitElement implements LovelaceRow {
});
mgr.castContext.addEventListener(
// eslint-disable-next-line no-undef
// @ts-ignore
cast.framework.CastContextEventType.SESSION_STATE_CHANGED,
(ev) => {
// On Android, opening a new session always results in SESSION_RESUMED.

View File

@ -25,6 +25,7 @@ import { HomeAssistant, Route } from "../../../src/types";
import { showRepositoriesDialog } from "../dialogs/repositories/show-dialog-repositories";
import { supervisorTabs } from "../hassio-tabs";
import "./hassio-addon-repository";
import { ActionDetail } from "@material/mwc-list/mwc-list-foundation";
const sortRepos = (a: HassioAddonRepository, b: HassioAddonRepository) => {
if (a.slug === "local") {
@ -97,14 +98,18 @@ class HassioAddonStore extends LitElement {
.tabs=${supervisorTabs}
>
<span slot="header">Add-on store</span>
<ha-button-menu corner="BOTTOM_START" slot="toolbar-icon">
<ha-button-menu
corner="BOTTOM_START"
slot="toolbar-icon"
@action=${this._handleAction}
>
<mwc-icon-button slot="trigger" alt="menu">
<ha-svg-icon path=${mdiDotsVertical}></ha-svg-icon>
</mwc-icon-button>
<mwc-list-item @tap=${this._manageRepositories}>
<mwc-list-item>
Repositories
</mwc-list-item>
<mwc-list-item @tap=${this.refreshData}>
<mwc-list-item>
Reload
</mwc-list-item>
</ha-button-menu>
@ -143,6 +148,17 @@ class HassioAddonStore extends LitElement {
this._loadData();
}
private _handleAction(ev: CustomEvent<ActionDetail>) {
switch (ev.detail.index) {
case 0:
this._manageRepositories();
break;
case 1:
this.refreshData();
break;
}
}
private apiCalled(ev) {
if (ev.detail.success) {
this._loadData();

View File

@ -94,7 +94,8 @@ class HassioMain extends ProvideHassLitMixin(HassRouterPage) {
applyThemesOnElement(
this.parentElement,
this.hass.themes,
this.hass.selectedTheme || this.hass.themes.default_theme
this.hass.selectedTheme?.theme || this.hass.themes.default_theme,
this.hass.selectedTheme
);
this.style.setProperty(

View File

@ -13,7 +13,7 @@
"lint:prettier": "prettier '**/src/**/*.{js,ts,json,css,md}' --check",
"format:prettier": "prettier '**/src/**/*.{js,ts,json,css,md}' --write",
"lint:types": "tsc",
"lint:lit": "lit-analyzer '**/src/**/*.ts'",
"lint:lit": "lit-analyzer '**/src/**/*.ts' --format markdown --outFile result.md",
"lint": "npm run lint:eslint && npm run lint:prettier && npm run lint:types",
"format": "npm run format:eslint && npm run format:prettier",
"mocha": "node_modules/.bin/ts-mocha -p test-mocha/tsconfig.test.json --opts test-mocha/mocha.opts",
@ -35,13 +35,14 @@
"@material/mwc-icon-button": "^0.17.2",
"@material/mwc-list": "^0.17.2",
"@material/mwc-menu": "^0.17.2",
"@material/mwc-radio": "^0.17.2",
"@material/mwc-ripple": "^0.17.2",
"@material/mwc-switch": "^0.17.2",
"@material/mwc-tab": "^0.17.2",
"@material/mwc-tab-bar": "^0.17.2",
"@material/top-app-bar": "=8.0.0-canary.a78ceb112.0",
"@mdi/js": "5.3.45",
"@mdi/svg": "5.3.45",
"@mdi/js": "5.4.55",
"@mdi/svg": "5.4.55",
"@polymer/app-layout": "^3.0.2",
"@polymer/app-route": "^3.0.2",
"@polymer/app-storage": "^3.0.2",
@ -74,6 +75,7 @@
"@polymer/paper-tooltip": "^3.0.1",
"@polymer/polymer": "3.1.0",
"@thomasloven/round-slider": "0.5.0",
"@types/chromecast-caf-sender": "^1.0.3",
"@vaadin/vaadin-combo-box": "^5.0.10",
"@vaadin/vaadin-date-picker": "^4.0.7",
"@vue/web-component-wrapper": "^1.2.0",
@ -99,7 +101,7 @@
"lit-element": "^2.3.1",
"lit-html": "^1.2.1",
"lit-virtualizer": "^0.4.2",
"marked": "^0.6.1",
"marked": "^1.1.1",
"mdn-polyfills": "^5.16.0",
"memoize-one": "^5.0.2",
"node-vibrant": "^3.1.5",
@ -107,7 +109,7 @@
"regenerator-runtime": "^0.13.2",
"resize-observer-polyfill": "^1.5.1",
"roboto-fontface": "^0.10.0",
"superstruct": "^0.6.1",
"superstruct": "^0.10.12",
"unfetch": "^4.1.0",
"vue": "^2.6.11",
"vue2-daterange-picker": "^0.5.1",
@ -135,11 +137,12 @@
"@rollup/plugin-replace": "^2.3.2",
"@types/chai": "^4.1.7",
"@types/chromecast-caf-receiver": "^3.0.12",
"@types/codemirror": "^0.0.78",
"@types/codemirror": "^0.0.97",
"@types/hls.js": "^0.12.3",
"@types/js-yaml": "^3.12.1",
"@types/leaflet": "^1.4.3",
"@types/leaflet-draw": "^1.0.1",
"@types/marked": "^1.1.0",
"@types/memoize-one": "4.1.0",
"@types/mocha": "^5.2.6",
"@types/resize-observer-browser": "^0.1.3",
@ -170,7 +173,7 @@
"html-minifier": "^4.0.0",
"husky": "^1.3.1",
"lint-staged": "^8.1.5",
"lit-analyzer": "^1.1.10",
"lit-analyzer": "^1.2.0",
"lodash.template": "^4.5.0",
"magic-string": "^0.25.7",
"map-stream": "^0.0.7",
@ -191,7 +194,7 @@
"source-map-url": "^0.4.0",
"systemjs": "^6.3.2",
"terser-webpack-plugin": "^3.0.6",
"ts-lit-plugin": "^1.1.10",
"ts-lit-plugin": "^1.2.0",
"ts-mocha": "^6.0.0",
"typescript": "^3.8.3",
"vinyl-buffer": "^1.0.1",

View File

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

View File

@ -1,4 +1,8 @@
/* eslint-disable no-undef, no-console */
import {
CastStateEventData,
SessionStateEventData,
} from "chromecast-caf-receiver/cast.framework";
import { Auth } from "home-assistant-js-websocket";
import { castApiAvailable } from "./cast_framework";
import { CAST_APP_ID, CAST_DEV, CAST_NS } from "./const";
@ -40,16 +44,13 @@ export class CastManager {
const context = this.castContext;
context.setOptions({
receiverApplicationId: CAST_APP_ID,
// @ts-ignore
autoJoinPolicy: chrome.cast.AutoJoinPolicy.ORIGIN_SCOPED,
});
context.addEventListener(
// @ts-ignore
cast.framework.CastContextEventType.SESSION_STATE_CHANGED,
(ev) => this._sessionStateChanged(ev)
);
context.addEventListener(
// @ts-ignore
cast.framework.CastContextEventType.CAST_STATE_CHANGED,
(ev) => this._castStateChanged(ev)
);
@ -118,7 +119,7 @@ export class CastManager {
}
}
private _sessionStateChanged(ev) {
private _sessionStateChanged(ev: SessionStateEventData) {
if (__DEV__) {
console.log("Cast session state changed", ev.sessionState);
}
@ -141,7 +142,7 @@ export class CastManager {
}
}
private _castStateChanged(ev) {
private _castStateChanged(ev: CastStateEventData) {
if (__DEV__) {
console.log("Cast state changed", ev.castState);
}

View File

@ -0,0 +1,113 @@
const expand_hex = (hex: string): string => {
let result = "";
for (const val of hex) {
result += val + val;
}
return result;
};
const rgb_hex = (component: number): string => {
const hex = Math.round(Math.min(Math.max(component, 0), 255)).toString(16);
return hex.length === 1 ? `0${hex}` : hex;
};
// Conversion between HEX and RGB
export const hex2rgb = (hex: string): [number, number, number] => {
hex = hex.replace("#", "");
if (hex.length === 3 || hex.length === 4) {
hex = expand_hex(hex);
}
return [
parseInt(hex.substring(0, 2), 16),
parseInt(hex.substring(2, 4), 16),
parseInt(hex.substring(4, 6), 16),
];
};
export const rgb2hex = (rgb: [number, number, number]): string => {
return `#${rgb_hex(rgb[0])}${rgb_hex(rgb[1])}${rgb_hex(rgb[2])}`;
};
// Conversion between LAB, XYZ and RGB from https://github.com/gka/chroma.js
// Copyright (c) 2011-2019, Gregor Aisch
// Constants for XYZ and LAB conversion
const Xn = 0.95047;
const Yn = 1;
const Zn = 1.08883;
const t0 = 0.137931034; // 4 / 29
const t1 = 0.206896552; // 6 / 29
const t2 = 0.12841855; // 3 * t1 * t1
const t3 = 0.008856452; // t1 * t1 * t1
const rgb_xyz = (r: number) => {
r /= 255;
if (r <= 0.04045) {
return r / 12.92;
}
return ((r + 0.055) / 1.055) ** 2.4;
};
const xyz_lab = (t: number) => {
if (t > t3) {
return t ** (1 / 3);
}
return t / t2 + t0;
};
const xyz_rgb = (r: number) => {
return 255 * (r <= 0.00304 ? 12.92 * r : 1.055 * r ** (1 / 2.4) - 0.055);
};
const lab_xyz = (t: number) => {
return t > t1 ? t * t * t : t2 * (t - t0);
};
// Conversions between RGB and LAB
const rgb2xyz = (rgb: [number, number, number]): [number, number, number] => {
let [r, g, b] = rgb;
r = rgb_xyz(r);
g = rgb_xyz(g);
b = rgb_xyz(b);
const x = xyz_lab((0.4124564 * r + 0.3575761 * g + 0.1804375 * b) / Xn);
const y = xyz_lab((0.2126729 * r + 0.7151522 * g + 0.072175 * b) / Yn);
const z = xyz_lab((0.0193339 * r + 0.119192 * g + 0.9503041 * b) / Zn);
return [x, y, z];
};
export const rgb2lab = (
rgb: [number, number, number]
): [number, number, number] => {
const [x, y, z] = rgb2xyz(rgb);
const l = 116 * y - 16;
return [l < 0 ? 0 : l, 500 * (x - y), 200 * (y - z)];
};
export const lab2rgb = (
lab: [number, number, number]
): [number, number, number] => {
const [l, a, b] = lab;
let y = (l + 16) / 116;
let x = isNaN(a) ? y : y + a / 500;
let z = isNaN(b) ? y : y - b / 200;
y = Yn * lab_xyz(y);
x = Xn * lab_xyz(x);
z = Zn * lab_xyz(z);
const r = xyz_rgb(3.2404542 * x - 1.5371385 * y - 0.4985314 * z); // D65 -> sRGB
const g = xyz_rgb(-0.969266 * x + 1.8760108 * y + 0.041556 * z);
const b_ = xyz_rgb(0.0556434 * x - 0.2040259 * y + 1.0572252 * z);
return [r, g, b_];
};
export const lab2hex = (lab: [number, number, number]): string => {
const rgb = lab2rgb(lab);
return rgb2hex(rgb);
};

16
src/common/color/lab.ts Normal file
View File

@ -0,0 +1,16 @@
// From https://github.com/gka/chroma.js
// Copyright (c) 2011-2019, Gregor Aisch
export const labDarken = (
lab: [number, number, number],
amount = 1
): [number, number, number] => {
return [lab[0] - 18 * amount, lab[1], lab[2]];
};
export const labBrighten = (
lab: [number, number, number],
amount = 1
): [number, number, number] => {
return labDarken(lab, -amount);
};

24
src/common/color/rgb.ts Normal file
View File

@ -0,0 +1,24 @@
const luminosity = (rgb: [number, number, number]): number => {
// http://www.w3.org/TR/WCAG20/#relativeluminancedef
const lum: [number, number, number] = [0, 0, 0];
for (let i = 0; i < rgb.length; i++) {
const chan = rgb[i] / 255;
lum[i] = chan <= 0.03928 ? chan / 12.92 : ((chan + 0.055) / 1.055) ** 2.4;
}
return 0.2126 * lum[0] + 0.7152 * lum[1] + 0.0722 * lum[2];
};
export const rgbContrast = (
color1: [number, number, number],
color2: [number, number, number]
) => {
const lum1 = luminosity(color1);
const lum2 = luminosity(color2);
if (lum1 > lum2) {
return (lum1 + 0.05) / (lum2 + 0.05);
}
return (lum2 + 0.05) / (lum1 + 0.05);
};

View File

@ -1,26 +1,20 @@
import { derivedStyles } from "../../resources/styles";
import { derivedStyles, darkStyles } from "../../resources/styles";
import { HomeAssistant, Theme } from "../../types";
import {
hex2rgb,
rgb2hex,
rgb2lab,
lab2rgb,
lab2hex,
} from "../color/convert-color";
import { rgbContrast } from "../color/rgb";
import { labDarken, labBrighten } from "../color/lab";
interface ProcessedTheme {
keys: { [key: string]: "" };
styles: { [key: string]: string };
}
const hexToRgb = (hex: string): string | null => {
const shorthandRegex = /^#?([a-f\d])([a-f\d])([a-f\d])$/i;
const checkHex = hex.replace(shorthandRegex, (_m, r, g, b) => {
return r + r + g + g + b + b;
});
const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(checkHex);
return result
? `${parseInt(result[1], 16)}, ${parseInt(result[2], 16)}, ${parseInt(
result[3],
16
)}`
: null;
};
let PROCESSED_THEMES: { [key: string]: ProcessedTheme } = {};
/**
@ -33,17 +27,56 @@ let PROCESSED_THEMES: { [key: string]: ProcessedTheme } = {};
export const applyThemesOnElement = (
element,
themes: HomeAssistant["themes"],
selectedTheme?: string
selectedTheme?: string,
themeOptions?: Partial<HomeAssistant["selectedTheme"]>
) => {
const newTheme = selectedTheme
? PROCESSED_THEMES[selectedTheme] || processTheme(selectedTheme, themes)
: undefined;
let cacheKey = selectedTheme;
let themeRules: Partial<Theme> = {};
if (!element._themes && !newTheme) {
if (selectedTheme === "default" && themeOptions) {
if (themeOptions.dark) {
cacheKey = `${cacheKey}__dark`;
themeRules = darkStyles;
}
if (themeOptions.primaryColor) {
cacheKey = `${cacheKey}__primary_${themeOptions.primaryColor}`;
const rgbPrimaryColor = hex2rgb(themeOptions.primaryColor);
const labPrimaryColor = rgb2lab(rgbPrimaryColor);
themeRules["primary-color"] = themeOptions.primaryColor;
const rgbLigthPrimaryColor = lab2rgb(labBrighten(labPrimaryColor));
themeRules["light-primary-color"] = rgb2hex(rgbLigthPrimaryColor);
themeRules["dark-primary-color"] = lab2hex(labDarken(labPrimaryColor));
themeRules["text-primary-color"] =
rgbContrast(rgbPrimaryColor, [33, 33, 33]) < 6 ? "#fff" : "#212121";
themeRules["text-light-primary-color"] =
rgbContrast(rgbLigthPrimaryColor, [33, 33, 33]) < 6
? "#fff"
: "#212121";
themeRules["state-icon-color"] = themeRules["dark-primary-color"];
}
if (themeOptions.accentColor) {
cacheKey = `${cacheKey}__accent_${themeOptions.accentColor}`;
themeRules["accent-color"] = themeOptions.accentColor;
const rgbAccentColor = hex2rgb(themeOptions.accentColor);
themeRules["text-accent-color"] =
rgbContrast(rgbAccentColor, [33, 33, 33]) < 6 ? "#fff" : "#212121";
}
}
if (selectedTheme && themes.themes[selectedTheme]) {
themeRules = themes.themes[selectedTheme];
}
if (!element._themes && !Object.keys(themeRules).length) {
// No styles to reset, and no styles to set
return;
}
const newTheme =
themeRules && cacheKey
? PROCESSED_THEMES[cacheKey] || processTheme(cacheKey, themeRules)
: undefined;
// Add previous set keys to reset them, and new theme
const styles = { ...element._themes, ...newTheme?.styles };
element._themes = newTheme?.keys;
@ -58,42 +91,45 @@ export const applyThemesOnElement = (
};
const processTheme = (
themeName: string,
themes: HomeAssistant["themes"]
cacheKey: string,
theme: Partial<Theme>
): ProcessedTheme | undefined => {
if (!themes.themes[themeName]) {
if (!theme || !Object.keys(theme).length) {
return undefined;
}
const theme: Theme = {
const combinedTheme: Partial<Theme> = {
...derivedStyles,
...themes.themes[themeName],
...theme,
};
const styles = {};
const keys = {};
for (const key of Object.keys(theme)) {
for (const key of Object.keys(combinedTheme)) {
const prefixedKey = `--${key}`;
const value = theme[key];
const value = combinedTheme[key]!;
styles[prefixedKey] = value;
keys[prefixedKey] = "";
// Try to create a rgb value for this key if it is a hex color
// Try to create a rgb value for this key if it is not a var
if (!value.startsWith("#")) {
// Not a hex color
// Can't convert non hex value
continue;
}
const rgbKey = `rgb-${key}`;
if (theme[rgbKey] !== undefined) {
if (combinedTheme[rgbKey] !== undefined) {
// Theme has it's own rgb value
continue;
}
const rgbValue = hexToRgb(value);
if (rgbValue !== null) {
try {
const rgbValue = hex2rgb(value).join(",");
const prefixedRgbKey = `--${rgbKey}`;
styles[prefixedRgbKey] = rgbValue;
keys[prefixedRgbKey] = "";
} catch (e) {
continue;
}
}
PROCESSED_THEMES[themeName] = { styles, keys };
PROCESSED_THEMES[cacheKey] = { styles, keys };
return { styles, keys };
};

View File

@ -1,4 +1,4 @@
import type { Map } from "leaflet";
import type { Map, TileLayer } from "leaflet";
// Sets up a Leaflet map on the provided DOM element
export type LeafletModuleType = typeof import("leaflet");
@ -6,9 +6,9 @@ export type LeafletDrawModuleType = typeof import("leaflet-draw");
export const setupLeafletMap = async (
mapElement: HTMLElement,
darkMode = false,
darkMode?: boolean,
draw = false
): Promise<[Map, LeafletModuleType]> => {
): Promise<[Map, LeafletModuleType, TileLayer]> => {
if (!mapElement.parentNode) {
throw new Error("Cannot setup Leaflet map on disconnected element");
}
@ -28,15 +28,28 @@ export const setupLeafletMap = async (
style.setAttribute("rel", "stylesheet");
mapElement.parentNode.appendChild(style);
map.setView([52.3731339, 4.8903147], 13);
createTileLayer(Leaflet, darkMode).addTo(map);
return [map, Leaflet];
const tileLayer = createTileLayer(Leaflet, Boolean(darkMode)).addTo(map);
return [map, Leaflet, tileLayer];
};
export const createTileLayer = (
export const replaceTileLayer = (
leaflet: LeafletModuleType,
map: Map,
tileLayer: TileLayer,
darkMode: boolean
): TileLayer => {
map.removeLayer(tileLayer);
tileLayer = createTileLayer(leaflet, darkMode);
tileLayer.addTo(map);
return tileLayer;
};
const createTileLayer = (
leaflet: LeafletModuleType,
darkMode: boolean
) => {
): TileLayer => {
return leaflet.tileLayer(
`https://{s}.basemaps.cartocdn.com/${
darkMode ? "dark_all" : "light_all"

View File

@ -13,7 +13,7 @@ export const batteryIcon = (
return "hass:battery-unknown";
}
var icon = "hass:battery";
let icon = "hass:battery";
const batteryRound = Math.round(battery / 10) * 10;
if (battery_charging && battery > 10) {
icon += `-charging-${batteryRound}`;

View File

@ -4,6 +4,6 @@ export const supportsFeature = (
stateObj: HassEntity,
feature: number
): boolean => {
// eslint-disable-next-line:no-bitwise
// eslint-disable-next-line no-bitwise
return (stateObj.attributes.supported_features! & feature) !== 0;
};

View File

@ -2,11 +2,3 @@ const validEntityId = /^(\w+)\.(\w+)$/;
export const isValidEntityId = (entityId: string) =>
validEntityId.test(entityId);
export const createValidEntityId = (input: string) =>
input
.toLowerCase()
.replace(/\s|'|\./g, "_") // replace spaces, points and quotes with underscore
.replace(/\W/g, "") // remove not allowed chars
.replace(/_{2,}/g, "_") // replace multiple underscores with 1
.replace(/_$/, ""); // remove underscores at the end

View File

@ -0,0 +1,14 @@
import {
RequestSelectedDetail,
ListItem,
} from "@material/mwc-list/mwc-list-item";
export const shouldHandleRequestSelectedEvent = (
ev: CustomEvent<RequestSelectedDetail>
): boolean => {
if (!ev.detail.selected && ev.detail.source !== "property") {
return false;
}
(ev.target as ListItem).selected = false;
return true;
};

View File

@ -1,5 +1,5 @@
// https://gist.github.com/hagemann/382adfc57adbd5af078dc93feef01fe1
export const slugify = (value: string, delimiter = "-") => {
export const slugify = (value: string, delimiter = "_") => {
const a =
"àáäâãåăæąçćčđďèéěėëêęğǵḧìíïîįłḿǹńňñòóöôœøṕŕřßşśšșťțùúüûǘůűūųẃẍÿýźžż·/_,:;";
const b = `aaaaaaaaacccddeeeeeeegghiiiiilmnnnnooooooprrsssssttuuuuuuuuuwxyyzzz${delimiter}${delimiter}${delimiter}${delimiter}${delimiter}${delimiter}`;

View File

@ -9,5 +9,9 @@ export function computeRTL(hass: HomeAssistant) {
}
export function computeRTLDirection(hass: HomeAssistant) {
return computeRTL(hass) ? "rtl" : "ltr";
return emitRTLDirection(computeRTL(hass));
}
export function emitRTLDirection(rtl: boolean) {
return rtl ? "rtl" : "ltr";
}

View File

@ -54,8 +54,8 @@ class HaCallServiceButton extends EventsMixin(PolymerElement) {
callService() {
this.progress = true;
// eslint-disable-next-line @typescript-eslint/no-this-alias
var el = this;
var eventData = {
const el = this;
const eventData = {
domain: this.domain,
service: this.service,
serviceData: this.serviceData,

View File

@ -78,7 +78,7 @@ class HaProgressButton extends PolymerElement {
}
tempClass(className) {
var classList = this.$.container.classList;
const classList = this.$.container.classList;
classList.add(className);
setTimeout(() => {
classList.remove(className);

View File

@ -98,6 +98,8 @@ export class HaDataTable extends LitElement {
@property({ type: String }) public noDataText?: string;
@property({ type: String }) public searchLabel?: string;
@property({ type: String }) public filter = "";
@internalProperty() private _filterable = false;
@ -202,6 +204,7 @@ export class HaDataTable extends LitElement {
<div class="table-header">
<search-input
@value-changed=${this._handleSearchChange}
.label=${this.searchLabel}
></search-input>
</div>
`
@ -538,7 +541,7 @@ export class HaDataTable extends LitElement {
border-radius: 4px;
border-width: 1px;
border-style: solid;
border-color: rgba(var(--rgb-primary-text-color), 0.12);
border-color: var(--divider-color);
display: inline-flex;
flex-direction: column;
box-sizing: border-box;
@ -556,7 +559,7 @@ export class HaDataTable extends LitElement {
}
.mdc-data-table__row ~ .mdc-data-table__row {
border-top: 1px solid rgba(var(--rgb-primary-text-color), 0.12);
border-top: 1px solid var(--divider-color);
}
.mdc-data-table__row:not(.mdc-data-table__row--selected):hover {
@ -575,7 +578,7 @@ export class HaDataTable extends LitElement {
height: 56px;
display: flex;
width: 100%;
border-bottom: 1px solid rgba(var(--rgb-primary-text-color), 0.12);
border-bottom: 1px solid var(--divider-color);
overflow-x: auto;
}
@ -828,7 +831,7 @@ export class HaDataTable extends LitElement {
right: 12px;
}
.table-header {
border-bottom: 1px solid rgba(var(--rgb-primary-text-color), 0.12);
border-bottom: 1px solid var(--divider-color);
padding: 0 16px;
}
search-input {

View File

@ -135,7 +135,7 @@ class DateRangePickerElement extends WrappedElement {
}
.daterangepicker td.in-range {
background-color: var(--light-primary-color);
color: var(--primary-text-color);
color: var(--text-light-primary-color, var(--primary-text-color));
}
.daterangepicker td.active,
.daterangepicker td.active:hover {

View File

@ -117,11 +117,7 @@ export abstract class HaDeviceAutomationPicker<
>
${this.NO_AUTOMATION_TEXT}
</paper-item>
<paper-item
key=${UNKNOWN_AUTOMATION_KEY}
.automation=${this.value}
hidden
>
<paper-item key=${UNKNOWN_AUTOMATION_KEY} hidden>
${this.UNKNOWN_AUTOMATION_TEXT}
</paper-item>
${this._automations.map(
@ -175,18 +171,17 @@ export abstract class HaDeviceAutomationPicker<
}
private _automationChanged(ev) {
this._setValue(ev.detail.item.automation);
if (ev.detail.item.automation) {
this._setValue(ev.detail.item.automation);
}
}
private _setValue(automation: T) {
if (this.value && deviceAutomationsEqual(automation, this.value)) {
return;
}
this.value = automation;
setTimeout(() => {
fireEvent(this, "change");
fireEvent(this, "value-changed", { value: automation });
}, 0);
fireEvent(this, "change");
fireEvent(this, "value-changed", { value: automation });
}
static get styles(): CSSResult {

View File

@ -23,10 +23,10 @@ export const HaIronFocusablesHelper = {
* @return {!Array<!HTMLElement>}
*/
getTabbableNodes: function (node) {
var result = [];
const result = [];
// If there is at least one element with tabindex > 0, we need to sort
// the final array by tabindex.
var needsSortByTabIndex = this._collectTabbableNodes(node, result);
const needsSortByTabIndex = this._collectTabbableNodes(node, result);
if (needsSortByTabIndex) {
return IronFocusablesHelper._sortByTabIndex(result);
}
@ -50,9 +50,9 @@ export const HaIronFocusablesHelper = {
) {
return false;
}
var element = /** @type {!HTMLElement} */ (node);
var tabIndex = IronFocusablesHelper._normalizedTabIndex(element);
var needsSort = tabIndex > 0;
const element = /** @type {!HTMLElement} */ (node);
const tabIndex = IronFocusablesHelper._normalizedTabIndex(element);
let needsSort = tabIndex > 0;
if (tabIndex >= 0) {
result.push(element);
}
@ -70,7 +70,7 @@ export const HaIronFocusablesHelper = {
// <input id="B" slot="b" tabindex="1">
// </div>
// TODO(valdrin) support ShadowDOM v1 when upgrading to Polymer v2.0.
var children;
let children;
if (element.localName === "content" || element.localName === "slot") {
children = dom(element).getDistributedNodes();
} else {
@ -80,7 +80,7 @@ export const HaIronFocusablesHelper = {
children = dom(element.shadowRoot || element.root || element).children;
// /////////////////////////
}
for (var i = 0; i < children.length; i++) {
for (let i = 0; i < children.length; i++) {
// Ensure method is always invoked to collect tabbable children.
needsSort = this._collectTabbableNodes(children[i], result) || needsSort;
}

View File

@ -81,7 +81,7 @@ export class HaStateLabelBadge extends LitElement {
? ""
: this.image
? this.image
: state.attributes.entity_picture}"
: state.attributes.entity_picture_local || state.attributes.entity_picture}"
.label="${this._computeLabel(domain, state, this._timerTimeRemaining)}"
.description="${this.name ? this.name : computeStateName(state)}"
></ha-label-badge>

View File

@ -73,10 +73,10 @@ export class StateBadge extends LitElement {
if (stateObj) {
// hide icon if we have entity picture
if (
(stateObj.attributes.entity_picture && !this.overrideIcon) ||
((stateObj.attributes.entity_picture_local || stateObj.attributes.entity_picture) && !this.overrideIcon) ||
this.overrideImage
) {
let imageUrl = this.overrideImage || stateObj.attributes.entity_picture;
let imageUrl = this.overrideImage || stateObj.attributes.entity_picture_local || stateObj.attributes.entity_picture;
if (this.hass) {
imageUrl = this.hass.hassUrl(imageUrl);
}

View File

@ -10,7 +10,6 @@ import {
} from "lit-element";
import "@material/mwc-button";
import "@material/mwc-menu";
import "@material/mwc-list/mwc-list-item";
import type { Menu, Corner } from "@material/mwc-menu";
import "./ha-icon-button";
@ -19,14 +18,30 @@ import "./ha-icon-button";
export class HaButtonMenu extends LitElement {
@property() public corner: Corner = "TOP_START";
@property({ type: Boolean }) public multi = false;
@property({ type: Boolean }) public activatable = false;
@query("mwc-menu") private _menu?: Menu;
public get items() {
return this._menu?.items;
}
public get selected() {
return this._menu?.selected;
}
protected render(): TemplateResult {
return html`
<div @click=${this._handleClick}>
<slot name="trigger"></slot>
</div>
<mwc-menu .corner=${this.corner}>
<mwc-menu
.corner=${this.corner}
.multi=${this.multi}
.activatable=${this.activatable}
>
<slot></slot>
</mwc-menu>
`;

View File

@ -66,7 +66,7 @@ export class HaCard extends LitElement {
}
:host ::slotted(.card-actions) {
border-top: 1px solid #e8e8e8;
border-top: 1px solid var(--divider-color, #e8e8e8);
padding: 5px 16px;
}
`;

View File

@ -188,10 +188,10 @@ class HaColorPicker extends EventsMixin(PolymerElement) {
// origin is wheel center
// returns {x: X, y: Y} object
convertToCanvasCoordinates(clientX, clientY) {
var svgPoint = this.interactionLayer.createSVGPoint();
const svgPoint = this.interactionLayer.createSVGPoint();
svgPoint.x = clientX;
svgPoint.y = clientY;
var cc = svgPoint.matrixTransform(
const cc = svgPoint.matrixTransform(
this.interactionLayer.getScreenCTM().inverse()
);
return { x: cc.x, y: cc.y };
@ -225,7 +225,7 @@ class HaColorPicker extends EventsMixin(PolymerElement) {
// Touch events
onTouchStart(ev) {
var touch = ev.changedTouches[0];
const touch = ev.changedTouches[0];
const cc = this.convertToCanvasCoordinates(touch.clientX, touch.clientY);
// return if we're not on the wheel
if (!this.isInWheel(cc.x, cc.y)) {
@ -275,8 +275,8 @@ class HaColorPicker extends EventsMixin(PolymerElement) {
// Process user input to color
processUserSelect(ev) {
var canvasXY = this.convertToCanvasCoordinates(ev.clientX, ev.clientY);
var hs = this.getColor(canvasXY.x, canvasXY.y);
const canvasXY = this.convertToCanvasCoordinates(ev.clientX, ev.clientY);
const hs = this.getColor(canvasXY.x, canvasXY.y);
this.onColorSelect(hs);
}
@ -319,11 +319,11 @@ class HaColorPicker extends EventsMixin(PolymerElement) {
// set marker position to the given color
setMarkerOnColor(hs) {
var dist = hs.s * this.radius;
var theta = ((hs.h - 180) / 180) * Math.PI;
var markerdX = -dist * Math.cos(theta);
var markerdY = -dist * Math.sin(theta);
var translateString = `translate(${markerdX},${markerdY})`;
const dist = hs.s * this.radius;
const theta = ((hs.h - 180) / 180) * Math.PI;
const markerdX = -dist * Math.cos(theta);
const markerdY = -dist * Math.sin(theta);
const translateString = `translate(${markerdX},${markerdY})`;
this.marker.setAttribute("transform", translateString);
this.tooltip.setAttribute("transform", translateString);
}
@ -358,8 +358,8 @@ class HaColorPicker extends EventsMixin(PolymerElement) {
// get angle (degrees)
getAngle(dX, dY) {
var theta = Math.atan2(-dY, -dX); // radians from the left edge, clockwise = positive
var angle = (theta / Math.PI) * 180 + 180; // degrees, clockwise from right
const theta = Math.atan2(-dY, -dX); // radians from the left edge, clockwise = positive
const angle = (theta / Math.PI) * 180 + 180; // degrees, clockwise from right
return angle;
}
@ -378,9 +378,9 @@ class HaColorPicker extends EventsMixin(PolymerElement) {
*/
getColor(x, y) {
var hue = this.getAngle(x, y); // degrees, clockwise from right
var relativeDistance = this.getDistance(x, y); // edge of radius = 1
var sat = Math.min(relativeDistance, 1); // Distance from center
const hue = this.getAngle(x, y); // degrees, clockwise from right
const relativeDistance = this.getDistance(x, y); // edge of radius = 1
const sat = Math.min(relativeDistance, 1); // Distance from center
return { h: hue, s: sat };
}
@ -402,9 +402,9 @@ class HaColorPicker extends EventsMixin(PolymerElement) {
if (this.saturationSegments === 1) {
hs.s = 1;
} else {
var segmentSize = 1 / this.saturationSegments;
var saturationStep = 1 / (this.saturationSegments - 1);
var calculatedSat = Math.floor(hs.s / segmentSize) * saturationStep;
const segmentSize = 1 / this.saturationSegments;
const saturationStep = 1 / (this.saturationSegments - 1);
const calculatedSat = Math.floor(hs.s / segmentSize) * saturationStep;
hs.s = Math.min(calculatedSat, 1);
}
}
@ -477,9 +477,9 @@ class HaColorPicker extends EventsMixin(PolymerElement) {
hueSegments = hueSegments || 360; // reset 0 segments to 360
const angleStep = 360 / hueSegments;
const halfAngleStep = angleStep / 2; // center segments on color
for (var angle = 0; angle <= 360; angle += angleStep) {
var startAngle = (angle - halfAngleStep) * (Math.PI / 180);
var endAngle = (angle + halfAngleStep + 1) * (Math.PI / 180);
for (let angle = 0; angle <= 360; angle += angleStep) {
const startAngle = (angle - halfAngleStep) * (Math.PI / 180);
const endAngle = (angle + halfAngleStep + 1) * (Math.PI / 180);
context.beginPath();
context.moveTo(cX, cY);
context.arc(
@ -492,7 +492,7 @@ class HaColorPicker extends EventsMixin(PolymerElement) {
);
context.closePath();
// gradient
var gradient = context.createRadialGradient(
const gradient = context.createRadialGradient(
cX,
cY,
0,
@ -507,8 +507,8 @@ class HaColorPicker extends EventsMixin(PolymerElement) {
if (saturationSegments > 0) {
const ratioStep = 1 / saturationSegments;
let ratio = 0;
for (var stop = 1; stop < saturationSegments; stop += 1) {
var prevLighness = lightness;
for (let stop = 1; stop < saturationSegments; stop += 1) {
const prevLighness = lightness;
ratio = stop * ratioStep;
lightness = 100 - 50 * ratio;
gradient.addColorStop(

View File

@ -95,7 +95,7 @@ class HaCoverControls extends PolymerElement {
if (stateObj.state === UNAVAILABLE) {
return true;
}
var assumedState = stateObj.attributes.assumed_state === true;
const assumedState = stateObj.attributes.assumed_state === true;
return (entityObj.isFullyOpen || entityObj.isOpening) && !assumedState;
}
@ -103,7 +103,7 @@ class HaCoverControls extends PolymerElement {
if (stateObj.state === UNAVAILABLE) {
return true;
}
var assumedState = stateObj.attributes.assumed_state === true;
const assumedState = stateObj.attributes.assumed_state === true;
return (entityObj.isFullyClosed || entityObj.isClosing) && !assumedState;
}

View File

@ -75,7 +75,7 @@ class HaCoverTiltControls extends PolymerElement {
if (stateObj.state === UNAVAILABLE) {
return true;
}
var assumedState = stateObj.attributes.assumed_state === true;
const assumedState = stateObj.attributes.assumed_state === true;
return entityObj.isFullyOpenTilt && !assumedState;
}
@ -83,7 +83,7 @@ class HaCoverTiltControls extends PolymerElement {
if (stateObj.state === UNAVAILABLE) {
return true;
}
var assumedState = stateObj.attributes.assumed_state === true;
const assumedState = stateObj.attributes.assumed_state === true;
return entityObj.isFullyClosedTilt && !assumedState;
}

View File

@ -17,6 +17,8 @@ import "./ha-svg-icon";
import "@polymer/paper-input/paper-input";
import "@material/mwc-list/mwc-list";
import "./date-range-picker";
import { computeRTLDirection } from "../common/util/compute_rtl";
import { ActionDetail } from "@material/mwc-list/mwc-list-foundation";
export interface DateRangePickerRanges {
[key: string]: [Date, Date];
@ -36,11 +38,14 @@ export class HaDateRangePicker extends LitElement {
@property({ type: Boolean }) private _hour24format = false;
@property({ type: String }) private _rtlDirection = "ltr";
protected updated(changedProps: PropertyValues) {
if (changedProps.has("hass")) {
const oldHass = changedProps.get("hass") as HomeAssistant | undefined;
if (!oldHass || oldHass.language !== this.hass.language) {
this._hour24format = this._compute24hourFormat();
this._rtlDirection = computeRTLDirection(this.hass);
}
}
}
@ -76,16 +81,14 @@ export class HaDateRangePicker extends LitElement {
></paper-input>
</div>
${this.ranges
? html`<div slot="ranges" class="date-range-ranges">
<mwc-list @click=${this._setDateRange}>
${Object.entries(this.ranges).map(
([name, dates]) => html`<mwc-list-item
.activated=${this.startDate.getTime() ===
dates[0].getTime() &&
this.endDate.getTime() === dates[1].getTime()}
.startDate=${dates[0]}
.endDate=${dates[1]}
>
? html`<div
slot="ranges"
class="date-range-ranges"
.dir=${this._rtlDirection}
>
<mwc-list @action=${this._setDateRange} activatable>
${Object.keys(this.ranges).map(
(name) => html`<mwc-list-item>
${name}
</mwc-list-item>`
)}
@ -116,12 +119,10 @@ export class HaDateRangePicker extends LitElement {
);
}
private _setDateRange(ev: Event) {
const target = ev.target as any;
const startDate = target.startDate;
const endDate = target.endDate;
private _setDateRange(ev: CustomEvent<ActionDetail>) {
const dateRange = Object.values(this.ranges!)[ev.detail.index];
const dateRangePicker = this._dateRangePicker;
dateRangePicker.clickRange([startDate, endDate]);
dateRangePicker.clickRange(dateRange);
dateRangePicker.clickedApply();
}

View File

@ -5,7 +5,7 @@ import "./ha-icon-button";
import { css, CSSResult, customElement, html } from "lit-element";
import type { Constructor, HomeAssistant } from "../types";
import { mdiClose } from "@mdi/js";
import { computeRTL } from "../common/util/compute_rtl";
import { computeRTLDirection } from "../common/util/compute_rtl";
const MwcDialog = customElements.get("mwc-dialog") as Constructor<Dialog>;
@ -14,8 +14,8 @@ export const createCloseHeading = (hass: HomeAssistant, title: string) => html`
<mwc-icon-button
aria-label=${hass.localize("ui.dialogs.generic.close")}
dialogAction="close"
?rtl=${computeRTL(hass)}
class="header_button"
dir=${computeRTLDirection(hass)}
>
<ha-svg-icon path=${mdiClose}></ha-svg-icon>
</mwc-icon-button>
@ -34,10 +34,12 @@ export class HaDialog extends MwcDialog {
style,
css`
.mdc-dialog {
--mdc-dialog-scroll-divider-color: var(--divider-color);
z-index: var(--dialog-z-index, 7);
}
.mdc-dialog__actions {
justify-content: var(--justify-action-buttons, flex-end);
padding-bottom: max(env(safe-area-inset-bottom), 8px);
}
.mdc-dialog__container {
align-items: var(--vertial-align-dialog, center);
@ -50,6 +52,12 @@ export class HaDialog extends MwcDialog {
position: var(--dialog-content-position, relative);
padding: var(--dialog-content-padding, 20px 24px);
}
:host([hideactions]) .mdc-dialog .mdc-dialog__content {
padding-bottom: max(
var(--dialog-content-padding, 20px),
env(safe-area-inset-bottom)
);
}
.mdc-dialog .mdc-dialog__surface {
position: var(--dialog-surface-position, relative);
min-height: var(--mdc-dialog-min-height, auto);
@ -61,7 +69,7 @@ export class HaDialog extends MwcDialog {
text-decoration: none;
color: inherit;
}
mwc-icon-button[rtl].header_button {
[dir="rtl"].header_button {
right: auto;
left: 16px;
}

View File

@ -100,7 +100,7 @@ export interface HaFormTimeData {
}
export interface HaFormElement extends LitElement {
schema: HaFormSchema;
schema: HaFormSchema | HaFormSchema[];
data?: HaFormDataContainer | HaFormData;
label?: string;
suffix?: string;
@ -110,7 +110,7 @@ export interface HaFormElement extends LitElement {
export class HaForm extends LitElement implements HaFormElement {
@property() public data!: HaFormDataContainer | HaFormData;
@property() public schema!: HaFormSchema;
@property() public schema!: HaFormSchema | HaFormSchema[];
@property() public error;
@ -190,7 +190,7 @@ export class HaForm extends LitElement implements HaFormElement {
: "";
}
private _computeError(error, schema: HaFormSchema) {
private _computeError(error, schema: HaFormSchema | HaFormSchema[]) {
return this.computeError ? this.computeError(error, schema) : error;
}
@ -203,7 +203,7 @@ export class HaForm extends LitElement implements HaFormElement {
private _valueChanged(ev: CustomEvent) {
ev.stopPropagation();
const schema = (ev.target as HaFormElement).schema;
const schema = (ev.target as HaFormElement).schema as HaFormSchema;
const data = this.data as HaFormDataContainer;
data[schema.name] = ev.detail.value;
fireEvent(this, "value-changed", {

View File

@ -9,6 +9,7 @@ import {
} from "lit-element";
import { styleMap } from "lit-html/directives/style-map";
import { afterNextRender } from "../common/util/render-status";
import { ifDefined } from "lit-html/directives/if-defined";
const getAngle = (value: number, min: number, max: number) => {
const percentage = getValueInPercentage(normalize(value, min, max), min, max);
@ -27,6 +28,9 @@ const getValueInPercentage = (value: number, min: number, max: number) => {
return (100 * newVal) / newMax;
};
// Workaround for https://github.com/home-assistant/frontend/issues/6467
const isSafari = /^((?!chrome|android).)*safari/i.test(navigator.userAgent);
@customElement("ha-gauge")
export class Gauge extends LitElement {
@property({ type: Number }) public min = 0;
@ -69,9 +73,28 @@ export class Gauge extends LitElement {
></path>
<path
class="value"
style=${styleMap({ transform: `rotate(${this._angle}deg)` })}
d="M 90 50.001 A 40 40 0 0 1 10 50"
></path>
style=${ifDefined(
!isSafari
? styleMap({ transform: `rotate(${this._angle}deg)` })
: undefined
)}
transform=${ifDefined(
isSafari ? `rotate(${this._angle} 50 50)` : undefined
)}
>
${
isSafari
? svg`<animateTransform
attributeName="transform"
type="rotate"
from="0 50 50"
to="${this._angle} 50 50"
dur="1s"
/>`
: ""
}
</path>
</svg>
<svg class="text">
<text class="value-text">
@ -106,8 +129,8 @@ export class Gauge extends LitElement {
fill: none;
stroke-width: 15;
stroke: var(--gauge-color);
transition: all 1000ms ease 0s;
transform-origin: 50% 100%;
transition: all 1s ease 0s;
}
.gauge {
display: block;

View File

@ -193,6 +193,7 @@ const mdiRemovedIcons = new Set([
"medium",
"meetup",
"mixcloud",
"mixer",
"nfc-off",
"npm-variant",
"npm-variant-outline",

View File

@ -23,7 +23,6 @@ class HaMarkdownElement extends UpdatingElement {
{
breaks: this.breaks,
gfm: true,
tables: true,
},
{
allowSvg: this.allowSvg,

View File

@ -0,0 +1,20 @@
import "@material/mwc-radio";
import type { Radio } from "@material/mwc-radio";
import { customElement } from "lit-element";
import type { Constructor } from "../types";
const MwcRadio = customElements.get("mwc-radio") as Constructor<Radio>;
@customElement("ha-radio")
export class HaRadio extends MwcRadio {
public firstUpdated() {
super.firstUpdated();
this.style.setProperty("--mdc-theme-secondary", "var(--primary-color)");
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-radio": HaRadio;
}
}

View File

@ -16,9 +16,9 @@ class HaServiceDescription extends PolymerElement {
}
_getDescription(hass, domain, service) {
var domainServices = hass.services[domain];
const domainServices = hass.services[domain];
if (!domainServices) return "";
var serviceObject = domainServices[service];
const serviceObject = domainServices[service];
if (!serviceObject) return "";
return serviceObject.description;
}

View File

@ -126,7 +126,7 @@ class HaSidebar extends LitElement {
// property used only in css
// @ts-ignore
@property({ type: Boolean, reflect: true }) private _rtl = false;
@property({ type: Boolean, reflect: true }) public rtl = false;
private _mouseLeaveTimeout?: number;
@ -312,6 +312,7 @@ class HaSidebar extends LitElement {
hass.panelUrl !== oldHass.panelUrl ||
hass.user !== oldHass.user ||
hass.localize !== oldHass.localize ||
hass.language !== oldHass.language ||
hass.states !== oldHass.states ||
hass.defaultPanel !== oldHass.defaultPanel
);
@ -339,12 +340,14 @@ class HaSidebar extends LitElement {
return;
}
this._rtl = computeRTL(this.hass);
const oldHass = changedProps.get("hass") as HomeAssistant | undefined;
if (!oldHass || oldHass.language !== this.hass.language) {
this.rtl = computeRTL(this.hass);
}
if (!SUPPORT_SCROLL_IF_NEEDED) {
return;
}
const oldHass = changedProps.get("hass") as HomeAssistant | undefined;
if (!oldHass || oldHass.panelUrl !== this.hass.panelUrl) {
const selectedEl = this.shadowRoot!.querySelector(".iron-selected");
if (selectedEl) {
@ -496,9 +499,12 @@ class HaSidebar extends LitElement {
width: 64px;
}
:host([expanded]) {
width: 256px;
width: calc(256px + env(safe-area-inset-left));
}
:host([rtl]) {
border-right: 0;
border-left: 1px solid var(--divider-color);
}
.menu {
box-sizing: border-box;
height: 65px;
@ -512,18 +518,25 @@ class HaSidebar extends LitElement {
background-color: var(--primary-background-color);
font-size: 20px;
align-items: center;
padding-left: calc(8.5px + env(safe-area-inset-left));
}
:host([rtl]) .menu {
padding-left: 8.5px;
padding-right: calc(8.5px + env(safe-area-inset-right));
}
:host([expanded]) .menu {
width: 256px;
width: calc(256px + env(safe-area-inset-left));
}
:host([rtl][expanded]) .menu {
width: calc(256px + env(safe-area-inset-right));
}
.menu mwc-icon-button {
color: var(--sidebar-icon-color);
}
:host([expanded]) .menu mwc-icon-button {
margin-right: 23px;
}
:host([expanded][_rtl]) .menu mwc-icon-button {
:host([expanded][rtl]) .menu mwc-icon-button {
margin-right: 0px;
margin-left: 23px;
}
@ -551,12 +564,18 @@ class HaSidebar extends LitElement {
display: flex;
flex-direction: column;
box-sizing: border-box;
height: calc(100% - 196px);
height: calc(100% - 196px - env(safe-area-inset-bottom));
overflow-y: auto;
overflow-x: hidden;
scrollbar-color: var(--scrollbar-thumb-color) transparent;
scrollbar-width: thin;
background: none;
margin-left: env(safe-area-inset-left);
}
:host([rtl]) paper-listbox {
margin-left: initial;
margin-right: env(safe-area-inset-right);
}
a {
@ -580,7 +599,7 @@ class HaSidebar extends LitElement {
:host([expanded]) paper-icon-item {
width: 240px;
}
:host([_rtl]) paper-icon-item {
:host([rtl]) paper-icon-item {
padding-left: auto;
padding-right: 12px;
}
@ -656,6 +675,11 @@ class HaSidebar extends LitElement {
}
.notifications-container {
display: flex;
margin-left: env(safe-area-inset-left);
}
:host([rtl]) .notifications-container {
margin-left: initial;
margin-right: env(safe-area-inset-right);
}
.notifications {
cursor: pointer;
@ -664,18 +688,23 @@ class HaSidebar extends LitElement {
flex: 1;
}
.profile {
margin-left: env(safe-area-inset-left);
}
:host([rtl]) .profile {
margin-left: initial;
margin-right: env(safe-area-inset-right);
}
.profile paper-icon-item {
padding-left: 4px;
}
:host([_rtl]) .profile paper-icon-item {
:host([rtl]) .profile paper-icon-item {
padding-left: auto;
padding-right: 4px;
}
.profile .item-text {
margin-left: 8px;
}
:host([_rtl]) .profile .item-text {
:host([rtl]) .profile .item-text {
margin-right: 8px;
}
@ -688,7 +717,7 @@ class HaSidebar extends LitElement {
line-height: 20px;
text-align: center;
padding: 0px 6px;
color: var(--text-primary-color);
color: var(--text-accent-color, var(--text-primary-color));
}
ha-svg-icon + .notification-badge {
position: absolute;
@ -735,7 +764,7 @@ class HaSidebar extends LitElement {
font-weight: 500;
}
:host([_rtl]) .menu mwc-icon-button {
:host([rtl]) .menu mwc-icon-button {
-webkit-transform: scaleX(-1);
transform: scaleX(-1);
}

View File

@ -50,6 +50,8 @@ export class HaYamlEditor extends LitElement {
try {
this._yaml = value && !isEmpty(value) ? safeDump(value) : "";
} catch (err) {
// eslint-disable-next-line no-console
console.error(err);
alert(`There was an error converting to YAML: ${err}`);
}
afterNextRender(() => {

View File

@ -6,6 +6,7 @@ import {
LeafletMouseEvent,
Map,
Marker,
TileLayer,
} from "leaflet";
import {
css,
@ -21,15 +22,19 @@ import { fireEvent } from "../../common/dom/fire_event";
import {
LeafletModuleType,
setupLeafletMap,
replaceTileLayer,
} from "../../common/dom/setup-leaflet-map";
import { nextRender } from "../../common/util/render-status";
import { defaultRadiusColor } from "../../data/zone";
import { HomeAssistant } from "../../types";
@customElement("ha-location-editor")
class LocationEditor extends LitElement {
@property() public location?: [number, number];
@property({ attribute: false }) public hass!: HomeAssistant;
@property() public radius?: number;
@property({ type: Array }) public location?: [number, number];
@property({ type: Number }) public radius?: number;
@property() public radiusColor?: string;
@ -46,6 +51,8 @@ class LocationEditor extends LitElement {
private _leafletMap?: Map;
private _tileLayer?: TileLayer;
private _locationMarker?: Marker | Circle;
public fitMap(): void {
@ -97,6 +104,22 @@ class LocationEditor extends LitElement {
if (changedProps.has("icon")) {
this._updateIcon();
}
if (changedProps.has("hass")) {
const oldHass = changedProps.get("hass") as HomeAssistant | undefined;
if (!oldHass || oldHass.themes.darkMode === this.hass.themes.darkMode) {
return;
}
if (!this._leafletMap || !this._tileLayer) {
return;
}
this._tileLayer = replaceTileLayer(
this.Leaflet,
this._leafletMap,
this._tileLayer,
this.hass.themes.darkMode
);
}
}
private get _mapEl(): HTMLDivElement {
@ -104,9 +127,9 @@ class LocationEditor extends LitElement {
}
private async _initMap(): Promise<void> {
[this._leafletMap, this.Leaflet] = await setupLeafletMap(
[this._leafletMap, this.Leaflet, this._tileLayer] = await setupLeafletMap(
this._mapEl,
false,
this.hass.themes.darkMode,
Boolean(this.radius)
);
this._leafletMap.addEventListener(
@ -255,9 +278,6 @@ class LocationEditor extends LitElement {
#map {
height: 100%;
}
.light {
color: #000000;
}
.leaflet-edit-move {
border-radius: 50%;
cursor: move !important;

View File

@ -6,6 +6,7 @@ import {
Map,
Marker,
MarkerOptions,
TileLayer,
} from "leaflet";
import {
css,
@ -21,8 +22,10 @@ import { fireEvent } from "../../common/dom/fire_event";
import {
LeafletModuleType,
setupLeafletMap,
replaceTileLayer,
} from "../../common/dom/setup-leaflet-map";
import { defaultRadiusColor } from "../../data/zone";
import { HomeAssistant } from "../../types";
declare global {
// for fire event
@ -47,6 +50,8 @@ export interface MarkerLocation {
@customElement("ha-locations-editor")
export class HaLocationsEditor extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property() public locations?: MarkerLocation[];
public fitZoom = 16;
@ -57,6 +62,8 @@ export class HaLocationsEditor extends LitElement {
// eslint-disable-next-line
private _leafletMap?: Map;
private _tileLayer?: TileLayer;
private _locationMarkers?: { [key: string]: Marker | Circle };
private _circles: { [key: string]: Circle } = {};
@ -116,6 +123,22 @@ export class HaLocationsEditor extends LitElement {
if (changedProps.has("locations")) {
this._updateMarkers();
}
if (changedProps.has("hass")) {
const oldHass = changedProps.get("hass") as HomeAssistant | undefined;
if (!oldHass || oldHass.themes.darkMode === this.hass.themes.darkMode) {
return;
}
if (!this._leafletMap || !this._tileLayer) {
return;
}
this._tileLayer = replaceTileLayer(
this.Leaflet,
this._leafletMap,
this._tileLayer,
this.hass.themes.darkMode
);
}
}
private get _mapEl(): HTMLDivElement {
@ -123,9 +146,9 @@ export class HaLocationsEditor extends LitElement {
}
private async _initMap(): Promise<void> {
[this._leafletMap, this.Leaflet] = await setupLeafletMap(
[this._leafletMap, this.Leaflet, this._tileLayer] = await setupLeafletMap(
this._mapEl,
false,
this.hass.themes.darkMode,
true
);
this._updateMarkers();
@ -290,9 +313,6 @@ export class HaLocationsEditor extends LitElement {
#map {
height: 100%;
}
.light {
color: #000000;
}
.leaflet-marker-draggable {
cursor: move !important;
}

View File

@ -1,5 +1,5 @@
import "../ha-icon-button";
import { Circle, Layer, Map, Marker } from "leaflet";
import { Circle, Layer, Map, Marker, TileLayer } from "leaflet";
import {
css,
CSSResult,
@ -13,6 +13,7 @@ import {
import {
LeafletModuleType,
setupLeafletMap,
replaceTileLayer,
} from "../../common/dom/setup-leaflet-map";
import { computeStateDomain } from "../../common/entity/compute_state_domain";
import { computeStateName } from "../../common/entity/compute_state_name";
@ -22,11 +23,11 @@ import { HomeAssistant } from "../../types";
@customElement("ha-map")
class HaMap extends LitElement {
@property({ attribute: false }) public hass?: HomeAssistant;
@property({ attribute: false }) public hass!: HomeAssistant;
@property() public entities?: string[];
@property() public darkMode = false;
@property() public darkMode?: boolean;
@property() public zoom?: number;
@ -35,6 +36,8 @@ class HaMap extends LitElement {
private _leafletMap?: Map;
private _tileLayer?: TileLayer;
// @ts-ignore
private _resizeObserver?: ResizeObserver;
@ -122,6 +125,20 @@ class HaMap extends LitElement {
if (changedProps.has("hass")) {
this._drawEntities();
this._fitMap();
const oldHass = changedProps.get("hass") as HomeAssistant | undefined;
if (!oldHass || oldHass.themes.darkMode === this.hass.themes.darkMode) {
return;
}
if (!this.Leaflet || !this._leafletMap || !this._tileLayer) {
return;
}
this._tileLayer = replaceTileLayer(
this.Leaflet,
this._leafletMap,
this._tileLayer,
this.hass.themes.darkMode
);
}
}
@ -130,9 +147,9 @@ class HaMap extends LitElement {
}
private async loadMap(): Promise<void> {
[this._leafletMap, this.Leaflet] = await setupLeafletMap(
[this._leafletMap, this.Leaflet, this._tileLayer] = await setupLeafletMap(
this._mapEl,
this.darkMode
this.darkMode ?? this.hass.themes.darkMode
);
this._drawEntities();
this._leafletMap.invalidateSize();
@ -229,7 +246,8 @@ class HaMap extends LitElement {
icon: Leaflet.divIcon({
html: iconHTML,
iconSize: [24, 24],
className: this.darkMode ? "dark" : "light",
className:
this.darkMode ?? this.hass.themes.darkMode ? "dark" : "light",
}),
interactive: false,
title,

View File

@ -322,9 +322,9 @@ export class PaperTimeInput extends PolymerElement {
* @return {boolean}
*/
validate() {
var valid = true;
let valid = true;
// Validate hour & min fields
if (!this.$.hour.validate() | !this.$.min.validate()) {
if (!this.$.hour.validate() || !this.$.min.validate()) {
valid = false;
}
// Validate second field

View File

@ -57,7 +57,7 @@ class StateBadge extends LitElement {
text-align: center;
background-color: var(--light-primary-color);
text-decoration: none;
color: var(--primary-text-color);
color: var(--text-light-primary-color, var(--primary-text-color));
overflow: hidden;
}

View File

@ -46,8 +46,8 @@ export interface MqttTrigger {
export interface GeoLocationTrigger {
platform: "geo_location";
source: "string";
zone: "string";
source: string;
zone: string;
event: "enter" | "leave";
}

View File

@ -1,5 +1,6 @@
import { computeStateName } from "../common/entity/compute_state_name";
import { HomeAssistant } from "../types";
import { HaFormSchema } from "../components/ha-form/ha-form";
export interface DeviceAutomation {
device_id: string;
@ -20,6 +21,10 @@ export interface DeviceTrigger extends DeviceAutomation {
platform: "device";
}
export interface DeviceCapabilities {
extra_fields: HaFormSchema[];
}
export const fetchDeviceActions = (hass: HomeAssistant, deviceId: string) =>
hass.callWS<DeviceAction[]>({
type: "device_automation/action/list",
@ -42,7 +47,7 @@ export const fetchDeviceActionCapabilities = (
hass: HomeAssistant,
action: DeviceAction
) =>
hass.callWS<DeviceAction[]>({
hass.callWS<DeviceCapabilities>({
type: "device_automation/action/capabilities",
action,
});
@ -51,7 +56,7 @@ export const fetchDeviceConditionCapabilities = (
hass: HomeAssistant,
condition: DeviceCondition
) =>
hass.callWS<DeviceCondition[]>({
hass.callWS<DeviceCapabilities>({
type: "device_automation/condition/capabilities",
condition,
});
@ -60,7 +65,7 @@ export const fetchDeviceTriggerCapabilities = (
hass: HomeAssistant,
trigger: DeviceTrigger
) =>
hass.callWS<DeviceTrigger[]>({
hass.callWS<DeviceCapabilities>({
type: "device_automation/trigger/capabilities",
trigger,
});

View File

@ -8,6 +8,7 @@ export interface DeviceRegistryEntry {
id: string;
config_entries: string[];
connections: Array<[string, string]>;
identifiers: Array<[string, string]>;
manufacturer: string;
model?: string;
name?: string;

View File

@ -58,6 +58,31 @@ export interface WaitAction {
timeout?: number;
}
export interface RepeatAction {
repeat: CountRepeat | WhileRepeat | UntilRepeat;
}
interface BaseRepeat {
sequence: Action[];
}
export interface CountRepeat extends BaseRepeat {
count: number;
}
export interface WhileRepeat extends BaseRepeat {
while: Condition[];
}
export interface UntilRepeat extends BaseRepeat {
until: Condition[];
}
export interface ChooseAction {
choose: [{ conditions: Condition[]; sequence: Action[] }];
default?: Action[];
}
export type Action =
| EventAction
| DeviceAction
@ -65,7 +90,9 @@ export type Action =
| Condition
| DelayAction
| SceneAction
| WaitAction;
| WaitAction
| RepeatAction
| ChooseAction;
export const triggerScript = (
hass: HomeAssistant,

View File

@ -165,7 +165,6 @@ class DialogConfigEntrySystemOptions extends LitElement {
haStyleDialog,
css`
ha-paper-dialog {
min-width: 400px;
max-width: 500px;
}
.init-spinner {

View File

@ -15,7 +15,8 @@ declare global {
}
}
interface HassDialog<T = HASSDomEvents[ValidHassDomEvent]> extends HTMLElement {
export interface HassDialog<T = HASSDomEvents[ValidHassDomEvent]>
extends HTMLElement {
showDialog(params: T);
closeDialog?: () => boolean | void;
}

View File

@ -123,12 +123,12 @@ class MoreInfoConfigurator extends PolymerElement {
}
fieldChanged(ev) {
var el = ev.target;
const el = ev.target;
this.fieldInput[el.name] = el.value;
}
submitClicked() {
var data = {
const data = {
configure_id: this.stateObj.attributes.configure_id,
fields: this.fieldInput,
};

View File

@ -97,7 +97,7 @@ class MoreInfoCover extends LocalizeMixin(PolymerElement) {
}
computeClassNames(stateObj) {
var classes = [
const classes = [
attributeClassNames(stateObj, [
"current_position",
"current_tilt_position",

View File

@ -140,8 +140,8 @@ class MoreInfoFan extends LocalizeMixin(EventsMixin(PolymerElement)) {
}
speedChanged(ev) {
var oldVal = this.stateObj.attributes.speed;
var newVal = ev.detail.value;
const oldVal = this.stateObj.attributes.speed;
const newVal = ev.detail.value;
if (!newVal || oldVal === newVal) return;
@ -152,8 +152,8 @@ class MoreInfoFan extends LocalizeMixin(EventsMixin(PolymerElement)) {
}
oscillationToggleChanged(ev) {
var oldVal = this.stateObj.attributes.oscillating;
var newVal = ev.target.checked;
const oldVal = this.stateObj.attributes.oscillating;
const newVal = ev.target.checked;
if (oldVal === newVal) return;

View File

@ -53,11 +53,11 @@ class MoreInfoGroup extends PolymerElement {
}
computeStates(stateObj, hass) {
var states = [];
var entIds = stateObj.attributes.entity_id || [];
const states = [];
const entIds = stateObj.attributes.entity_id || [];
for (var i = 0; i < entIds.length; i++) {
var state = hass.states[entIds[i]];
for (let i = 0; i < entIds.length; i++) {
const state = hass.states[entIds[i]];
if (state) {
states.push(state);

View File

@ -78,14 +78,14 @@ class DatetimeInput extends PolymerElement {
if (stateObj.state === "unknown") {
return "";
}
var monthFiller;
let monthFiller;
if (stateObj.attributes.month < 10) {
monthFiller = "0";
} else {
monthFiller = "";
}
var dayFiller;
let dayFiller;
if (stateObj.attributes.day < 10) {
dayFiller = "0";
} else {
@ -119,15 +119,18 @@ class DatetimeInput extends PolymerElement {
};
if (this.stateObj.attributes.has_time) {
changed |=
changed =
changed ||
parseInt(this.selectedMinute) !== this.stateObj.attributes.minute;
changed |= parseInt(this.selectedHour) !== this.stateObj.attributes.hour;
changed =
changed ||
parseInt(this.selectedHour) !== this.stateObj.attributes.hour;
if (this.selectedMinute < 10) {
minuteFiller = "0";
} else {
minuteFiller = "";
}
var timeStr =
const timeStr =
this.selectedHour + ":" + minuteFiller + this.selectedMinute;
serviceData.time = timeStr;
}
@ -144,7 +147,7 @@ class DatetimeInput extends PolymerElement {
this.stateObj.attributes.day
);
changed |= dateValState !== dateValInput;
changed = changed || dateValState !== dateValInput;
serviceData.date = this.selectedDate;
}

View File

@ -290,8 +290,8 @@ class MoreInfoLight extends LocalizeMixin(EventsMixin(PolymerElement)) {
}
effectChanged(ev) {
var oldVal = this.stateObj.attributes.effect;
var newVal = ev.detail.value;
const oldVal = this.stateObj.attributes.effect;
const newVal = ev.detail.value;
if (!newVal || oldVal === newVal) return;
@ -302,7 +302,7 @@ class MoreInfoLight extends LocalizeMixin(EventsMixin(PolymerElement)) {
}
brightnessSliderChanged(ev) {
var bri = parseInt(ev.target.value, 10);
const bri = parseInt(ev.target.value, 10);
if (isNaN(bri)) return;
@ -313,7 +313,7 @@ class MoreInfoLight extends LocalizeMixin(EventsMixin(PolymerElement)) {
}
ctSliderChanged(ev) {
var ct = parseInt(ev.target.value, 10);
const ct = parseInt(ev.target.value, 10);
if (isNaN(ct)) return;
@ -324,7 +324,7 @@ class MoreInfoLight extends LocalizeMixin(EventsMixin(PolymerElement)) {
}
wvSliderChanged(ev) {
var wv = parseInt(ev.target.value, 10);
const wv = parseInt(ev.target.value, 10);
if (isNaN(wv)) return;

View File

@ -329,8 +329,8 @@ class MoreInfoMediaPlayer extends LocalizeMixin(EventsMixin(PolymerElement)) {
handleSourceChanged(ev) {
if (!this.playerObj) return;
var oldVal = this.playerObj.source;
var newVal = ev.detail.value;
const oldVal = this.playerObj.source;
const newVal = ev.detail.value;
if (!newVal || oldVal === newVal) return;
@ -340,8 +340,8 @@ class MoreInfoMediaPlayer extends LocalizeMixin(EventsMixin(PolymerElement)) {
handleSoundModeChanged(ev) {
if (!this.playerObj) return;
var oldVal = this.playerObj.soundMode;
var newVal = ev.detail.value;
const oldVal = this.playerObj.soundMode;
const newVal = ev.detail.value;
if (!newVal || oldVal === newVal) return;

View File

@ -193,7 +193,7 @@ class MoreInfoWaterHeater extends LocalizeMixin(EventsMixin(PolymerElement)) {
4: "has-away_mode",
};
var classes = [featureClassNames(stateObj, _featureClassNames)];
const classes = [featureClassNames(stateObj, _featureClassNames)];
classes.push("more-info-water_heater");

View File

@ -254,7 +254,7 @@ export class MoreInfoDialog extends LitElement {
ha-header-bar {
--mdc-theme-on-primary: var(--primary-text-color);
--mdc-theme-primary: var(--card-background-color);
--mdc-theme-primary: var(--mdc-theme-surface);
flex-shrink: 0;
border-bottom: 1px solid
var(--mdc-dialog-scroll-divider-color, rgba(0, 0, 0, 0.12));
@ -302,13 +302,15 @@ export class MoreInfoDialog extends LitElement {
}
}
state-history-charts {
margin-top: 16px 0;
}
ha-dialog[data-domain="camera"] {
--dialog-content-padding: 0;
}
state-card-content,
state-history-charts {
display: block;
margin-bottom: 16px;
}
`,
];
}

View File

@ -29,9 +29,17 @@ export class HuiNotificationDrawer extends EventsMixin(
width: calc(100% - 32px);
}
div[main-title] {
padding-left: env(safe-area-inset-left);
padding-right: env(safe-area-inset-right);
}
.notifications {
overflow-y: auto;
padding-top: 16px;
padding-left: env(safe-area-inset-left);
padding-right: env(safe-area-inset-right);
padding-bottom: env(safe-area-inset-bottom);
height: calc(100% - 65px);
box-sizing: border-box;
background-color: var(--primary-background-color);

View File

@ -426,7 +426,7 @@ export class HaVoiceCommandDialog extends LitElement {
text-align: right;
border-bottom-right-radius: 0px;
background-color: var(--light-primary-color);
color: var(--primary-text-color);
color: var(--text-light-primary-color, var(--primary-text-color));
}
.message.hass {

View File

@ -84,7 +84,7 @@ function initPushNotifications() {
}
self.addEventListener("push", function (event) {
var data;
let data;
if (event.data) {
data = event.data.json();
if (data.dismiss) {
@ -113,8 +113,6 @@ function initPushNotifications() {
});
self.addEventListener("notificationclick", function (event) {
var url;
notificationEventCallback("clicked", event);
event.notification.close();
@ -127,7 +125,7 @@ function initPushNotifications() {
return;
}
url = event.notification.data.url;
const url = event.notification.data.url;
if (!url) return;
@ -137,8 +135,8 @@ function initPushNotifications() {
type: "window",
})
.then(function (windowClients) {
var i;
var client;
let i;
let client;
for (i = 0; i < windowClients.length; i++) {
client = windowClients[i];
if (client.url === url && "focus" in client) {

View File

@ -180,7 +180,9 @@ export const provideHass = (
config: demoConfig,
themes: {
default_theme: "default",
default_dark_theme: null,
themes: {},
darkMode: false,
},
panels: demoPanels,
services: demoServices,
@ -253,7 +255,7 @@ export const provideHass = (
mockTheme(theme) {
invalidateThemeCache();
hass().updateHass({
selectedTheme: theme ? "mock" : "default",
selectedTheme: { theme: theme ? "mock" : "default" },
themes: {
...hass().themes,
themes: {
@ -265,7 +267,7 @@ export const provideHass = (
applyThemesOnElement(
document.documentElement,
themes,
selectedTheme as string
selectedTheme!.theme
);
},

View File

@ -1,4 +1,4 @@
<meta name='viewport' content='width=device-width, user-scalable=no'>
<meta name='viewport' content='width=device-width, user-scalable=no, viewport-fit=cover'>
<style>
body {
font-family: Roboto, sans-serif;
@ -7,6 +7,6 @@
font-weight: 400;
margin: 0;
padding: 0;
height: 100vh;
height: 100%;
}
</style>

View File

@ -5,14 +5,16 @@
<link rel="preload" href="<%= latestAppJS %>" as="script" crossorigin="use-credentials" />
<%= renderTemplate('_header') %>
<title>Home Assistant</title>
<link rel="mask-icon" href="/static/icons/mask-icon.svg" color="#03a9f4" />
<link
rel="apple-touch-icon"
sizes="180x180"
href="/static/icons/favicon-apple-180x180.png"
/>
<link rel="mask-icon" href="/static/icons/mask-icon.svg" color="#03a9f4" />
<meta name="apple-itunes-app" content="app-id=1099568401" />
<meta name="apple-mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-status-bar-style" content="default">
<meta name="apple-mobile-web-app-title" content="Home Assistant">
<meta
name="msapplication-square70x70logo"
content="/static/icons/tile-win-70x70.png"
@ -33,6 +35,7 @@
<meta name="mobile-web-app-capable" content="yes" />
<meta name="referrer" content="same-origin" />
<meta name="theme-color" content="#THEMEC" />
<meta name="color-scheme" content="dark light" />
<style>
#ha-init-skeleton::before {
display: block;
@ -41,7 +44,7 @@
background-color: #THEMEC;
}
html {
background-color: var(--primary-background-color, #fafafa);
background-color: var(--primary-background-color);
}
</style>
</head>

View File

@ -77,6 +77,8 @@ class HaAppLayout extends customElements.get("app-header-layout") {
/* Using 'transform' will cause 'position: fixed' elements to behave like
'position: absolute' relative to this element. */
transform: translate(0);
margin-left: env(safe-area-inset-left);
margin-right: env(safe-area-inset-right);
}
@media print {

View File

@ -60,6 +60,11 @@ class HassSubpage extends LitElement {
background-color: var(--primary-background-color);
}
:host([narrow]) {
width: 100%;
position: fixed;
}
.toolbar {
display: flex;
align-items: center;

View File

@ -26,9 +26,9 @@ import { computeRTLDirection } from "../common/util/compute_rtl";
export class HaTabsSubpageDataTable extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property() public isWide!: boolean;
@property({ type: Boolean }) public isWide = false;
@property({ type: Boolean, reflect: true }) public narrow!: boolean;
@property({ type: Boolean, reflect: true }) public narrow = false;
/**
* Object with the columns.
@ -110,6 +110,7 @@ export class HaTabsSubpageDataTable extends LitElement {
<hass-tabs-subpage
.hass=${this.hass}
.narrow=${this.narrow}
.isWide=${this.isWide}
.backPath=${this.backPath}
.backCallback=${this.backCallback}
.route=${this.route}
@ -168,38 +169,37 @@ export class HaTabsSubpageDataTable extends LitElement {
? html`
<div slot="header">
<slot name="header">
<slot name="header">
<div class="table-header">
<search-input
.filter=${this.filter}
no-label-float
no-underline
@value-changed=${this._handleSearchChange}
.label=${this.hass.localize(
"ui.components.data-table.search"
)}
>
</search-input>
${this.activeFilters
? html`<div class="active-filters">
${this.hass.localize(
"ui.panel.config.filtering.filtering_by"
)}
${this.activeFilters.join(", ")}
<mwc-button @click=${this._clearFilter}
>${this.hass.localize(
"ui.panel.config.filtering.clear"
)}</mwc-button
>
</div>`
: ""}
</div></slot
></slot
>
<div class="table-header">
<search-input
.filter=${this.filter}
no-label-float
no-underline
@value-changed=${this._handleSearchChange}
.label=${this.hass.localize(
"ui.components.data-table.search"
)}
>
</search-input>
${this.activeFilters
? html`<div class="active-filters">
${this.hass.localize(
"ui.panel.config.filtering.filtering_by"
)}
${this.activeFilters.join(", ")}
<mwc-button @click=${this._clearFilter}
>${this.hass.localize(
"ui.panel.config.filtering.clear"
)}</mwc-button
>
</div>`
: ""}
</div>
</slot>
</div>
`
: html` <div slot="header"></div> `}
</ha-data-table>
<div slot="fab"><slot name="fab"></slot></div>
</hass-tabs-subpage>
`;
}

View File

@ -22,6 +22,7 @@ import "../components/ha-svg-icon";
import "../components/ha-icon";
import "../components/ha-tab";
import { restoreScroll } from "../common/decorators/restore-scroll";
import { computeRTL } from "../common/util/compute_rtl";
export interface PageNavigation {
path: string;
@ -53,6 +54,11 @@ class HassTabsSubpage extends LitElement {
@property({ type: Boolean, reflect: true }) public narrow = false;
@property({ type: Boolean, reflect: true, attribute: "is-wide" })
public isWide = false;
@property({ type: Boolean, reflect: true }) public rtl = false;
@internalProperty() private _activeTab?: PageNavigation;
// @ts-ignore
@ -107,6 +113,14 @@ class HassTabsSubpage extends LitElement {
`${this.route.prefix}${this.route.path}`.includes(tab.path)
);
}
if (changedProperties.has("hass")) {
const oldHass = changedProperties.get("hass") as
| HomeAssistant
| undefined;
if (!oldHass || oldHass.language !== this.hass.language) {
this.rtl = computeRTL(this.hass);
}
}
}
protected render(): TemplateResult {
@ -152,6 +166,7 @@ class HassTabsSubpage extends LitElement {
<div class="content" @scroll=${this._saveScrollPos}>
<slot></slot>
</div>
<div id="fab"><slot name="fab"></slot></div>
`;
}
@ -184,6 +199,11 @@ class HassTabsSubpage extends LitElement {
background-color: var(--primary-background-color);
}
:host([narrow]) {
width: 100%;
position: fixed;
}
ha-menu-button {
margin-right: 24px;
}
@ -215,9 +235,10 @@ class HassTabsSubpage extends LitElement {
background-color: var(--sidebar-background-color);
border-top: 1px solid var(--divider-color);
justify-content: space-between;
z-index: 1;
z-index: 2;
font-size: 12px;
width: 100%;
padding-bottom: env(safe-area-inset-bottom);
}
#tabbar:not(.bottom-bar) {
@ -247,7 +268,11 @@ class HassTabsSubpage extends LitElement {
.content {
position: relative;
width: 100%;
width: calc(
100% - env(safe-area-inset-left) - env(safe-area-inset-right)
);
margin-left: env(safe-area-inset-left);
margin-right: env(safe-area-inset-right);
height: calc(100% - 65px);
overflow-y: auto;
overflow: auto;
@ -256,6 +281,30 @@ class HassTabsSubpage extends LitElement {
:host([narrow]) .content {
height: calc(100% - 128px);
height: calc(100% - 128px - env(safe-area-inset-bottom));
}
#fab {
position: fixed;
right: calc(16px + env(safe-area-inset-right));
bottom: calc(16px + env(safe-area-inset-bottom));
z-index: 1;
}
:host([narrow]) #fab {
bottom: calc(84px + env(safe-area-inset-bottom));
}
#fab[is-wide] {
bottom: 24px;
right: 24px;
}
:host([rtl]) #fab {
right: auto;
left: calc(16px + env(safe-area-inset-left));
}
:host([rtl][is-wide]) #fab {
bottom: 24px;
left: 24px;
right: auto;
}
`;
}

View File

@ -152,16 +152,13 @@ class HomeAssistantMain extends LitElement {
--app-drawer-width: 64px;
}
:host([expanded]) {
--app-drawer-width: 256px;
--app-drawer-width: calc(256px + env(safe-area-inset-left));
}
partial-panel-resolver,
ha-sidebar {
/* allow a light tap highlight on the actual interface elements */
-webkit-tap-highlight-color: rgba(0, 0, 0, 0.1);
}
partial-panel-resolver {
height: 100%;
}
`;
}
}

View File

@ -90,6 +90,7 @@ class OnboardingCoreConfig extends LitElement {
<div class="row">
<ha-location-editor
class="flex"
.hass=${this.hass}
.location=${this._locationValue}
.fitZoom=${14}
@change=${this._locationChanged}

View File

@ -211,6 +211,7 @@ class HAFullCalendar extends LitElement {
:host {
display: flex;
flex-direction: column;
--fc-theme-standard-border-color: var(--divider-color);
}
.header {
@ -234,6 +235,10 @@ class HAFullCalendar extends LitElement {
flex-grow: 0;
}
a {
color: var(--primary-text-color);
}
.controls {
display: flex;
justify-content: space-between;
@ -259,6 +264,10 @@ class HAFullCalendar extends LitElement {
background-color: var(--card-background-color);
}
.fc-theme-standard .fc-scrollgrid {
border: 1px solid var(--divider-color);
}
.fc-scrollgrid-section-header td {
border: none;
}
@ -293,14 +302,15 @@ class HAFullCalendar extends LitElement {
td.fc-day-today .fc-daygrid-day-number {
height: 24px;
color: #fff;
background-color: #1a73e8;
color: var(--text-primary-color);
background-color: var(--primary-color);
border-radius: 50%;
display: inline-block;
text-align: center;
white-space: nowrap;
width: max-content;
min-width: 24px;
line-height: 140%;
}
.fc-daygrid-day-events {

View File

@ -11,7 +11,7 @@ import {
} from "lit-element";
import { styleMap } from "lit-html/directives/style-map";
import "@polymer/app-layout/app-header-layout/app-header-layout";
import "../../layouts/ha-app-layout";
import "@polymer/app-layout/app-header/app-header";
import "@polymer/app-layout/app-toolbar/app-toolbar";
import "@material/mwc-checkbox";
@ -63,7 +63,7 @@ class PanelCalendar extends LitElement {
protected render(): TemplateResult {
return html`
<app-header-layout>
<ha-app-layout>
<app-header fixed slot="header">
<app-toolbar>
<ha-menu-button
@ -106,7 +106,7 @@ class PanelCalendar extends LitElement {
@view-changed=${this._handleViewChanged}
></ha-full-calendar>
</div>
</app-header-layout>
</ha-app-layout>
`;
}

View File

@ -38,7 +38,6 @@ import {
showAreaRegistryDetailDialog,
} from "./show-dialog-area-registry-detail";
import { mdiPlus } from "@mdi/js";
import { computeRTL } from "../../../common/util/compute_rtl";
@customElement("ha-config-areas-dashboard")
export class HaConfigAreasDashboard extends LitElement {
@ -106,6 +105,7 @@ export class HaConfigAreasDashboard extends LitElement {
<hass-tabs-subpage-data-table
.hass=${this.hass}
.narrow=${this.narrow}
.isWide=${this.isWide}
back-path="/config"
.tabs=${configSections.integrations}
.route=${this.route}
@ -123,18 +123,16 @@ export class HaConfigAreasDashboard extends LitElement {
icon="hass:help-circle"
@click=${this._showHelp}
></ha-icon-button>
<mwc-fab
slot="fab"
title="${this.hass.localize(
"ui.panel.config.areas.picker.create_area"
)}"
@click=${this._createArea}
>
<ha-svg-icon slot="icon" path=${mdiPlus}></ha-svg-icon>
</mwc-fab>
</hass-tabs-subpage-data-table>
<mwc-fab
?is-wide=${this.isWide}
?narrow=${this.narrow}
?rtl=${computeRTL(this.hass!)}
title="${this.hass.localize(
"ui.panel.config.areas.picker.create_area"
)}"
@click=${this._createArea}
>
<ha-svg-icon slot="icon" path=${mdiPlus}></ha-svg-icon>
</mwc-fab>
`;
}
@ -183,28 +181,6 @@ export class HaConfigAreasDashboard extends LitElement {
--app-header-background-color: var(--sidebar-background-color);
--app-header-text-color: var(--sidebar-text-color);
}
mwc-fab {
position: fixed;
bottom: 16px;
right: 16px;
z-index: 1;
}
mwc-fab[is-wide] {
bottom: 24px;
right: 24px;
}
mwc-fab[narrow] {
bottom: 84px;
}
mwc-fab[rtl] {
right: auto;
left: 16px;
}
mwc-fab[is-wide][rtl] {
bottom: 24px;
left: 24px;
right: auto;
}
`;
}
}

View File

@ -15,6 +15,7 @@ import {
LitElement,
property,
internalProperty,
PropertyValues,
} from "lit-element";
import { dynamicElement } from "../../../../common/dom/dynamic-element-directive";
import { fireEvent } from "../../../../common/dom/fire_event";
@ -29,6 +30,11 @@ import "./types/ha-automation-action-event";
import "./types/ha-automation-action-scene";
import "./types/ha-automation-action-service";
import "./types/ha-automation-action-wait_template";
import "./types/ha-automation-action-repeat";
import "./types/ha-automation-action-choose";
import { handleStructError } from "../../../lovelace/common/structs/handle-errors";
import { ActionDetail } from "@material/mwc-list/mwc-list-foundation";
import { haStyle } from "../../../../resources/styles";
const OPTIONS = [
"condition",
@ -38,6 +44,8 @@ const OPTIONS = [
"scene",
"service",
"wait_template",
"repeat",
"choose",
];
const getType = (action: Action) => {
@ -87,12 +95,26 @@ export default class HaAutomationActionRow extends LitElement {
@property() public totalActions!: number;
@internalProperty() private _warnings?: string[];
@internalProperty() private _uiModeAvailable = true;
@internalProperty() private _yamlMode = false;
protected updated(changedProperties: PropertyValues) {
if (!changedProperties.has("action")) {
return;
}
this._uiModeAvailable = Boolean(getType(this.action));
if (!this._uiModeAvailable && !this._yamlMode) {
this._yamlMode = true;
}
}
protected render() {
const type = getType(this.action);
const selected = type ? OPTIONS.indexOf(type) : -1;
const yamlMode = this._yamlMode || selected === -1;
const yamlMode = this._yamlMode;
return html`
<ha-card>
@ -128,17 +150,14 @@ export default class HaAutomationActionRow extends LitElement {
</mwc-icon-button>
`
: ""}
<ha-button-menu corner="BOTTOM_START">
<ha-button-menu corner="BOTTOM_START" @action=${this._handleAction}>
<mwc-icon-button
slot="trigger"
.title=${this.hass.localize("ui.common.menu")}
.label=${this.hass.localize("ui.common.overflow_menu")}
><ha-svg-icon path=${mdiDotsVertical}></ha-svg-icon>
</mwc-icon-button>
<mwc-list-item
@tap=${this._switchYamlMode}
.disabled=${selected === -1}
>
<mwc-list-item .disabled=${!this._uiModeAvailable}>
${yamlMode
? this.hass.localize(
"ui.panel.config.automation.editor.edit_ui"
@ -152,30 +171,39 @@ export default class HaAutomationActionRow extends LitElement {
"ui.panel.config.automation.editor.actions.duplicate"
)}
</mwc-list-item>
<mwc-list-item @tap=${this._onDelete}>
<mwc-list-item>
${this.hass.localize(
"ui.panel.config.automation.editor.actions.delete"
)}
</mwc-list-item>
</ha-button-menu>
</div>
${this._warnings
? html`<div class="warning">
UI editor is not supported for this config:
<br />
<ul>
${this._warnings.map((warning) => html`<li>${warning}</li>`)}
</ul>
You can still edit your config in yaml.
</div>`
: ""}
${yamlMode
? html`
<div style="margin-right: 24px;">
${selected === -1
? html`
${this.hass.localize(
"ui.panel.config.automation.editor.actions.unsupported_action",
"action",
type
)}
`
: ""}
<ha-yaml-editor
.defaultValue=${this.action}
@value-changed=${this._onYamlChange}
></ha-yaml-editor>
</div>
${selected === -1
? html`
${this.hass.localize(
"ui.panel.config.automation.editor.actions.unsupported_action",
"action",
type
)}
`
: ""}
<h2>Edit in YAML</h2>
<ha-yaml-editor
.defaultValue=${this.action}
@value-changed=${this._onYamlChange}
></ha-yaml-editor>
`
: html`
<paper-dropdown-menu-light
@ -200,7 +228,7 @@ export default class HaAutomationActionRow extends LitElement {
)}
</paper-listbox>
</paper-dropdown-menu-light>
<div>
<div @ui-mode-not-available=${this._handleUiModeNotAvailable}>
${dynamicElement(`ha-automation-action-${type}`, {
hass: this.hass,
action: this.action,
@ -212,6 +240,13 @@ export default class HaAutomationActionRow extends LitElement {
`;
}
private _handleUiModeNotAvailable(ev: CustomEvent) {
this._warnings = handleStructError(ev.detail);
if (!this._yamlMode) {
this._yamlMode = true;
}
}
private _moveUp() {
fireEvent(this, "move-action", { direction: "up" });
}
@ -220,6 +255,19 @@ export default class HaAutomationActionRow extends LitElement {
fireEvent(this, "move-action", { direction: "down" });
}
private _handleAction(ev: CustomEvent<ActionDetail>) {
switch (ev.detail.index) {
case 0:
this._switchYamlMode();
break;
case 1:
break;
case 2:
this._onDelete();
break;
}
}
private _onDelete() {
showConfirmationDialog(this, {
text: this.hass.localize(
@ -241,6 +289,11 @@ export default class HaAutomationActionRow extends LitElement {
return;
}
this._uiModeAvailable = OPTIONS.includes(type);
if (!this._uiModeAvailable && !this._yamlMode) {
this._yamlMode = false;
}
if (type !== getType(this.action)) {
const elClass = customElements.get(`ha-automation-action-${type}`);
@ -264,26 +317,30 @@ export default class HaAutomationActionRow extends LitElement {
this._yamlMode = !this._yamlMode;
}
static get styles(): CSSResult {
return css`
.card-menu {
position: absolute;
top: 0;
right: 0;
z-index: 3;
--mdc-theme-text-primary-on-background: var(--primary-text-color);
}
.rtl .card-menu {
right: auto;
left: 0;
}
ha-button-menu {
margin: 8px;
}
mwc-list-item[disabled] {
--mdc-theme-text-primary-on-background: var(--disabled-text-color);
}
`;
static get styles(): CSSResult[] {
return [
haStyle,
css`
.card-menu {
float: right;
z-index: 3;
--mdc-theme-text-primary-on-background: var(--primary-text-color);
}
.rtl .card-menu {
float: left;
}
mwc-list-item[disabled] {
--mdc-theme-text-primary-on-background: var(--disabled-text-color);
}
.warning {
color: var(--warning-color);
margin-bottom: 8px;
}
.warning ul {
margin: 4px 0;
}
`,
];
}
}

View File

@ -0,0 +1,176 @@
import "@polymer/paper-input/paper-input";
import {
customElement,
LitElement,
property,
CSSResult,
css,
} from "lit-element";
import { html } from "lit-html";
import { Action, ChooseAction } from "../../../../../data/script";
import { HomeAssistant } from "../../../../../types";
import { ActionElement } from "../ha-automation-action-row";
import "../../condition/ha-automation-condition-editor";
import "@polymer/paper-listbox/paper-listbox";
import { fireEvent } from "../../../../../common/dom/fire_event";
import "../ha-automation-action";
import { Condition } from "../../../../../data/automation";
import { haStyle } from "../../../../../resources/styles";
import { mdiDelete } from "@mdi/js";
@customElement("ha-automation-action-choose")
export class HaChooseAction extends LitElement implements ActionElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property() public action!: ChooseAction;
public static get defaultConfig() {
return { choose: [{ conditions: [], sequence: [] }], default: [] };
}
protected render() {
const action = this.action;
return html`
${action.choose.map(
(option, idx) => html`<ha-card>
<mwc-icon-button
.idx=${idx}
@click=${this._removeOption}
title=${this.hass.localize(
"ui.panel.config.automation.editor.actions.type.choose.remove_option"
)}
>
<ha-svg-icon path=${mdiDelete}></ha-svg-icon>
</mwc-icon-button>
<div class="card-content">
<h2>
${this.hass.localize(
"ui.panel.config.automation.editor.actions.type.choose.option",
"number",
idx + 1
)}:
</h2>
<h3>
${this.hass.localize(
"ui.panel.config.automation.editor.actions.type.choose.conditions"
)}:
</h3>
<ha-automation-condition
.conditions=${option.conditions}
.hass=${this.hass}
.idx=${idx}
@value-changed=${this._conditionChanged}
></ha-automation-condition>
<h3>
${this.hass.localize(
"ui.panel.config.automation.editor.actions.type.choose.sequence"
)}:
</h3>
<ha-automation-action
.actions=${option.sequence}
.hass=${this.hass}
.idx=${idx}
@value-changed=${this._actionChanged}
></ha-automation-action>
</div>
</ha-card>`
)}
<ha-card>
<div class="card-actions add-card">
<mwc-button @click=${this._addOption}>
${this.hass.localize(
"ui.panel.config.automation.editor.actions.type.choose.add_option"
)}
</mwc-button>
</div>
</ha-card>
<h2>
${this.hass.localize(
"ui.panel.config.automation.editor.actions.type.choose.default"
)}:
</h2>
<ha-automation-action
.actions=${action.default || []}
@value-changed=${this._defaultChanged}
.hass=${this.hass}
></ha-automation-action>
`;
}
private _conditionChanged(ev: CustomEvent) {
ev.stopPropagation();
const value = ev.detail.value as Condition[];
const index = (ev.target as any).idx;
const choose = [...this.action.choose];
choose[index].conditions = value;
fireEvent(this, "value-changed", {
value: { ...this.action, choose },
});
}
private _actionChanged(ev: CustomEvent) {
ev.stopPropagation();
const value = ev.detail.value as Action[];
const index = (ev.target as any).idx;
const choose = [...this.action.choose];
choose[index].sequence = value;
fireEvent(this, "value-changed", {
value: { ...this.action, choose },
});
}
private _addOption() {
const choose = [...this.action.choose];
choose.push({ conditions: [], sequence: [] });
fireEvent(this, "value-changed", {
value: { ...this.action, choose },
});
}
private _removeOption(ev: CustomEvent) {
const index = (ev.currentTarget as any).idx;
const choose = [...this.action.choose];
choose.splice(index, 1);
fireEvent(this, "value-changed", {
value: { ...this.action, choose },
});
}
private _defaultChanged(ev: CustomEvent) {
ev.stopPropagation();
const value = ev.detail.value as Action[];
fireEvent(this, "value-changed", {
value: {
...this.action,
default: value,
},
});
}
static get styles(): CSSResult[] {
return [
haStyle,
css`
ha-card {
margin-top: 16px;
}
.add-card mwc-button {
display: block;
text-align: center;
}
mwc-icon-button {
position: absolute;
right: 0;
padding: 4px;
}
`,
];
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-automation-action-choose": HaChooseAction;
}
}

View File

@ -14,6 +14,7 @@ import {
DeviceAction,
deviceAutomationsEqual,
fetchDeviceActionCapabilities,
DeviceCapabilities,
} from "../../../../../data/device_automation";
import { HomeAssistant } from "../../../../../types";
@ -21,11 +22,11 @@ import { HomeAssistant } from "../../../../../types";
export class HaDeviceAction extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property() public action!: DeviceAction;
@property({ type: Object }) public action!: DeviceAction;
@internalProperty() private _deviceId?: string;
@internalProperty() private _capabilities?;
@internalProperty() private _capabilities?: DeviceCapabilities;
private _origAction?: DeviceAction;
@ -37,20 +38,20 @@ export class HaDeviceAction extends LitElement {
};
}
private _extraFieldsData = memoizeOne((capabilities, action: DeviceAction) =>
capabilities && capabilities.extra_fields
? capabilities.extra_fields.map((item) => {
return { [item.name]: action[item.name] };
})
: undefined
private _extraFieldsData = memoizeOne(
(action: DeviceAction, capabilities: DeviceCapabilities) => {
const extraFieldsData: { [key: string]: any } = {};
capabilities.extra_fields.forEach((item) => {
if (action[item.name] !== undefined) {
extraFieldsData![item.name] = action[item.name];
}
});
return extraFieldsData;
}
);
protected render() {
const deviceId = this._deviceId || this.action.device_id;
const extraFieldsData = this._extraFieldsData(
this._capabilities,
this.action
);
return html`
<ha-device-picker
@ -70,10 +71,10 @@ export class HaDeviceAction extends LitElement {
"ui.panel.config.automation.editor.actions.type.device_id.action"
)}
></ha-device-action-picker>
${extraFieldsData
${this._capabilities?.extra_fields
? html`
<ha-form
.data=${Object.assign({}, ...extraFieldsData)}
.data=${this._extraFieldsData(this.action, this._capabilities)}
.schema=${this._capabilities.extra_fields}
.computeLabel=${this._extraFieldsComputeLabelCallback(
this.hass.localize
@ -97,6 +98,7 @@ export class HaDeviceAction extends LitElement {
protected updated(changedPros) {
const prevAction = changedPros.get("action");
if (prevAction && !deviceAutomationsEqual(prevAction, this.action)) {
this._deviceId = undefined;
this._getCapabilities();
}
}
@ -104,7 +106,7 @@ export class HaDeviceAction extends LitElement {
private async _getCapabilities() {
this._capabilities = this.action.domain
? await fetchDeviceActionCapabilities(this.hass, this.action)
: null;
: undefined;
}
private _devicePicked(ev) {

View File

@ -0,0 +1,180 @@
import "@polymer/paper-input/paper-input";
import { customElement, LitElement, property, CSSResult } from "lit-element";
import { html } from "lit-html";
import {
RepeatAction,
Action,
CountRepeat,
WhileRepeat,
UntilRepeat,
} from "../../../../../data/script";
import { HomeAssistant } from "../../../../../types";
import { ActionElement } from "../ha-automation-action-row";
import "../../condition/ha-automation-condition-editor";
import type { PaperListboxElement } from "@polymer/paper-listbox";
import "@polymer/paper-listbox/paper-listbox";
import { fireEvent } from "../../../../../common/dom/fire_event";
import "../ha-automation-action";
import { Condition } from "../../../../lovelace/common/validate-condition";
import { haStyle } from "../../../../../resources/styles";
const OPTIONS = ["count", "while", "until"];
const getType = (action) => {
return OPTIONS.find((option) => option in action);
};
@customElement("ha-automation-action-repeat")
export class HaRepeatAction extends LitElement implements ActionElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public action!: RepeatAction;
public static get defaultConfig() {
return { repeat: { count: 2, sequence: [] } };
}
protected render() {
const action = this.action.repeat;
const type = getType(action);
const selected = type ? OPTIONS.indexOf(type) : -1;
return html`
<paper-dropdown-menu-light
.label=${this.hass.localize(
"ui.panel.config.automation.editor.actions.type.repeat.type_select"
)}
no-animations
>
<paper-listbox
slot="dropdown-content"
.selected=${selected}
@iron-select=${this._typeChanged}
>
${OPTIONS.map(
(opt) => html`
<paper-item .action=${opt}>
${this.hass.localize(
`ui.panel.config.automation.editor.actions.type.repeat.type.${opt}.label`
)}
</paper-item>
`
)}
</paper-listbox>
</paper-dropdown-menu-light>
${type === "count"
? html`<paper-input
.label=${this.hass.localize(
"ui.panel.config.automation.editor.actions.type.repeat.type.count.label"
)}
name="count"
.value=${(action as CountRepeat).count || "0"}
@value-changed=${this._countChanged}
></paper-input>`
: ""}
${type === "while"
? html` <h3>
${this.hass.localize(
`ui.panel.config.automation.editor.actions.type.repeat.type.while.conditions`
)}:
</h3>
<ha-automation-condition
.conditions=${(action as WhileRepeat).while || []}
.hass=${this.hass}
@value-changed=${this._conditionChanged}
></ha-automation-condition>`
: ""}
${type === "until"
? html` <h3>
${this.hass.localize(
`ui.panel.config.automation.editor.actions.type.repeat.type.until.conditions`
)}:
</h3>
<ha-automation-condition
.conditions=${(action as UntilRepeat).until || []}
.hass=${this.hass}
@value-changed=${this._conditionChanged}
></ha-automation-condition>`
: ""}
<h3>
${this.hass.localize(
"ui.panel.config.automation.editor.actions.type.repeat.sequence"
)}:
</h3>
<ha-automation-action
.actions=${action.sequence}
@value-changed=${this._actionChanged}
.hass=${this.hass}
></ha-automation-action>
`;
}
private _typeChanged(ev: CustomEvent) {
const type = ((ev.target as PaperListboxElement)?.selectedItem as any)
?.action;
if (!type || type === getType(this.action.repeat)) {
return;
}
const value = type === "count" ? 2 : [];
fireEvent(this, "value-changed", {
value: {
repeat: { [type]: value, sequence: this.action.repeat.sequence },
},
});
}
private _conditionChanged(ev: CustomEvent) {
ev.stopPropagation();
const value = ev.detail.value as Condition[];
fireEvent(this, "value-changed", {
value: {
repeat: {
...this.action.repeat,
[getType(this.action.repeat)!]: value,
},
},
});
}
private _actionChanged(ev: CustomEvent) {
ev.stopPropagation();
const value = ev.detail.value as Action[];
fireEvent(this, "value-changed", {
value: {
repeat: {
...this.action.repeat,
sequence: value,
},
},
});
}
private _countChanged(ev: CustomEvent): void {
const newVal = ev.detail.value;
if ((this.action.repeat as CountRepeat).count === newVal) {
return;
}
fireEvent(this, "value-changed", {
value: {
repeat: {
...this.action.repeat,
count: newVal,
},
},
});
}
static get styles(): CSSResult {
return haStyle;
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-automation-action-repeat": HaRepeatAction;
}
}

View File

@ -19,12 +19,20 @@ import { ServiceAction } from "../../../../../data/script";
import type { PolymerChangedEvent } from "../../../../../polymer-types";
import type { HomeAssistant } from "../../../../../types";
import { ActionElement, handleChangeEvent } from "../ha-automation-action-row";
import { assert, optional, object, string } from "superstruct";
import { EntityId } from "../../../../lovelace/common/structs/is-entity-id";
const actionStruct = object({
service: optional(string()),
entity_id: optional(EntityId),
data: optional(object()),
});
@customElement("ha-automation-action-service")
export class HaServiceAction extends LitElement implements ActionElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property() public action!: ServiceAction;
@property({ attribute: false }) public action!: ServiceAction;
@query("ha-yaml-editor") private _yamlEditor?: HaYamlEditor;
@ -60,6 +68,11 @@ export class HaServiceAction extends LitElement implements ActionElement {
if (!changedProperties.has("action")) {
return;
}
try {
assert(this.action, actionStruct);
} catch (error) {
fireEvent(this, "ui-mode-not-available", error);
}
if (this._actionData && this._actionData !== this.action.data) {
if (this._yamlEditor) {
this._yamlEditor.setValue(this.action.data);

View File

@ -2,7 +2,13 @@ import "@polymer/paper-dropdown-menu/paper-dropdown-menu-light";
import "@polymer/paper-item/paper-item";
import "@polymer/paper-listbox/paper-listbox";
import type { PaperListboxElement } from "@polymer/paper-listbox/paper-listbox";
import { customElement, html, LitElement, property } from "lit-element";
import {
customElement,
html,
LitElement,
property,
CSSResult,
} from "lit-element";
import { dynamicElement } from "../../../../common/dom/dynamic-element-directive";
import { fireEvent } from "../../../../common/dom/fire_event";
import "../../../../components/ha-card";
@ -19,6 +25,7 @@ import "./types/ha-automation-condition-sun";
import "./types/ha-automation-condition-template";
import "./types/ha-automation-condition-time";
import "./types/ha-automation-condition-zone";
import { haStyle } from "../../../../resources/styles";
const OPTIONS = [
"device",
@ -47,21 +54,20 @@ export default class HaAutomationConditionEditor extends LitElement {
return html`
${yamlMode
? html`
<div style="margin-right: 24px;">
${selected === -1
? html`
${this.hass.localize(
"ui.panel.config.automation.editor.conditions.unsupported_condition",
"condition",
this.condition.condition
)}
`
: ""}
<ha-yaml-editor
.defaultValue=${this.condition}
@value-changed=${this._onYamlChange}
></ha-yaml-editor>
</div>
${selected === -1
? html`
${this.hass.localize(
"ui.panel.config.automation.editor.conditions.unsupported_condition",
"condition",
this.condition.condition
)}
`
: ""}
<h2>Edit in YAML</h2>
<ha-yaml-editor
.defaultValue=${this.condition}
@value-changed=${this._onYamlChange}
></ha-yaml-editor>
`
: html`
<paper-dropdown-menu-light
@ -123,6 +129,10 @@ export default class HaAutomationConditionEditor extends LitElement {
}
fireEvent(this, "value-changed", { value: ev.detail.value });
}
static get styles(): CSSResult {
return haStyle;
}
}
declare global {

View File

@ -18,6 +18,7 @@ import { Condition } from "../../../../data/automation";
import { showConfirmationDialog } from "../../../../dialogs/generic/show-dialog-box";
import { HomeAssistant } from "../../../../types";
import "./ha-automation-condition-editor";
import { ActionDetail } from "@material/mwc-list/mwc-list-foundation";
export interface ConditionElement extends LitElement {
condition: Condition;
@ -64,14 +65,14 @@ export default class HaAutomationConditionRow extends LitElement {
<ha-card>
<div class="card-content">
<div class="card-menu">
<ha-button-menu corner="BOTTOM_START">
<ha-button-menu corner="BOTTOM_START" @action=${this._handleAction}>
<mwc-icon-button
.title=${this.hass.localize("ui.common.menu")}
.label=${this.hass.localize("ui.common.overflow_menu")}
slot="trigger"
><ha-svg-icon path=${mdiDotsVertical}></ha-svg-icon
></mwc-icon-button>
<mwc-list-item @tap=${this._switchYamlMode}>
<mwc-list-item>
${this._yamlMode
? this.hass.localize(
"ui.panel.config.automation.editor.edit_ui"
@ -85,7 +86,7 @@ export default class HaAutomationConditionRow extends LitElement {
"ui.panel.config.automation.editor.actions.duplicate"
)}
</mwc-list-item>
<mwc-list-item @tap=${this._onDelete}>
<mwc-list-item>
${this.hass.localize(
"ui.panel.config.automation.editor.actions.delete"
)}
@ -102,6 +103,19 @@ export default class HaAutomationConditionRow extends LitElement {
`;
}
private _handleAction(ev: CustomEvent<ActionDetail>) {
switch (ev.detail.index) {
case 0:
this._switchYamlMode();
break;
case 1:
break;
case 2:
this._onDelete();
break;
}
}
private _onDelete() {
showConfirmationDialog(this, {
text: this.hass.localize(
@ -122,18 +136,12 @@ export default class HaAutomationConditionRow extends LitElement {
static get styles(): CSSResult {
return css`
.card-menu {
position: absolute;
top: 0;
right: 0;
float: right;
z-index: 3;
--mdc-theme-text-primary-on-background: var(--primary-text-color);
}
.rtl .card-menu {
right: auto;
left: 0;
}
ha-button-menu {
margin: 8px;
float: left;
}
mwc-list-item[disabled] {
--mdc-theme-text-primary-on-background: var(--disabled-text-color);

View File

@ -13,18 +13,20 @@ import {
deviceAutomationsEqual,
DeviceCondition,
fetchDeviceConditionCapabilities,
DeviceCapabilities,
} from "../../../../../data/device_automation";
import { HomeAssistant } from "../../../../../types";
import memoizeOne from "memoize-one";
@customElement("ha-automation-condition-device")
export class HaDeviceCondition extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property() public condition!: DeviceCondition;
@property({ type: Object }) public condition!: DeviceCondition;
@internalProperty() private _deviceId?: string;
@internalProperty() private _capabilities?;
@internalProperty() private _capabilities?: DeviceCapabilities;
private _origCondition?: DeviceCondition;
@ -36,16 +38,21 @@ export class HaDeviceCondition extends LitElement {
};
}
private _extraFieldsData = memoizeOne(
(condition: DeviceCondition, capabilities: DeviceCapabilities) => {
const extraFieldsData: { [key: string]: any } = {};
capabilities.extra_fields.forEach((item) => {
if (condition[item.name] !== undefined) {
extraFieldsData![item.name] = condition[item.name];
}
});
return extraFieldsData;
}
);
protected render() {
const deviceId = this._deviceId || this.condition.device_id;
const extraFieldsData =
this._capabilities && this._capabilities.extra_fields
? this._capabilities.extra_fields.map((item) => {
return { [item.name]: this.condition[item.name] };
})
: undefined;
return html`
<ha-device-picker
.value=${deviceId}
@ -64,10 +71,10 @@ export class HaDeviceCondition extends LitElement {
"ui.panel.config.automation.editor.conditions.type.device.condition"
)}
></ha-device-condition-picker>
${extraFieldsData
${this._capabilities?.extra_fields
? html`
<ha-form
.data=${Object.assign({}, ...extraFieldsData)}
.data=${this._extraFieldsData(this.condition, this._capabilities)}
.schema=${this._capabilities.extra_fields}
.computeLabel=${this._extraFieldsComputeLabelCallback(
this.hass.localize
@ -103,7 +110,7 @@ export class HaDeviceCondition extends LitElement {
this._capabilities = condition.domain
? await fetchDeviceConditionCapabilities(this.hass, condition)
: null;
: undefined;
}
private _devicePicked(ev) {
@ -141,3 +148,9 @@ export class HaDeviceCondition extends LitElement {
) || schema.name;
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-automation-condition-device": HaDeviceCondition;
}
}

View File

@ -13,9 +13,7 @@ import {
PropertyValues,
TemplateResult,
} from "lit-element";
import { classMap } from "lit-html/directives/class-map";
import { navigate } from "../../../common/navigate";
import { computeRTL } from "../../../common/util/compute_rtl";
import "../../../components/ha-card";
import "../../../components/ha-svg-icon";
import "@material/mwc-fab";
@ -46,10 +44,18 @@ import "./trigger/ha-automation-trigger";
import { HaDeviceTrigger } from "./trigger/types/ha-automation-trigger-device";
import { mdiContentSave } from "@mdi/js";
import { PaperListboxElement } from "@polymer/paper-listbox";
import { classMap } from "lit-html/directives/class-map";
const MODES = ["single", "restart", "queued", "parallel"];
const MODES_MAX = ["queued", "parallel"];
declare global {
// for fire event
interface HASSDomEvents {
"ui-mode-not-available": Error;
}
}
export class HaAutomationEditor extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@ -65,7 +71,7 @@ export class HaAutomationEditor extends LitElement {
@internalProperty() private _config?: AutomationConfig;
@internalProperty() private _dirty?: boolean;
@internalProperty() private _dirty = false;
@internalProperty() private _errors?: string;
@ -305,16 +311,10 @@ export class HaAutomationEditor extends LitElement {
`
: ""}
<mwc-fab
?is-wide="${this.isWide}"
?narrow="${this.narrow}"
?dirty="${this._dirty}"
.title="${this.hass.localize(
"ui.panel.config.automation.editor.save"
)}"
slot="fab"
class=${classMap({ dirty: this._dirty })}
.title=${this.hass.localize("ui.panel.config.automation.editor.save")}
@click=${this._saveAutomation}
class="${classMap({
rtl: computeRTL(this.hass),
})}"
>
<ha-svg-icon slot="icon" path=${mdiContentSave}></ha-svg-icon>
</mwc-fab>
@ -404,6 +404,10 @@ export class HaAutomationEditor extends LitElement {
const mode = ((ev.target as PaperListboxElement)?.selectedItem as any)
?.mode;
if (mode === this._config!.mode) {
return;
}
this._config = { ...this._config!, mode };
if (!MODES_MAX.includes(mode)) {
delete this._config.max;
@ -531,35 +535,12 @@ export class HaAutomationEditor extends LitElement {
margin-right: 8px;
}
mwc-fab {
position: fixed;
bottom: 16px;
right: 16px;
z-index: 1;
margin-bottom: -80px;
transition: margin-bottom 0.3s;
position: relative;
bottom: calc(-80px - env(safe-area-inset-bottom));
transition: bottom 0.3s;
}
mwc-fab[is-wide] {
bottom: 24px;
right: 24px;
}
mwc-fab[narrow] {
bottom: 84px;
margin-bottom: -140px;
}
mwc-fab[dirty] {
margin-bottom: 0;
}
mwc-fab.rtl {
right: auto;
left: 16px;
}
mwc-fab[is-wide].rtl {
bottom: 24px;
right: auto;
left: 24px;
mwc-fab.dirty {
bottom: 0;
}
`,
];

View File

@ -1,13 +1,12 @@
import "../../../components/ha-icon-button";
import "@polymer/paper-tooltip/paper-tooltip";
import {
css,
CSSResultArray,
customElement,
html,
LitElement,
property,
TemplateResult,
CSSResult,
} from "lit-element";
import { ifDefined } from "lit-html/directives/if-defined";
import memoizeOne from "memoize-one";
@ -15,7 +14,6 @@ import { isComponentLoaded } from "../../../common/config/is_component_loaded";
import { formatDateTime } from "../../../common/datetime/format_date_time";
import { fireEvent } from "../../../common/dom/fire_event";
import { computeStateName } from "../../../common/entity/compute_state_name";
import { computeRTL } from "../../../common/util/compute_rtl";
import { DataTableColumnContainer } from "../../../components/data-table/ha-data-table";
import "../../../components/entity/ha-entity-toggle";
import "@material/mwc-fab";
@ -169,19 +167,16 @@ class HaAutomationPicker extends LitElement {
)}
hasFab
>
<mwc-fab
slot="fab"
title=${this.hass.localize(
"ui.panel.config.automation.picker.add_automation"
)}
@click=${this._createNew}
>
<ha-svg-icon slot="icon" path=${mdiPlus}></ha-svg-icon>
</mwc-fab>
</hass-tabs-subpage-data-table>
<mwc-fab
slot="fab"
?is-wide=${this.isWide}
?narrow=${this.narrow}
title=${this.hass.localize(
"ui.panel.config.automation.picker.add_automation"
)}
?rtl=${computeRTL(this.hass)}
@click=${this._createNew}
>
<ha-svg-icon slot="icon" path=${mdiPlus}></ha-svg-icon>
</mwc-fab>
`;
}
@ -207,37 +202,8 @@ class HaAutomationPicker extends LitElement {
});
}
static get styles(): CSSResultArray {
return [
haStyle,
css`
mwc-fab {
position: fixed;
bottom: 16px;
right: 16px;
z-index: 1;
cursor: pointer;
}
mwc-fab[is-wide] {
bottom: 24px;
right: 24px;
}
mwc-fab[narrow] {
bottom: 84px;
}
mwc-fab[rtl] {
right: auto;
left: 16px;
}
mwc-fab[rtl][is-wide] {
bottom: 24px;
right: auto;
left: 24px;
}
`,
];
static get styles(): CSSResult {
return haStyle;
}
}

View File

@ -34,6 +34,8 @@ import "./types/ha-automation-trigger-time";
import "./types/ha-automation-trigger-time_pattern";
import "./types/ha-automation-trigger-webhook";
import "./types/ha-automation-trigger-zone";
import { ActionDetail } from "@material/mwc-list/mwc-list-foundation";
import { haStyle } from "../../../../resources/styles";
const OPTIONS = [
"device",
@ -93,17 +95,14 @@ export default class HaAutomationTriggerRow extends LitElement {
<ha-card>
<div class="card-content">
<div class="card-menu">
<ha-button-menu corner="BOTTOM_START">
<ha-button-menu corner="BOTTOM_START" @action=${this._handleAction}>
<mwc-icon-button
slot="trigger"
.title=${this.hass.localize("ui.common.menu")}
.label=${this.hass.localize("ui.common.overflow_menu")}
><ha-svg-icon path=${mdiDotsVertical}></ha-svg-icon
></mwc-icon-button>
<mwc-list-item
@tap=${this._switchYamlMode}
.disabled=${selected === -1}
>
<mwc-list-item .disabled=${selected === -1}>
${yamlMode
? this.hass.localize(
"ui.panel.config.automation.editor.edit_ui"
@ -117,7 +116,7 @@ export default class HaAutomationTriggerRow extends LitElement {
"ui.panel.config.automation.editor.actions.duplicate"
)}
</mwc-list-item>
<mwc-list-item @tap=${this._onDelete}>
<mwc-list-item>
${this.hass.localize(
"ui.panel.config.automation.editor.actions.delete"
)}
@ -126,21 +125,20 @@ export default class HaAutomationTriggerRow extends LitElement {
</div>
${yamlMode
? html`
<div style="margin-right: 24px;">
${selected === -1
? html`
${this.hass.localize(
"ui.panel.config.automation.editor.triggers.unsupported_platform",
"platform",
this.trigger.platform
)}
`
: ""}
<ha-yaml-editor
.defaultValue=${this.trigger}
@value-changed=${this._onYamlChange}
></ha-yaml-editor>
</div>
${selected === -1
? html`
${this.hass.localize(
"ui.panel.config.automation.editor.triggers.unsupported_platform",
"platform",
this.trigger.platform
)}
`
: ""}
<h2>Edit in YAML</h2>
<ha-yaml-editor
.defaultValue=${this.trigger}
@value-changed=${this._onYamlChange}
></ha-yaml-editor>
`
: html`
<paper-dropdown-menu-light
@ -177,6 +175,19 @@ export default class HaAutomationTriggerRow extends LitElement {
`;
}
private _handleAction(ev: CustomEvent<ActionDetail>) {
switch (ev.detail.index) {
case 0:
this._switchYamlMode();
break;
case 1:
break;
case 2:
this._onDelete();
break;
}
}
private _onDelete() {
showConfirmationDialog(this, {
text: this.hass.localize(
@ -222,26 +233,23 @@ export default class HaAutomationTriggerRow extends LitElement {
this._yamlMode = !this._yamlMode;
}
static get styles(): CSSResult {
return css`
.card-menu {
position: absolute;
top: 0;
right: 0;
z-index: 3;
--mdc-theme-text-primary-on-background: var(--primary-text-color);
}
.rtl .card-menu {
right: auto;
left: 0;
}
ha-button-menu {
margin: 8px;
}
mwc-list-item[disabled] {
--mdc-theme-text-primary-on-background: var(--disabled-text-color);
}
`;
static get styles(): CSSResult[] {
return [
haStyle,
css`
.card-menu {
float: right;
z-index: 3;
--mdc-theme-text-primary-on-background: var(--primary-text-color);
}
.rtl .card-menu {
float: left;
}
mwc-list-item[disabled] {
--mdc-theme-text-primary-on-background: var(--disabled-text-color);
}
`,
];
}
}

View File

@ -13,18 +13,20 @@ import {
deviceAutomationsEqual,
DeviceTrigger,
fetchDeviceTriggerCapabilities,
DeviceCapabilities,
} from "../../../../../data/device_automation";
import { HomeAssistant } from "../../../../../types";
import memoizeOne from "memoize-one";
@customElement("ha-automation-trigger-device")
export class HaDeviceTrigger extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property() public trigger!: DeviceTrigger;
@property({ type: Object }) public trigger!: DeviceTrigger;
@internalProperty() private _deviceId?: string;
@internalProperty() private _capabilities?;
@internalProperty() private _capabilities?: DeviceCapabilities;
private _origTrigger?: DeviceTrigger;
@ -36,16 +38,21 @@ export class HaDeviceTrigger extends LitElement {
};
}
private _extraFieldsData = memoizeOne(
(trigger: DeviceTrigger, capabilities: DeviceCapabilities) => {
const extraFieldsData: { [key: string]: any } = {};
capabilities.extra_fields.forEach((item) => {
if (trigger[item.name] !== undefined) {
extraFieldsData![item.name] = trigger[item.name];
}
});
return extraFieldsData;
}
);
protected render() {
const deviceId = this._deviceId || this.trigger.device_id;
const extraFieldsData =
this._capabilities && this._capabilities.extra_fields
? this._capabilities.extra_fields.map((item) => {
return { [item.name]: this.trigger[item.name] };
})
: undefined;
return html`
<ha-device-picker
.value=${deviceId}
@ -64,10 +71,10 @@ export class HaDeviceTrigger extends LitElement {
"ui.panel.config.automation.editor.triggers.type.device.trigger"
)}
></ha-device-trigger-picker>
${extraFieldsData
${this._capabilities?.extra_fields
? html`
<ha-form
.data=${Object.assign({}, ...extraFieldsData)}
.data=${this._extraFieldsData(this.trigger, this._capabilities)}
.schema=${this._capabilities.extra_fields}
.computeLabel=${this._extraFieldsComputeLabelCallback(
this.hass.localize
@ -100,7 +107,7 @@ export class HaDeviceTrigger extends LitElement {
this._capabilities = trigger.domain
? await fetchDeviceTriggerCapabilities(this.hass, trigger)
: null;
: undefined;
}
private _devicePicked(ev) {
@ -120,7 +127,7 @@ export class HaDeviceTrigger extends LitElement {
fireEvent(this, "value-changed", { value: trigger });
}
private _extraFieldsChanged(ev) {
private _extraFieldsChanged(ev: CustomEvent) {
ev.stopPropagation();
fireEvent(this, "value-changed", {
value: {
@ -138,3 +145,9 @@ export class HaDeviceTrigger extends LitElement {
) || schema.name;
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-automation-trigger-device": HaDeviceTrigger;
}
}

View File

@ -61,6 +61,7 @@ class ConfigCoreForm extends LitElement {
<div class="row">
<ha-location-editor
class="flex"
.hass=${this.hass}
.location=${this._locationValue}
@change=${this._locationChanged}
></ha-location-editor>

View File

@ -1,4 +1,4 @@
import "@polymer/app-layout/app-header-layout/app-header-layout";
import "../../../layouts/ha-app-layout";
import "@polymer/app-layout/app-header/app-header";
import "@polymer/app-layout/app-toolbar/app-toolbar";
import {
@ -127,7 +127,7 @@ class HaConfigDashboard extends LitElement {
}
return html`
<app-header-layout>
<ha-app-layout>
<app-header fixed slot="header">
<app-toolbar>
<ha-menu-button
@ -138,7 +138,7 @@ class HaConfigDashboard extends LitElement {
</app-header>
${content}
</app-header-layout>
</ha-app-layout>
`;
}

View File

@ -13,7 +13,6 @@ import { ifDefined } from "lit-html/directives/if-defined";
import memoizeOne from "memoize-one";
import { isComponentLoaded } from "../../../common/config/is_component_loaded";
import { computeStateName } from "../../../common/entity/compute_state_name";
import { createValidEntityId } from "../../../common/entity/valid_entity_id";
import { compare } from "../../../common/string/compare";
import "../../../components/entity/ha-battery-icon";
import "../../../components/ha-icon-next";
@ -44,6 +43,7 @@ import { configSections } from "../ha-panel-config";
import "./device-detail/ha-device-entities-card";
import "./device-detail/ha-device-info-card";
import { showDeviceAutomationDialog } from "./device-detail/show-dialog-device-automation";
import { slugify } from "../../../common/string/slugify";
export interface EntityRegistryStateEntry extends EntityRegistryEntry {
stateName?: string | null;
@ -556,11 +556,11 @@ export class HaConfigDevicePage extends LitElement {
}
if (renameEntityid) {
const oldSearch = createValidEntityId(oldDeviceName);
const oldSearch = slugify(oldDeviceName);
if (entity.entity_id.includes(oldSearch)) {
newEntityId = entity.entity_id.replace(
oldSearch,
createValidEntityId(newDeviceName)
slugify(newDeviceName)
);
}
}
@ -571,7 +571,6 @@ export class HaConfigDevicePage extends LitElement {
return updateEntityRegistryEntry(this.hass!, entity.entity_id, {
name: newName || name,
disabled_by: entity.disabled_by,
new_entity_id: newEntityId || entity.entity_id,
});
});

View File

@ -235,7 +235,7 @@ export class DialogEntityEditor extends LitElement {
css`
ha-header-bar {
--mdc-theme-on-primary: var(--primary-text-color);
--mdc-theme-primary: var(--card-background-color);
--mdc-theme-primary: var(--mdc-theme-surface);
flex-shrink: 0;
}

View File

@ -241,6 +241,7 @@ export class EntityRegistrySettings extends LitElement {
display: flex;
justify-content: space-between;
padding: 8px;
padding-bottom: max(env(safe-area-inset-bottom), 8px);
background-color: var(--mdc-theme-surface, #fff);
}
ha-switch {

View File

@ -57,6 +57,7 @@ import {
showEntityEditorDialog,
} from "./show-dialog-entity-editor";
import { mdiFilterVariant } from "@mdi/js";
import type { RequestSelectedDetail } from "@material/mwc-list/mwc-list-item";
export interface StateEntity extends EntityRegistryEntry {
readonly?: boolean;
@ -449,7 +450,7 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) {
>
</div>`
: ""}
<ha-button-menu corner="BOTTOM_START">
<ha-button-menu corner="BOTTOM_START" multi>
<mwc-icon-button
slot="trigger"
.label=${this.hass!.localize(
@ -462,8 +463,9 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) {
<ha-svg-icon path=${mdiFilterVariant}></ha-svg-icon>
</mwc-icon-button>
<mwc-list-item
@click="${this._showDisabledChanged}"
@request-selected="${this._showDisabledChanged}"
graphic="control"
.selected=${this._showDisabled}
>
<ha-checkbox
slot="graphic"
@ -474,8 +476,9 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) {
)}
</mwc-list-item>
<mwc-list-item
@click="${this._showRestoredChanged}"
@request-selected="${this._showRestoredChanged}"
graphic="control"
.selected=${this._showUnavailable}
>
<ha-checkbox
slot="graphic"
@ -486,8 +489,9 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) {
)}
</mwc-list-item>
<mwc-list-item
@click="${this._showReadOnlyChanged}"
@request-selected="${this._showReadOnlyChanged}"
graphic="control"
.selected=${this._showReadOnly}
>
<ha-checkbox
slot="graphic"
@ -579,16 +583,25 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) {
}
}
private _showDisabledChanged() {
this._showDisabled = !this._showDisabled;
private _showDisabledChanged(ev: CustomEvent<RequestSelectedDetail>) {
if (ev.detail.source !== "property") {
return;
}
this._showDisabled = ev.detail.selected;
}
private _showRestoredChanged() {
this._showUnavailable = !this._showUnavailable;
private _showRestoredChanged(ev: CustomEvent<RequestSelectedDetail>) {
if (ev.detail.source !== "property") {
return;
}
this._showUnavailable = ev.detail.selected;
}
private _showReadOnlyChanged() {
this._showReadOnly = !this._showReadOnly;
private _showReadOnlyChanged(ev: CustomEvent<RequestSelectedDetail>) {
if (ev.detail.source !== "property") {
return;
}
this._showReadOnly = ev.detail.selected;
}
private _handleSearchChange(ev: CustomEvent) {

View File

@ -182,9 +182,9 @@ class HaEntityConfig extends PolymerElement {
return;
}
var oldEntityId = oldEntities[this.selectedEntity].entity_id;
const oldEntityId = oldEntities[this.selectedEntity].entity_id;
var newIndex = entities.findIndex(function (ent) {
const newIndex = entities.findIndex(function (ent) {
return ent.entity_id === oldEntityId;
});
@ -198,12 +198,12 @@ class HaEntityConfig extends PolymerElement {
entityChanged(index) {
if (!this.entities || !this.formEl) return;
var entity = this.entities[index];
const entity = this.entities[index];
if (!entity) return;
this.formState = "loading";
// eslint-disable-next-line @typescript-eslint/no-this-alias
var el = this;
const el = this;
this.formEl.loadEntity(entity).then(function () {
el.formState = "editing";
});
@ -212,7 +212,7 @@ class HaEntityConfig extends PolymerElement {
saveEntity() {
this.formState = "saving";
// eslint-disable-next-line @typescript-eslint/no-this-alias
var el = this;
const el = this;
this.formEl.saveEntity().then(function () {
el.formState = "editing";
});

View File

@ -5,8 +5,6 @@ import "@polymer/paper-listbox/paper-listbox";
import "@polymer/paper-tooltip/paper-tooltip";
import { HassEntity } from "home-assistant-js-websocket";
import {
css,
CSSResult,
customElement,
html,
LitElement,
@ -34,7 +32,6 @@ import { HELPER_DOMAINS } from "./const";
import { showHelperDetailDialog } from "./show-dialog-helper-detail";
import "../../../components/ha-svg-icon";
import { mdiPlus } from "@mdi/js";
import { computeRTL } from "../../../common/util/compute_rtl";
@customElement("ha-config-helpers")
export class HaConfigHelpers extends LitElement {
@ -160,18 +157,16 @@ export class HaConfigHelpers extends LitElement {
"ui.panel.config.helpers.picker.no_helpers"
)}
>
<mwc-fab
slot="fab"
title="${this.hass.localize(
"ui.panel.config.helpers.picker.add_helper"
)}"
@click=${this._createHelpler}
>
<ha-svg-icon slot="icon" path=${mdiPlus}></ha-svg-icon>
</mwc-fab>
</hass-tabs-subpage-data-table>
<mwc-fab
?is-wide=${this.isWide}
?narrow=${this.narrow}
?rtl=${computeRTL(this.hass!)}
title="${this.hass.localize(
"ui.panel.config.helpers.picker.add_helper"
)}"
@click=${this._createHelpler}
>
<ha-svg-icon slot="icon" path=${mdiPlus}></ha-svg-icon>
</mwc-fab>
`;
}
@ -215,31 +210,4 @@ export class HaConfigHelpers extends LitElement {
private _createHelpler() {
showHelperDetailDialog(this);
}
static get styles(): CSSResult {
return css`
mwc-fab {
position: fixed;
bottom: 16px;
right: 16px;
z-index: 1;
}
mwc-fab[is-wide] {
bottom: 24px;
right: 24px;
}
mwc-fab[narrow] {
bottom: 84px;
}
mwc-fab[rtl] {
right: auto;
left: 16px;
}
mwc-fab[is-wide][rtl] {
bottom: 24px;
left: 24px;
right: auto;
}
`;
}
}

View File

@ -21,7 +21,6 @@ import { HASSDomEvent } from "../../../common/dom/fire_event";
import "../../../common/search/search-input";
import { caseInsensitiveCompare } from "../../../common/string/compare";
import { LocalizeFunc } from "../../../common/translations/localize";
import { computeRTL } from "../../../common/util/compute_rtl";
import { nextRender } from "../../../common/util/render-status";
import "../../../components/entity/ha-state-icon";
import "../../../components/ha-button-menu";
@ -276,7 +275,11 @@ class HaConfigIntegrations extends SubscribeMixin(LitElement) {
</div>
`
: ""}
<ha-button-menu corner="BOTTOM_START" slot="toolbar-icon">
<ha-button-menu
corner="BOTTOM_START"
slot="toolbar-icon"
@action=${this._toggleShowIgnored}
>
<mwc-icon-button
.title=${this.hass.localize("ui.common.menu")}
.label=${this.hass.localize("ui.common.overflow_menu")}
@ -284,7 +287,7 @@ class HaConfigIntegrations extends SubscribeMixin(LitElement) {
>
<ha-svg-icon path=${mdiDotsVertical}></ha-svg-icon>
</mwc-icon-button>
<mwc-list-item @click=${this._toggleShowIgnored}>
<mwc-list-item>
${this.hass.localize(
this._showIgnored
? "ui.panel.config.integrations.ignore.hide_ignored"
@ -455,12 +458,10 @@ class HaConfigIntegrations extends SubscribeMixin(LitElement) {
: ""}
</div>
<mwc-fab
slot="fab"
aria-label=${this.hass.localize("ui.panel.config.integrations.new")}
title=${this.hass.localize("ui.panel.config.integrations.new")}
@click=${this._createFlow}
?is-wide=${this.isWide}
?narrow=${this.narrow}
?rtl=${computeRTL(this.hass!)}
>
<ha-svg-icon slot="icon" path=${mdiPlus}></ha-svg-icon>
</mwc-fab>
@ -710,28 +711,6 @@ class HaConfigIntegrations extends SubscribeMixin(LitElement) {
h2 {
margin-top: 0;
}
mwc-fab {
position: fixed;
bottom: 16px;
right: 16px;
z-index: 1;
}
mwc-fab[is-wide] {
bottom: 24px;
right: 24px;
}
mwc-fab[narrow] {
bottom: 84px;
}
mwc-fab[rtl] {
right: auto;
left: 16px;
}
mwc-fab[is-wide][rtl] {
bottom: 24px;
left: 24px;
right: auto;
}
`,
];
}

Some files were not shown because too many files have changed in this diff Show More