Compare commits

..

1 Commits

Author SHA1 Message Date
Paulus Schoutsen 368973b668 Add app alias 2020-06-21 12:37:12 -07:00
717 changed files with 13377 additions and 39998 deletions
+4 -8
View File
@@ -1,7 +1,7 @@
{
"extends": [
"airbnb-typescript/base",
"plugin:@typescript-eslint/recommended",
"airbnb-typescript/base",
"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": 2,
"no-bitwise": 0,
"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,
@@ -66,11 +66,7 @@
"import/prefer-default-export": 0,
"import/no-unresolved": 0,
"import/no-cycle": 0,
"import/extensions": [
2,
"ignorePackages",
{ "ts": "never", "js": "never" }
],
"import/extensions": 0,
"no-restricted-syntax": ["error", "LabeledStatement", "WithStatement"],
"object-curly-newline": 0,
"default-case": 0,
+1 -1
View File
@@ -51,7 +51,7 @@ DO NOT DELETE ANY TEXT from this template! Otherwise, your issue may be closed w
<!--
Provide details about the versions you are using, which helps us reproducing
and finding the issue quicker. Version information is found in the
Home Assistant frontend: Configuration -> Info.
Home Assistant frontend: Developer tools -> Info.
Browser version and operating system is important! Please try to replicate
your issue in a different browser and be sure to include your findings.
+4 -2
View File
@@ -34,8 +34,10 @@ jobs:
run: yarn install
env:
CI: true
- name: Build resources
run: ./node_modules/.bin/gulp gen-icons-json build-translations gather-gallery-demos
- name: Build icons
run: ./node_modules/.bin/gulp gen-icons-json
- name: Build translations
run: ./node_modules/.bin/gulp build-translations
- name: Run eslint
run: ./node_modules/.bin/eslint '{**/src,src}/**/*.{js,ts,html}' --ignore-path .gitignore
- name: Run tsc
-60
View File
@@ -1,60 +0,0 @@
name: "CodeQL"
on:
push:
branches: [dev, master]
pull_request:
# The branches below must be a subset of the branches above
branches: [dev]
jobs:
analyze:
name: Analyze
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
# Override automatic language detection by changing the below list
# Supported options are ['csharp', 'cpp', 'go', 'java', 'javascript', 'python']
language: ['javascript']
# Learn more...
# https://docs.github.com/en/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#overriding-automatic-language-detection
steps:
- name: Checkout repository
uses: actions/checkout@v2
with:
# We must fetch at least the immediate parents so that if this is
# a pull request then we can checkout the head.
fetch-depth: 2
# If this run was triggered by a pull request event, then checkout
# the head of the pull request instead of the merge commit.
- run: git checkout HEAD^2
if: ${{ github.event_name == 'pull_request' }}
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@v1
with:
languages: ${{ matrix.language }}
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
# If this step fails, then you should remove it and run the build manually (see below)
- name: Autobuild
uses: github/codeql-action/autobuild@v1
# ️ Command-line programs to run using the OS shell.
# 📚 https://git.io/JvXDl
# ✏️ If the Autobuild fails above, remove it and uncomment the following three lines
# and modify them (or add more) to build your code if your project
# uses a compiled language
#- run: |
# make bootstrap
# make release
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v1
+1 -1
View File
@@ -24,7 +24,7 @@ dist
.vscode/*
!.vscode/extensions.json
# Cast dev settings
# Cast dev settings
src/cast/dev_const.ts
# Secrets
+3 -2
View File
@@ -57,7 +57,7 @@ module.exports.babelOptions = ({ latestBuild }) => ({
].filter(Boolean),
plugins: [
// Part of ES2018. Converts {...a, b: 2} to Object.assign({}, a, {b: 2})
!latestBuild && [
[
"@babel/plugin-proposal-object-rest-spread",
{ loose: true, useBuiltIns: true },
],
@@ -73,7 +73,7 @@ module.exports.babelOptions = ({ latestBuild }) => ({
require("@babel/plugin-proposal-class-properties").default,
{ loose: true },
],
].filter(Boolean),
],
});
// Are already ES5, cause warnings when babelified.
@@ -178,6 +178,7 @@ module.exports.config = {
publicPath: publicPath(latestBuild, paths.hassio_publicPath),
isProdBuild,
latestBuild,
dontHash: new Set(["entrypoint"]),
};
},
+6 -39
View File
@@ -90,6 +90,8 @@ gulp.task("gen-pages-prod", (done) => {
});
gulp.task("gen-index-app-dev", (done) => {
// In dev mode we don't mangle names, so we hardcode urls. That way we can
// run webpack as last in watch mode, which blocks output.
const content = renderTemplate("index", {
latestAppJS: "/frontend_latest/app.js",
latestCoreJS: "/frontend_latest/core.js",
@@ -199,6 +201,8 @@ gulp.task("gen-index-cast-prod", (done) => {
});
gulp.task("gen-index-demo-dev", (done) => {
// In dev mode we don't mangle names, so we hardcode urls. That way we can
// run webpack as last in watch mode, which blocks output.
const content = renderDemoTemplate("index", {
latestDemoJS: "/frontend_latest/main.js",
@@ -236,6 +240,8 @@ gulp.task("gen-index-demo-prod", (done) => {
});
gulp.task("gen-index-gallery-dev", (done) => {
// In dev mode we don't mangle names, so we hardcode urls. That way we can
// run webpack as last in watch mode, which blocks output.
const content = renderGalleryTemplate("index", {
latestGalleryJS: "./frontend_latest/entrypoint.js",
});
@@ -263,42 +269,3 @@ gulp.task("gen-index-gallery-prod", (done) => {
);
done();
});
gulp.task("gen-index-hassio-dev", async () => {
writeHassioEntrypoint(
`${paths.hassio_publicPath}/frontend_latest/entrypoint.js`,
`${paths.hassio_publicPath}/frontend_es5/entrypoint.js`
);
});
gulp.task("gen-index-hassio-prod", async () => {
const latestManifest = require(path.resolve(
paths.hassio_output_latest,
"manifest.json"
));
const es5Manifest = require(path.resolve(
paths.hassio_output_es5,
"manifest.json"
));
writeHassioEntrypoint(
latestManifest["entrypoint.js"],
es5Manifest["entrypoint.js"]
);
});
function writeHassioEntrypoint(latestEntrypoint, es5Entrypoint) {
fs.mkdirSync(paths.hassio_output_root, { recursive: true });
fs.writeFileSync(
path.resolve(paths.hassio_output_root, "entrypoint.js"),
`
try {
new Function("import('${latestEntrypoint}')")();
} catch (err) {
var el = document.createElement('script');
el.src = '${es5Entrypoint}';
document.body.appendChild(el);
}
`,
{ encoding: "utf-8" }
);
}
+2 -38
View File
@@ -1,10 +1,7 @@
// Run demo develop mode
const gulp = require("gulp");
const fs = require("fs");
const path = require("path");
const env = require("../env");
const paths = require("../paths");
require("./clean.js");
require("./translations.js");
@@ -15,31 +12,6 @@ require("./service-worker.js");
require("./entry-html.js");
require("./rollup.js");
gulp.task("gather-gallery-demos", async function gatherDemos() {
const files = await fs.promises.readdir(
path.resolve(paths.gallery_dir, "src/demos")
);
let content = "export const DEMOS = {\n";
for (const file of files) {
const demoId = path.basename(file, ".ts");
const demoPath = "../src/demos/" + demoId;
content += ` "${demoId}": () => import("${demoPath}"),\n`;
}
content += "};";
const galleryBuild = path.resolve(paths.gallery_dir, "build");
fs.mkdirSync(galleryBuild, { recursive: true });
fs.writeFileSync(
path.resolve(galleryBuild, "import-demos.ts"),
content,
"utf-8"
);
});
gulp.task(
"develop-gallery",
gulp.series(
@@ -48,11 +20,7 @@ gulp.task(
},
"clean-gallery",
"translations-enable-merge-backend",
gulp.parallel(
"gen-icons-json",
"build-translations",
"gather-gallery-demos"
),
gulp.parallel("gen-icons-json", "build-translations"),
"copy-static-gallery",
"gen-index-gallery-dev",
env.useRollup() ? "rollup-dev-server-gallery" : "webpack-dev-server-gallery"
@@ -67,11 +35,7 @@ gulp.task(
},
"clean-gallery",
"translations-enable-merge-backend",
gulp.parallel(
"gen-icons-json",
"build-translations",
"gather-gallery-demos"
),
gulp.parallel("gen-icons-json", "build-translations"),
"copy-static-gallery",
env.useRollup() ? "rollup-prod-gallery" : "webpack-prod-gallery",
"gen-index-gallery-prod"
+1 -9
View File
@@ -11,7 +11,6 @@ const META_PATH = path.resolve(ICON_PACKAGE_PATH, "meta.json");
const PACKAGE_PATH = path.resolve(ICON_PACKAGE_PATH, "package.json");
const ICON_PATH = path.resolve(ICON_PACKAGE_PATH, "svg");
const OUTPUT_DIR = path.resolve(__dirname, "../../build/mdi");
const REMOVED_ICONS_PATH = path.resolve(__dirname, "../removedIcons.json");
const encoding = "utf8";
@@ -26,13 +25,6 @@ const getMeta = () => {
});
};
const addRemovedMeta = (meta) => {
const file = fs.readFileSync(REMOVED_ICONS_PATH, { encoding });
const removed = JSON.parse(file);
const combinedMeta = [...meta, ...removed];
return combinedMeta.sort((a, b) => a.name.localeCompare(b.name));
};
const splitBySize = (meta) => {
const chunks = [];
const CHUNK_SIZE = 50000;
@@ -77,7 +69,7 @@ const findDifferentiator = (curString, prevString) => {
};
gulp.task("gen-icons-json", (done) => {
const meta = addRemovedMeta(getMeta());
const meta = getMeta();
const split = splitBySize(meta);
if (!fs.existsSync(OUTPUT_DIR)) {
+20 -2
View File
@@ -11,6 +11,24 @@ require("./webpack.js");
require("./compress.js");
require("./rollup.js");
async function writeEntrypointJS() {
// We ship two builds and we need to do feature detection on what version to load.
fs.mkdirSync(paths.hassio_output_root, { recursive: true });
fs.writeFileSync(
path.resolve(paths.hassio_output_root, "entrypoint.js"),
`
try {
new Function("import('${paths.hassio_publicPath}/frontend_latest/entrypoint.js')")();
} catch (err) {
var el = document.createElement('script');
el.src = '${paths.hassio_publicPath}/frontend_es5/entrypoint.js';
document.body.appendChild(el);
}
`,
{ encoding: "utf-8" }
);
}
gulp.task(
"develop-hassio",
gulp.series(
@@ -19,7 +37,7 @@ gulp.task(
},
"clean-hassio",
"gen-icons-json",
"gen-index-hassio-dev",
writeEntrypointJS,
env.useRollup() ? "rollup-watch-hassio" : "webpack-watch-hassio"
)
);
@@ -33,7 +51,7 @@ gulp.task(
"clean-hassio",
"gen-icons-json",
env.useRollup() ? "rollup-prod-hassio" : "webpack-prod-hassio",
"gen-index-hassio-prod",
writeEntrypointJS,
...// Don't compress running tests
(env.isTest() ? [] : ["compress-hassio"])
)
-5
View File
@@ -34,11 +34,6 @@ module.exports = {
hassio_dir: path.resolve(__dirname, "../hassio"),
hassio_output_root: path.resolve(__dirname, "../hassio/build"),
hassio_output_latest: path.resolve(
__dirname,
"../hassio/build/frontend_latest"
),
hassio_output_es5: path.resolve(__dirname, "../hassio/build/frontend_es5"),
hassio_publicPath: "/api/hassio/app",
translations_src: path.resolve(__dirname, "../src/translations"),
-1
View File
@@ -1 +0,0 @@
[]
+16 -14
View File
@@ -37,21 +37,23 @@
<body>
<%= renderTemplate('_js_base') %>
<script>
import("<%= latestLauncherJS %>");
window.latestJS = true;
<script type="module" crossorigin="use-credentials">
import "<%= latestLauncherJS %>";
</script>
<script>
if (!window.latestJS) {
<% if (useRollup) { %>
_ls("/static/js/s.min.js").onload = function() {
System.import("<%= es5LauncherJS %>");
};
<% } else { %>
_ls("<%= es5LauncherJS %>");
<% } %>
}
<script nomodule>
(function() {
// // Safari 10.1 supports type=module but ignores nomodule, so we add this check.
if (!isS101) {
<% if (useRollup) { %>
_ls("/static/js/s.min.js").onload = function() {
System.import("<%= es5LauncherJS %>");
};
<% } else { %>
_ls("<%= es5LauncherJS %>");
<% } %>
}
})();
</script>
<hc-layout subtitle="FAQ">
@@ -251,7 +253,7 @@ http:
<script>
var _gaq = [["_setAccount", "UA-57927901-9"], ["_trackPageview"]];
(function (d, t) {
(function(d, t) {
var g = d.createElement(t),
s = d.getElementsByTagName(t)[0];
g.src =
+15 -13
View File
@@ -28,21 +28,23 @@
<hc-connect></hc-connect>
<script>
import("<%= latestLauncherJS %>");
window.latestJS = true;
<script type="module" crossorigin="use-credentials">
import "<%= latestLauncherJS %>";
</script>
<script>
if (!window.latestJS) {
<% if (useRollup) { %>
_ls("/static/js/s.min.js").onload = function() {
System.import("<%= es5LauncherJS %>");
};
<% } else { %>
_ls("<%= es5LauncherJS %>");
<% } %>
}
<script nomodule>
(function() {
// // Safari 10.1 supports type=module but ignores nomodule, so we add this check.
if (!isS101) {
<% if (useRollup) { %>
_ls("/static/js/s.min.js").onload = function() {
System.import("<%= es5LauncherJS %>");
};
<% } else { %>
_ls("<%= es5LauncherJS %>");
<% } %>
}
})();
</script>
<script>
(function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){
+2 -2
View File
@@ -1,3 +1,3 @@
import "../../../src/resources/ha-style";
import "../../../src/resources/roboto";
import "~app/resources/ha-style";
import "~app/resources/roboto";
import "./layout/hc-connect";
+4 -5
View File
@@ -8,7 +8,6 @@ import {
html,
LitElement,
property,
internalProperty,
TemplateResult,
} from "lit-element";
import { CastManager } from "../../../../src/cast/cast_manager";
@@ -29,7 +28,7 @@ import {
getLovelaceCollection,
LovelaceConfig,
} from "../../../../src/data/lovelace";
import "../../../../src/layouts/hass-loading-screen";
import "../../../../src/layouts/loading-screen";
import { generateDefaultViewConfig } from "../../../../src/panels/lovelace/common/generate-lovelace-config";
import "./hc-layout";
import "@material/mwc-button/mwc-button";
@@ -42,13 +41,13 @@ class HcCast extends LitElement {
@property() public castManager!: CastManager;
@internalProperty() private askWrite = false;
@property() private askWrite = false;
@internalProperty() private lovelaceConfig?: LovelaceConfig | null;
@property() private lovelaceConfig?: LovelaceConfig | null;
protected render(): TemplateResult {
if (this.lovelaceConfig === undefined) {
return html` <hass-loading-screen no-toolbar></hass-loading-screen>> `;
return html` <loading-screen></loading-screen>> `;
}
const error =
+9 -9
View File
@@ -17,8 +17,8 @@ import {
customElement,
html,
LitElement,
property,
TemplateResult,
internalProperty,
} from "lit-element";
import { CastManager, getCastManager } from "../../../../src/cast/cast_manager";
import { castSendShowDemo } from "../../../../src/cast/receiver_messages";
@@ -27,7 +27,7 @@ import {
saveTokens,
} from "../../../../src/common/auth/token_storage";
import "../../../../src/components/ha-icon";
import "../../../../src/layouts/hass-loading-screen";
import "../../../../src/layouts/loading-screen";
import { registerServiceWorker } from "../../../../src/util/register-service-worker";
import "./hc-layout";
@@ -60,19 +60,19 @@ const INTRO = html`
@customElement("hc-connect")
export class HcConnect extends LitElement {
@internalProperty() private loading = false;
@property() private loading = false;
// If we had stored credentials but we cannot connect,
// show a screen asking retry or logout.
@internalProperty() private cannotConnect = false;
@property() private cannotConnect = false;
@internalProperty() private error?: string | TemplateResult;
@property() private error?: string | TemplateResult;
@internalProperty() private auth?: Auth;
@property() private auth?: Auth;
@internalProperty() private connection?: Connection;
@property() private connection?: Connection;
@internalProperty() private castManager?: CastManager | null;
@property() private castManager?: CastManager | null;
private openDemo = false;
@@ -98,7 +98,7 @@ export class HcConnect extends LitElement {
}
if (this.castManager === undefined || this.loading) {
return html` <hass-loading-screen no-toolbar></hass-loading-screen> `;
return html` <loading-screen></loading-screen> `;
}
if (this.castManager === null) {
+3 -3
View File
@@ -1,7 +1,7 @@
/* eslint-disable no-undef */
import { CAST_NS } from "../../../src/cast/const";
import { HassMessage } from "../../../src/cast/receiver_messages";
import "../../../src/resources/custom-card-support";
import { CAST_NS } from "~app/cast/const";
import { HassMessage } from "~app/cast/receiver_messages";
import "~app/resources/custom-card-support";
import { castContext } from "./cast_context";
import { HcMain } from "./layout/hc-main";
import { ReceivedMessage } from "./types";
+3 -9
View File
@@ -1,10 +1,4 @@
import {
customElement,
html,
property,
internalProperty,
TemplateResult,
} from "lit-element";
import { customElement, html, property, TemplateResult } from "lit-element";
import { mockHistory } from "../../../../demo/src/stubs/history";
import { LovelaceConfig } from "../../../../src/data/lovelace";
import {
@@ -19,9 +13,9 @@ import "./hc-lovelace";
@customElement("hc-demo")
class HcDemo extends HassElement {
@property({ attribute: false }) public lovelacePath!: string;
@property() public lovelacePath!: string;
@internalProperty() private _lovelaceConfig?: LovelaceConfig;
@property() private _lovelaceConfig?: LovelaceConfig;
protected render(): TemplateResult {
if (!this._lovelaceConfig) {
+1 -1
View File
@@ -11,7 +11,7 @@ import { HomeAssistant } from "../../../../src/types";
@customElement("hc-launch-screen")
class HcLaunchScreen extends LitElement {
@property({ attribute: false }) public hass?: HomeAssistant;
@property() public hass?: HomeAssistant;
@property() public error?: string;
+2 -5
View File
@@ -16,14 +16,12 @@ import "./hc-launch-screen";
@customElement("hc-lovelace")
class HcLovelace extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property() public hass!: HomeAssistant;
@property({ attribute: false }) public lovelaceConfig!: LovelaceConfig;
@property() public lovelaceConfig!: LovelaceConfig;
@property() public viewPath?: string | number;
public urlPath?: string | null;
protected render(): TemplateResult {
const index = this._viewIndex;
if (index === undefined) {
@@ -37,7 +35,6 @@ class HcLovelace extends LitElement {
const lovelace: Lovelace = {
config: this.lovelaceConfig,
editMode: false,
urlPath: this.urlPath!,
enableFullEditMode: () => undefined,
mode: "storage",
language: "en",
+5 -11
View File
@@ -3,12 +3,7 @@ import {
getAuth,
UnsubscribeFunc,
} from "home-assistant-js-websocket";
import {
customElement,
html,
internalProperty,
TemplateResult,
} from "lit-element";
import { customElement, html, property, TemplateResult } from "lit-element";
import { CAST_NS } from "../../../../src/cast/const";
import {
ConnectMessage,
@@ -36,13 +31,13 @@ let resourcesLoaded = false;
@customElement("hc-main")
export class HcMain extends HassElement {
@internalProperty() private _showDemo = false;
@property() private _showDemo = false;
@internalProperty() private _lovelaceConfig?: LovelaceConfig;
@property() private _lovelaceConfig?: LovelaceConfig;
@internalProperty() private _lovelacePath: string | number | null = null;
@property() private _lovelacePath: string | number | null = null;
@internalProperty() private _error?: string;
@property() private _error?: string;
private _unsubLovelace?: UnsubscribeFunc;
@@ -87,7 +82,6 @@ export class HcMain extends HassElement {
.hass=${this.hass}
.lovelaceConfig=${this._lovelaceConfig}
.viewPath=${this._lovelacePath}
.urlPath=${this._urlPath}
@config-refresh=${this._generateLovelaceConfig}
></hc-lovelace>
`;
+2 -2
View File
@@ -26,9 +26,9 @@ export const demoThemeJimpower = () => ({
"switch-checked-color": "var(--accent-color)",
"paper-dialog-background-color": "#434954",
"secondary-text-color": "#5294E2",
"error-color": "#E45E65",
"google-red-500": "#E45E65",
"divider-color": "rgba(0, 0, 0, .12)",
"success-color": "#39E949",
"google-green-500": "#39E949",
"switch-unchecked-button-color": "var(--disabled-text-color)",
"label-badge-border-color": "green",
"paper-listbox-color": "var(--primary-color)",
+2 -2
View File
@@ -27,9 +27,9 @@ export const demoThemeKernehed = () => ({
"switch-checked-color": "var(--accent-color)",
"paper-dialog-background-color": "#292929",
"secondary-text-color": "#b58e31",
"error-color": "#b58e31",
"google-red-500": "#b58e31",
"divider-color": "rgba(0, 0, 0, .12)",
"success-color": "#2980b9",
"google-green-500": "#2980b9",
"switch-unchecked-button-color": "var(--disabled-text-color)",
"label-badge-border-color": "green",
"paper-listbox-color": "#777777",
+3 -2
View File
@@ -4,7 +4,7 @@ import {
customElement,
html,
LitElement,
internalProperty,
property,
TemplateResult,
} from "lit-element";
import { CastManager } from "../../../src/cast/cast_manager";
@@ -20,7 +20,7 @@ import { HomeAssistant } from "../../../src/types";
class CastDemoRow extends LitElement implements LovelaceRow {
public hass!: HomeAssistant;
@internalProperty() private _castManager?: CastManager | null;
@property() private _castManager?: CastManager | null;
public setConfig(_config: CastConfig): void {
// No config possible.
@@ -52,6 +52,7 @@ 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.
+5 -6
View File
@@ -1,16 +1,15 @@
import "@material/mwc-button";
import "@polymer/paper-spinner/paper-spinner-lite";
import {
css,
CSSResult,
html,
LitElement,
property,
internalProperty,
TemplateResult,
} from "lit-element";
import { until } from "lit-html/directives/until";
import "../../../src/components/ha-card";
import "../../../src/components/ha-circular-progress";
import { LovelaceCardConfig } from "../../../src/data/lovelace";
import { MockHomeAssistant } from "../../../src/fake_data/provide_hass";
import { Lovelace, LovelaceCard } from "../../../src/panels/lovelace/types";
@@ -22,11 +21,11 @@ import {
} from "../configs/demo-configs";
export class HADemoCard extends LitElement implements LovelaceCard {
@property({ attribute: false }) public lovelace?: Lovelace;
@property() public lovelace?: Lovelace;
@property({ attribute: false }) public hass!: MockHomeAssistant;
@property() public hass!: MockHomeAssistant;
@internalProperty() private _switching?: boolean;
@property() private _switching?: boolean;
private _hidden = localStorage.hide_demo_card;
@@ -50,7 +49,7 @@ export class HADemoCard extends LitElement implements LovelaceCard {
<div class="picker">
<div class="label">
${this._switching
? html`<ha-circular-progress active></ha-circular-progress>`
? html` <paper-spinner-lite active></paper-spinner-lite> `
: until(
selectedDemoConfig.then(
(conf) => html`
+15 -15
View File
@@ -86,26 +86,26 @@
<%= renderTemplate('_js_base') %>
<%= renderTemplate('_preload_roboto') %>
<script>
import("<%= latestDemoJS %>");
window.latestJS = true;
</script>
<script type="module" src="<%= latestDemoJS %>"></script>
<script>
if (!window.latestJS) {
<% if (useRollup) { %>
_ls("/static/js/s.min.js").onload = function() {
System.import("<%= es5DemoJS %>");
};
<% } else { %>
_ls("<%= es5DemoJS %>");
<% } %>
}
<script nomodule>
(function() {
// // Safari 10.1 supports type=module but ignores nomodule, so we add this check.
if (!isS101) {
<% if (useRollup) { %>
_ls("/static/js/s.min.js").onload = function() {
System.import("<%= es5DemoJS %>");
};
<% } else { %>
_ls("<%= es5DemoJS %>");
<% } %>
}
})();
</script>
<script>
var _gaq = [["_setAccount", "UA-57927901-5"], ["_trackPageview"]];
(function (d, t) {
(function(d, t) {
var g = d.createElement(t),
s = d.getElementsByTagName(t)[0];
g.src =
+2 -2
View File
@@ -2,8 +2,8 @@ import "@polymer/app-layout/app-toolbar/app-toolbar";
import { html } from "@polymer/polymer/lib/utils/html-tag";
/* eslint-plugin-disable lit */
import { PolymerElement } from "@polymer/polymer/polymer-element";
import "../../../src/components/ha-switch";
import "../../../src/components/ha-formfield";
import "~app/components/ha-switch";
import "~app/components/ha-formfield";
import "./demo-card";
class DemoCards extends PolymerElement {
+1 -1
View File
@@ -2,8 +2,8 @@ import { html } from "@polymer/polymer/lib/utils/html-tag";
/* eslint-plugin-disable lit */
import { PolymerElement } from "@polymer/polymer/polymer-element";
import "../../../src/components/ha-card";
import "../../../src/dialogs/more-info/controls/more-info-content";
import "../../../src/state-summary/state-card-content";
import "./more-info-content";
class DemoMoreInfo extends PolymerElement {
static get template() {
@@ -1,73 +0,0 @@
import { HassEntity } from "home-assistant-js-websocket";
import { property, PropertyValues, UpdatingElement } from "lit-element";
import dynamicContentUpdater from "../../../src/common/dom/dynamic_content_updater";
import { stateMoreInfoType } from "../../../src/common/entity/state_more_info_type";
import "../../../src/dialogs/more-info/controls/more-info-alarm_control_panel";
import "../../../src/dialogs/more-info/controls/more-info-automation";
import "../../../src/dialogs/more-info/controls/more-info-camera";
import "../../../src/dialogs/more-info/controls/more-info-climate";
import "../../../src/dialogs/more-info/controls/more-info-configurator";
import "../../../src/dialogs/more-info/controls/more-info-counter";
import "../../../src/dialogs/more-info/controls/more-info-cover";
import "../../../src/dialogs/more-info/controls/more-info-default";
import "../../../src/dialogs/more-info/controls/more-info-fan";
import "../../../src/dialogs/more-info/controls/more-info-group";
import "../../../src/dialogs/more-info/controls/more-info-humidifier";
import "../../../src/dialogs/more-info/controls/more-info-input_datetime";
import "../../../src/dialogs/more-info/controls/more-info-light";
import "../../../src/dialogs/more-info/controls/more-info-lock";
import "../../../src/dialogs/more-info/controls/more-info-media_player";
import "../../../src/dialogs/more-info/controls/more-info-person";
import "../../../src/dialogs/more-info/controls/more-info-script";
import "../../../src/dialogs/more-info/controls/more-info-sun";
import "../../../src/dialogs/more-info/controls/more-info-timer";
import "../../../src/dialogs/more-info/controls/more-info-vacuum";
import "../../../src/dialogs/more-info/controls/more-info-water_heater";
import "../../../src/dialogs/more-info/controls/more-info-weather";
import { HomeAssistant } from "../../../src/types";
class MoreInfoContent extends UpdatingElement {
@property({ attribute: false }) public hass?: HomeAssistant;
@property() public stateObj?: HassEntity;
private _detachedChild?: ChildNode;
protected firstUpdated(): void {
this.style.position = "relative";
this.style.display = "block";
}
// This is not a lit element, but an updating element, so we implement update
protected update(changedProps: PropertyValues): void {
super.update(changedProps);
const stateObj = this.stateObj;
const hass = this.hass;
if (!stateObj || !hass) {
if (this.lastChild) {
this._detachedChild = this.lastChild;
// Detach child to prevent it from doing work.
this.removeChild(this.lastChild);
}
return;
}
if (this._detachedChild) {
this.appendChild(this._detachedChild);
this._detachedChild = undefined;
}
const moreInfoType =
stateObj.attributes && "custom_ui_more_info" in stateObj.attributes
? stateObj.attributes.custom_ui_more_info
: "more-info-" + stateMoreInfoType(stateObj);
dynamicContentUpdater(this, moreInfoType.toUpperCase(), {
hass,
stateObj,
});
}
}
customElements.define("more-info-content", MoreInfoContent);
+21
View File
@@ -420,6 +420,15 @@ export default {
last_changed: "2018-07-19T10:44:46.105940+00:00",
last_updated: "2018-07-19T10:44:46.105940+00:00",
},
"weblink.router": {
entity_id: "weblink.router",
state: "http://192.168.1.1",
attributes: {
friendly_name: "Router",
},
last_changed: "2018-07-19T10:44:46.107286+00:00",
last_updated: "2018-07-19T10:44:46.107286+00:00",
},
"group.all_plants": {
entity_id: "group.all_plants",
state: "ok",
@@ -1081,6 +1090,18 @@ export default {
last_changed: "2018-07-19T10:44:46.510448+00:00",
last_updated: "2018-07-19T10:44:46.510448+00:00",
},
"history_graph.recent_switches": {
entity_id: "history_graph.recent_switches",
state: "unknown",
attributes: {
hours_to_show: 1,
refresh: 60,
entity_id: ["switch.ac", "switch.decorative_lights"],
friendly_name: "Recent Switches",
},
last_changed: "2018-07-19T10:44:46.512351+00:00",
last_updated: "2018-07-19T10:44:46.512351+00:00",
},
"scene.switch_on_and_off": {
entity_id: "scene.switch_on_and_off",
state: "scening",
+1 -1
View File
@@ -3,10 +3,10 @@ import { html } from "@polymer/polymer/lib/utils/html-tag";
import { PolymerElement } from "@polymer/polymer/polymer-element";
import "../../../src/components/ha-card";
import { SUPPORT_BRIGHTNESS } from "../../../src/data/light";
import "../../../src/dialogs/more-info/controls/more-info-content";
import { getEntity } from "../../../src/fake_data/entity";
import { provideHass } from "../../../src/fake_data/provide_hass";
import "../components/demo-more-infos";
import "../components/more-info-content";
const ENTITIES = [
getEntity("light", "bed_light", "on", {
+7 -6
View File
@@ -1,18 +1,19 @@
import "@polymer/app-layout/app-header-layout/app-header-layout";
import "@polymer/app-layout/app-header/app-header";
import "@polymer/app-layout/app-toolbar/app-toolbar";
import "../../src/components/ha-icon-button";
import "../../src/components/ha-icon";
import "@polymer/paper-item/paper-item";
import "@polymer/paper-item/paper-item-body";
import { html } from "@polymer/polymer/lib/utils/html-tag";
/* eslint-plugin-disable lit */
import { PolymerElement } from "@polymer/polymer/polymer-element";
import "../../src/components/ha-card";
import "../../src/components/ha-icon";
import "../../src/components/ha-icon-button";
import "../../src/managers/notification-manager";
import "../../src/styles/polymer-ha-style";
// eslint-disable-next-line import/extensions
import { DEMOS } from "../build/import-demos";
// eslint-disable-next-line no-undef
const DEMOS = require.context("./demos", true, /^(.*\.(ts$))[^.]*$/im);
const fixPath = (path) => path.substr(2, path.length - 5);
@@ -162,7 +163,7 @@ class HaGallery extends PolymerElement {
},
_demos: {
type: Array,
value: Object.keys(DEMOS),
value: DEMOS.keys().map(fixPath),
},
_lovelaceDemos: {
type: Array,
@@ -209,7 +210,7 @@ class HaGallery extends PolymerElement {
while (root.lastChild) root.removeChild(root.lastChild);
if (demo) {
DEMOS[demo]();
DEMOS(`./${demo}.ts`);
const el = document.createElement(demo);
root.appendChild(el);
}
@@ -21,7 +21,7 @@ import { filterAndSort } from "../components/hassio-filter-addons";
import { hassioStyle } from "../resources/hassio-style";
class HassioAddonRepositoryEl extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property() public hass!: HomeAssistant;
@property() public repo!: HassioAddonRepository;
+8 -26
View File
@@ -1,11 +1,9 @@
import "@material/mwc-icon-button/mwc-icon-button";
import { ActionDetail } from "@material/mwc-list/mwc-list-foundation";
import "@material/mwc-list/mwc-list-item";
import { mdiDotsVertical } from "@mdi/js";
import {
css,
CSSResult,
internalProperty,
LitElement,
property,
PropertyValues,
@@ -20,12 +18,11 @@ import {
HassioAddonRepository,
reloadHassioAddons,
} from "../../../src/data/hassio/addon";
import { extractApiErrorMessage } from "../../../src/data/hassio/common";
import "../../../src/layouts/hass-loading-screen";
import "../../../src/layouts/hass-tabs-subpage";
import "../../../src/layouts/loading-screen";
import { HomeAssistant, Route } from "../../../src/types";
import { showRepositoriesDialog } from "../dialogs/repositories/show-dialog-repositories";
import { supervisorTabs } from "../hassio-tabs";
import { supervisorTabs } from "../hassio-panel";
import "./hassio-addon-repository";
const sortRepos = (a: HassioAddonRepository, b: HassioAddonRepository) => {
@@ -55,7 +52,7 @@ class HassioAddonStore extends LitElement {
@property({ attribute: false }) private _repos?: HassioAddonRepository[];
@internalProperty() private _filter?: string;
@property() private _filter?: string;
public async refreshData() {
this._repos = undefined;
@@ -99,23 +96,19 @@ class HassioAddonStore extends LitElement {
.tabs=${supervisorTabs}
>
<span slot="header">Add-on store</span>
<ha-button-menu
corner="BOTTOM_START"
slot="toolbar-icon"
@action=${this._handleAction}
>
<ha-button-menu corner="BOTTOM_START" slot="toolbar-icon">
<mwc-icon-button slot="trigger" alt="menu">
<ha-svg-icon path=${mdiDotsVertical}></ha-svg-icon>
</mwc-icon-button>
<mwc-list-item>
<mwc-list-item @tap=${this._manageRepositories}>
Repositories
</mwc-list-item>
<mwc-list-item>
<mwc-list-item @tap=${this.refreshData}>
Reload
</mwc-list-item>
</ha-button-menu>
${repos.length === 0
? html`<hass-loading-screen no-toolbar></hass-loading-screen>`
? html`<loading-screen></loading-screen>`
: html`
<div class="search">
<search-input
@@ -149,17 +142,6 @@ 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();
@@ -180,7 +162,7 @@ class HassioAddonStore extends LitElement {
this._repos.sort(sortRepos);
this._addons = addonsInfo.addons;
} catch (err) {
alert(extractApiErrorMessage(err));
alert("Failed to fetch add-on info");
}
}
@@ -9,7 +9,6 @@ import {
html,
LitElement,
property,
internalProperty,
PropertyValues,
TemplateResult,
} from "lit-element";
@@ -28,7 +27,6 @@ import { haStyle } from "../../../../src/resources/styles";
import { HomeAssistant } from "../../../../src/types";
import { suggestAddonRestart } from "../../dialogs/suggestAddonRestart";
import { hassioStyle } from "../../resources/hassio-style";
import "../../../../src/components/buttons/ha-progress-button";
@customElement("hassio-addon-audio")
class HassioAddonAudio extends LitElement {
@@ -36,15 +34,15 @@ class HassioAddonAudio extends LitElement {
@property({ attribute: false }) public addon!: HassioAddonDetails;
@internalProperty() private _error?: string;
@property() private _error?: string;
@internalProperty() private _inputDevices?: HassioHardwareAudioDevice[];
@property() private _inputDevices?: HassioHardwareAudioDevice[];
@internalProperty() private _outputDevices?: HassioHardwareAudioDevice[];
@property() private _outputDevices?: HassioHardwareAudioDevice[];
@internalProperty() private _selectedInput!: null | string;
@property() private _selectedInput!: null | string;
@internalProperty() private _selectedOutput!: null | string;
@property() private _selectedOutput!: null | string;
protected render(): TemplateResult {
return html`
@@ -92,9 +90,7 @@ class HassioAddonAudio extends LitElement {
</paper-dropdown-menu>
</div>
<div class="card-actions">
<ha-progress-button @click=${this._saveSettings}>
Save
</ha-progress-button>
<mwc-button @click=${this._saveSettings}>Save</mwc-button>
</div>
</ha-card>
`;
@@ -111,7 +107,7 @@ class HassioAddonAudio extends LitElement {
display: block;
}
.errors {
color: var(--error-color);
color: var(--google-red-500);
margin-bottom: 16px;
}
paper-item {
@@ -175,10 +171,7 @@ class HassioAddonAudio extends LitElement {
}
}
private async _saveSettings(ev: CustomEvent): Promise<void> {
const button = ev.currentTarget as any;
button.progress = true;
private async _saveSettings(): Promise<void> {
this._error = undefined;
const data: HassioAddonSetOptionParams = {
audio_input:
@@ -188,14 +181,12 @@ class HassioAddonAudio extends LitElement {
};
try {
await setHassioAddonOption(this.hass, this.addon.slug, data);
if (this.addon?.state === "started") {
await suggestAddonRestart(this, this.hass, this.addon);
}
} catch {
this._error = "Failed to set addon audio device";
}
button.progress = false;
if (!this._error && this.addon?.state === "started") {
await suggestAddonRestart(this, this.hass, this.addon);
}
}
}
@@ -1,3 +1,4 @@
import "@polymer/paper-spinner/paper-spinner-lite";
import {
css,
CSSResult,
@@ -11,7 +12,6 @@ import { HassioAddonDetails } from "../../../../src/data/hassio/addon";
import { haStyle } from "../../../../src/resources/styles";
import { HomeAssistant } from "../../../../src/types";
import { hassioStyle } from "../../resources/hassio-style";
import "../../../../src/components/ha-circular-progress";
import "./hassio-addon-audio";
import "./hassio-addon-config";
import "./hassio-addon-network";
@@ -24,7 +24,7 @@ class HassioAddonConfigDashboard extends LitElement {
protected render(): TemplateResult {
if (!this.addon) {
return html`<ha-circular-progress active></ha-circular-progress>`;
return html` <paper-spinner-lite active></paper-spinner-lite> `;
}
return html`
<div class="content">
@@ -5,7 +5,6 @@ import {
CSSResult,
customElement,
html,
internalProperty,
LitElement,
property,
PropertyValues,
@@ -13,7 +12,6 @@ import {
TemplateResult,
} from "lit-element";
import { fireEvent } from "../../../../src/common/dom/fire_event";
import "../../../../src/components/buttons/ha-progress-button";
import "../../../../src/components/ha-card";
import "../../../../src/components/ha-yaml-editor";
import type { HaYamlEditor } from "../../../../src/components/ha-yaml-editor";
@@ -22,7 +20,6 @@ import {
HassioAddonSetOptionParams,
setHassioAddonOption,
} from "../../../../src/data/hassio/addon";
import { extractApiErrorMessage } from "../../../../src/data/hassio/common";
import { showConfirmationDialog } from "../../../../src/dialogs/generic/show-dialog-box";
import { haStyle } from "../../../../src/resources/styles";
import type { HomeAssistant } from "../../../../src/types";
@@ -35,7 +32,7 @@ class HassioAddonConfig extends LitElement {
@property({ attribute: false }) public addon!: HassioAddonDetails;
@internalProperty() private _error?: string;
@property() private _error?: string;
@property({ type: Boolean }) private _configHasChanged = false;
@@ -57,20 +54,50 @@ class HassioAddonConfig extends LitElement {
${valid ? "" : html` <div class="errors">Invalid YAML</div> `}
</div>
<div class="card-actions">
<ha-progress-button class="warning" @click=${this._resetTapped}>
<mwc-button class="warning" @click=${this._resetTapped}>
Reset to defaults
</ha-progress-button>
<ha-progress-button
</mwc-button>
<mwc-button
@click=${this._saveTapped}
.disabled=${!this._configHasChanged || !valid}
>
Save
</ha-progress-button>
</mwc-button>
</div>
</ha-card>
`;
}
static get styles(): CSSResult[] {
return [
haStyle,
hassioStyle,
css`
:host {
display: block;
}
ha-card {
display: block;
}
.card-actions {
display: flex;
justify-content: space-between;
}
.errors {
color: var(--google-red-500);
margin-top: 16px;
}
iron-autogrow-textarea {
width: 100%;
font-family: monospace;
}
.syntaxerror {
color: var(--google-red-500);
}
`,
];
}
protected updated(changedProperties: PropertyValues): void {
super.updated(changedProperties);
if (changedProperties.has("addon")) {
@@ -83,10 +110,7 @@ class HassioAddonConfig extends LitElement {
this.requestUpdate();
}
private async _resetTapped(ev: CustomEvent): Promise<void> {
const button = ev.currentTarget as any;
button.progress = true;
private async _resetTapped(): Promise<void> {
const confirmed = await showConfirmationDialog(this, {
title: this.addon.name,
text: "Are you sure you want to reset all your options?",
@@ -95,7 +119,6 @@ class HassioAddonConfig extends LitElement {
});
if (!confirmed) {
button.progress = false;
return;
}
@@ -113,17 +136,13 @@ class HassioAddonConfig extends LitElement {
};
fireEvent(this, "hass-api-called", eventdata);
} catch (err) {
this._error = `Failed to reset addon configuration, ${extractApiErrorMessage(
err
)}`;
this._error = `Failed to reset addon configuration, ${
err.body?.message || err
}`;
}
button.progress = false;
}
private async _saveTapped(ev: CustomEvent): Promise<void> {
const button = ev.currentTarget as any;
button.progress = true;
private async _saveTapped(): Promise<void> {
let data: HassioAddonSetOptionParams;
this._error = undefined;
try {
@@ -143,45 +162,14 @@ class HassioAddonConfig extends LitElement {
path: "options",
};
fireEvent(this, "hass-api-called", eventdata);
if (this.addon?.state === "started") {
await suggestAddonRestart(this, this.hass, this.addon);
}
} catch (err) {
this._error = `Failed to save addon configuration, ${extractApiErrorMessage(
err
)}`;
this._error = `Failed to save addon configuration, ${
err.body?.message || err
}`;
}
if (!this._error && this.addon?.state === "started") {
await suggestAddonRestart(this, this.hass, this.addon);
}
button.progress = false;
}
static get styles(): CSSResult[] {
return [
haStyle,
hassioStyle,
css`
:host {
display: block;
}
ha-card {
display: block;
}
.card-actions {
display: flex;
justify-content: space-between;
}
.errors {
color: var(--error-color);
margin-top: 16px;
}
iron-autogrow-textarea {
width: 100%;
font-family: monospace;
}
.syntaxerror {
color: var(--error-color);
}
`,
];
}
}
@@ -4,21 +4,18 @@ import {
CSSResult,
customElement,
html,
internalProperty,
LitElement,
property,
PropertyValues,
TemplateResult,
} from "lit-element";
import { fireEvent } from "../../../../src/common/dom/fire_event";
import "../../../../src/components/buttons/ha-progress-button";
import "../../../../src/components/ha-card";
import {
HassioAddonDetails,
HassioAddonSetOptionParams,
setHassioAddonOption,
} from "../../../../src/data/hassio/addon";
import { extractApiErrorMessage } from "../../../../src/data/hassio/common";
import { haStyle } from "../../../../src/resources/styles";
import { HomeAssistant } from "../../../../src/types";
import { suggestAddonRestart } from "../../dialogs/suggestAddonRestart";
@@ -40,9 +37,9 @@ class HassioAddonNetwork extends LitElement {
@property({ attribute: false }) public addon!: HassioAddonDetails;
@internalProperty() private _error?: string;
@property() private _error?: string;
@internalProperty() private _config?: NetworkItem[];
@property() private _config?: NetworkItem[];
public connectedCallback(): void {
super.connectedCallback();
@@ -74,7 +71,7 @@ class HassioAddonNetwork extends LitElement {
<paper-input
@value-changed=${this._configChanged}
placeholder="disabled"
.value=${item.host ? String(item.host) : ""}
.value=${String(item.host)}
.container=${item.container}
no-label-float
></paper-input>
@@ -87,17 +84,38 @@ class HassioAddonNetwork extends LitElement {
</table>
</div>
<div class="card-actions">
<ha-progress-button class="warning" @click=${this._resetTapped}>
<mwc-button class="warning" @click=${this._resetTapped}>
Reset to defaults
</ha-progress-button>
<ha-progress-button @click=${this._saveTapped}>
Save
</ha-progress-button>
</mwc-button>
<mwc-button @click=${this._saveTapped}>Save</mwc-button>
</div>
</ha-card>
`;
}
static get styles(): CSSResult[] {
return [
haStyle,
hassioStyle,
css`
:host {
display: block;
}
ha-card {
display: block;
}
.errors {
color: var(--google-red-500);
margin-bottom: 16px;
}
.card-actions {
display: flex;
justify-content: space-between;
}
`,
];
}
protected update(changedProperties: PropertyValues): void {
super.update(changedProperties);
if (changedProperties.has("addon")) {
@@ -130,10 +148,7 @@ class HassioAddonNetwork extends LitElement {
});
}
private async _resetTapped(ev: CustomEvent): Promise<void> {
const button = ev.currentTarget as any;
button.progress = true;
private async _resetTapped(): Promise<void> {
const data: HassioAddonSetOptionParams = {
network: null,
};
@@ -146,22 +161,17 @@ class HassioAddonNetwork extends LitElement {
path: "option",
};
fireEvent(this, "hass-api-called", eventdata);
if (this.addon?.state === "started") {
await suggestAddonRestart(this, this.hass, this.addon);
}
} catch (err) {
this._error = `Failed to set addon network configuration, ${extractApiErrorMessage(
err
)}`;
this._error = `Failed to set addon network configuration, ${
err.body?.message || err
}`;
}
if (!this._error && this.addon?.state === "started") {
await suggestAddonRestart(this, this.hass, this.addon);
}
button.progress = false;
}
private async _saveTapped(ev: CustomEvent): Promise<void> {
const button = ev.currentTarget as any;
button.progress = true;
private async _saveTapped(): Promise<void> {
this._error = undefined;
const networkconfiguration = {};
this._config!.forEach((item) => {
@@ -180,38 +190,14 @@ class HassioAddonNetwork extends LitElement {
path: "option",
};
fireEvent(this, "hass-api-called", eventdata);
if (this.addon?.state === "started") {
await suggestAddonRestart(this, this.hass, this.addon);
}
} catch (err) {
this._error = `Failed to set addon network configuration, ${extractApiErrorMessage(
err
)}`;
this._error = `Failed to set addon network configuration, ${
err.body?.message || err
}`;
}
if (!this._error && this.addon?.state === "started") {
await suggestAddonRestart(this, this.hass, this.addon);
}
button.progress = false;
}
static get styles(): CSSResult[] {
return [
haStyle,
hassioStyle,
css`
:host {
display: block;
}
ha-card {
display: block;
}
.errors {
color: var(--error-color);
margin-bottom: 16px;
}
.card-actions {
display: flex;
justify-content: space-between;
}
`,
];
}
}
@@ -1,21 +1,19 @@
import "@polymer/paper-spinner/paper-spinner-lite";
import {
css,
CSSResult,
customElement,
html,
internalProperty,
LitElement,
property,
TemplateResult,
} from "lit-element";
import "../../../../src/components/ha-circular-progress";
import "../../../../src/components/ha-markdown";
import {
fetchHassioAddonDocumentation,
HassioAddonDetails,
} from "../../../../src/data/hassio/addon";
import { extractApiErrorMessage } from "../../../../src/data/hassio/common";
import "../../../../src/layouts/hass-loading-screen";
import "../../../../src/layouts/loading-screen";
import { haStyle } from "../../../../src/resources/styles";
import { HomeAssistant } from "../../../../src/types";
import { hassioStyle } from "../../resources/hassio-style";
@@ -26,9 +24,9 @@ class HassioAddonDocumentationDashboard extends LitElement {
@property({ attribute: false }) public addon?: HassioAddonDetails;
@internalProperty() private _error?: string;
@property() private _error?: string;
@internalProperty() private _content?: string;
@property() private _content?: string;
public async connectedCallback(): Promise<void> {
super.connectedCallback();
@@ -37,7 +35,7 @@ class HassioAddonDocumentationDashboard extends LitElement {
protected render(): TemplateResult {
if (!this.addon) {
return html`<ha-circular-progress active></ha-circular-progress>`;
return html` <paper-spinner-lite active></paper-spinner-lite> `;
}
return html`
<div class="content">
@@ -46,7 +44,7 @@ class HassioAddonDocumentationDashboard extends LitElement {
<div class="card-content">
${this._content
? html`<ha-markdown .content=${this._content}></ha-markdown>`
: html`<hass-loading-screen no-toolbar></hass-loading-screen>`}
: html`<loading-screen></loading-screen>`}
</div>
</ha-card>
</div>
@@ -81,9 +79,9 @@ class HassioAddonDocumentationDashboard extends LitElement {
this.addon!.slug
);
} catch (err) {
this._error = `Failed to get addon documentation, ${extractApiErrorMessage(
err
)}`;
this._error = `Failed to get addon documentation, ${
err.body?.message || err
}`;
}
}
}
@@ -4,6 +4,7 @@ import {
mdiInformationVariant,
mdiMathLog,
} from "@mdi/js";
import "@polymer/paper-spinner/paper-spinner-lite";
import {
css,
CSSResult,
@@ -19,7 +20,6 @@ import {
HassioAddonDetails,
} from "../../../src/data/hassio/addon";
import "../../../src/layouts/hass-tabs-subpage";
import "../../../src/components/ha-circular-progress";
import type { PageNavigation } from "../../../src/layouts/hass-tabs-subpage";
import { haStyle } from "../../../src/resources/styles";
import { HomeAssistant, Route } from "../../../src/types";
@@ -56,7 +56,7 @@ class HassioAddonDashboard extends LitElement {
protected render(): TemplateResult {
if (!this.addon) {
return html`<ha-circular-progress active></ha-circular-progress>`;
return html` <paper-spinner-lite active></paper-spinner-lite> `;
}
const addonTabs: PageNavigation[] = [
@@ -1,3 +1,4 @@
import "@polymer/paper-spinner/paper-spinner-lite";
import {
css,
CSSResult,
@@ -8,7 +9,6 @@ import {
TemplateResult,
} from "lit-element";
import { HassioAddonDetails } from "../../../../src/data/hassio/addon";
import "../../../../src/components/ha-circular-progress";
import { haStyle } from "../../../../src/resources/styles";
import { HomeAssistant } from "../../../../src/types";
import { hassioStyle } from "../../resources/hassio-style";
@@ -24,7 +24,7 @@ class HassioAddonInfoDashboard extends LitElement {
protected render(): TemplateResult {
if (!this.addon) {
return html`<ha-circular-progress active></ha-circular-progress>`;
return html` <paper-spinner-lite active></paper-spinner-lite> `;
}
return html`
+236 -359
View File
@@ -9,17 +9,18 @@ import {
mdiExclamationThick,
mdiFlask,
mdiHomeAssistant,
mdiInformation,
mdiKey,
mdiNetwork,
mdiPound,
mdiShield,
} from "@mdi/js";
import "@polymer/paper-tooltip/paper-tooltip";
import {
css,
CSSResult,
customElement,
html,
internalProperty,
LitElement,
property,
TemplateResult,
@@ -33,27 +34,19 @@ import "../../../../src/components/buttons/ha-progress-button";
import "../../../../src/components/ha-card";
import "../../../../src/components/ha-label-badge";
import "../../../../src/components/ha-markdown";
import "../../../../src/components/ha-settings-row";
import "../../../../src/components/ha-svg-icon";
import "../../../../src/components/ha-switch";
import {
fetchHassioAddonChangelog,
fetchHassioAddonInfo,
HassioAddonDetails,
HassioAddonSetOptionParams,
HassioAddonSetSecurityParams,
installHassioAddon,
setHassioAddonOption,
setHassioAddonSecurity,
startHassioAddon,
uninstallHassioAddon,
validateHassioAddonOption,
} from "../../../../src/data/hassio/addon";
import { extractApiErrorMessage } from "../../../../src/data/hassio/common";
import {
showAlertDialog,
showConfirmationDialog,
} from "../../../../src/dialogs/generic/show-dialog-box";
import { showConfirmationDialog } from "../../../../src/dialogs/generic/show-dialog-box";
import { haStyle } from "../../../../src/resources/styles";
import { HomeAssistant } from "../../../../src/types";
import "../../components/hassio-card-content";
@@ -131,7 +124,9 @@ class HassioAddonInfo extends LitElement {
@property({ attribute: false }) public addon!: HassioAddonDetails;
@internalProperty() private _error?: string;
@property() private _error?: string;
@property({ type: Boolean }) private _installing = false;
protected render(): TemplateResult {
return html`
@@ -246,23 +241,19 @@ class HassioAddonInfo extends LitElement {
`
: ""}
<div class="security">
${this.addon.stage !== "stable"
? html` <ha-label-badge
class=${classMap({
yellow: this.addon.stage === "experimental",
red: this.addon.stage === "deprecated",
})}
@click=${this._showMoreInfo}
id="stage"
label="stage"
description=""
>
<ha-svg-icon
.path=${STAGE_ICON[this.addon.stage]}
></ha-svg-icon>
</ha-label-badge>`
: ""}
<ha-label-badge
class=${classMap({
green: this.addon.stage === "stable",
yellow: this.addon.stage === "experimental",
red: this.addon.stage === "deprecated",
})}
@click=${this._showMoreInfo}
id="stage"
label="stage"
description=""
>
<ha-svg-icon .path=${STAGE_ICON[this.addon.stage]}></ha-svg-icon>
</ha-label-badge>
<ha-label-badge
class=${classMap({
green: [5, 6].includes(Number(this.addon.rating)),
@@ -390,94 +381,67 @@ class HassioAddonInfo extends LitElement {
${this.addon.version
? html`
<div class="addon-options">
<ha-settings-row ?three-line=${this.narrow}>
<span slot="heading">
Start on boot
</span>
<span slot="description">
Make the add-on start during a system boot
</span>
<ha-switch
@change=${this._startOnBootToggled}
.checked=${this.addon.boot === "auto"}
haptic
></ha-switch>
</ha-settings-row>
${this.addon.startup !== "once"
? html`
<ha-settings-row ?three-line=${this.narrow}>
<span slot="heading">
Watchdog
</span>
<span slot="description">
This will start the add-on if it crashes
</span>
<ha-switch
@change=${this._watchdogToggled}
.checked=${this.addon.watchdog}
haptic
></ha-switch>
</ha-settings-row>
`
: ""}
${this.addon.auto_update || this.hass.userData?.showAdvanced
? html`
<ha-settings-row ?three-line=${this.narrow}>
<span slot="heading">
Auto update
</span>
<span slot="description">
Auto update the add-on when there is a new version
available
</span>
<ha-switch
@change=${this._autoUpdateToggled}
.checked=${this.addon.auto_update}
haptic
></ha-switch>
</ha-settings-row>
`
: ""}
${this.addon.ingress
? html`
<ha-settings-row ?three-line=${this.narrow}>
<span slot="heading">
Show in sidebar
</span>
<span slot="description">
${this._computeCannotIngressSidebar
? "This option requires Home Assistant 0.92 or later."
: "Add this add-on to your sidebar"}
</span>
<ha-switch
@change=${this._panelToggled}
.checked=${this.addon.ingress_panel}
.disabled=${this._computeCannotIngressSidebar}
haptic
></ha-switch>
</ha-settings-row>
`
: ""}
${this._computeUsesProtectedOptions
? html`
<ha-settings-row ?three-line=${this.narrow}>
<span slot="heading">
Protection mode
</span>
<span slot="description">
Blocks elevated system access from the add-on
</span>
<ha-switch
@change=${this._protectionToggled}
.checked=${this.addon.protected}
haptic
></ha-switch>
</ha-settings-row>
`
: ""}
<div class="state">
<div>Start on boot</div>
<ha-switch
@change=${this._startOnBootToggled}
.checked=${this.addon.boot === "auto"}
haptic
></ha-switch>
</div>
${this.addon.auto_update || this.hass.userData?.showAdvanced
? html`
<div class="state">
<div>Auto update</div>
<ha-switch
@change=${this._autoUpdateToggled}
.checked=${this.addon.auto_update}
haptic
></ha-switch>
</div>
`
: ""}
${this.addon.ingress
? html`
<div class="state">
<div>Show in sidebar</div>
<ha-switch
@change=${this._panelToggled}
.checked=${this.addon.ingress_panel}
.disabled=${this._computeCannotIngressSidebar}
haptic
></ha-switch>
${this._computeCannotIngressSidebar
? html`
<span>
This option requires Home Assistant 0.92 or
later.
</span>
`
: ""}
</div>
`
: ""}
${this._computeUsesProtectedOptions
? html`
<div class="state">
<div>
Protection mode
<span>
<ha-svg-icon path=${mdiInformation}></ha-svg-icon>
<paper-tooltip>
Grant the add-on elevated system access.
</paper-tooltip>
</span>
</div>
<ha-switch
@change=${this._protectionToggled}
.checked=${this.addon.protected}
haptic
></ha-switch>
</div>
`
: ""}
`
: ""}
${this._error ? html` <div class="errors">${this._error}</div> ` : ""}
@@ -503,9 +467,12 @@ class HassioAddonInfo extends LitElement {
</ha-call-api-button>
`
: html`
<ha-progress-button @click=${this._startClicked}>
<ha-call-api-button
.hass=${this.hass}
.path="hassio/addons/${this.addon.slug}/start"
>
Start
</ha-progress-button>
</ha-call-api-button>
`}
${this._computeShowWebUI
? html`
@@ -529,12 +496,12 @@ class HassioAddonInfo extends LitElement {
</mwc-button>
`
: ""}
<ha-progress-button
<mwc-button
class=" right warning"
@click=${this._uninstallClicked}
>
Uninstall
</ha-progress-button>
</mwc-button>
${this.addon.build
? html`
<ha-call-api-button
@@ -556,7 +523,8 @@ class HassioAddonInfo extends LitElement {
`
: ""}
<ha-progress-button
.disabled=${!this.addon.available}
.disabled=${!this.addon.available || this._installing}
.progress=${this._installing}
@click=${this._installClicked}
>
Install
@@ -579,6 +547,137 @@ class HassioAddonInfo extends LitElement {
`;
}
static get styles(): CSSResult[] {
return [
haStyle,
hassioStyle,
css`
:host {
display: block;
}
ha-card {
display: block;
margin-bottom: 16px;
}
ha-card.warning {
background-color: var(--google-red-500);
color: white;
}
ha-card.warning .card-header {
color: white;
}
ha-card.warning .card-content {
color: white;
}
ha-card.warning mwc-button {
--mdc-theme-primary: white !important;
}
.warning {
color: var(--google-red-500);
--mdc-theme-primary: var(--google-red-500);
}
.light-color {
color: var(--secondary-text-color);
}
.addon-header {
padding-left: 8px;
font-size: 24px;
color: var(--ha-card-header-color, --primary-text-color);
}
.addon-version {
float: right;
font-size: 15px;
vertical-align: middle;
}
.errors {
color: var(--google-red-500);
margin-bottom: 16px;
}
.description {
margin-bottom: 16px;
}
img.logo {
max-height: 60px;
margin: 16px 0;
display: block;
}
.state {
display: flex;
margin: 33px 0;
}
.state div {
width: 180px;
display: inline-block;
}
.state ha-svg-icon {
width: 16px;
height: 16px;
color: var(--secondary-text-color);
}
ha-switch {
display: flex;
}
ha-svg-icon.running {
color: var(--paper-green-400);
}
ha-svg-icon.stopped {
color: var(--google-red-300);
}
ha-call-api-button {
font-weight: 500;
color: var(--primary-color);
}
.right {
float: right;
}
protection-enable mwc-button {
--mdc-theme-primary: white;
}
.description a {
color: var(--primary-color);
}
.red {
--ha-label-badge-color: var(--label-badge-red, #df4c1e);
}
.blue {
--ha-label-badge-color: var(--label-badge-blue, #039be5);
}
.green {
--ha-label-badge-color: var(--label-badge-green, #0da035);
}
.yellow {
--ha-label-badge-color: var(--label-badge-yellow, #f4b400);
}
.security {
margin-bottom: 16px;
}
.card-actions {
display: flow-root;
}
.security h3 {
margin-bottom: 8px;
font-weight: normal;
}
.security ha-label-badge {
cursor: pointer;
margin-right: 4px;
--ha-label-badge-padding: 8px 0 0 0;
}
.changelog {
display: contents;
}
.changelog-link {
color: var(--primary-color);
text-decoration: underline;
cursor: pointer;
}
ha-markdown {
padding: 16px;
}
`,
];
}
private get _computeHassioApi(): boolean {
return (
this.addon.hassio_api &&
@@ -663,29 +762,7 @@ class HassioAddonInfo extends LitElement {
};
fireEvent(this, "hass-api-called", eventdata);
} catch (err) {
this._error = `Failed to set addon option, ${extractApiErrorMessage(
err
)}`;
}
}
private async _watchdogToggled(): Promise<void> {
this._error = undefined;
const data: HassioAddonSetOptionParams = {
watchdog: !this.addon.watchdog,
};
try {
await setHassioAddonOption(this.hass, this.addon.slug, data);
const eventdata = {
success: true,
response: undefined,
path: "option",
};
fireEvent(this, "hass-api-called", eventdata);
} catch (err) {
this._error = `Failed to set addon option, ${extractApiErrorMessage(
err
)}`;
this._error = `Failed to set addon option, ${err.body?.message || err}`;
}
}
@@ -703,9 +780,7 @@ class HassioAddonInfo extends LitElement {
};
fireEvent(this, "hass-api-called", eventdata);
} catch (err) {
this._error = `Failed to set addon option, ${extractApiErrorMessage(
err
)}`;
this._error = `Failed to set addon option, ${err.body?.message || err}`;
}
}
@@ -723,9 +798,9 @@ class HassioAddonInfo extends LitElement {
};
fireEvent(this, "hass-api-called", eventdata);
} catch (err) {
this._error = `Failed to set addon security option, ${extractApiErrorMessage(
err
)}`;
this._error = `Failed to set addon security option, ${
err.body?.message || err
}`;
}
}
@@ -743,13 +818,12 @@ class HassioAddonInfo extends LitElement {
};
fireEvent(this, "hass-api-called", eventdata);
} catch (err) {
this._error = `Failed to set addon option, ${extractApiErrorMessage(
err
)}`;
this._error = `Failed to set addon option, ${err.body?.message || err}`;
}
}
private async _openChangelog(): Promise<void> {
this._error = undefined;
try {
const content = await fetchHassioAddonChangelog(
this.hass,
@@ -760,17 +834,15 @@ class HassioAddonInfo extends LitElement {
content,
});
} catch (err) {
showAlertDialog(this, {
title: "Failed to get addon changelog",
text: extractApiErrorMessage(err),
});
this._error = `Failed to get addon changelog, ${
err.body?.message || err
}`;
}
}
private async _installClicked(ev: CustomEvent): Promise<void> {
const button = ev.currentTarget as any;
button.progress = true;
private async _installClicked(): Promise<void> {
this._error = undefined;
this._installing = true;
try {
await installHassioAddon(this.hass, this.addon.slug);
const eventdata = {
@@ -780,62 +852,12 @@ class HassioAddonInfo extends LitElement {
};
fireEvent(this, "hass-api-called", eventdata);
} catch (err) {
showAlertDialog(this, {
title: "Failed to install addon",
text: extractApiErrorMessage(err),
});
this._error = `Failed to install addon, ${err.body?.message || err}`;
}
button.progress = false;
this._installing = false;
}
private async _startClicked(ev: CustomEvent): Promise<void> {
const button = ev.currentTarget as any;
button.progress = true;
try {
const validate = await validateHassioAddonOption(
this.hass,
this.addon.slug
);
if (!validate.data.valid) {
await showConfirmationDialog(this, {
title: "Failed to start addon - configruation validation faled!",
text: validate.data.message.split(" Got ")[0],
confirm: () => this._openConfiguration(),
confirmText: "Go to configruation",
dismissText: "Cancel",
});
button.progress = false;
return;
}
} catch (err) {
showAlertDialog(this, {
title: "Failed to validate addon configuration",
text: extractApiErrorMessage(err),
});
button.progress = false;
return;
}
try {
await startHassioAddon(this.hass, this.addon.slug);
this.addon = await fetchHassioAddonInfo(this.hass, this.addon.slug);
} catch (err) {
showAlertDialog(this, {
title: "Failed to start addon",
text: extractApiErrorMessage(err),
});
}
button.progress = false;
}
private _openConfiguration(): void {
navigate(this, `/hassio/addon/${this.addon.slug}/config`);
}
private async _uninstallClicked(ev: CustomEvent): Promise<void> {
const button = ev.currentTarget as any;
button.progress = true;
private async _uninstallClicked(): Promise<void> {
const confirmed = await showConfirmationDialog(this, {
title: this.addon.name,
text: "Are you sure you want to uninstall this add-on?",
@@ -844,7 +866,6 @@ class HassioAddonInfo extends LitElement {
});
if (!confirmed) {
button.progress = false;
return;
}
@@ -858,152 +879,8 @@ class HassioAddonInfo extends LitElement {
};
fireEvent(this, "hass-api-called", eventdata);
} catch (err) {
showAlertDialog(this, {
title: "Failed to uninstall addon",
text: extractApiErrorMessage(err),
});
this._error = `Failed to uninstall addon, ${err.body?.message || err}`;
}
button.progress = false;
}
static get styles(): CSSResult[] {
return [
haStyle,
hassioStyle,
css`
:host {
display: block;
}
ha-card {
display: block;
margin-bottom: 16px;
}
ha-card.warning {
background-color: var(--error-color);
color: white;
}
ha-card.warning .card-header {
color: white;
}
ha-card.warning .card-content {
color: white;
}
ha-card.warning mwc-button {
--mdc-theme-primary: white !important;
}
.warning {
color: var(--error-color);
--mdc-theme-primary: var(--error-color);
}
.light-color {
color: var(--secondary-text-color);
}
.addon-header {
padding-left: 8px;
font-size: 24px;
color: var(--ha-card-header-color, --primary-text-color);
}
.addon-version {
float: right;
font-size: 15px;
vertical-align: middle;
}
.errors {
color: var(--error-color);
margin-bottom: 16px;
}
.description {
margin-bottom: 16px;
}
img.logo {
max-height: 60px;
margin: 16px 0;
display: block;
}
ha-switch {
display: flex;
}
ha-svg-icon.running {
color: var(--paper-green-400);
}
ha-svg-icon.stopped {
color: var(--google-red-300);
}
ha-call-api-button {
font-weight: 500;
color: var(--primary-color);
}
.right {
float: right;
}
protection-enable mwc-button {
--mdc-theme-primary: white;
}
.description a {
color: var(--primary-color);
}
.red {
--ha-label-badge-color: var(--label-badge-red, #df4c1e);
}
.blue {
--ha-label-badge-color: var(--label-badge-blue, #039be5);
}
.green {
--ha-label-badge-color: var(--label-badge-green, #0da035);
}
.yellow {
--ha-label-badge-color: var(--label-badge-yellow, #f4b400);
}
.security {
margin-bottom: 16px;
}
.card-actions {
display: flow-root;
}
.security h3 {
margin-bottom: 8px;
font-weight: normal;
}
.security ha-label-badge {
cursor: pointer;
margin-right: 4px;
--ha-label-badge-padding: 8px 0 0 0;
}
.changelog {
display: contents;
}
.changelog-link {
color: var(--primary-color);
text-decoration: underline;
cursor: pointer;
}
ha-markdown {
padding: 16px;
}
ha-settings-row {
padding: 0;
height: 54px;
width: 100%;
}
ha-settings-row > span[slot="description"] {
white-space: normal;
color: var(--secondary-text-color);
}
ha-settings-row[three-line] {
height: 74px;
}
.addon-options {
max-width: 50%;
}
@media (max-width: 720px) {
.addon-options {
max-width: 100%;
}
}
`,
];
}
}
declare global {
@@ -1,3 +1,4 @@
import "@polymer/paper-spinner/paper-spinner-lite";
import {
css,
CSSResult,
@@ -8,7 +9,6 @@ import {
TemplateResult,
} from "lit-element";
import { HassioAddonDetails } from "../../../../src/data/hassio/addon";
import "../../../../src/components/ha-circular-progress";
import { haStyle } from "../../../../src/resources/styles";
import { HomeAssistant } from "../../../../src/types";
import { hassioStyle } from "../../resources/hassio-style";
@@ -22,7 +22,7 @@ class HassioAddonLogDashboard extends LitElement {
protected render(): TemplateResult {
if (!this.addon) {
return html` <ha-circular-progress active></ha-circular-progress> `;
return html` <paper-spinner-lite active></paper-spinner-lite> `;
}
return html`
<div class="content">
@@ -4,7 +4,6 @@ import {
CSSResult,
customElement,
html,
internalProperty,
LitElement,
property,
TemplateResult,
@@ -14,7 +13,6 @@ import {
fetchHassioAddonLogs,
HassioAddonDetails,
} from "../../../../src/data/hassio/addon";
import { extractApiErrorMessage } from "../../../../src/data/hassio/common";
import { haStyle } from "../../../../src/resources/styles";
import { HomeAssistant } from "../../../../src/types";
import "../../components/hassio-ansi-to-html";
@@ -26,9 +24,9 @@ class HassioAddonLogs extends LitElement {
@property({ attribute: false }) public addon!: HassioAddonDetails;
@internalProperty() private _error?: string;
@property() private _error?: string;
@internalProperty() private _content?: string;
@property() private _content?: string;
public async connectedCallback(): Promise<void> {
super.connectedCallback();
@@ -64,7 +62,7 @@ class HassioAddonLogs extends LitElement {
display: block;
}
.errors {
color: var(--error-color);
color: var(--google-red-500);
margin-bottom: 16px;
}
`,
@@ -76,7 +74,7 @@ class HassioAddonLogs extends LitElement {
try {
this._content = await fetchHassioAddonLogs(this.hass, this.addon.slug);
} catch (err) {
this._error = `Failed to get addon logs, ${extractApiErrorMessage(err)}`;
this._error = `Failed to get addon logs, ${err.body?.message || err}`;
}
}
+1 -1
View File
@@ -21,7 +21,7 @@ interface State {
class HassioAnsiToHtml extends LitElement {
@property() public content!: string;
protected render(): TemplateResult | void {
public render(): TemplateResult | void {
return html`${this._parseTextToColoredPre(this.content)}`;
}
+2 -2
View File
@@ -14,7 +14,7 @@ import { HomeAssistant } from "../../../src/types";
@customElement("hassio-card-content")
class HassioCardContent extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property() public hass!: HomeAssistant;
@property() public title!: string;
@@ -98,7 +98,7 @@ class HassioCardContent extends LitElement {
color: var(--paper-item-icon-color);
}
ha-svg-icon.not_available {
color: var(--error-color);
color: var(--google-red-500);
}
.title {
color: var(--primary-text-color);
+1 -1
View File
@@ -19,7 +19,7 @@ import { hassioStyle } from "../resources/hassio-style";
@customElement("hassio-addons")
class HassioAddons extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property() public hass!: HomeAssistant;
@property() public addons?: HassioAddonInfo[];
+1 -1
View File
@@ -15,7 +15,7 @@ import {
import "../../../src/layouts/hass-tabs-subpage";
import { haStyle } from "../../../src/resources/styles";
import { HomeAssistant, Route } from "../../../src/types";
import { supervisorTabs } from "../hassio-tabs";
import { supervisorTabs } from "../hassio-panel";
import "./hassio-addons";
import "./hassio-update";
+21 -46
View File
@@ -5,43 +5,33 @@ import {
CSSResult,
customElement,
html,
internalProperty,
LitElement,
property,
TemplateResult,
} from "lit-element";
import "../../../src/components/buttons/ha-progress-button";
import "../../../src/components/buttons/ha-call-api-button";
import "../../../src/components/ha-card";
import "../../../src/components/ha-svg-icon";
import {
extractApiErrorMessage,
HassioResponse,
ignoredStatusCodes,
} from "../../../src/data/hassio/common";
import { HassioHassOSInfo } from "../../../src/data/hassio/host";
import {
HassioHomeAssistantInfo,
HassioSupervisorInfo,
} from "../../../src/data/hassio/supervisor";
import {
showAlertDialog,
showConfirmationDialog,
} from "../../../src/dialogs/generic/show-dialog-box";
import { haStyle } from "../../../src/resources/styles";
import { HomeAssistant } from "../../../src/types";
import { hassioStyle } from "../resources/hassio-style";
@customElement("hassio-update")
export class HassioUpdate extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property() public hass!: HomeAssistant;
@property({ attribute: false }) public hassInfo: HassioHomeAssistantInfo;
@property() public hassInfo: HassioHomeAssistantInfo;
@property({ attribute: false }) public hassOsInfo?: HassioHassOSInfo;
@property() public hassOsInfo?: HassioHassOSInfo;
@property() public supervisorInfo: HassioSupervisorInfo;
@internalProperty() private _error?: string;
@property() private _error?: string;
protected render(): TemplateResult {
const updatesAvailable: number = [
@@ -135,46 +125,31 @@ export class HassioUpdate extends LitElement {
<a href="${releaseNotesUrl}" target="_blank" rel="noreferrer">
<mwc-button>Release notes</mwc-button>
</a>
<ha-progress-button
.apiPath=${apiPath}
.name=${name}
.version=${lastVersion}
@click=${this._confirmUpdate}
<ha-call-api-button
.hass=${this.hass}
.path=${apiPath}
@hass-api-called=${this._apiCalled}
>
Update
</ha-progress-button>
</ha-call-api-button>
</div>
</ha-card>
`;
}
private async _confirmUpdate(ev): Promise<void> {
const item = ev.currentTarget;
item.progress = true;
const confirmed = await showConfirmationDialog(this, {
title: `Update ${item.name}`,
text: `Are you sure you want to update ${item.name} to version ${item.version}?`,
confirmText: "update",
dismissText: "cancel",
});
if (!confirmed) {
item.progress = false;
private _apiCalled(ev): void {
if (ev.detail.success) {
this._error = "";
return;
}
try {
await this.hass.callApi<HassioResponse<void>>("POST", item.apiPath);
} catch (err) {
// Only show an error if the status code was not expected (user behind proxy)
// or no status at all(connection terminated)
if (err.status_code && !ignoredStatusCodes.has(err.status_code)) {
showAlertDialog(this, {
title: "Update failed",
text: extractApiErrorMessage(err),
});
}
const response = ev.detail.response;
if (typeof response.body === "object") {
this._error = response.body.message || "Unknown error";
} else {
this._error = response.body;
}
item.progress = false;
}
static get styles(): CSSResult[] {
@@ -205,7 +180,7 @@ export class HassioUpdate extends LitElement {
text-align: right;
}
.errors {
color: var(--error-color);
color: var(--google-red-500);
padding: 16px;
}
a {
@@ -5,7 +5,6 @@ import {
html,
LitElement,
property,
internalProperty,
TemplateResult,
} from "lit-element";
import { createCloseHeading } from "../../../../src/components/ha-dialog";
@@ -17,13 +16,13 @@ import { HassioMarkdownDialogParams } from "./show-dialog-hassio-markdown";
@customElement("dialog-hassio-markdown")
class HassioMarkdownDialog extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property() public hass!: HomeAssistant;
@property() public title!: string;
@property() public content!: string;
@internalProperty() private _opened = false;
@property() private _opened = false;
public showDialog(params: HassioMarkdownDialogParams) {
this.title = params.title;
@@ -31,10 +30,6 @@ class HassioMarkdownDialog extends LitElement {
this._opened = true;
}
public closeDialog() {
this._opened = false;
}
protected render(): TemplateResult {
if (!this._opened) {
return html``;
@@ -42,7 +37,7 @@ class HassioMarkdownDialog extends LitElement {
return html`
<ha-dialog
open
@closed=${this.closeDialog}
@closing=${this._closeDialog}
.heading=${createCloseHeading(this.hass, this.title)}
>
<ha-markdown .content=${this.content || ""}></ha-markdown>
@@ -50,6 +45,10 @@ class HassioMarkdownDialog extends LitElement {
`;
}
private _closeDialog(): void {
this._opened = false;
}
static get styles(): CSSResult[] {
return [
haStyleDialog,
@@ -1,333 +0,0 @@
import "@material/mwc-button/mwc-button";
import "@material/mwc-icon-button";
import "@material/mwc-tab";
import "@material/mwc-tab-bar";
import { mdiClose } from "@mdi/js";
import { PaperInputElement } from "@polymer/paper-input/paper-input";
import {
css,
CSSResult,
customElement,
html,
internalProperty,
LitElement,
property,
TemplateResult,
} from "lit-element";
import { cache } from "lit-html/directives/cache";
import { fireEvent } from "../../../../src/common/dom/fire_event";
import "../../../../src/components/ha-circular-progress";
import "../../../../src/components/ha-dialog";
import "../../../../src/components/ha-formfield";
import "../../../../src/components/ha-header-bar";
import "../../../../src/components/ha-radio";
import type { HaRadio } from "../../../../src/components/ha-radio";
import "../../../../src/components/ha-related-items";
import "../../../../src/components/ha-svg-icon";
import { extractApiErrorMessage } from "../../../../src/data/hassio/common";
import {
NetworkInterface,
updateNetworkInterface,
} from "../../../../src/data/hassio/network";
import {
showAlertDialog,
showConfirmationDialog,
} from "../../../../src/dialogs/generic/show-dialog-box";
import { HassDialog } from "../../../../src/dialogs/make-dialog-manager";
import { haStyleDialog } from "../../../../src/resources/styles";
import type { HomeAssistant } from "../../../../src/types";
import { HassioNetworkDialogParams } from "./show-dialog-network";
@customElement("dialog-hassio-network")
export class DialogHassioNetwork extends LitElement implements HassDialog {
@property({ attribute: false }) public hass!: HomeAssistant;
@internalProperty() private _prosessing = false;
@internalProperty() private _params?: HassioNetworkDialogParams;
@internalProperty() private _network!: {
interface: string;
data: NetworkInterface;
}[];
@internalProperty() private _curTabIndex = 0;
@internalProperty() private _device?: {
interface: string;
data: NetworkInterface;
};
@internalProperty() private _dirty = false;
public async showDialog(params: HassioNetworkDialogParams): Promise<void> {
this._params = params;
this._dirty = false;
this._curTabIndex = 0;
this._network = Object.keys(params.network?.interfaces)
.map((device) => ({
interface: device,
data: params.network.interfaces[device],
}))
.sort((a, b) => {
return a.data.primary > b.data.primary ? -1 : 1;
});
this._device = this._network[this._curTabIndex];
this._device.data.nameservers = String(this._device.data.nameservers);
await this.updateComplete;
}
public closeDialog(): void {
this._params = undefined;
this._prosessing = false;
fireEvent(this, "dialog-closed", { dialog: this.localName });
}
protected render(): TemplateResult {
if (!this._params || !this._network) {
return html``;
}
return html`
<ha-dialog
open
scrimClickAction
escapeKeyAction
.heading=${true}
hideActions
@closed=${this.closeDialog}
>
<div slot="heading">
<ha-header-bar>
<span slot="title">
Network settings
</span>
<mwc-icon-button slot="actionItems" dialogAction="cancel">
<ha-svg-icon .path=${mdiClose}></ha-svg-icon>
</mwc-icon-button>
</ha-header-bar>
${this._network.length > 1
? html` <mwc-tab-bar
.activeIndex=${this._curTabIndex}
@MDCTabBar:activated=${this._handleTabActivated}
>${this._network.map(
(device) =>
html`<mwc-tab
.id=${device.interface}
.label=${device.interface}
>
</mwc-tab>`
)}
</mwc-tab-bar>`
: ""}
</div>
${cache(this._renderTab())}
</ha-dialog>
`;
}
private _renderTab() {
return html` <div class="form container">
<ha-formfield label="DHCP">
<ha-radio
@change=${this._handleRadioValueChanged}
value="dhcp"
name="method"
?checked=${this._device!.data.method === "dhcp"}
>
</ha-radio>
</ha-formfield>
<ha-formfield label="Static">
<ha-radio
@change=${this._handleRadioValueChanged}
value="static"
name="method"
?checked=${this._device!.data.method === "static"}
>
</ha-radio>
</ha-formfield>
${this._device!.data.method !== "dhcp"
? html` <paper-input
class="flex-auto"
id="ip_address"
label="IP address/Netmask"
.value="${this._device!.data.ip_address}"
@value-changed=${this._handleInputValueChanged}
></paper-input>
<paper-input
class="flex-auto"
id="gateway"
label="Gateway address"
.value="${this._device!.data.gateway}"
@value-changed=${this._handleInputValueChanged}
></paper-input>
<paper-input
class="flex-auto"
id="nameservers"
label="DNS servers"
.value="${this._device!.data.nameservers as string}"
@value-changed=${this._handleInputValueChanged}
></paper-input>
NB!: If you are changing IP or gateway addresses, you might lose
the connection.`
: ""}
</div>
<div class="buttons">
<mwc-button label="close" @click=${this.closeDialog}> </mwc-button>
<mwc-button @click=${this._updateNetwork} ?disabled=${!this._dirty}>
${this._prosessing
? html`<ha-circular-progress active></ha-circular-progress>`
: "Update"}
</mwc-button>
</div>`;
}
private async _updateNetwork() {
this._prosessing = true;
let options: Partial<NetworkInterface> = {
method: this._device!.data.method,
};
if (options.method !== "dhcp") {
options = {
...options,
address: this._device!.data.ip_address,
gateway: this._device!.data.gateway,
dns: String(this._device!.data.nameservers).split(","),
};
}
try {
await updateNetworkInterface(this.hass, this._device!.interface, options);
} catch (err) {
showAlertDialog(this, {
title: "Failed to change network settings",
text: extractApiErrorMessage(err),
});
this._prosessing = false;
return;
}
this._params?.loadData();
this.closeDialog();
}
private async _handleTabActivated(ev: CustomEvent): Promise<void> {
if (this._dirty) {
const confirm = await showConfirmationDialog(this, {
text:
"You have unsaved changes, these will get lost if you change tabs, do you want to continue?",
confirmText: "yes",
dismissText: "no",
});
if (!confirm) {
this.requestUpdate("_device");
return;
}
}
this._curTabIndex = ev.detail.index;
this._device = this._network[ev.detail.index];
this._device.data.nameservers = String(this._device.data.nameservers);
}
private _handleRadioValueChanged(ev: CustomEvent): void {
const value = (ev.target as HaRadio).value as "dhcp" | "static";
if (!value || !this._device || this._device!.data.method === value) {
return;
}
this._dirty = true;
this._device!.data.method = value;
this.requestUpdate("_device");
}
private _handleInputValueChanged(ev: CustomEvent): void {
const value: string | null | undefined = (ev.target as PaperInputElement)
.value;
const id = (ev.target as PaperInputElement).id;
if (!value || !this._device || this._device.data[id] === value) {
return;
}
this._dirty = true;
this._device.data[id] = value;
}
static get styles(): CSSResult[] {
return [
haStyleDialog,
css`
ha-header-bar {
--mdc-theme-on-primary: var(--primary-text-color);
--mdc-theme-primary: var(--mdc-theme-surface);
flex-shrink: 0;
}
mwc-tab-bar {
border-bottom: 1px solid
var(--mdc-dialog-scroll-divider-color, rgba(0, 0, 0, 0.12));
}
ha-dialog {
--dialog-content-position: static;
--dialog-content-padding: 0;
--dialog-z-index: 6;
}
@media all and (min-width: 451px) and (min-height: 501px) {
.container {
width: 400px;
}
}
.content {
display: block;
padding: 20px 24px;
}
/* overrule the ha-style-dialog max-height on small screens */
@media all and (max-width: 450px), all and (max-height: 500px) {
ha-header-bar {
--mdc-theme-primary: var(--app-header-background-color);
--mdc-theme-on-primary: var(--app-header-text-color, white);
}
}
mwc-button.warning {
--mdc-theme-primary: var(--error-color);
}
:host([rtl]) app-toolbar {
direction: rtl;
text-align: right;
}
.container {
padding: 20px 24px;
}
.form {
margin-bottom: 53px;
}
.buttons {
position: absolute;
bottom: 0;
width: 100%;
box-sizing: border-box;
border-top: 1px solid
var(--mdc-dialog-scroll-divider-color, rgba(0, 0, 0, 0.12));
display: flex;
justify-content: space-between;
padding: 8px;
padding-bottom: max(env(safe-area-inset-bottom), 8px);
background-color: var(--mdc-theme-surface, #fff);
}
`,
];
}
}
declare global {
interface HTMLElementTagNameMap {
"dialog-hassio-network": DialogHassioNetwork;
}
}
@@ -1,22 +0,0 @@
import { fireEvent } from "../../../../src/common/dom/fire_event";
import { NetworkInfo } from "../../../../src/data/hassio/network";
import "./dialog-hassio-network";
export interface HassioNetworkDialogParams {
network: NetworkInfo;
loadData: () => Promise<void>;
}
export const showNetworkDialog = (
element: HTMLElement,
dialogParams: HassioNetworkDialogParams
): void => {
fireEvent(element, "show-dialog", {
dialogTag: "dialog-hassio-network",
dialogImport: () =>
import(
/* webpackChunkName: "dialog-hassio-network" */ "./dialog-hassio-network"
),
dialogParams,
});
};
@@ -5,26 +5,24 @@ import "@polymer/paper-input/paper-input";
import type { PaperInputElement } from "@polymer/paper-input/paper-input";
import "@polymer/paper-item/paper-item";
import "@polymer/paper-item/paper-item-body";
import "@polymer/paper-spinner/paper-spinner";
import {
css,
CSSResult,
customElement,
html,
internalProperty,
LitElement,
property,
query,
TemplateResult,
} from "lit-element";
import memoizeOne from "memoize-one";
import "../../../../src/components/ha-circular-progress";
import "../../../../src/components/ha-dialog";
import "../../../../src/components/ha-svg-icon";
import {
fetchHassioAddonsInfo,
HassioAddonRepository,
} from "../../../../src/data/hassio/addon";
import { extractApiErrorMessage } from "../../../../src/data/hassio/common";
import { setSupervisorOption } from "../../../../src/data/hassio/supervisor";
import { haStyle, haStyleDialog } from "../../../../src/resources/styles";
import type { HomeAssistant } from "../../../../src/types";
@@ -41,11 +39,11 @@ class HassioRepositoriesDialog extends LitElement {
@query("#repository_input") private _optionInput?: PaperInputElement;
@internalProperty() private _opened = false;
@property() private _opened = false;
@internalProperty() private _prosessing = false;
@property() private _prosessing = false;
@internalProperty() private _error?: string;
@property() private _error?: string;
public async showDialog(_dialogParams: any): Promise<void> {
this._dialogParams = _dialogParams;
@@ -110,7 +108,7 @@ class HassioRepositoriesDialog extends LitElement {
></paper-input>
<mwc-button @click=${this._addRepository}>
${this._prosessing
? html`<ha-circular-progress active></ha-circular-progress>`
? html`<paper-spinner active></paper-spinner>`
: "Add"}
</mwc-button>
</div>
@@ -191,7 +189,7 @@ class HassioRepositoriesDialog extends LitElement {
input.value = "";
} catch (err) {
this._error = extractApiErrorMessage(err);
this._error = err.message;
}
this._prosessing = false;
}
@@ -223,7 +221,7 @@ class HassioRepositoriesDialog extends LitElement {
await this._dialogParams!.loadData();
} catch (err) {
this._error = extractApiErrorMessage(err);
this._error = err.message;
}
}
}
@@ -7,7 +7,6 @@ import {
CSSResult,
customElement,
html,
internalProperty,
LitElement,
property,
TemplateResult,
@@ -15,12 +14,10 @@ import {
import { createCloseHeading } from "../../../../src/components/ha-dialog";
import "../../../../src/components/ha-svg-icon";
import { getSignedPath } from "../../../../src/data/auth";
import { extractApiErrorMessage } from "../../../../src/data/hassio/common";
import {
fetchHassioSnapshotInfo,
HassioSnapshotDetail,
} from "../../../../src/data/hassio/snapshot";
import { showConfirmationDialog } from "../../../../src/dialogs/generic/show-dialog-box";
import { PolymerChangedEvent } from "../../../../src/polymer-types";
import { haStyleDialog } from "../../../../src/resources/styles";
import { HomeAssistant } from "../../../../src/types";
@@ -71,21 +68,21 @@ interface FolderItem {
@customElement("dialog-hassio-snapshot")
class HassioSnapshotDialog extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property() public hass!: HomeAssistant;
@internalProperty() private _error?: string;
@property() private _error?: string;
@internalProperty() private _snapshot?: HassioSnapshotDetail;
@property() private _snapshot?: HassioSnapshotDetail;
@internalProperty() private _folders!: FolderItem[];
@property() private _folders!: FolderItem[];
@internalProperty() private _addons!: AddonItem[];
@property() private _addons!: AddonItem[];
@internalProperty() private _dialogParams?: HassioSnapshotDialogParams;
@property() private _dialogParams?: HassioSnapshotDialogParams;
@internalProperty() private _snapshotPassword!: string;
@property() private _snapshotPassword!: string;
@internalProperty() private _restoreHass: boolean | null | undefined = true;
@property() private _restoreHass: boolean | null | undefined = true;
public async showDialog(params: HassioSnapshotDialogParams) {
this._snapshot = await fetchHassioSnapshotInfo(this.hass, params.slug);
@@ -227,7 +224,7 @@ class HassioSnapshotDialog extends LitElement {
}
.warning,
.error {
color: var(--error-color);
color: var(--google-red-500);
}
.buttons {
display: flex;
@@ -268,12 +265,8 @@ class HassioSnapshotDialog extends LitElement {
this._snapshotPassword = ev.detail.value;
}
private async _partialRestoreClicked() {
if (
!(await showConfirmationDialog(this, {
title: "Are you sure you want partially to restore this snapshot?",
}))
) {
private _partialRestoreClicked() {
if (!confirm("Are you sure you want to restore this snapshot?")) {
return;
}
@@ -318,13 +311,8 @@ class HassioSnapshotDialog extends LitElement {
);
}
private async _fullRestoreClicked() {
if (
!(await showConfirmationDialog(this, {
title:
"Are you sure you want to wipe your system and restore this snapshot?",
}))
) {
private _fullRestoreClicked() {
if (!confirm("Are you sure you want to restore this snapshot?")) {
return;
}
@@ -349,12 +337,8 @@ class HassioSnapshotDialog extends LitElement {
);
}
private async _deleteClicked() {
if (
!(await showConfirmationDialog(this, {
title: "Are you sure you want to delete this snapshot?",
}))
) {
private _deleteClicked() {
if (!confirm("Are you sure you want to delete this snapshot?")) {
return;
}
@@ -380,7 +364,7 @@ class HassioSnapshotDialog extends LitElement {
`/api/hassio/snapshots/${this._snapshot!.slug}/download`
);
} catch (err) {
alert(`Error: ${extractApiErrorMessage(err)}`);
alert(`Error: ${err.message}`);
return;
}
+1 -2
View File
@@ -3,7 +3,6 @@ import {
HassioAddonDetails,
restartHassioAddon,
} from "../../../src/data/hassio/addon";
import { extractApiErrorMessage } from "../../../src/data/hassio/common";
import {
showAlertDialog,
showConfirmationDialog,
@@ -27,7 +26,7 @@ export const suggestAddonRestart = async (
} catch (err) {
showAlertDialog(element, {
title: "Failed to restart",
text: extractApiErrorMessage(err),
text: err.body.message,
});
}
}
+2 -2
View File
@@ -1,5 +1,5 @@
import "../../src/resources/compatibility";
import "../../src/resources/roboto";
import "~app/resources/compatibility";
import "~app/resources/roboto";
import "./hassio-main";
const styleEl = document.createElement("style");
+230 -63
View File
@@ -1,35 +1,111 @@
import {
html,
PropertyValues,
customElement,
LitElement,
property,
} from "lit-element";
import "./hassio-router";
import { urlSyncMixin } from "../../src/state/url-sync-mixin";
import { ProvideHassLitMixin } from "../../src/mixins/provide-hass-lit-mixin";
import { HomeAssistant, Route } from "../../src/types";
import { HassioPanelInfo } from "../../src/data/hassio/supervisor";
import { PolymerElement } from "@polymer/polymer";
import { customElement, property, PropertyValues } from "lit-element";
import { applyThemesOnElement } from "../../src/common/dom/apply_themes_on_element";
import { fireEvent } from "../../src/common/dom/fire_event";
import { navigate } from "../../src/common/navigate";
import { fetchHassioAddonInfo } from "../../src/data/hassio/addon";
import {
fetchHassioHassOsInfo,
fetchHassioHostInfo,
HassioHassOSInfo,
HassioHostInfo,
} from "../../src/data/hassio/host";
import {
createHassioSession,
fetchHassioHomeAssistantInfo,
fetchHassioSupervisorInfo,
fetchHassioInfo,
HassioHomeAssistantInfo,
HassioInfo,
HassioPanelInfo,
HassioSupervisorInfo,
} from "../../src/data/hassio/supervisor";
import {
AlertDialogParams,
showAlertDialog,
} from "../../src/dialogs/generic/show-dialog-box";
import { makeDialogManager } from "../../src/dialogs/make-dialog-manager";
import { atLeastVersion } from "../../src/common/config/version";
import {
HassRouterPage,
RouterOptions,
} from "../../src/layouts/hass-router-page";
import { ProvideHassLitMixin } from "../../src/mixins/provide-hass-lit-mixin";
import "../../src/resources/ha-style";
import { HomeAssistant } from "../../src/types";
// Don't codesplit it, that way the dashboard always loads fast.
import "./hassio-panel";
@customElement("hassio-main")
export class HassioMain extends urlSyncMixin(ProvideHassLitMixin(LitElement)) {
@property({ attribute: false }) public hass!: HomeAssistant;
class HassioMain extends ProvideHassLitMixin(HassRouterPage) {
@property() public hass!: HomeAssistant;
@property() public panel!: HassioPanelInfo;
@property() public narrow!: boolean;
@property() public route?: Route;
protected routerOptions: RouterOptions = {
// Hass.io has a page with tabs, so we route all non-matching routes to it.
defaultPage: "dashboard",
initialLoad: () => this._fetchData(),
showLoading: true,
routes: {
dashboard: {
tag: "hassio-panel",
cache: true,
},
snapshots: "dashboard",
store: "dashboard",
system: "dashboard",
addon: {
tag: "hassio-addon-dashboard",
load: () =>
import(
/* webpackChunkName: "hassio-addon-dashboard" */ "./addon-view/hassio-addon-dashboard"
),
},
ingress: {
tag: "hassio-ingress-view",
load: () =>
import(
/* webpackChunkName: "hassio-ingress-view" */ "./ingress-view/hassio-ingress-view"
),
},
},
};
@property() private _supervisorInfo: HassioSupervisorInfo;
@property() private _hostInfo: HassioHostInfo;
@property() private _hassioInfo?: HassioInfo;
@property() private _hassOsInfo?: HassioHassOSInfo;
@property() private _hassInfo: HassioHomeAssistantInfo;
protected firstUpdated(changedProps: PropertyValues) {
super.firstUpdated(changedProps);
this._applyTheme();
applyThemesOnElement(
this.parentElement,
this.hass.themes,
this.hass.selectedTheme || this.hass.themes.default_theme
);
this.style.setProperty(
"--app-header-background-color",
"var(--sidebar-background-color)"
);
this.style.setProperty(
"--app-header-text-color",
"var(--sidebar-text-color)"
);
this.style.setProperty(
"--app-header-border-bottom",
"1px solid var(--divider-color)"
);
this.addEventListener("hass-api-called", (ev) => this._apiCalled(ev));
// Paulus - March 17, 2019
// We went to a single hass-toggle-menu event in HA 0.90. However, the
// supervisor UI can also run under older versions of Home Assistant.
@@ -62,61 +138,152 @@ export class HassioMain extends urlSyncMixin(ProvideHassLitMixin(LitElement)) {
});
});
makeDialogManager(this, this.shadowRoot!);
makeDialogManager(this, document.body);
}
protected updated(changedProps: PropertyValues) {
super.updated(changedProps);
const oldHass = changedProps.get("hass") as HomeAssistant | undefined;
if (!oldHass) {
protected updatePageEl(el) {
// the tabs page does its own routing so needs full route.
const route = el.nodeName === "HASSIO-PANEL" ? this.route : this.routeTail;
if ("setProperties" in el) {
// As long as we have Polymer pages
(el as PolymerElement).setProperties({
hass: this.hass,
narrow: this.narrow,
supervisorInfo: this._supervisorInfo,
hassioInfo: this._hassioInfo,
hostInfo: this._hostInfo,
hassInfo: this._hassInfo,
hassOsInfo: this._hassOsInfo,
route,
});
} else {
el.hass = this.hass;
el.narrow = this.narrow;
el.supervisorInfo = this._supervisorInfo;
el.hassioInfo = this._hassioInfo;
el.hostInfo = this._hostInfo;
el.hassInfo = this._hassInfo;
el.hassOsInfo = this._hassOsInfo;
el.route = route;
}
}
private async _fetchData() {
if (this.panel.config && this.panel.config.ingress) {
await this._redirectIngress(this.panel.config.ingress);
return;
}
if (oldHass.themes !== this.hass.themes) {
this._applyTheme();
const [supervisorInfo, hostInfo, hassInfo, hassioInfo] = await Promise.all([
fetchHassioSupervisorInfo(this.hass),
fetchHassioHostInfo(this.hass),
fetchHassioHomeAssistantInfo(this.hass),
fetchHassioInfo(this.hass),
]);
this._supervisorInfo = supervisorInfo;
this._hassioInfo = hassioInfo;
this._hostInfo = hostInfo;
this._hassInfo = hassInfo;
if (this._hostInfo.features && this._hostInfo.features.includes("hassos")) {
this._hassOsInfo = await fetchHassioHassOsInfo(this.hass);
}
}
protected render() {
return html`
<hassio-router
.hass=${this.hass}
.route=${this.route}
.panel=${this.panel}
.narrow=${this.narrow}
></hassio-router>
`;
}
private async _redirectIngress(addonSlug: string) {
// When we trigger a navigation, we sleep to make sure we don't
// show the hassio dashboard before navigating away.
const awaitAlert = async (
alertParams: AlertDialogParams,
action: () => void
) => {
await new Promise((resolve) => {
alertParams.confirm = resolve;
showAlertDialog(this, alertParams);
});
action();
await new Promise((resolve) => setTimeout(resolve, 1000));
};
private _applyTheme() {
let themeName: string;
let options: Partial<HomeAssistant["selectedTheme"]> | undefined;
if (atLeastVersion(this.hass.config.version, 0, 114)) {
themeName =
this.hass.selectedTheme?.theme ||
(this.hass.themes.darkMode && this.hass.themes.default_dark_theme
? this.hass.themes.default_dark_theme!
: this.hass.themes.default_theme);
options = this.hass.selectedTheme;
if (themeName === "default" && options?.dark === undefined) {
options = {
...this.hass.selectedTheme,
dark: this.hass.themes.darkMode,
};
}
} else {
themeName =
((this.hass.selectedTheme as unknown) as string) ||
this.hass.themes.default_theme;
}
applyThemesOnElement(
this.parentElement,
this.hass.themes,
themeName,
options
const createSessionPromise = createHassioSession(this.hass).then(
() => true,
() => false
);
let addon;
try {
addon = await fetchHassioAddonInfo(this.hass, addonSlug);
} catch (err) {
await awaitAlert(
{
text: "Unable to fetch add-on info to start Ingress",
title: "Supervisor",
},
() => history.back()
);
return;
}
if (!addon.ingress_url) {
await awaitAlert(
{
text: "Add-on does not support Ingress",
title: addon.name,
},
() => history.back()
);
return;
}
if (addon.state !== "started") {
await awaitAlert(
{
text: "Add-on is not running. Please start it first",
title: addon.name,
},
() => navigate(this, `/hassio/addon/${addon.slug}/info`, true)
);
return;
}
if (!(await createSessionPromise)) {
await awaitAlert(
{
text: "Unable to create an Ingress session",
title: addon.name,
},
() => history.back()
);
return;
}
location.assign(addon.ingress_url);
// await a promise that doesn't resolve, so we show the loading screen
// while we load the next page.
await new Promise(() => undefined);
}
private _apiCalled(ev) {
if (!ev.detail.success) {
return;
}
let tries = 1;
const tryUpdate = () => {
this._fetchData().catch(() => {
tries += 1;
setTimeout(tryUpdate, Math.min(tries, 5) * 1000);
});
};
tryUpdate();
}
}
+25 -15
View File
@@ -1,11 +1,10 @@
import { mdiBackupRestore, mdiCogs, mdiStore, mdiViewDashboard } from "@mdi/js";
import {
customElement,
html,
LitElement,
property,
TemplateResult,
css,
CSSResult,
} from "lit-element";
import { HassioHassOSInfo, HassioHostInfo } from "../../src/data/hassio/host";
import {
@@ -13,9 +12,33 @@ import {
HassioSupervisorInfo,
HassioInfo,
} from "../../src/data/hassio/supervisor";
import type { PageNavigation } from "../../src/layouts/hass-tabs-subpage";
import { HomeAssistant, Route } from "../../src/types";
import "./hassio-panel-router";
export const supervisorTabs: PageNavigation[] = [
{
name: "Dashboard",
path: `/hassio/dashboard`,
iconPath: mdiViewDashboard,
},
{
name: "Add-on store",
path: `/hassio/store`,
iconPath: mdiStore,
},
{
name: "Snapshots",
path: `/hassio/snapshots`,
iconPath: mdiBackupRestore,
},
{
name: "System",
path: `/hassio/system`,
iconPath: mdiCogs,
},
];
@customElement("hassio-panel")
class HassioPanel extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@@ -35,9 +58,6 @@ class HassioPanel extends LitElement {
@property({ attribute: false }) public hassOsInfo!: HassioHassOSInfo;
protected render(): TemplateResult {
if (!this.supervisorInfo) {
return html``;
}
return html`
<hassio-panel-router
.route=${this.route}
@@ -51,16 +71,6 @@ class HassioPanel extends LitElement {
></hassio-panel-router>
`;
}
static get styles(): CSSResult {
return css`
:host {
--app-header-background-color: var(--sidebar-background-color);
--app-header-text-color: var(--sidebar-text-color);
--app-header-border-bottom: 1px solid var(--divider-color);
}
`;
}
}
declare global {
-150
View File
@@ -1,150 +0,0 @@
import {
customElement,
property,
internalProperty,
PropertyValues,
} from "lit-element";
import {
fetchHassioHassOsInfo,
fetchHassioHostInfo,
HassioHassOSInfo,
HassioHostInfo,
} from "../../src/data/hassio/host";
import {
fetchHassioHomeAssistantInfo,
fetchHassioSupervisorInfo,
fetchHassioInfo,
HassioHomeAssistantInfo,
HassioInfo,
HassioPanelInfo,
HassioSupervisorInfo,
} from "../../src/data/hassio/supervisor";
import {
HassRouterPage,
RouterOptions,
} from "../../src/layouts/hass-router-page";
import "../../src/resources/ha-style";
import { HomeAssistant } from "../../src/types";
// Don't codesplit it, that way the dashboard always loads fast.
import "./hassio-panel";
@customElement("hassio-router")
class HassioRouter extends HassRouterPage {
@property({ attribute: false }) public hass!: HomeAssistant;
@property() public panel!: HassioPanelInfo;
@property() public narrow!: boolean;
protected routerOptions: RouterOptions = {
// Hass.io has a page with tabs, so we route all non-matching routes to it.
defaultPage: "dashboard",
initialLoad: () => this._fetchData(),
showLoading: true,
routes: {
dashboard: {
tag: "hassio-panel",
cache: true,
},
snapshots: "dashboard",
store: "dashboard",
system: "dashboard",
addon: {
tag: "hassio-addon-dashboard",
load: () =>
import(
/* webpackChunkName: "hassio-addon-dashboard" */ "./addon-view/hassio-addon-dashboard"
),
},
ingress: {
tag: "hassio-ingress-view",
load: () =>
import(
/* webpackChunkName: "hassio-ingress-view" */ "./ingress-view/hassio-ingress-view"
),
},
},
};
@internalProperty() private _supervisorInfo: HassioSupervisorInfo;
@internalProperty() private _hostInfo: HassioHostInfo;
@internalProperty() private _hassioInfo?: HassioInfo;
@internalProperty() private _hassOsInfo?: HassioHassOSInfo;
@internalProperty() private _hassInfo: HassioHomeAssistantInfo;
protected firstUpdated(changedProps: PropertyValues) {
super.firstUpdated(changedProps);
this.addEventListener("hass-api-called", (ev) => this._apiCalled(ev));
}
protected updatePageEl(el) {
// the tabs page does its own routing so needs full route.
const route = el.nodeName === "HASSIO-PANEL" ? this.route : this.routeTail;
el.hass = this.hass;
el.narrow = this.narrow;
el.supervisorInfo = this._supervisorInfo;
el.hassioInfo = this._hassioInfo;
el.hostInfo = this._hostInfo;
el.hassInfo = this._hassInfo;
el.hassOsInfo = this._hassOsInfo;
el.route = route;
if (el.localName === "hassio-ingress-view") {
el.ingressPanel = this.panel.config && this.panel.config.ingress;
}
}
private async _fetchData() {
if (this.panel.config && this.panel.config.ingress) {
this._redirectIngress(this.panel.config.ingress);
return;
}
const [supervisorInfo, hostInfo, hassInfo, hassioInfo] = await Promise.all([
fetchHassioSupervisorInfo(this.hass),
fetchHassioHostInfo(this.hass),
fetchHassioHomeAssistantInfo(this.hass),
fetchHassioInfo(this.hass),
]);
this._supervisorInfo = supervisorInfo;
this._hassioInfo = hassioInfo;
this._hostInfo = hostInfo;
this._hassInfo = hassInfo;
if (this._hostInfo.features && this._hostInfo.features.includes("hassos")) {
this._hassOsInfo = await fetchHassioHassOsInfo(this.hass);
}
}
private _redirectIngress(addonSlug: string) {
this.route = { prefix: "/hassio", path: `/ingress/${addonSlug}` };
}
private _apiCalled(ev) {
if (!ev.detail.success) {
return;
}
let tries = 1;
const tryUpdate = () => {
this._fetchData().catch(() => {
tries += 1;
setTimeout(tryUpdate, Math.min(tries, 5) * 1000);
});
};
tryUpdate();
}
}
declare global {
interface HTMLElementTagNameMap {
"hassio-router": HassioRouter;
}
}
-25
View File
@@ -1,25 +0,0 @@
import { mdiBackupRestore, mdiCogs, mdiStore, mdiViewDashboard } from "@mdi/js";
import type { PageNavigation } from "../../src/layouts/hass-tabs-subpage";
export const supervisorTabs: PageNavigation[] = [
{
name: "Dashboard",
path: `/hassio/dashboard`,
iconPath: mdiViewDashboard,
},
{
name: "Add-on store",
path: `/hassio/store`,
iconPath: mdiStore,
},
{
name: "Snapshots",
path: `/hassio/snapshots`,
iconPath: mdiBackupRestore,
},
{
name: "System",
path: `/hassio/system`,
iconPath: mdiCogs,
},
];
+25 -117
View File
@@ -5,7 +5,6 @@ import {
html,
LitElement,
property,
internalProperty,
PropertyValues,
TemplateResult,
} from "lit-element";
@@ -17,56 +16,29 @@ import { createHassioSession } from "../../../src/data/hassio/supervisor";
import "../../../src/layouts/hass-loading-screen";
import "../../../src/layouts/hass-subpage";
import { HomeAssistant, Route } from "../../../src/types";
import { showAlertDialog } from "../../../src/dialogs/generic/show-dialog-box";
import { navigate } from "../../../src/common/navigate";
import { mdiMenu } from "@mdi/js";
import { fireEvent } from "../../../src/common/dom/fire_event";
@customElement("hassio-ingress-view")
class HassioIngressView extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property() public hass!: HomeAssistant;
@property() public route!: Route;
@property() public ingressPanel = false;
@internalProperty() private _addon?: HassioAddonDetails;
@property({ type: Boolean })
public narrow = false;
@property() private _addon?: HassioAddonDetails;
protected render(): TemplateResult {
if (!this._addon) {
return html` <hass-loading-screen></hass-loading-screen> `;
}
const iframe = html`<iframe src=${this._addon.ingress_url!}></iframe>`;
if (!this.ingressPanel) {
return html`<hass-subpage
.header=${this._addon.name}
.narrow=${this.narrow}
>
${iframe}
</hass-subpage>`;
}
return html`${this.narrow || this.hass.dockedSidebar === "always_hidden"
? html`<div class="header">
<mwc-icon-button
aria-label=${this.hass.localize("ui.sidebar.sidebar_toggle")}
@click=${this._toggleMenu}
>
<ha-svg-icon path=${mdiMenu}></ha-svg-icon>
</mwc-icon-button>
<div class="main-title">${this._addon.name}</div>
</div>
${iframe}`
: iframe}`;
return html`
<hass-subpage .header=${this._addon.name} hassio>
<iframe src=${this._addon.ingress_url}></iframe>
</hass-subpage>
`;
}
protected updated(changedProps: PropertyValues) {
super.updated(changedProps);
super.firstUpdated(changedProps);
if (!changedProps.has("route")) {
return;
@@ -83,56 +55,27 @@ class HassioIngressView extends LitElement {
}
private async _fetchData(addonSlug: string) {
const createSessionPromise = createHassioSession(this.hass).then(
() => true,
() => false
);
let addon;
try {
addon = await fetchHassioAddonInfo(this.hass, addonSlug);
const [addon] = await Promise.all([
fetchHassioAddonInfo(this.hass, addonSlug).catch(() => {
throw new Error("Failed to fetch add-on info");
}),
createHassioSession(this.hass).catch(() => {
throw new Error("Failed to create an ingress session");
}),
]);
if (!addon.ingress) {
throw new Error("This add-on does not support ingress");
}
this._addon = addon;
} catch (err) {
await showAlertDialog(this, {
text: "Unable to fetch add-on info to start Ingress",
title: "Supervisor",
});
// eslint-disable-next-line
console.error(err);
alert(err.message || "Unknown error starting ingress.");
history.back();
return;
}
if (!addon.ingress_url) {
await showAlertDialog(this, {
text: "Add-on does not support Ingress",
title: addon.name,
});
history.back();
return;
}
if (addon.state !== "started") {
await showAlertDialog(this, {
text: "Add-on is not running. Please start it first",
title: addon.name,
});
navigate(this, `/hassio/addon/${addon.slug}/info`, true);
return;
}
if (!(await createSessionPromise)) {
await showAlertDialog(this, {
text: "Unable to create an Ingress session",
title: addon.name,
});
history.back();
return;
}
this._addon = addon;
}
private _toggleMenu(): void {
fireEvent(this, "hass-toggle-menu");
}
static get styles(): CSSResult {
@@ -143,41 +86,6 @@ class HassioIngressView extends LitElement {
height: 100%;
border: 0;
}
.header + iframe {
height: calc(100% - 40px);
}
.header {
display: flex;
align-items: center;
font-size: 16px;
height: 40px;
padding: 0 16px;
pointer-events: none;
background-color: var(--app-header-background-color);
font-weight: 400;
color: var(--app-header-text-color, white);
border-bottom: var(--app-header-border-bottom, none);
box-sizing: border-box;
--mdc-icon-size: 20px;
}
.main-title {
margin: 0 0 0 24px;
line-height: 20px;
flex-grow: 1;
}
mwc-icon-button {
pointer-events: auto;
}
hass-subpage {
--app-header-background-color: var(--sidebar-background-color);
--app-header-text-color: var(--sidebar-text-color);
--app-header-border-bottom: 1px solid var(--divider-color);
}
`;
}
}
+24 -25
View File
@@ -13,17 +13,14 @@ import {
CSSResultArray,
customElement,
html,
internalProperty,
LitElement,
property,
PropertyValues,
TemplateResult,
} from "lit-element";
import { fireEvent } from "../../../src/common/dom/fire_event";
import "../../../src/components/buttons/ha-progress-button";
import "../../../src/components/ha-card";
import "../../../src/components/ha-svg-icon";
import { extractApiErrorMessage } from "../../../src/data/hassio/common";
import {
createHassioFullSnapshot,
createHassioPartialSnapshot,
@@ -40,7 +37,7 @@ import { haStyle } from "../../../src/resources/styles";
import { HomeAssistant, Route } from "../../../src/types";
import "../components/hassio-card-content";
import { showHassioSnapshotDialog } from "../dialogs/snapshot/show-dialog-hassio-snapshot";
import { supervisorTabs } from "../hassio-tabs";
import { supervisorTabs } from "../hassio-panel";
import { hassioStyle } from "../resources/hassio-style";
interface CheckboxItem {
@@ -59,19 +56,19 @@ class HassioSnapshots extends LitElement {
@property({ attribute: false }) public supervisorInfo!: HassioSupervisorInfo;
@internalProperty() private _snapshotName = "";
@property() private _snapshotName = "";
@internalProperty() private _snapshotPassword = "";
@property() private _snapshotPassword = "";
@internalProperty() private _snapshotHasPassword = false;
@property() private _snapshotHasPassword = false;
@internalProperty() private _snapshotType: HassioSnapshot["type"] = "full";
@property() private _snapshotType: HassioSnapshot["type"] = "full";
@internalProperty() private _snapshots?: HassioSnapshot[] = [];
@property() private _snapshots?: HassioSnapshot[] = [];
@internalProperty() private _addonList: CheckboxItem[] = [];
@property() private _addonList: CheckboxItem[] = [];
@internalProperty() private _folderList: CheckboxItem[] = [
@property() private _folderList: CheckboxItem[] = [
{
slug: "homeassistant",
name: "Home Assistant configuration",
@@ -79,11 +76,12 @@ class HassioSnapshots extends LitElement {
},
{ slug: "ssl", name: "SSL", checked: true },
{ slug: "share", name: "Share", checked: true },
{ slug: "media", name: "Media", checked: true },
{ slug: "addons/local", name: "Local add-ons", checked: true },
];
@internalProperty() private _error = "";
@property() private _creatingSnapshot = false;
@property() private _error = "";
public async refreshData() {
await reloadHassioSnapshots(this.hass);
@@ -193,9 +191,12 @@ class HassioSnapshots extends LitElement {
: undefined}
</div>
<div class="card-actions">
<ha-progress-button @click=${this._createSnapshot}>
<mwc-button
.disabled=${this._creatingSnapshot}
@click=${this._createSnapshot}
>
Create
</ha-progress-button>
</mwc-button>
</div>
</ha-card>
</div>
@@ -228,7 +229,7 @@ class HassioSnapshots extends LitElement {
.icon=${snapshot.type === "full"
? mdiPackageVariantClosed
: mdiPackageVariant}
icon-class="snapshot"
.icon-class="snapshot"
></hassio-card-content>
</div>
</ha-card>
@@ -242,7 +243,7 @@ class HassioSnapshots extends LitElement {
protected firstUpdated(changedProps: PropertyValues) {
super.firstUpdated(changedProps);
this.refreshData();
this._updateSnapshots();
}
protected updated(changedProps: PropertyValues) {
@@ -291,20 +292,17 @@ class HassioSnapshots extends LitElement {
this._snapshots = await fetchHassioSnapshots(this.hass);
this._snapshots.sort((a, b) => (a.date < b.date ? 1 : -1));
} catch (err) {
this._error = extractApiErrorMessage(err);
this._error = err.message;
}
}
private async _createSnapshot(ev: CustomEvent): Promise<void> {
const button = ev.currentTarget as any;
button.progress = true;
private async _createSnapshot() {
this._error = "";
if (this._snapshotHasPassword && !this._snapshotPassword.length) {
this._error = "Please enter a password.";
button.progress = false;
return;
}
this._creatingSnapshot = true;
await this.updateComplete;
const name =
@@ -344,9 +342,10 @@ class HassioSnapshots extends LitElement {
this._updateSnapshots();
fireEvent(this, "hass-api-called", { success: true, response: null });
} catch (err) {
this._error = extractApiErrorMessage(err);
this._error = err.message;
} finally {
this._creatingSnapshot = false;
}
button.progress = false;
}
private _computeDetails(snapshot: HassioSnapshot) {
+146 -255
View File
@@ -1,31 +1,17 @@
import "@material/mwc-button";
import { ActionDetail } from "@material/mwc-list/mwc-list-foundation";
import "@material/mwc-list/mwc-list-item";
import { mdiDotsVertical } from "@mdi/js";
import { safeDump } from "js-yaml";
import {
css,
CSSResult,
customElement,
html,
internalProperty,
LitElement,
property,
TemplateResult,
} from "lit-element";
import memoizeOne from "memoize-one";
import "../../../src/components/buttons/ha-progress-button";
import "../../../src/components/ha-button-menu";
import "../../../src/components/ha-card";
import "../../../src/components/ha-settings-row";
import {
extractApiErrorMessage,
ignoredStatusCodes,
} from "../../../src/data/hassio/common";
import "../../../src/components/buttons/ha-call-api-button";
import { fetchHassioHardwareInfo } from "../../../src/data/hassio/hardware";
import {
changeHostOptions,
configSyncOS,
fetchHassioHostInfo,
HassioHassOSInfo,
HassioHostInfo as HassioHostInfoType,
@@ -33,10 +19,6 @@ import {
shutdownHost,
updateOS,
} from "../../../src/data/hassio/host";
import {
fetchNetworkInfo,
NetworkInfo,
} from "../../../src/data/hassio/network";
import { HassioInfo } from "../../../src/data/hassio/supervisor";
import {
showAlertDialog,
@@ -46,191 +28,176 @@ import {
import { haStyle } from "../../../src/resources/styles";
import { HomeAssistant } from "../../../src/types";
import { showHassioMarkdownDialog } from "../dialogs/markdown/show-dialog-hassio-markdown";
import { showNetworkDialog } from "../dialogs/network/show-dialog-network";
import { hassioStyle } from "../resources/hassio-style";
@customElement("hassio-host-info")
class HassioHostInfo extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property() public hass!: HomeAssistant;
@property() public hostInfo!: HassioHostInfoType;
@property({ attribute: false }) public hassioInfo!: HassioInfo;
@property({ attribute: false }) public hassOsInfo!: HassioHassOSInfo;
@property() public hassOsInfo!: HassioHassOSInfo;
@internalProperty() public _networkInfo?: NetworkInfo;
@property() private _errors?: string;
protected render(): TemplateResult | void {
const primaryIpAddress = this.hostInfo.features.includes("network")
? this._primaryIpAddress(this._networkInfo!)
: "";
public render(): TemplateResult | void {
return html`
<ha-card header="Host System">
<ha-card>
<div class="card-content">
<h2>Host system</h2>
<table class="info">
<tbody>
<tr>
<td>Hostname</td>
<td>${this.hostInfo.hostname}</td>
</tr>
<tr>
<td>System</td>
<td>${this.hostInfo.operating_system}</td>
</tr>
${!this.hostInfo.features.includes("hassos")
? html`<tr>
<td>Docker version</td>
<td>${this.hassioInfo.docker}</td>
</tr>`
: ""}
${this.hostInfo.deployment
? html`
<tr>
<td>Deployment</td>
<td>${this.hostInfo.deployment}</td>
</tr>
`
: ""}
</tbody>
</table>
<mwc-button raised @click=${this._showHardware} class="info">
Hardware
</mwc-button>
${this.hostInfo.features.includes("hostname")
? html`<ha-settings-row>
<span slot="heading">
Hostname
</span>
<span slot="description">
${this.hostInfo.hostname}
</span>
? html`
<mwc-button
title="Change the hostname"
label="Change"
raised
@click=${this._changeHostnameClicked}
class="info"
>
Change hostname
</mwc-button>
</ha-settings-row>`
`
: ""}
${this.hostInfo.features.includes("network")
? html` <ha-settings-row>
<span slot="heading">
IP address
</span>
<span slot="description">
${primaryIpAddress}
</span>
<mwc-button
title="Change the network"
label="Change"
@click=${this._changeNetworkClicked}
>
</mwc-button>
</ha-settings-row>`
: ""}
<ha-settings-row>
<span slot="heading">
Operating system
</span>
<span slot="description">
${this.hostInfo.operating_system}
</span>
${this.hostInfo.version !== this.hostInfo.version_latest &&
this.hostInfo.features.includes("hassos")
? html`
<ha-progress-button
title="Update the host OS"
@click=${this._osUpdate}
>
Update
</ha-progress-button>
`
: ""}
</ha-settings-row>
${!this.hostInfo.features.includes("hassos")
? html`<ha-settings-row>
<span slot="heading">
Docker version
</span>
<span slot="description">
${this.hassioInfo.docker}
</span>
</ha-settings-row>`
: ""}
${this.hostInfo.deployment
? html`<ha-settings-row>
<span slot="heading">
Deployment
</span>
<span slot="description">
${this.hostInfo.deployment}
</span>
</ha-settings-row>`
${this._errors
? html` <div class="errors">Error: ${this._errors}</div> `
: ""}
</div>
<div class="card-actions">
${this.hostInfo.features.includes("reboot")
? html`
<ha-progress-button
title="Reboot the host OS"
class="warning"
@click=${this._hostReboot}
<mwc-button class="warning" @click=${this._rebootHost}
>Reboot</mwc-button
>
Reboot
</ha-progress-button>
`
: ""}
${this.hostInfo.features.includes("shutdown")
? html`
<ha-progress-button
title="Shutdown the host OS"
class="warning"
@click=${this._hostShutdown}
<mwc-button class="warning" @click=${this._shutdownHost}
>Shutdown</mwc-button
>
Shutdown
</ha-progress-button>
`
: ""}
<ha-button-menu
corner="BOTTOM_START"
@action=${this._handleMenuAction}
>
<mwc-icon-button slot="trigger">
<ha-svg-icon .path=${mdiDotsVertical}></ha-svg-icon>
</mwc-icon-button>
<mwc-list-item title="Show a list of hardware">
Hardware
</mwc-list-item>
${this.hostInfo.features.includes("hassos")
? html`<mwc-list-item
${this.hostInfo.features.includes("hassos")
? html`
<ha-call-api-button
class="warning"
.hass=${this.hass}
path="hassio/os/config/sync"
title="Load HassOS configs or updates from USB"
>Import from USB</ha-call-api-button
>
Import from USB
</mwc-list-item>`
: ""}
</ha-button-menu>
`
: ""}
${this.hostInfo.version !== this.hostInfo.version_latest
? html` <mwc-button @click=${this._updateOS}>Update</mwc-button> `
: ""}
</div>
</ha-card>
`;
}
protected firstUpdated(): void {
this._loadData();
static get styles(): CSSResult[] {
return [
haStyle,
hassioStyle,
css`
ha-card {
height: 100%;
width: 100%;
}
.card-content {
color: var(--primary-text-color);
box-sizing: border-box;
height: calc(100% - 47px);
}
.info {
width: 100%;
}
.info td:nth-child(2) {
text-align: right;
}
.errors {
color: var(--google-red-500);
margin-top: 16px;
}
mwc-button.info {
max-width: calc(50% - 12px);
}
table.info {
margin-bottom: 10px;
}
.warning {
--mdc-theme-primary: var(--google-red-500);
}
`,
];
}
private _primaryIpAddress = memoizeOne((network_info: NetworkInfo) => {
if (!network_info) {
return "";
}
return Object.keys(network_info?.interfaces)
.map((device) => network_info.interfaces[device])
.find((device) => device.primary)?.ip_address;
});
protected firstUpdated(): void {
this.addEventListener("hass-api-called", (ev) => this._apiCalled(ev));
}
private async _handleMenuAction(ev: CustomEvent<ActionDetail>) {
switch (ev.detail.index) {
case 0:
await this._showHardware();
break;
case 1:
await this._importFromUSB();
break;
private _apiCalled(ev): void {
if (ev.detail.success) {
this._errors = undefined;
return;
}
const response = ev.detail.response;
this._errors =
typeof response.body === "object"
? response.body.message || "Unknown error"
: response.body;
}
private async _showHardware(): Promise<void> {
try {
const content = await fetchHassioHardwareInfo(this.hass);
const content = this._objectToMarkdown(
await fetchHassioHardwareInfo(this.hass)
);
showHassioMarkdownDialog(this, {
title: "Hardware",
content: `<pre>${safeDump(content, { indent: 2 })}</pre>`,
content,
});
} catch (err) {
showAlertDialog(this, {
title: "Failed to get Hardware list",
text: extractApiErrorMessage(err),
showHassioMarkdownDialog(this, {
title: "Hardware",
content: "Error getting hardware info",
});
}
}
private async _hostReboot(ev: CustomEvent): Promise<void> {
const button = ev.currentTarget as any;
button.progress = true;
private async _rebootHost(): Promise<void> {
const confirmed = await showConfirmationDialog(this, {
title: "Reboot",
text: "Are you sure you want to reboot the host?",
@@ -239,28 +206,20 @@ class HassioHostInfo extends LitElement {
});
if (!confirmed) {
button.progress = false;
return;
}
try {
await rebootHost(this.hass);
} catch (err) {
// Ignore connection errors, these are all expected
if (err.status_code && !ignoredStatusCodes.has(err.status_code)) {
showAlertDialog(this, {
title: "Failed to reboot",
text: extractApiErrorMessage(err),
});
}
showAlertDialog(this, {
title: "Failed to reboot",
text: err.body.message,
});
}
button.progress = false;
}
private async _hostShutdown(ev: CustomEvent): Promise<void> {
const button = ev.currentTarget as any;
button.progress = true;
private async _shutdownHost(): Promise<void> {
const confirmed = await showConfirmationDialog(this, {
title: "Shutdown",
text: "Are you sure you want to shutdown the host?",
@@ -269,28 +228,20 @@ class HassioHostInfo extends LitElement {
});
if (!confirmed) {
button.progress = false;
return;
}
try {
await shutdownHost(this.hass);
} catch (err) {
// Ignore connection errors, these are all expected
if (err.status_code && !ignoredStatusCodes.has(err.status_code)) {
showAlertDialog(this, {
title: "Failed to shutdown",
text: extractApiErrorMessage(err),
});
}
showAlertDialog(this, {
title: "Failed to shutdown",
text: err.body.message,
});
}
button.progress = false;
}
private async _osUpdate(ev: CustomEvent): Promise<void> {
const button = ev.currentTarget as any;
button.progress = true;
private async _updateOS(): Promise<void> {
const confirmed = await showConfirmationDialog(this, {
title: "Update",
text: "Are you sure you want to update the OS?",
@@ -299,7 +250,6 @@ class HassioHostInfo extends LitElement {
});
if (!confirmed) {
button.progress = false;
return;
}
@@ -308,17 +258,30 @@ class HassioHostInfo extends LitElement {
} catch (err) {
showAlertDialog(this, {
title: "Failed to update",
text: extractApiErrorMessage(err),
text: err.body.message,
});
}
button.progress = false;
}
private async _changeNetworkClicked(): Promise<void> {
showNetworkDialog(this, {
network: this._networkInfo!,
loadData: () => this._loadData(),
private _objectToMarkdown(obj, indent = ""): string {
let data = "";
Object.keys(obj).forEach((key) => {
if (typeof obj[key] !== "object") {
data += `${indent}- ${key}: ${obj[key]}\n`;
} else {
data += `${indent}- ${key}:\n`;
if (Array.isArray(obj[key])) {
if (obj[key].length) {
data +=
`${indent} - ` + obj[key].join(`\n${indent} - `) + "\n";
}
} else {
data += this._objectToMarkdown(obj[key], ` ${indent}`);
}
}
});
return data;
}
private async _changeHostnameClicked(): Promise<void> {
@@ -337,83 +300,11 @@ class HassioHostInfo extends LitElement {
} catch (err) {
showAlertDialog(this, {
title: "Setting hostname failed",
text: extractApiErrorMessage(err),
text: err.body.message,
});
}
}
}
private async _importFromUSB(): Promise<void> {
try {
await configSyncOS(this.hass);
this.hostInfo = await fetchHassioHostInfo(this.hass);
} catch (err) {
showAlertDialog(this, {
title: "Failed to import from USB",
text: extractApiErrorMessage(err),
});
}
}
private async _loadData(): Promise<void> {
this._networkInfo = await fetchNetworkInfo(this.hass);
}
static get styles(): CSSResult[] {
return [
haStyle,
hassioStyle,
css`
ha-card {
height: 100%;
justify-content: space-between;
flex-direction: column;
display: flex;
}
.card-actions {
height: 48px;
border-top: none;
display: flex;
justify-content: space-between;
align-items: center;
}
ha-settings-row {
padding: 0;
height: 54px;
width: 100%;
}
ha-settings-row[three-line] {
height: 74px;
}
ha-settings-row > span[slot="description"] {
white-space: normal;
color: var(--secondary-text-color);
}
.warning {
--mdc-theme-primary: var(--error-color);
}
ha-button-menu {
color: var(--secondary-text-color);
--mdc-menu-min-width: 200px;
}
@media (min-width: 563px) {
paper-listbox {
max-height: 150px;
overflow: auto;
}
}
paper-item {
cursor: pointer;
min-height: 35px;
}
mwc-list-item ha-svg-icon {
color: var(--secondary-text-color);
}
`,
];
}
}
declare global {
+133 -252
View File
@@ -1,3 +1,4 @@
import "@material/mwc-button";
import {
css,
CSSResult,
@@ -7,262 +8,94 @@ import {
property,
TemplateResult,
} from "lit-element";
import "../../../src/components/buttons/ha-progress-button";
import { fireEvent } from "../../../src/common/dom/fire_event";
import "../../../src/components/buttons/ha-call-api-button";
import "../../../src/components/ha-card";
import "../../../src/components/ha-settings-row";
import "../../../src/components/ha-switch";
import { HassioHostInfo as HassioHostInfoType } from "../../../src/data/hassio/host";
import {
HassioSupervisorInfo as HassioSupervisorInfoType,
reloadSupervisor,
setSupervisorOption,
SupervisorOptions,
updateSupervisor,
fetchHassioSupervisorInfo,
} from "../../../src/data/hassio/supervisor";
import {
showAlertDialog,
showConfirmationDialog,
} from "../../../src/dialogs/generic/show-dialog-box";
import { showConfirmationDialog } from "../../../src/dialogs/generic/show-dialog-box";
import { haStyle } from "../../../src/resources/styles";
import { HomeAssistant } from "../../../src/types";
import { hassioStyle } from "../resources/hassio-style";
import { extractApiErrorMessage } from "../../../src/data/hassio/common";
@customElement("hassio-supervisor-info")
class HassioSupervisorInfo extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property() public hass!: HomeAssistant;
@property() public supervisorInfo!: HassioSupervisorInfoType;
@property() public hostInfo!: HassioHostInfoType;
@property() private _errors?: string;
protected render(): TemplateResult | void {
public render(): TemplateResult | void {
return html`
<ha-card header="Supervisor">
<ha-card>
<div class="card-content">
<ha-settings-row>
<span slot="heading">
Version
</span>
<span slot="description">
${this.supervisorInfo.version}
</span>
</ha-settings-row>
<ha-settings-row>
<span slot="heading">
Newest version
</span>
<span slot="description">
${this.supervisorInfo.version_latest}
</span>
${this.supervisorInfo.version !== this.supervisorInfo.version_latest
? html`
<ha-progress-button
title="Update the supervisor"
@click=${this._supervisorUpdate}
>
Update
</ha-progress-button>
`
: ""}
</ha-settings-row>
<ha-settings-row>
<span slot="heading">
Channel
</span>
<span slot="description">
${this.supervisorInfo.channel}
</span>
${this.supervisorInfo.channel === "beta"
? html`
<ha-progress-button
@click=${this._toggleBeta}
title="Get stable updates for Home Assistant, supervisor and host"
>
Leave beta channel
</ha-progress-button>
`
: this.supervisorInfo.channel === "stable"
? html`
<ha-progress-button
@click=${this._toggleBeta}
title="Get beta updates for Home Assistant (RCs), supervisor and host"
>
Join beta channel
</ha-progress-button>
`
: ""}
</ha-settings-row>
${this.supervisorInfo?.supported
? html` <ha-settings-row three-line>
<span slot="heading">
Share diagnostics
</span>
<div slot="description" class="diagnostics-description">
Share crash reports and diagnostic information.
<button
class="link"
title="Show more information about this"
@click=${this._diagnosticsInformationDialog}
>
Learn more
</button>
</div>
<ha-switch
haptic
.checked=${this.supervisorInfo.diagnostics}
@change=${this._toggleDiagnostics}
></ha-switch>
</ha-settings-row>`
: html`<div class="error">
You are running an unsupported installation.
<a
href="https://github.com/home-assistant/architecture/blob/master/adr/${this.hostInfo.features.includes(
"hassos"
)
? "0015-home-assistant-os.md"
: "0014-home-assistant-supervised.md"}"
target="_blank"
rel="noreferrer"
title="Learn more about how you can make your system compliant"
>
Learn More
</a>
</div>`}
<h2>Supervisor</h2>
<table class="info">
<tbody>
<tr>
<td>Version</td>
<td>${this.supervisorInfo.version}</td>
</tr>
<tr>
<td>Latest version</td>
<td>${this.supervisorInfo.version_latest}</td>
</tr>
${this.supervisorInfo.channel !== "stable"
? html`
<tr>
<td>Channel</td>
<td>${this.supervisorInfo.channel}</td>
</tr>
`
: ""}
</tbody>
</table>
${this._errors
? html` <div class="errors">Error: ${this._errors}</div> `
: ""}
</div>
<div class="card-actions">
<ha-progress-button
@click=${this._supervisorReload}
title="Reload parts of the supervisor."
<ha-call-api-button .hass=${this.hass} path="hassio/supervisor/reload"
>Reload</ha-call-api-button
>
Reload
</ha-progress-button>
${this.supervisorInfo.version !== this.supervisorInfo.version_latest
? html`
<ha-call-api-button
.hass=${this.hass}
path="hassio/supervisor/update"
>Update</ha-call-api-button
>
`
: ""}
${this.supervisorInfo.channel === "beta"
? html`
<ha-call-api-button
.hass=${this.hass}
path="hassio/supervisor/options"
.data=${{ channel: "stable" }}
>Leave beta channel</ha-call-api-button
>
`
: ""}
${this.supervisorInfo.channel === "stable"
? html`
<mwc-button
@click=${this._joinBeta}
class="warning"
title="Get beta updates for Home Assistant (RCs), supervisor and host"
>Join beta channel</mwc-button
>
`
: ""}
</div>
</ha-card>
`;
}
private async _toggleBeta(ev: CustomEvent): Promise<void> {
const button = ev.currentTarget as any;
button.progress = true;
if (this.supervisorInfo.channel === "stable") {
const confirmed = await showConfirmationDialog(this, {
title: "WARNING",
text: html` Beta releases are for testers and early adopters and can
contain unstable code changes.
<br />
<b>
Make sure you have backups of your data before you activate this
feature.
</b>
<br /><br />
This includes beta releases for:
<li>Home Assistant Core</li>
<li>Home Assistant Supervisor</li>
<li>Home Assistant Operating System</li>
<br />
Do you want to join the beta channel?`,
confirmText: "join beta",
dismissText: "no",
});
if (!confirmed) {
button.progress = false;
return;
}
}
try {
const data: Partial<SupervisorOptions> = {
channel: this.supervisorInfo.channel === "stable" ? "beta" : "stable",
};
await setSupervisorOption(this.hass, data);
await reloadSupervisor(this.hass);
this.supervisorInfo = await fetchHassioSupervisorInfo(this.hass);
} catch (err) {
showAlertDialog(this, {
title: "Failed to set supervisor option",
text: extractApiErrorMessage(err),
});
}
button.progress = false;
}
private async _supervisorReload(ev: CustomEvent): Promise<void> {
const button = ev.currentTarget as any;
button.progress = true;
try {
await reloadSupervisor(this.hass);
this.supervisorInfo = await fetchHassioSupervisorInfo(this.hass);
} catch (err) {
showAlertDialog(this, {
title: "Failed to reload the supervisor",
text: extractApiErrorMessage(err),
});
}
button.progress = false;
}
private async _supervisorUpdate(ev: CustomEvent): Promise<void> {
const button = ev.currentTarget as any;
button.progress = true;
const confirmed = await showConfirmationDialog(this, {
title: "Update supervisor",
text: `Are you sure you want to update supervisor to version ${this.supervisorInfo.version_latest}?`,
confirmText: "update",
dismissText: "cancel",
});
if (!confirmed) {
button.progress = false;
return;
}
try {
await updateSupervisor(this.hass);
} catch (err) {
showAlertDialog(this, {
title: "Failed to update the supervisor",
text: extractApiErrorMessage(err),
});
}
button.progress = false;
}
private async _diagnosticsInformationDialog(): Promise<void> {
await showAlertDialog(this, {
title: "Help Improve Home Assistant",
text: html`Would you want to automatically share crash reports and
diagnostic information when the supervisor encounters unexpected errors?
<br /><br />
This will allow us to fix the problems, the information is only
accessible to the Home Assistant Core team and will not be shared with
others.
<br /><br />
The data does not include any private/sensitive information and you can
disable this in settings at any time you want.`,
});
}
private async _toggleDiagnostics(): Promise<void> {
try {
const data: SupervisorOptions = {
diagnostics: !this.supervisorInfo?.diagnostics,
};
await setSupervisorOption(this.hass, data);
} catch (err) {
showAlertDialog(this, {
title: "Failed to set supervisor option",
text: extractApiErrorMessage(err),
});
}
}
static get styles(): CSSResult[] {
return [
haStyle,
@@ -270,35 +103,83 @@ class HassioSupervisorInfo extends LitElement {
css`
ha-card {
height: 100%;
justify-content: space-between;
flex-direction: column;
display: flex;
}
.card-actions {
height: 48px;
border-top: none;
display: flex;
justify-content: space-between;
align-items: center;
}
button.link {
color: var(--primary-color);
}
ha-settings-row {
padding: 0;
height: 54px;
width: 100%;
}
ha-settings-row[three-line] {
height: 74px;
.card-content {
color: var(--primary-text-color);
box-sizing: border-box;
height: calc(100% - 47px);
}
ha-settings-row > div[slot="description"] {
white-space: normal;
color: var(--secondary-text-color);
.info {
width: 100%;
}
.info td:nth-child(2) {
text-align: right;
}
.errors {
color: var(--google-red-500);
margin-top: 16px;
}
`,
];
}
protected firstUpdated(): void {
this.addEventListener("hass-api-called", (ev) => this._apiCalled(ev));
}
private _apiCalled(ev): void {
if (ev.detail.success) {
this._errors = undefined;
return;
}
const response = ev.detail.response;
this._errors =
typeof response.body === "object"
? response.body.message || "Unknown error"
: response.body;
}
private async _joinBeta() {
const confirmed = await showConfirmationDialog(this, {
title: "WARNING",
text: html` Beta releases are for testers and early adopters and can
contain unstable code changes.
<br />
<b>
Make sure you have backups of your data before you activate this
feature.
</b>
<br /><br />
This includes beta releases for:
<li>Home Assistant Core</li>
<li>Home Assistant Supervisor</li>
<li>Home Assistant Operating System</li>
<br />
Do you want to join the beta channel?`,
confirmText: "join beta",
dismissText: "no",
});
if (!confirmed) {
return;
}
try {
const data: SupervisorOptions = { channel: "beta" };
await setSupervisorOption(this.hass, data);
const eventdata = {
success: true,
response: undefined,
path: "option",
};
fireEvent(this, "hass-api-called", eventdata);
} catch (err) {
this._errors = `Error joining beta channel, ${err.body?.message || err}`;
}
}
}
declare global {
+37 -42
View File
@@ -7,16 +7,13 @@ import {
CSSResult,
customElement,
html,
internalProperty,
LitElement,
property,
TemplateResult,
} from "lit-element";
import "../../../src/components/buttons/ha-progress-button";
import "../../../src/components/ha-card";
import { extractApiErrorMessage } from "../../../src/data/hassio/common";
import { fetchHassioLogs } from "../../../src/data/hassio/supervisor";
import "../../../src/layouts/hass-loading-screen";
import "../../../src/layouts/loading-screen";
import { haStyle } from "../../../src/resources/styles";
import { HomeAssistant } from "../../../src/types";
import "../components/hassio-ansi-to-html";
@@ -58,18 +55,18 @@ const logProviders: LogProvider[] = [
class HassioSupervisorLog extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@internalProperty() private _error?: string;
@property() private _error?: string;
@internalProperty() private _selectedLogProvider = "supervisor";
@property() private _selectedLogProvider = "supervisor";
@internalProperty() private _content?: string;
@property() private _content?: string;
public async connectedCallback(): Promise<void> {
super.connectedCallback();
await this._loadData();
}
protected render(): TemplateResult | void {
public render(): TemplateResult | void {
return html`
<ha-card>
${this._error ? html` <div class="errors">${this._error}</div> ` : ""}
@@ -101,52 +98,21 @@ class HassioSupervisorLog extends LitElement {
? html`<hassio-ansi-to-html
.content=${this._content}
></hassio-ansi-to-html>`
: html`<hass-loading-screen no-toolbar></hass-loading-screen>`}
: html`<loading-screen></loading-screen>`}
</div>
<div class="card-actions">
<ha-progress-button @click=${this._refresh}>
Refresh
</ha-progress-button>
<mwc-button @click=${this._refresh}>Refresh</mwc-button>
</div>
</ha-card>
`;
}
private async _setLogProvider(ev): Promise<void> {
const provider = ev.detail.item.getAttribute("provider");
this._selectedLogProvider = provider;
this._loadData();
}
private async _refresh(ev: CustomEvent): Promise<void> {
const button = ev.currentTarget as any;
button.progress = true;
await this._loadData();
button.progress = false;
}
private async _loadData(): Promise<void> {
this._error = undefined;
try {
this._content = await fetchHassioLogs(
this.hass,
this._selectedLogProvider
);
} catch (err) {
this._error = `Failed to get supervisor logs, ${extractApiErrorMessage(
err
)}`;
}
}
static get styles(): CSSResult[] {
return [
haStyle,
hassioStyle,
css`
ha-card {
margin-top: 8px;
width: 100%;
}
pre {
@@ -157,12 +123,41 @@ class HassioSupervisorLog extends LitElement {
width: 96%;
}
.errors {
color: var(--error-color);
color: var(--google-red-500);
margin-bottom: 16px;
}
.card-content {
padding-top: 0px;
}
`,
];
}
private async _setLogProvider(ev): Promise<void> {
const provider = ev.detail.item.getAttribute("provider");
this._selectedLogProvider = provider;
await this._loadData();
}
private async _loadData(): Promise<void> {
this._error = undefined;
this._content = undefined;
try {
this._content = await fetchHassioLogs(
this.hass,
this._selectedLogProvider
);
} catch (err) {
this._error = `Failed to get supervisor logs, ${
err.body?.message || err
}`;
}
}
private async _refresh(): Promise<void> {
await this._loadData();
}
}
declare global {
+5 -4
View File
@@ -12,13 +12,13 @@ import {
HassioHostInfo,
} from "../../../src/data/hassio/host";
import {
HassioInfo,
HassioSupervisorInfo,
HassioInfo,
} from "../../../src/data/hassio/supervisor";
import "../../../src/layouts/hass-tabs-subpage";
import { haStyle } from "../../../src/resources/styles";
import { HomeAssistant, Route } from "../../../src/types";
import { supervisorTabs } from "../hassio-tabs";
import { supervisorTabs } from "../hassio-panel";
import { hassioStyle } from "../resources/hassio-style";
import "./hassio-host-info";
import "./hassio-supervisor-info";
@@ -40,7 +40,7 @@ class HassioSystem extends LitElement {
@property({ attribute: false }) public hassOsInfo!: HassioHassOSInfo;
protected render(): TemplateResult | void {
public render(): TemplateResult | void {
return html`
<hass-tabs-subpage
.hass=${this.hass}
@@ -52,10 +52,10 @@ class HassioSystem extends LitElement {
>
<span slot="header">System</span>
<div class="content">
<h1>Information</h1>
<div class="card-group">
<hassio-supervisor-info
.hass=${this.hass}
.hostInfo=${this.hostInfo}
.supervisorInfo=${this.supervisorInfo}
></hassio-supervisor-info>
<hassio-host-info
@@ -65,6 +65,7 @@ class HassioSystem extends LitElement {
.hassOsInfo=${this.hassOsInfo}
></hassio-host-info>
</div>
<h1>System log</h1>
<hassio-supervisor-log .hass=${this.hass}></hassio-supervisor-log>
</div>
</hass-tabs-subpage>
+47 -49
View File
@@ -8,12 +8,12 @@
"version": "1.0.0",
"scripts": {
"build": "script/build_frontend",
"lint:eslint": "eslint \"**/src/**/*.{js,ts,html}\" --ignore-path .gitignore",
"format:eslint": "eslint \"**/src/**/*.{js,ts,html}\" --fix --ignore-path .gitignore",
"lint:prettier": "prettier \"**/src/**/*.{js,ts,json,css,md}\" --check",
"format:prettier": "prettier \"**/src/**/*.{js,ts,json,css,md}\" --write",
"lint:eslint": "eslint '**/src/**/*.{js,ts,html}' --ignore-path .gitignore",
"format:eslint": "eslint '**/src/**/*.{js,ts,html}' --fix --ignore-path .gitignore",
"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\" --format markdown --outFile result.md",
"lint:lit": "lit-analyzer '**/src/**/*.ts'",
"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",
@@ -22,30 +22,23 @@
"author": "Paulus Schoutsen <Paulus@PaulusSchoutsen.nl> (http://paulusschoutsen.nl)",
"license": "Apache-2.0",
"dependencies": {
"~app": "file:./src",
"@formatjs/intl-pluralrules": "^1.5.8",
"@fullcalendar/common": "5.1.0",
"@fullcalendar/core": "5.1.0",
"@fullcalendar/daygrid": "5.1.0",
"@fullcalendar/interaction": "5.1.0",
"@fullcalendar/list": "5.1.0",
"@material/chips": "=8.0.0-canary.096a7a066.0",
"@material/circular-progress": "=8.0.0-canary.a78ceb112.0",
"@material/mwc-button": "^0.18.0",
"@material/mwc-checkbox": "^0.18.0",
"@material/mwc-dialog": "^0.18.0",
"@material/mwc-fab": "^0.18.0",
"@material/mwc-formfield": "^0.18.0",
"@material/mwc-icon-button": "^0.18.0",
"@material/mwc-list": "^0.18.0",
"@material/mwc-menu": "^0.18.0",
"@material/mwc-radio": "^0.18.0",
"@material/mwc-ripple": "^0.18.0",
"@material/mwc-switch": "^0.18.0",
"@material/mwc-tab": "^0.18.0",
"@material/mwc-tab-bar": "^0.18.0",
"@material/top-app-bar": "=8.0.0-canary.096a7a066.0",
"@mdi/js": "5.5.55",
"@mdi/svg": "5.5.55",
"@fullcalendar/core": "^5.0.0-beta.2",
"@fullcalendar/daygrid": "^5.0.0-beta.2",
"@material/chips": "7.0.0-canary.d92d8c93e.0",
"@material/mwc-button": "^0.15.0",
"@material/mwc-checkbox": "^0.15.0",
"@material/mwc-dialog": "^0.15.0",
"@material/mwc-fab": "^0.15.0",
"@material/mwc-formfield": "^0.15.0",
"@material/mwc-icon-button": "^0.15.0",
"@material/mwc-list": "^0.15.0",
"@material/mwc-menu": "^0.15.0",
"@material/mwc-ripple": "^0.15.0",
"@material/mwc-switch": "^0.15.0",
"@mdi/js": "4.9.95",
"@mdi/svg": "4.9.95",
"@polymer/app-layout": "^3.0.2",
"@polymer/app-route": "^3.0.2",
"@polymer/app-storage": "^3.0.2",
@@ -55,6 +48,7 @@
"@polymer/iron-image": "^3.0.1",
"@polymer/iron-input": "^3.0.1",
"@polymer/iron-label": "^3.0.1",
"@polymer/iron-media-query": "^3.0.1",
"@polymer/iron-overlay-behavior": "^3.0.2",
"@polymer/iron-resizable-behavior": "^3.0.1",
"@polymer/paper-card": "^3.0.1",
@@ -72,14 +66,13 @@
"@polymer/paper-radio-group": "^3.0.1",
"@polymer/paper-ripple": "^3.0.1",
"@polymer/paper-slider": "^3.0.1",
"@polymer/paper-spinner": "^3.0.2",
"@polymer/paper-styles": "^3.0.1",
"@polymer/paper-tabs": "^3.0.1",
"@polymer/paper-toast": "^3.0.1",
"@polymer/paper-tooltip": "^3.0.1",
"@polymer/polymer": "3.1.0",
"@thomasloven/round-slider": "0.5.0",
"@types/chromecast-caf-sender": "^1.0.3",
"@types/sortablejs": "^1.10.6",
"@vaadin/vaadin-combo-box": "^5.0.10",
"@vaadin/vaadin-date-picker": "^4.0.7",
"@vue/web-component-wrapper": "^1.2.0",
@@ -89,15 +82,14 @@
"codemirror": "^5.49.0",
"comlink": "^4.3.0",
"cpx": "^1.5.0",
"cropperjs": "^1.5.7",
"deep-clone-simple": "^1.1.1",
"deep-freeze": "^0.0.1",
"es6-object-assign": "^1.1.0",
"fecha": "^4.2.0",
"fuse.js": "^6.0.0",
"google-timezones-json": "^1.0.2",
"hls.js": "^0.13.2",
"home-assistant-js-websocket": "^5.4.1",
"hls.js": "^0.12.4",
"home-assistant-js-websocket": "^5.3.0",
"idb-keyval": "^3.2.0",
"intl-messageformat": "^8.3.9",
"js-yaml": "^3.13.1",
@@ -106,17 +98,15 @@
"lit-element": "^2.3.1",
"lit-html": "^1.2.1",
"lit-virtualizer": "^0.4.2",
"marked": "^1.1.1",
"marked": "^0.6.1",
"mdn-polyfills": "^5.16.0",
"memoize-one": "^5.0.2",
"node-vibrant": "^3.1.5",
"proxy-polyfill": "^0.3.1",
"punycode": "^2.1.1",
"regenerator-runtime": "^0.13.2",
"resize-observer-polyfill": "^1.5.1",
"roboto-fontface": "^0.10.0",
"sortablejs": "^1.10.2",
"superstruct": "^0.10.12",
"superstruct": "^0.6.1",
"unfetch": "^4.1.0",
"vue": "^2.6.11",
"vue2-daterange-picker": "^0.5.1",
@@ -144,14 +134,13 @@
"@rollup/plugin-replace": "^2.3.2",
"@types/chai": "^4.1.7",
"@types/chromecast-caf-receiver": "^3.0.12",
"@types/codemirror": "^0.0.97",
"@types/codemirror": "^0.0.78",
"@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": "^7.0.2",
"@types/mocha": "^5.2.6",
"@types/resize-observer-browser": "^0.1.3",
"@types/webspeechapi": "^0.0.29",
"@typescript-eslint/eslint-plugin": "^2.28.0",
@@ -162,7 +151,7 @@
"eslint": "^6.8.0",
"eslint-config-airbnb-typescript": "^7.2.1",
"eslint-config-prettier": "^6.10.1",
"eslint-import-resolver-webpack": "^0.12.2",
"eslint-import-resolver-webpack": "^0.12.1",
"eslint-plugin-disable": "^2.0.1",
"eslint-plugin-import": "^2.20.2",
"eslint-plugin-lit": "^1.2.0",
@@ -180,12 +169,12 @@
"html-minifier": "^4.0.0",
"husky": "^1.3.1",
"lint-staged": "^8.1.5",
"lit-analyzer": "^1.2.0",
"lit-analyzer": "^1.1.10",
"lodash.template": "^4.5.0",
"magic-string": "^0.25.7",
"map-stream": "^0.0.7",
"merge-stream": "^1.0.1",
"mocha": "^7.2.0",
"mocha": "^6.0.2",
"object-hash": "^2.0.3",
"open": "^7.0.4",
"prettier": "^2.0.4",
@@ -201,8 +190,8 @@
"source-map-url": "^0.4.0",
"systemjs": "^6.3.2",
"terser-webpack-plugin": "^3.0.6",
"ts-lit-plugin": "^1.2.0",
"ts-mocha": "^7.0.0",
"ts-lit-plugin": "^1.1.10",
"ts-mocha": "^6.0.0",
"typescript": "^3.8.3",
"vinyl-buffer": "^1.0.1",
"vinyl-source-stream": "^2.0.0",
@@ -220,10 +209,19 @@
"@polymer/polymer": "3.1.0",
"lit-html": "1.2.1",
"lit-element": "2.3.1",
"@material/animation": "8.0.0-canary.096a7a066.0",
"@material/base": "8.0.0-canary.096a7a066.0",
"@material/feature-targeting": "8.0.0-canary.096a7a066.0",
"@material/theme": "8.0.0-canary.096a7a066.0"
"@material/animation": "7.0.0-canary.d92d8c93e.0",
"@material/base": "7.0.0-canary.d92d8c93e.0",
"@material/checkbox": "7.0.0-canary.d92d8c93e.0",
"@material/density": "7.0.0-canary.d92d8c93e.0",
"@material/dom": "7.0.0-canary.d92d8c93e.0",
"@material/elevation": "7.0.0-canary.d92d8c93e.0",
"@material/feature-targeting": "7.0.0-canary.d92d8c93e.0",
"@material/ripple": "7.0.0-canary.d92d8c93e.0",
"@material/rtl": "7.0.0-canary.d92d8c93e.0",
"@material/shape": "7.0.0-canary.d92d8c93e.0",
"@material/theme": "7.0.0-canary.d92d8c93e.0",
"@material/touch-target": "7.0.0-canary.d92d8c93e.0",
"@material/typography": "7.0.0-canary.d92d8c93e.0"
},
"main": "src/home-assistant.js",
"husky": {
+2 -19
View File
@@ -16,31 +16,14 @@ function today() {
)}${String(now.getDate()).padStart(2, "0")}.0`;
}
function auto(version) {
const todayVersion = today();
if (todayVersion !== version) {
return todayVersion;
}
return patch(version);
}
const methods = {
patch,
today,
auto,
};
async function main(args) {
let method;
let commit;
if (args.length === 0) {
method = methods.auto;
commit = true;
} else {
method = args.length > 0 && methods[args[0]];
commit = args.length > 1 && args[1] == "--commit";
}
const method = args.length > 0 && methods[args[0]];
const commit = args.length > 1 && args[1] == "--commit";
if (!method) {
console.error(
+1 -1
View File
@@ -2,7 +2,7 @@ from setuptools import setup, find_packages
setup(
name="home-assistant-frontend",
version="20200916.0",
version="20200620.0",
description="The Home Assistant frontend",
url="https://github.com/home-assistant/home-assistant-polymer",
author="The Home Assistant Authors",
+4 -5
View File
@@ -5,7 +5,6 @@ import {
html,
LitElement,
property,
internalProperty,
PropertyValues,
TemplateResult,
} from "lit-element";
@@ -29,13 +28,13 @@ class HaAuthFlow extends litLocalizeLiteMixin(LitElement) {
@property() public oauth2State?: string;
@internalProperty() private _state: State = "loading";
@property() private _state: State = "loading";
@internalProperty() private _stepData: any = {};
@property() private _stepData: any = {};
@internalProperty() private _step?: DataEntryFlowStep;
@property() private _step?: DataEntryFlowStep;
@internalProperty() private _errorMessage?: string;
@property() private _errorMessage?: string;
protected render() {
return html`
+3 -5
View File
@@ -4,7 +4,6 @@ import {
html,
LitElement,
property,
internalProperty,
PropertyValues,
} from "lit-element";
import {
@@ -16,7 +15,6 @@ import { litLocalizeLiteMixin } from "../mixins/lit-localize-lite-mixin";
import { registerServiceWorker } from "../util/register-service-worker";
import "./ha-auth-flow";
import { extractSearchParamsObject } from "../common/url/search-params";
import punycode from "punycode";
import(/* webpackChunkName: "pick-auth-provider" */ "./ha-pick-auth-provider");
@@ -27,9 +25,9 @@ class HaAuthorize extends litLocalizeLiteMixin(LitElement) {
@property() public oauth2State?: string;
@internalProperty() private _authProvider?: AuthProvider;
@property() private _authProvider?: AuthProvider;
@internalProperty() private _authProviders?: AuthProvider[];
@property() private _authProviders?: AuthProvider[];
constructor() {
super();
@@ -76,7 +74,7 @@ class HaAuthorize extends litLocalizeLiteMixin(LitElement) {
${this.localize(
"ui.panel.page-authorize.authorizing_client",
"clientId",
this.clientId ? punycode.toASCII(this.clientId) : this.clientId
this.clientId
)}
</p>
${loggingInWith}
+131
View File
@@ -0,0 +1,131 @@
import "@polymer/paper-card/paper-card";
import { html } from "@polymer/polymer/lib/utils/html-tag";
/* eslint-plugin-disable lit */
import { PolymerElement } from "@polymer/polymer/polymer-element";
import { computeStateName } from "../common/entity/compute_state_name";
import "../components/state-history-charts";
import "../data/ha-state-history-data";
import { EventsMixin } from "../mixins/events-mixin";
/*
* @appliesMixin EventsMixin
*/
class HaHistoryGraphCard extends EventsMixin(PolymerElement) {
static get template() {
return html`
<style>
paper-card:not([dialog]) .content {
padding: 0 16px 16px;
}
paper-card[dialog] {
padding-top: 16px;
background-color: transparent;
}
paper-card {
width: 100%;
/* prevent new stacking context, chart tooltip needs to overflow */
position: static;
}
.header {
@apply --paper-font-headline;
line-height: 40px;
color: var(--primary-text-color);
padding: 20px 16px 12px;
@apply --paper-font-common-nowrap;
}
paper-card[dialog] .header {
display: none;
}
</style>
<ha-state-history-data
hass="[[hass]]"
filter-type="recent-entity"
entity-id="[[computeHistoryEntities(stateObj)]]"
data="{{stateHistory}}"
is-loading="{{stateHistoryLoading}}"
cache-config="[[cacheConfig]]"
></ha-state-history-data>
<paper-card
dialog$="[[inDialog]]"
on-click="cardTapped"
elevation="[[computeElevation(inDialog)]]"
>
<div class="header">[[computeTitle(stateObj)]]</div>
<div class="content">
<state-history-charts
hass="[[hass]]"
history-data="[[stateHistory]]"
is-loading-data="[[stateHistoryLoading]]"
up-to-now
no-single
>
</state-history-charts>
</div>
</paper-card>
`;
}
static get properties() {
return {
hass: Object,
stateObj: {
type: Object,
observer: "stateObjObserver",
},
inDialog: {
type: Boolean,
value: false,
},
stateHistory: Object,
stateHistoryLoading: Boolean,
cacheConfig: {
type: Object,
value: {
refresh: 0,
cacheKey: null,
hoursToShow: 24,
},
},
};
}
stateObjObserver(stateObj) {
if (!stateObj) return;
if (
this.cacheConfig.cacheKey !== stateObj.entity_id ||
this.cacheConfig.refresh !== (stateObj.attributes.refresh || 0) ||
this.cacheConfig.hoursToShow !== (stateObj.attributes.hours_to_show || 24)
) {
this.cacheConfig = {
refresh: stateObj.attributes.refresh || 0,
cacheKey: stateObj.entity_id,
hoursToShow: stateObj.attributes.hours_to_show || 24,
};
}
}
computeTitle(stateObj) {
return computeStateName(stateObj);
}
computeContentClass(inDialog) {
return inDialog ? "" : "content";
}
computeHistoryEntities(stateObj) {
return stateObj.attributes.entity_id;
}
computeElevation(inDialog) {
return inDialog ? 0 : 1;
}
cardTapped(ev) {
const mq = window.matchMedia("(min-width: 610px) and (min-height: 550px)");
if (mq.matches) {
ev.stopPropagation();
this.fire("hass-more-info", { entityId: this.stateObj.entity_id });
}
}
}
customElements.define("ha-history_graph-card", HaHistoryGraphCard);
+5 -6
View File
@@ -1,8 +1,4 @@
/* 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";
@@ -44,13 +40,16 @@ 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)
);
@@ -119,7 +118,7 @@ export class CastManager {
}
}
private _sessionStateChanged(ev: SessionStateEventData) {
private _sessionStateChanged(ev) {
if (__DEV__) {
console.log("Cast session state changed", ev.sessionState);
}
@@ -142,7 +141,7 @@ export class CastManager {
}
}
private _castStateChanged(ev: CastStateEventData) {
private _castStateChanged(ev) {
if (__DEV__) {
console.log("Cast state changed", ev.castState);
}
-113
View File
@@ -1,113 +0,0 @@
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
View File
@@ -1,16 +0,0 @@
// 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
View File
@@ -1,24 +0,0 @@
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);
};
@@ -1,9 +0,0 @@
import { HomeAssistant } from "../../types";
/** Return an array of domains with the service. */
export const componentsWithService = (
hass: HomeAssistant,
service: string
): Array<string> =>
hass &&
Object.keys(hass.services).filter((key) => service in hass.services[key]);
-9
View File
@@ -1,9 +0,0 @@
import { HomeAssistant } from "../../types";
/** Return if a service is loaded. */
export const isServiceLoaded = (
hass: HomeAssistant,
domain: string,
service: string
): boolean =>
hass && domain in hass.services && service in hass.services[domain];
+10 -3
View File
@@ -22,6 +22,7 @@ export const DOMAINS_WITH_CARD = [
"timer",
"vacuum",
"water_heater",
"weblink",
];
/** Domains with separate more info dialog. */
@@ -35,7 +36,7 @@ export const DOMAINS_WITH_MORE_INFO = [
"cover",
"fan",
"group",
"humidifier",
"history_graph",
"input_datetime",
"light",
"lock",
@@ -44,6 +45,7 @@ export const DOMAINS_WITH_MORE_INFO = [
"script",
"sun",
"timer",
"updater",
"vacuum",
"water_heater",
"weather",
@@ -55,10 +57,16 @@ export const DOMAINS_HIDE_MORE_INFO = [
"input_select",
"input_text",
"scene",
"weblink",
];
/** Domains that should have the history hidden in the more info dialog. */
export const DOMAINS_MORE_INFO_NO_HISTORY = ["camera", "configurator", "scene"];
export const DOMAINS_MORE_INFO_NO_HISTORY = [
"camera",
"configurator",
"history_graph",
"scene",
];
/** States that we consider "off". */
export const STATES_OFF = ["closed", "locked", "off"];
@@ -71,7 +79,6 @@ export const DOMAINS_TOGGLE = new Set([
"switch",
"group",
"automation",
"humidifier",
]);
/** Temperature units. */
+16 -14
View File
@@ -20,29 +20,31 @@ export default function relativeTime(
let delta = (compareTime.getTime() - dateObj.getTime()) / 1000;
const tense = delta >= 0 ? "past" : "future";
delta = Math.abs(delta);
let roundedDelta = Math.round(delta);
if (roundedDelta === 0) {
return localize("ui.components.relative_time.just_now");
}
let unit = "week";
let timeDesc;
for (let i = 0; i < tests.length; i++) {
if (roundedDelta < tests[i]) {
unit = langKey[i];
if (delta < tests[i]) {
delta = Math.floor(delta);
timeDesc = localize(
`ui.components.relative_time.duration.${langKey[i]}`,
"count",
delta
);
break;
}
delta /= tests[i];
roundedDelta = Math.round(delta);
}
const timeDesc = localize(
`ui.components.relative_time.duration.${unit}`,
"count",
roundedDelta
);
if (timeDesc === undefined) {
delta = Math.floor(delta);
timeDesc = localize(
"ui.components.relative_time.duration.week",
"count",
delta
);
}
return options.includeTense === false
? timeDesc
-155
View File
@@ -1,155 +0,0 @@
import { UnsubscribeFunc } from "home-assistant-js-websocket";
import { PropertyDeclaration, UpdatingElement } from "lit-element";
import type { ClassElement } from "../../types";
type Callback = (oldValue: any, newValue: any) => void;
class Storage {
constructor() {
window.addEventListener("storage", (ev: StorageEvent) => {
if (ev.key && this.hasKey(ev.key)) {
this._storage[ev.key] = ev.newValue
? JSON.parse(ev.newValue)
: ev.newValue;
if (this._listeners[ev.key]) {
this._listeners[ev.key].forEach((listener) =>
listener(
ev.oldValue ? JSON.parse(ev.oldValue) : ev.oldValue,
this._storage[ev.key!]
)
);
}
}
});
}
private _storage: { [storageKey: string]: any } = {};
private _listeners: {
[storageKey: string]: Callback[];
} = {};
public addFromStorage(storageKey: any): void {
if (!this._storage[storageKey]) {
const data = window.localStorage.getItem(storageKey);
if (data) {
this._storage[storageKey] = JSON.parse(data);
}
}
}
public subscribeChanges(
storageKey: string,
callback: Callback
): UnsubscribeFunc {
if (this._listeners[storageKey]) {
this._listeners[storageKey].push(callback);
} else {
this._listeners[storageKey] = [callback];
}
return () => {
this.unsubscribeChanges(storageKey, callback);
};
}
public unsubscribeChanges(storageKey: string, callback: Callback) {
if (!(storageKey in this._listeners)) {
return;
}
const index = this._listeners[storageKey].indexOf(callback);
if (index !== -1) {
this._listeners[storageKey].splice(index, 1);
}
}
public hasKey(storageKey: string): any {
return storageKey in this._storage;
}
public getValue(storageKey: string): any {
return this._storage[storageKey];
}
public setValue(storageKey: string, value: any): any {
this._storage[storageKey] = value;
try {
window.localStorage.setItem(storageKey, JSON.stringify(value));
} catch (err) {
// Safari in private mode doesn't allow localstorage
}
}
}
const storage = new Storage();
export const LocalStorage = (
storageKey?: string,
property?: boolean,
propertyOptions?: PropertyDeclaration
): any => {
return (clsElement: ClassElement) => {
const key = String(clsElement.key);
storageKey = storageKey || String(clsElement.key);
const initVal = clsElement.initializer
? clsElement.initializer()
: undefined;
storage.addFromStorage(storageKey);
const subscribe = (el: UpdatingElement): UnsubscribeFunc =>
storage.subscribeChanges(storageKey!, (oldValue) => {
el.requestUpdate(clsElement.key, oldValue);
});
const getValue = (): any => {
return storage.hasKey(storageKey!)
? storage.getValue(storageKey!)
: initVal;
};
const setValue = (el: UpdatingElement, value: any) => {
let oldValue: unknown | undefined;
if (property) {
oldValue = getValue();
}
storage.setValue(storageKey!, value);
if (property) {
el.requestUpdate(clsElement.key, oldValue);
}
};
return {
kind: "method",
placement: "prototype",
key: clsElement.key,
descriptor: {
set(this: UpdatingElement, value: unknown) {
setValue(this, value);
},
get() {
return getValue();
},
enumerable: true,
configurable: true,
},
finisher(cls: typeof UpdatingElement) {
if (property) {
const connectedCallback = cls.prototype.connectedCallback;
const disconnectedCallback = cls.prototype.disconnectedCallback;
cls.prototype.connectedCallback = function () {
connectedCallback.call(this);
this[`__unbsubLocalStorage${key}`] = subscribe(this);
};
cls.prototype.disconnectedCallback = function () {
disconnectedCallback.call(this);
this[`__unbsubLocalStorage${key}`]();
};
cls.createProperty(clsElement.key, {
noAccessor: true,
...propertyOptions,
});
}
},
};
};
};
-33
View File
@@ -1,33 +0,0 @@
import type { LitElement } from "lit-element";
import type { ClassElement } from "../../types";
export const restoreScroll = (selector: string): any => {
return (element: ClassElement) => ({
kind: "method",
placement: "prototype",
key: element.key,
descriptor: {
set(this: LitElement, value: number) {
this[`__${String(element.key)}`] = value;
},
get(this: LitElement) {
return this[`__${String(element.key)}`];
},
enumerable: true,
configurable: true,
},
finisher(cls: typeof LitElement) {
const connectedCallback = cls.prototype.connectedCallback;
cls.prototype.connectedCallback = function () {
connectedCallback.call(this);
if (this[element.key]) {
const target = this.renderRoot.querySelector(selector);
if (!target) {
return;
}
target.scrollTop = this[element.key];
}
};
},
});
};
+34 -70
View File
@@ -1,20 +1,26 @@
import { darkStyles, derivedStyles } from "../../resources/styles";
import { derivedStyles } from "../../resources/styles";
import { HomeAssistant, Theme } from "../../types";
import {
hex2rgb,
lab2hex,
lab2rgb,
rgb2hex,
rgb2lab,
} from "../color/convert-color";
import { labBrighten, labDarken } from "../color/lab";
import { rgbContrast } from "../color/rgb";
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 } = {};
/**
@@ -27,56 +33,17 @@ let PROCESSED_THEMES: { [key: string]: ProcessedTheme } = {};
export const applyThemesOnElement = (
element,
themes: HomeAssistant["themes"],
selectedTheme?: string,
themeOptions?: Partial<HomeAssistant["selectedTheme"]>
selectedTheme?: string
) => {
let cacheKey = selectedTheme;
let themeRules: Partial<Theme> = {};
const newTheme = selectedTheme
? PROCESSED_THEMES[selectedTheme] || processTheme(selectedTheme, themes)
: undefined;
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) {
if (!element._themes && !newTheme) {
// 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;
@@ -91,45 +58,42 @@ export const applyThemesOnElement = (
};
const processTheme = (
cacheKey: string,
theme: Partial<Theme>
themeName: string,
themes: HomeAssistant["themes"]
): ProcessedTheme | undefined => {
if (!theme || !Object.keys(theme).length) {
if (!themes.themes[themeName]) {
return undefined;
}
const combinedTheme: Partial<Theme> = {
const theme: Theme = {
...derivedStyles,
...theme,
...themes.themes[themeName],
};
const styles = {};
const keys = {};
for (const key of Object.keys(combinedTheme)) {
for (const key of Object.keys(theme)) {
const prefixedKey = `--${key}`;
const value = String(combinedTheme[key]);
const value = theme[key];
styles[prefixedKey] = value;
keys[prefixedKey] = "";
// Try to create a rgb value for this key if it is not a var
// Try to create a rgb value for this key if it is a hex color
if (!value.startsWith("#")) {
// Can't convert non hex value
// Not a hex color
continue;
}
const rgbKey = `rgb-${key}`;
if (combinedTheme[rgbKey] !== undefined) {
if (theme[rgbKey] !== undefined) {
// Theme has it's own rgb value
continue;
}
try {
const rgbValue = hex2rgb(value).join(",");
const rgbValue = hexToRgb(value);
if (rgbValue !== null) {
const prefixedRgbKey = `--${rgbKey}`;
styles[prefixedRgbKey] = rgbValue;
keys[prefixedRgbKey] = "";
} catch (e) {
continue;
}
}
PROCESSED_THEMES[cacheKey] = { styles, keys };
PROCESSED_THEMES[themeName] = { styles, keys };
return { styles, keys };
};
-8
View File
@@ -1,8 +0,0 @@
export const deepActiveElement = (
root: DocumentOrShadowRoot = document
): Element | null => {
if (root.activeElement?.shadowRoot?.activeElement) {
return deepActiveElement(root.activeElement.shadowRoot);
}
return root.activeElement;
};
+7 -20
View File
@@ -1,4 +1,4 @@
import type { Map, TileLayer } from "leaflet";
import type { Map } 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?: boolean,
darkMode = false,
draw = false
): Promise<[Map, LeafletModuleType, TileLayer]> => {
): Promise<[Map, LeafletModuleType]> => {
if (!mapElement.parentNode) {
throw new Error("Cannot setup Leaflet map on disconnected element");
}
@@ -28,28 +28,15 @@ export const setupLeafletMap = async (
style.setAttribute("rel", "stylesheet");
mapElement.parentNode.appendChild(style);
map.setView([52.3731339, 4.8903147], 13);
createTileLayer(Leaflet, darkMode).addTo(map);
const tileLayer = createTileLayer(Leaflet, Boolean(darkMode)).addTo(map);
return [map, Leaflet, tileLayer];
return [map, Leaflet];
};
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 = (
export const createTileLayer = (
leaflet: LeafletModuleType,
darkMode: boolean
): TileLayer => {
) => {
return leaflet.tileLayer(
`https://{s}.basemaps.cartocdn.com/${
darkMode ? "dark_all" : "light_all"
-28
View File
@@ -1,28 +0,0 @@
/** Return an icon representing a battery state. */
import { HassEntity } from "home-assistant-js-websocket";
export const batteryIcon = (
batteryState: HassEntity,
batteryChargingState?: HassEntity
) => {
const battery = Number(batteryState.state);
const battery_charging =
batteryChargingState && batteryChargingState.state === "on";
if (isNaN(battery)) {
return "hass:battery-unknown";
}
let icon = "hass:battery";
const batteryRound = Math.round(battery / 10) * 10;
if (battery_charging && battery > 10) {
icon += `-charging-${batteryRound}`;
} else if (battery_charging) {
icon += "-outline";
} else if (battery <= 5) {
icon += "-alert";
} else if (battery > 5 && battery < 95) {
icon += `-${batteryRound}`;
}
return icon;
};
+20 -22
View File
@@ -3,51 +3,49 @@ import { HassEntity } from "home-assistant-js-websocket";
/** Return an icon representing a binary sensor state. */
export const binarySensorIcon = (state: HassEntity) => {
const is_off = state.state && state.state === "off";
const activated = state.state && state.state === "off";
switch (state.attributes.device_class) {
case "battery":
return is_off ? "hass:battery" : "hass:battery-outline";
case "battery_charging":
return is_off ? "hass:battery" : "hass:battery-charging";
return activated ? "hass:battery" : "hass:battery-outline";
case "cold":
return is_off ? "hass:thermometer" : "hass:snowflake";
return activated ? "hass:thermometer" : "hass:snowflake";
case "connectivity":
return is_off ? "hass:server-network-off" : "hass:server-network";
return activated ? "hass:server-network-off" : "hass:server-network";
case "door":
return is_off ? "hass:door-closed" : "hass:door-open";
return activated ? "hass:door-closed" : "hass:door-open";
case "garage_door":
return is_off ? "hass:garage" : "hass:garage-open";
return activated ? "hass:garage" : "hass:garage-open";
case "gas":
case "power":
case "problem":
case "safety":
case "smoke":
return is_off ? "hass:shield-check" : "hass:alert";
return activated ? "hass:shield-check" : "hass:alert";
case "heat":
return is_off ? "hass:thermometer" : "hass:fire";
return activated ? "hass:thermometer" : "hass:fire";
case "light":
return is_off ? "hass:brightness-5" : "hass:brightness-7";
return activated ? "hass:brightness-5" : "hass:brightness-7";
case "lock":
return is_off ? "hass:lock" : "hass:lock-open";
return activated ? "hass:lock" : "hass:lock-open";
case "moisture":
return is_off ? "hass:water-off" : "hass:water";
return activated ? "hass:water-off" : "hass:water";
case "motion":
return is_off ? "hass:walk" : "hass:run";
return activated ? "hass:walk" : "hass:run";
case "occupancy":
return is_off ? "hass:home-outline" : "hass:home";
return activated ? "hass:home-outline" : "hass:home";
case "opening":
return is_off ? "hass:square" : "hass:square-outline";
return activated ? "hass:square" : "hass:square-outline";
case "plug":
return is_off ? "hass:power-plug-off" : "hass:power-plug";
return activated ? "hass:power-plug-off" : "hass:power-plug";
case "presence":
return is_off ? "hass:home-outline" : "hass:home";
return activated ? "hass:home-outline" : "hass:home";
case "sound":
return is_off ? "hass:music-note-off" : "hass:music-note";
return activated ? "hass:music-note-off" : "hass:music-note";
case "vibration":
return is_off ? "hass:crop-portrait" : "hass:vibrate";
return activated ? "hass:crop-portrait" : "hass:vibrate";
case "window":
return is_off ? "hass:window-closed" : "hass:window-open";
return activated ? "hass:window-closed" : "hass:window-open";
default:
return is_off ? "hass:radiobox-blank" : "hass:checkbox-marked-circle";
return activated ? "hass:radiobox-blank" : "hass:checkbox-marked-circle";
}
};
@@ -55,12 +55,6 @@ export const computeStateDisplay = (
return formatDateTime(date, language);
}
if (domain === "humidifier") {
if (stateObj.state === "on" && stateObj.attributes.humidity) {
return `${stateObj.attributes.humidity}%`;
}
}
return (
// Return device class translation
(stateObj.attributes.device_class &&
+5 -5
View File
@@ -8,27 +8,26 @@ import { DEFAULT_DOMAIN_ICON } from "../const";
const fixedIcons = {
alert: "hass:alert",
alexa: "hass:amazon-alexa",
air_quality: "hass:air-filter",
automation: "hass:robot",
calendar: "hass:calendar",
camera: "hass:video",
climate: "hass:thermostat",
configurator: "hass:cog",
configurator: "hass:settings",
conversation: "hass:text-to-speech",
counter: "hass:counter",
device_tracker: "hass:account",
fan: "hass:fan",
google_assistant: "hass:google-assistant",
group: "hass:google-circles-communities",
history_graph: "hass:chart-line",
homeassistant: "hass:home-assistant",
homekit: "hass:home-automation",
humidifier: "hass:air-humidifier",
image_processing: "hass:image-filter-frames",
input_boolean: "hass:toggle-switch-outline",
input_datetime: "hass:calendar-clock",
input_number: "hass:ray-vertex",
input_select: "hass:format-list-bulleted",
input_text: "hass:form-textbox",
input_text: "hass:textbox",
light: "hass:lightbulb",
mailbox: "hass:mailbox",
notify: "hass:comment-alert",
@@ -43,11 +42,12 @@ const fixedIcons = {
simple_alarm: "hass:bell",
sun: "hass:white-balance-sunny",
switch: "hass:flash",
timer: "hass:timer-outline",
timer: "hass:timer",
updater: "hass:cloud-upload",
vacuum: "hass:robot-vacuum",
water_heater: "hass:thermometer",
weather: "hass:weather-cloudy",
weblink: "hass:open-in-new",
zone: "hass:map-marker-radius",
};
+11 -11
View File
@@ -14,20 +14,20 @@ export const getViewEntities = (
view.attributes.entity_id.forEach((entityId) => {
const entity = entities[entityId];
if (!entity) {
return;
}
if (entity && !entity.attributes.hidden) {
viewEntities[entity.entity_id] = entity;
viewEntities[entity.entity_id] = entity;
if (computeDomain(entity.entity_id) === "group") {
const groupEntities = getGroupEntities(entities, entity as GroupEntity);
if (computeDomain(entity.entity_id) === "group") {
const groupEntities = getGroupEntities(entities, entity as GroupEntity);
Object.keys(groupEntities).forEach((grEntityId) => {
const grEntity = groupEntities[grEntityId];
Object.keys(groupEntities).forEach((grEntityId) => {
const grEntity = groupEntities[grEntityId];
viewEntities[grEntityId] = grEntity;
});
if (!grEntity.attributes.hidden) {
viewEntities[grEntityId] = grEntity;
}
});
}
}
});
+23 -6
View File
@@ -2,19 +2,14 @@
import { HassEntity } from "home-assistant-js-websocket";
import { UNIT_C, UNIT_F } from "../const";
import { domainIcon } from "./domain_icon";
import { batteryIcon } from "./battery_icon";
const fixedDeviceClassIcons = {
current: "hass:current-ac",
energy: "hass:flash",
humidity: "hass:water-percent",
illuminance: "hass:brightness-5",
temperature: "hass:thermometer",
pressure: "hass:gauge",
power: "hass:flash",
power_factor: "hass:angle-acute",
signal_strength: "hass:wifi",
voltage: "hass:sine-wave",
};
export const sensorIcon = (state: HassEntity) => {
@@ -24,7 +19,29 @@ export const sensorIcon = (state: HassEntity) => {
return fixedDeviceClassIcons[dclass];
}
if (dclass === "battery") {
return batteryIcon(state);
const battery = Number(state.state);
if (isNaN(battery)) {
return "hass:battery-unknown";
}
const batteryRound = Math.round(battery / 10) * 10;
if (batteryRound >= 100) {
return "hass:battery";
}
if (batteryRound <= 0) {
return "hass:battery-alert";
}
// Will return one of the following icons: (listed so extractor picks up)
// hass:battery-10
// hass:battery-20
// hass:battery-30
// hass:battery-40
// hass:battery-50
// hass:battery-60
// hass:battery-70
// hass:battery-80
// hass:battery-90
// We obscure 'hass' in iconname so this name does not get picked up
return `${"hass"}:battery-${batteryRound}`;
}
const unit = state.attributes.unit_of_measurement;
+1 -2
View File
@@ -3,10 +3,9 @@ import { HomeAssistant } from "../../types";
import { DOMAINS_WITH_CARD } from "../const";
import { canToggleState } from "./can_toggle_state";
import { computeStateDomain } from "./compute_state_domain";
import { UNAVAILABLE } from "../../data/entity";
export const stateCardType = (hass: HomeAssistant, stateObj: HassEntity) => {
if (stateObj.state === UNAVAILABLE) {
if (stateObj.state === "unavailable") {
return "display";
}
+1 -1
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;
};
+1 -6
View File
@@ -1,12 +1,7 @@
import { HassEntity } from "home-assistant-js-websocket";
import durationToSeconds from "../datetime/duration_to_seconds";
export const timerTimeRemaining = (
stateObj: HassEntity
): undefined | number => {
if (!stateObj.attributes.remaining) {
return undefined;
}
export const timerTimeRemaining = (stateObj: HassEntity) => {
let timeRemaining = durationToSeconds(stateObj.attributes.remaining);
if (stateObj.state === "active") {
+8
View File
@@ -2,3 +2,11 @@ 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
@@ -1,14 +0,0 @@
import {
ListItem,
RequestSelectedDetail,
} 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.currentTarget as ListItem).selected = false;
return true;
};
+1 -4
View File
@@ -26,9 +26,6 @@ class SearchInput extends LitElement {
@property({ type: Boolean })
public autofocus = false;
@property({ type: String })
public label?: string;
public focus() {
this.shadowRoot!.querySelector("paper-input")!.focus();
}
@@ -46,7 +43,7 @@ class SearchInput extends LitElement {
<paper-input
class=${classMap({ "no-underline": this.noUnderline })}
.autofocus=${this.autofocus}
.label=${this.label || "Search"}
label="Search"
.value=${this.filter}
@value-changed=${this._filterInputChanged}
.noLabelFloat=${this.noLabelFloat}
+8 -8
View File
@@ -1,19 +1,19 @@
// https://gist.github.com/hagemann/382adfc57adbd5af078dc93feef01fe1
export const slugify = (value: string, delimiter = "_") => {
export const slugify = (value: string) => {
const a =
"àáäâãåăæąçćčđďèéěėëêęğǵḧìíïîįłḿǹńňñòóöôœøṕŕřßşśšșťțùúüûǘůűūųẃẍÿýźžż·/_,:;";
const b = `aaaaaaaaacccddeeeeeeegghiiiiilmnnnnooooooprrsssssttuuuuuuuuuwxyyzzz${delimiter}${delimiter}${delimiter}${delimiter}${delimiter}${delimiter}`;
const b =
"aaaaaaaaacccddeeeeeeegghiiiiilmnnnnooooooprrsssssttuuuuuuuuuwxyyzzz------";
const p = new RegExp(a.split("").join("|"), "g");
return value
.toString()
.toLowerCase()
.replace(/\s+/g, delimiter) // Replace spaces with delimiter
.replace(/\s+/g, "-") // Replace spaces with -
.replace(p, (c) => b.charAt(a.indexOf(c))) // Replace special characters
.replace(/&/g, `${delimiter}and${delimiter}`) // Replace & with 'and'
.replace(/&/g, "-and-") // Replace & with 'and'
.replace(/[^\w-]+/g, "") // Remove all non-word characters
.replace(/-/, delimiter) // Replace - with delimiter
.replace(new RegExp(`/${delimiter}${delimiter}+/`, "g"), delimiter) // Replace multiple delimiters with single delimiter
.replace(new RegExp(`/^${delimiter}+/`), "") // Trim delimiter from start of text
.replace(new RegExp(`/-+$/`), ""); // Trim delimiter from end of text
.replace(/--+/g, "-") // Replace multiple - with single -
.replace(/^-+/, "") // Trim - from start of text
.replace(/-+$/, ""); // Trim - from end of text
};
+1 -7
View File
@@ -8,7 +8,6 @@ export const iconColorCSS = css`
ha-icon[data-domain="camera"][data-state="streaming"],
ha-icon[data-domain="cover"][data-state="open"],
ha-icon[data-domain="fan"][data-state="on"],
ha-icon[data-domain="humidifier"][data-state="on"],
ha-icon[data-domain="light"][data-state="on"],
ha-icon[data-domain="input_boolean"][data-state="on"],
ha-icon[data-domain="lock"][data-state="unlocked"],
@@ -19,12 +18,7 @@ export const iconColorCSS = css`
ha-icon[data-domain="sun"][data-state="above_horizon"],
ha-icon[data-domain="switch"][data-state="on"],
ha-icon[data-domain="timer"][data-state="active"],
ha-icon[data-domain="vacuum"][data-state="cleaning"],
ha-icon[data-domain="group"][data-state="on"],
ha-icon[data-domain="group"][data-state="home"],
ha-icon[data-domain="group"][data-state="open"],
ha-icon[data-domain="group"][data-state="locked"],
ha-icon[data-domain="group"][data-state="problem"] {
ha-icon[data-domain="vacuum"][data-state="cleaning"] {
color: var(--paper-item-icon-active-color, #fdd835);
}

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