Compare commits

..

1 Commits

Author SHA1 Message Date
Donnie
3fccd63bd3 Remove debounced filter until select all issue is addressed 2020-10-21 14:11:26 -07:00
563 changed files with 10525 additions and 24477 deletions

View File

@@ -1,13 +0,0 @@
# See here for image contents: https://github.com/microsoft/vscode-dev-containers/tree/v0.148.1/containers/python-3/.devcontainer/base.Dockerfile
FROM mcr.microsoft.com/vscode/devcontainers/python:0-3.9
ENV \
DEBIAN_FRONTEND=noninteractive \
DEVCONTAINER=true \
PATH=$PATH:./node_modules/.bin
# Install nvm
COPY .nvmrc /tmp/.nvmrc
RUN \
su vscode -c \
"source /usr/local/share/nvm/nvm.sh && nvm install $(cat /tmp/.nvmrc) 2>&1"

View File

@@ -1,31 +0,0 @@
{
"name": "Home Assistant Frontend",
"build": {
"dockerfile": "Dockerfile",
"context": ".."
},
"appPort": 8123,
"context": "..",
"postCreateCommand": "script/bootstrap",
"extensions": [
"github.vscode-pull-request-github",
"dbaeumer.vscode-eslint",
"ms-vscode.vscode-typescript-tslint-plugin",
"esbenp.prettier-vscode",
"bierner.lit-html",
"runem.lit-plugin",
"ms-python.vscode-pylance"
],
"settings": {
"terminal.integrated.shell.linux": "/bin/bash",
"files.eol": "\n",
"editor.tabSize": 2,
"editor.formatOnPaste": false,
"editor.formatOnSave": true,
"editor.formatOnType": true,
"[typescript]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"files.trimTrailingWhitespace": true
}
}

View File

@@ -18,8 +18,8 @@
<!--
Describe the big picture of your changes here to communicate to the
maintainers why we should accept this pull request. If it fixes a bug
or resolves a feature request, be sure to link to that issue or discussion
in the additional information section.
or resolves a feature request, be sure to link to that issue in the
additional information section.
-->
## Type of change
@@ -56,7 +56,7 @@
-->
- This PR fixes or closes issue: fixes #
- This PR is related to issue or discussion:
- This PR is related to issue:
- Link to documentation pull request:
## Checklist

27
.github/lock.yml vendored Normal file
View File

@@ -0,0 +1,27 @@
# Configuration for Lock Threads - https://github.com/dessant/lock-threads
# Number of days of inactivity before a closed issue or pull request is locked
daysUntilLock: 1
# Skip issues and pull requests created before a given timestamp. Timestamp must
# follow ISO 8601 (`YYYY-MM-DD`). Set to `false` to disable
skipCreatedBefore: 2020-01-01
# Issues and pull requests with these labels will be ignored. Set to `[]` to disable
exemptLabels: []
# Label to add before locking, such as `outdated`. Set to `false` to disable
lockLabel: false
# Comment to post before locking. Set to `false` to disable
lockComment: false
# Assign `resolved` as the reason for locking. Set to `false` to disable
setLockReason: false
# Limit to only `issues` or `pulls`
only: pulls
# Optionally, specify configuration settings just for `issues` or `pulls`
issues:
daysUntilLock: 30

56
.github/stale.yml vendored Normal file
View File

@@ -0,0 +1,56 @@
# Configuration for probot-stale - https://github.com/probot/stale
# Number of days of inactivity before an Issue or Pull Request becomes stale
daysUntilStale: 90
# Number of days of inactivity before an Issue or Pull Request with the stale label is closed.
# Set to false to disable. If disabled, issues still need to be closed manually, but will remain marked as stale.
daysUntilClose: 7
# Only issues or pull requests with all of these labels are check if stale. Defaults to `[]` (disabled)
onlyLabels: []
# Issues or Pull Requests with these labels will never be considered stale. Set to `[]` to disable
exemptLabels:
- feature request
- Help wanted
- to do
# Set to true to ignore issues in a project (defaults to false)
exemptProjects: true
# Set to true to ignore issues in a milestone (defaults to false)
exemptMilestones: true
# Set to true to ignore issues with an assignee (defaults to false)
exemptAssignees: false
# Label to use when marking as stale
staleLabel: stale
# Comment to post when marking as stale. Set to `false` to disable
markComment: >
There hasn't been any activity on this issue recently. Due to the high number
of incoming GitHub notifications, we have to clean some of the old issues,
as many of them have already been resolved with the latest updates.
Please make sure to update to the latest Home Assistant version and check
if that solves the issue. Let us know if that works for you by adding a
comment 👍
This issue now has been marked as stale and will be closed if no further
activity occurs. Thank you for your contributions.
# Comment to post when removing the stale label.
# unmarkComment: >
# Your comment here.
# Comment to post when closing a stale Issue or Pull Request.
# closeComment: >
# Your comment here.
# Limit the number of actions per hour, from 1-30. Default is 30
limitPerRun: 30
# Limit to only `issues` or `pulls`
only: issues

View File

@@ -1,20 +0,0 @@
name: Lock
# yamllint disable-line rule:truthy
on:
schedule:
- cron: "0 * * * *"
jobs:
lock:
runs-on: ubuntu-latest
steps:
- uses: dessant/lock-threads@v2.0.1
with:
github-token: ${{ github.token }}
issue-lock-inactive-days: "30"
issue-exclude-created-before: "2020-10-01T00:00:00Z"
issue-lock-reason: ""
pr-lock-inactive-days: "1"
pr-exclude-created-before: "2020-11-01T00:00:00Z"
pr-lock-reason: ""

View File

@@ -1,42 +0,0 @@
name: Stale
# yamllint disable-line rule:truthy
on:
schedule:
- cron: "0 * * * *"
jobs:
stale:
runs-on: ubuntu-latest
steps:
- name: 90 days stale policy
uses: actions/stale@v3.0.13
with:
repo-token: ${{ secrets.GITHUB_TOKEN }}
days-before-stale: 90
days-before-close: 7
operations-per-run: 25
remove-stale-when-updated: true
stale-issue-label: "stale"
exempt-issue-labels: "no-stale,Help%20wanted,help-wanted,feature-request,feature%20request"
stale-issue-message: >
There hasn't been any activity on this issue recently. Due to the
high number of incoming GitHub notifications, we have to clean some
of the old issues, as many of them have already been resolved with
the latest updates.
Please make sure to update to the latest Home Assistant version and
check if that solves the issue. Let us know if that works for you by
adding a comment 👍
This issue has now been marked as stale and will be closed if no
further activity occurs. Thank you for your contributions.
stale-pr-label: "stale"
exempt-pr-labels: "no-stale"
stale-pr-message: >
There hasn't been any activity on this pull request recently. This
pull request has been automatically marked as stale because of that
and will be closed if no further activity occurs within 7 days.
Thank you for your contributions.

5
.gitignore vendored
View File

@@ -23,8 +23,6 @@ dist
# vscode
.vscode/*
!.vscode/extensions.json
!.vscode/launch.json
!.vscode/tasks.json
# Cast dev settings
src/cast/dev_const.ts
@@ -35,6 +33,3 @@ yarn-error.log
#asdf
.tool-versions
# Home Assistant config
/config

6
.hound.yml Normal file
View File

@@ -0,0 +1,6 @@
jshint:
enabled: false
eslint:
enabled: true
config_file: .eslintrc-hound.json

44
.vscode/launch.json vendored
View File

@@ -1,44 +0,0 @@
{
// https://github.com/microsoft/vscode-js-debug/blob/master/OPTIONS.md
"configurations": [
{
"name": "Debug Frontend",
"request": "launch",
"type": "pwa-chrome",
"url": "http://localhost:8123/",
"webRoot": "${workspaceFolder}/hass_frontend",
"disableNetworkCache": true,
"preLaunchTask": "Develop Frontend",
"outFiles": [
"${workspaceFolder}/hass_frontend/frontend_latest/*.js"
]
},
{
"name": "Debug Gallery",
"request": "launch",
"type": "pwa-chrome",
"url": "http://localhost:8100/",
"webRoot": "${workspaceFolder}/gallery/dist",
"disableNetworkCache": true,
"preLaunchTask": "Develop Gallery"
},
{
"name": "Debug Demo",
"request": "launch",
"type": "pwa-chrome",
"url": "http://localhost:8090/",
"webRoot": "${workspaceFolder}/demo/dist",
"disableNetworkCache": true,
"preLaunchTask": "Develop Demo"
},
{
"name": "Debug Cast",
"request": "launch",
"type": "pwa-chrome",
"url": "http://localhost:8080/",
"webRoot": "${workspaceFolder}/cast/dist",
"disableNetworkCache": true,
"preLaunchTask": "Develop Cast"
},
]
}

208
.vscode/tasks.json vendored
View File

@@ -1,208 +0,0 @@
{
"version": "2.0.0",
"tasks": [
{
"label": "Develop Frontend",
"type": "gulp",
"task": "develop-app",
// Sync changes here to other tasks until issue resolved
// https://github.com/Microsoft/vscode/issues/61497
"problemMatcher": {
"owner": "ha-build",
"source": "ha-build",
"fileLocation": "absolute",
"severity": "error",
"pattern": [
{
"regexp": "(SyntaxError): (.+): (.+) \\((\\d+):(\\d+)\\)",
"severity": 1,
"file": 2,
"message": 3,
"line": 4,
"column": 5
}
],
"background": {
"activeOnStart": true,
"beginsPattern": "Changes detected. Starting compilation",
"endsPattern": "Build done @"
}
},
"isBackground": true,
"group": {
"kind": "build",
"isDefault": true
},
"runOptions": {
"instanceLimit": 1
}
},
{
"label": "Develop Supervisor panel",
"type": "gulp",
"task": "develop-hassio",
"problemMatcher": {
"owner": "ha-build",
"source": "ha-build",
"fileLocation": "absolute",
"severity": "error",
"pattern": [
{
"regexp": "(SyntaxError): (.+): (.+) \\((\\d+):(\\d+)\\)",
"severity": 1,
"file": 2,
"message": 3,
"line": 4,
"column": 5
}
],
"background": {
"activeOnStart": true,
"beginsPattern": "Changes detected. Starting compilation",
"endsPattern": "Build done @"
}
},
"isBackground": true,
"group": "build",
"runOptions": {
"instanceLimit": 1
}
},
{
"label": "Develop Gallery",
"type": "gulp",
"task": "develop-gallery",
"problemMatcher": {
"owner": "ha-build",
"source": "ha-build",
"fileLocation": "absolute",
"severity": "error",
"pattern": [
{
"regexp": "(SyntaxError): (.+): (.+) \\((\\d+):(\\d+)\\)",
"severity": 1,
"file": 2,
"message": 3,
"line": 4,
"column": 5
}
],
"background": {
"activeOnStart": true,
"beginsPattern": "Changes detected. Starting compilation",
"endsPattern": "Build done @"
}
},
"isBackground": true,
"group": "build",
"runOptions": {
"instanceLimit": 1
}
},
{
"label": "Develop Demo",
"type": "gulp",
"task": "develop-demo",
"problemMatcher": {
"owner": "ha-build",
"source": "ha-build",
"fileLocation": "absolute",
"severity": "error",
"pattern": [
{
"regexp": "(SyntaxError): (.+): (.+) \\((\\d+):(\\d+)\\)",
"severity": 1,
"file": 2,
"message": 3,
"line": 4,
"column": 5
}
],
"background": {
"activeOnStart": true,
"beginsPattern": "Changes detected. Starting compilation",
"endsPattern": "Build done @"
}
},
"isBackground": true,
"group": "build",
"runOptions": {
"instanceLimit": 1
}
},
{
"label": "Develop Cast",
"type": "gulp",
"task": "develop-cast",
"problemMatcher": {
"owner": "ha-build",
"source": "ha-build",
"fileLocation": "absolute",
"severity": "error",
"pattern": [
{
"regexp": "(SyntaxError): (.+): (.+) \\((\\d+):(\\d+)\\)",
"severity": 1,
"file": 2,
"message": 3,
"line": 4,
"column": 5
}
],
"background": {
"activeOnStart": true,
"beginsPattern": "Changes detected. Starting compilation",
"endsPattern": "Build done @"
}
},
"isBackground": true,
"group": "build",
"runOptions": {
"instanceLimit": 1
}
},
{
"label": "Run HA Core in devcontainer",
"type": "shell",
"command": "script/core",
"isBackground": true,
"group": {
"kind": "build",
"isDefault": true
},
"problemMatcher": [],
"runOptions": {
"instanceLimit": 1
}
},
{
"label": "Run HA Core for Supervisor in devcontainer",
"type": "shell",
"command": "HASSIO=${input:supervisorHost} HASSIO_TOKEN=${input:supervisorToken} script/core",
"isBackground": true,
"group": {
"kind": "build",
"isDefault": true
},
"problemMatcher": [],
"runOptions": {
"instanceLimit": 1
}
}
],
"inputs": [
{
"id": "supervisorHost",
"type": "promptString",
"description": "The IP of the Supervisor host running the Remote API proxy add-on"
},
{
"id": "supervisorToken",
"type": "promptString",
"description": "The token for the Remote API proxy add-on"
}
]
}

View File

@@ -1,39 +0,0 @@
# Bundling Home Assistant Frontend
The Home Assistant build pipeline contains various steps to prepare a build.
- Generating icon files to be included
- Generating translation files to be included
- Converting TypeScript, CSS and JSON files to JavaScript
- Bundling
- Minifying the files
- Generating the HTML entrypoint files
- Generating the service worker
- Compressing the files
## Converting files
Currently in Home Assistant we use a bundler to convert TypeScript, CSS and JSON files to JavaScript files that the browser understands.
We currently rely on Webpack but also have experimental Rollup support. Both of these programs bundle the converted files in both production and development.
For development, bundling is optional. We just want to get the right files in the browser.
Responsibilities of the converter during development:
- Convert TypeScript to JavaScript
- Convert CSS to JavaScript that sets the content as the default export
- Convert JSON to JavaScript that sets the content as the default export
- Make sure import, dynamic import and web worker references work
- Add extensions where missing
- Resolve absolute package imports
- Filter out specific imports/packages
- Replace constants with values
In production, the following responsibilities are added:
- Minify HTML
- Bundle multiple imports so that the browser can fetch less files
- Generate a second version that is ES5 compatible
Configuration for all these steps are specified in [bundle.js](bundle.js).

View File

@@ -44,7 +44,7 @@ module.exports.definedVars = ({ isProdBuild, latestBuild, defineOverlay }) => ({
});
module.exports.terserOptions = (latestBuild) => ({
safari10: !latestBuild,
safari10: true,
ecma: latestBuild ? undefined : 5,
output: { comments: false },
});
@@ -55,6 +55,7 @@ module.exports.babelOptions = ({ latestBuild }) => ({
!latestBuild && [
require("@babel/preset-env").default,
{
modules: false,
useBuiltIns: "entry",
corejs: "3.6",
},
@@ -70,6 +71,7 @@ module.exports.babelOptions = ({ latestBuild }) => ({
// Only support the syntax, Webpack will handle it.
"@babel/plugin-syntax-import-meta",
"@babel/plugin-syntax-dynamic-import",
"@babel/plugin-syntax-top-level-await",
"@babel/plugin-proposal-optional-chaining",
"@babel/plugin-proposal-nullish-coalescing-operator",
[
@@ -117,7 +119,7 @@ BundleConfig {
*/
module.exports.config = {
app({ isProdBuild, latestBuild, isStatsBuild, isWDS }) {
app({ isProdBuild, latestBuild, isStatsBuild }) {
return {
entry: {
service_worker: "./src/entrypoints/service_worker.ts",
@@ -132,7 +134,6 @@ module.exports.config = {
isProdBuild,
latestBuild,
isStatsBuild,
isWDS,
};
},

View File

@@ -6,9 +6,6 @@ module.exports = {
useRollup() {
return process.env.ROLLUP === "1";
},
useWDS() {
return process.env.WDS === "1";
},
isProdBuild() {
return (
process.env.NODE_ENV === "production" || module.exports.isStatsBuild()

View File

@@ -12,7 +12,6 @@ require("./webpack.js");
require("./service-worker.js");
require("./entry-html.js");
require("./rollup.js");
require("./wds.js");
gulp.task(
"develop-app",
@@ -29,11 +28,7 @@ gulp.task(
"build-translations"
),
"copy-static-app",
env.useWDS()
? "wds-watch-app"
: env.useRollup()
? "rollup-watch-app"
: "webpack-watch-app"
env.useRollup() ? "rollup-watch-app" : "webpack-watch-app"
)
);

View File

@@ -19,7 +19,6 @@ const renderTemplate = (pth, data = {}, pathFunc = templatePath) => {
return compiled({
...data,
useRollup: env.useRollup(),
useWDS: env.useWDS(),
renderTemplate,
});
};
@@ -91,23 +90,10 @@ gulp.task("gen-pages-prod", (done) => {
});
gulp.task("gen-index-app-dev", (done) => {
let latestAppJS, latestCoreJS, latestCustomPanelJS;
if (env.useWDS()) {
latestAppJS = "http://localhost:8000/src/entrypoints/app.ts";
latestCoreJS = "http://localhost:8000/src/entrypoints/core.ts";
latestCustomPanelJS =
"http://localhost:8000/src/entrypoints/custom-panel.ts";
} else {
latestAppJS = "/frontend_latest/app.js";
latestCoreJS = "/frontend_latest/core.js";
latestCustomPanelJS = "/frontend_latest/custom-panel.js";
}
const content = renderTemplate("index", {
latestAppJS,
latestCoreJS,
latestCustomPanelJS,
latestAppJS: "/frontend_latest/app.js",
latestCoreJS: "/frontend_latest/core.js",
latestCustomPanelJS: "/frontend_latest/custom-panel.js",
es5AppJS: "/frontend_es5/app.js",
es5CoreJS: "/frontend_es5/core.js",

View File

@@ -7,6 +7,7 @@ const gulp = require("gulp");
const fs = require("fs");
const foreach = require("gulp-foreach");
const merge = require("gulp-merge-json");
const minify = require("gulp-jsonminify");
const rename = require("gulp-rename");
const transform = require("gulp-json-transform");
const { mapFiles } = require("../util");
@@ -33,10 +34,21 @@ String.prototype.rsplit = function (sep, maxsplit) {
: split;
};
// Panel translations which should be split from the core translations.
const TRANSLATION_FRAGMENTS = Object.keys(
require("../../src/translations/en.json").ui.panel
);
// Panel translations which should be split from the core translations. These
// should mirror the fragment definitions in polymer.json, so that we load
// additional resources at equivalent points.
const TRANSLATION_FRAGMENTS = [
"config",
"history",
"logbook",
"mailbox",
"profile",
"shopping-list",
"page-authorize",
"page-demo",
"page-onboarding",
"developer-tools",
];
function recursiveFlatten(prefix, data) {
let output = {};
@@ -289,6 +301,7 @@ gulp.task("build-flattened-translations", function () {
return flatten(data);
})
)
.pipe(minify())
.pipe(
rename((filePath) => {
if (filePath.dirname === "core") {

View File

@@ -1,11 +0,0 @@
// Tasks to run Rollup
const gulp = require("gulp");
const { startDevServer } = require("@web/dev-server");
gulp.task("wds-watch-app", () => {
startDevServer({
config: {
watch: true,
},
});
});

View File

@@ -18,14 +18,6 @@ const bothBuilds = (createConfigFunc, params) => [
createConfigFunc({ ...params, latestBuild: false }),
];
/**
* @param {{
* compiler: import("webpack").Compiler,
* contentBase: string,
* port: number,
* listenHost?: string
* }}
*/
const runDevServer = ({
compiler,
contentBase,
@@ -41,13 +33,10 @@ const runDevServer = ({
throw err;
}
// Server listening
log(
"[webpack-dev-server]",
`Project is running at http://localhost:${port}`
);
log("[webpack-dev-server]", `http://localhost:${port}`);
});
const doneHandler = (done) => (err, stats) => {
const handler = (done) => (err, stats) => {
if (err) {
log.error(err.stack || err);
if (err.details) {
@@ -56,31 +45,22 @@ const doneHandler = (done) => (err, stats) => {
return;
}
if (stats.hasErrors() || stats.hasWarnings()) {
console.log(stats.toString("minimal"));
}
log(`Build done @ ${new Date().toLocaleTimeString()}`);
if (stats.hasErrors() || stats.hasWarnings()) {
log.warn(stats.toString("minimal"));
}
if (done) {
done();
}
};
const prodBuild = (conf) =>
new Promise((resolve) => {
webpack(
conf,
// Resolve promise when done. Because we pass a callback, webpack closes itself
doneHandler(resolve)
);
});
gulp.task("webpack-watch-app", () => {
// This command will run forever because we don't close compiler
// we are not calling done, so this command will run forever
webpack(createAppConfig({ isProdBuild: false, latestBuild: true })).watch(
{ ignored: /build-translations/ },
doneHandler()
handler()
);
gulp.watch(
path.join(paths.translations_src, "en.json"),
@@ -88,12 +68,15 @@ gulp.task("webpack-watch-app", () => {
);
});
gulp.task("webpack-prod-app", () =>
prodBuild(
bothBuilds(createAppConfig, {
isProdBuild: true,
})
)
gulp.task(
"webpack-prod-app",
() =>
new Promise((resolve) =>
webpack(
bothBuilds(createAppConfig, { isProdBuild: true }),
handler(resolve)
)
)
);
gulp.task("webpack-dev-server-demo", () => {
@@ -104,12 +87,17 @@ gulp.task("webpack-dev-server-demo", () => {
});
});
gulp.task("webpack-prod-demo", () =>
prodBuild(
bothBuilds(createDemoConfig, {
isProdBuild: true,
})
)
gulp.task(
"webpack-prod-demo",
() =>
new Promise((resolve) =>
webpack(
bothBuilds(createDemoConfig, {
isProdBuild: true,
}),
handler(resolve)
)
)
);
gulp.task("webpack-dev-server-cast", () => {
@@ -122,30 +110,41 @@ gulp.task("webpack-dev-server-cast", () => {
});
});
gulp.task("webpack-prod-cast", () =>
prodBuild(
bothBuilds(createCastConfig, {
isProdBuild: true,
})
)
gulp.task(
"webpack-prod-cast",
() =>
new Promise((resolve) =>
webpack(
bothBuilds(createCastConfig, {
isProdBuild: true,
}),
handler(resolve)
)
)
);
gulp.task("webpack-watch-hassio", () => {
// This command will run forever because we don't close compiler
// we are not calling done, so this command will run forever
webpack(
createHassioConfig({
isProdBuild: false,
latestBuild: true,
})
).watch({}, doneHandler());
).watch({}, handler());
});
gulp.task("webpack-prod-hassio", () =>
prodBuild(
bothBuilds(createHassioConfig, {
isProdBuild: true,
})
)
gulp.task(
"webpack-prod-hassio",
() =>
new Promise((resolve) =>
webpack(
bothBuilds(createHassioConfig, {
isProdBuild: true,
}),
handler(resolve)
)
)
);
gulp.task("webpack-dev-server-gallery", () => {
@@ -157,11 +156,17 @@ gulp.task("webpack-dev-server-gallery", () => {
});
});
gulp.task("webpack-prod-gallery", () =>
prodBuild(
createGalleryConfig({
isProdBuild: true,
latestBuild: true,
})
)
gulp.task(
"webpack-prod-gallery",
() =>
new Promise((resolve) =>
webpack(
createGalleryConfig({
isProdBuild: true,
latestBuild: true,
}),
handler(resolve)
)
)
);

View File

@@ -1,4 +1,4 @@
const path = require("path");
var path = require("path");
module.exports = {
polymer_dir: path.resolve(__dirname, ".."),

View File

@@ -1,3 +1,5 @@
const path = require("path");
module.exports = function (userOptions = {}) {
// Files need to be absolute paths.
// This only works if the file has no exports

View File

@@ -3,7 +3,7 @@ const path = require("path");
const commonjs = require("@rollup/plugin-commonjs");
const resolve = require("@rollup/plugin-node-resolve");
const json = require("@rollup/plugin-json");
const babel = require("@rollup/plugin-babel").babel;
const babel = require("rollup-plugin-babel");
const replace = require("@rollup/plugin-replace");
const visualizer = require("rollup-plugin-visualizer");
const { string } = require("rollup-plugin-string");
@@ -31,7 +31,6 @@ const createRollupConfig = ({
isStatsBuild,
publicPath,
dontHash,
isWDS,
}) => {
return {
/**
@@ -62,7 +61,6 @@ const createRollupConfig = ({
...bundle.babelOptions({ latestBuild }),
extensions,
exclude: bundle.babelExclude(),
babelHelpers: isWDS ? "inline" : "bundled",
}),
string({
// Import certain extensions as strings
@@ -71,21 +69,19 @@ const createRollupConfig = ({
replace(
bundle.definedVars({ isProdBuild, latestBuild, defineOverlay })
),
!isWDS &&
manifest({
publicPath,
}),
!isWDS && worker(),
!isWDS && dontHashPlugin({ dontHash }),
!isWDS && isProdBuild && terser(bundle.terserOptions(latestBuild)),
!isWDS &&
isStatsBuild &&
manifest({
publicPath,
}),
worker(),
dontHashPlugin({ dontHash }),
isProdBuild && terser(bundle.terserOptions(latestBuild)),
isStatsBuild &&
visualizer({
// https://github.com/btd/rollup-plugin-visualizer#options
open: true,
sourcemap: true,
}),
].filter(Boolean),
],
},
/**
* @type { import("rollup").OutputOptions }
@@ -112,13 +108,12 @@ const createRollupConfig = ({
};
};
const createAppConfig = ({ isProdBuild, latestBuild, isStatsBuild, isWDS }) => {
const createAppConfig = ({ isProdBuild, latestBuild, isStatsBuild }) => {
return createRollupConfig(
bundle.config.app({
isProdBuild,
latestBuild,
isStatsBuild,
isWDS,
})
);
};

View File

@@ -4,21 +4,6 @@ const TerserPlugin = require("terser-webpack-plugin");
const ManifestPlugin = require("webpack-manifest-plugin");
const paths = require("./paths.js");
const bundle = require("./bundle");
const log = require("fancy-log");
class LogStartCompilePlugin {
ignoredFirst = false;
apply(compiler) {
compiler.hooks.beforeCompile.tap("LogStartCompilePlugin", () => {
if (!this.ignoredFirst) {
this.ignoredFirst = true;
return;
}
log("Changes detected. Starting compilation");
});
}
}
const createWebpackConfig = ({
entry,
@@ -60,12 +45,17 @@ const createWebpackConfig = ({
optimization: {
minimizer: [
new TerserPlugin({
cache: true,
parallel: true,
extractComments: true,
sourceMap: true,
terserOptions: bundle.terserOptions(latestBuild),
}),
],
},
experiments: {
topLevelAwait: true,
},
plugins: [
new ManifestPlugin({
// Only include the JS of entrypoints
@@ -110,17 +100,7 @@ const createWebpackConfig = ({
new RegExp(bundle.emptyPackages({ latestBuild }).join("|")),
path.resolve(paths.polymer_dir, "src/util/empty.js")
),
// We need to change the import of the polyfill for EventTarget, so we replace the polyfill file with our customized one
new webpack.NormalModuleReplacementPlugin(
new RegExp(
require.resolve(
"lit-virtualizer/lib/uni-virtualizer/lib/polyfillLoaders/EventTarget.js"
)
),
path.resolve(paths.polymer_dir, "src/resources/EventTarget-ponyfill.js")
),
!isProdBuild && new LogStartCompilePlugin(),
].filter(Boolean),
],
resolve: {
extensions: [".ts", ".js", ".json"],
},

View File

@@ -3,10 +3,22 @@ import { Lovelace } from "../../../src/panels/lovelace/types";
import { DemoConfig } from "./types";
export const demoConfigs: Array<() => Promise<DemoConfig>> = [
() => import("./arsaboo").then((mod) => mod.demoArsaboo),
() => import("./teachingbirds").then((mod) => mod.demoTeachingbirds),
() => import("./kernehed").then((mod) => mod.demoKernehed),
() => import("./jimpower").then((mod) => mod.demoJimpower),
() =>
import(/* webpackChunkName: "arsaboo" */ "./arsaboo").then(
(mod) => mod.demoArsaboo
),
() =>
import(/* webpackChunkName: "teachingbirds" */ "./teachingbirds").then(
(mod) => mod.demoTeachingbirds
),
() =>
import(/* webpackChunkName: "kernehed" */ "./kernehed").then(
(mod) => mod.demoKernehed
),
() =>
import(/* webpackChunkName: "jimpower" */ "./jimpower").then(
(mod) => mod.demoJimpower
),
];
// eslint-disable-next-line import/no-mutable-exports

View File

@@ -7,183 +7,205 @@ export const demoLovelaceTeachingbirds: DemoConfig["lovelace"] = () => ({
cards: [
{ type: "custom:ha-demo-card" },
{
type: "grid",
columns: 4,
cards: [
{
image: "/assets/teachingbirds/isa_square.jpg",
type: "picture-entity",
show_name: false,
tap_action: {
action: "more-info",
},
entity: "sensor.presence_isa",
},
{
image: "/assets/teachingbirds/Stefan_square.jpg",
type: "picture-entity",
show_name: false,
tap_action: {
action: "more-info",
},
entity: "sensor.presence_stefan",
},
{
image: "/assets/teachingbirds/background_square.png",
elements: [
cards: [
{
state_image: {
on: "/assets/teachingbirds/radiator_on.jpg",
off: "/assets/teachingbirds/radiator_off.jpg",
},
type: "image",
style: {
width: "100%",
top: "50%",
left: "50%",
},
image: "/assets/teachingbirds/isa_square.jpg",
type: "picture-entity",
show_name: false,
tap_action: {
action: "more-info",
},
entity: "switch.stefan_radiator_3",
entity: "sensor.presence_isa",
},
{
style: {
top: "90%",
left: "50%",
image: "/assets/teachingbirds/Stefan_square.jpg",
type: "picture-entity",
show_name: false,
tap_action: {
action: "more-info",
},
type: "state-label",
entity: "sensor.presence_stefan",
},
{
image: "/assets/teachingbirds/background_square.png",
elements: [
{
state_image: {
on: "/assets/teachingbirds/radiator_on.jpg",
off: "/assets/teachingbirds/radiator_off.jpg",
},
type: "image",
style: {
width: "100%",
top: "50%",
left: "50%",
},
tap_action: {
action: "more-info",
},
entity: "switch.stefan_radiator_3",
},
{
style: {
top: "90%",
left: "50%",
},
type: "state-label",
entity: "sensor.temperature_stefan",
},
],
type: "picture-elements",
},
{
image: "/assets/teachingbirds/background_square.png",
elements: [
{
style: {
"--mdc-icon-size": "100%",
top: "50%",
left: "50%",
},
type: "icon",
tap_action: {
action: "navigate",
navigation_path: "/lovelace/home_info",
},
icon: "mdi:car",
},
],
type: "picture-elements",
},
],
type: "horizontal-stack",
},
{
cards: [
{
show_name: false,
type: "picture-entity",
name: "Alarm",
image: "/assets/teachingbirds/House_square.jpg",
entity: "alarm_control_panel.house",
},
{
name: "Roomba",
image: "/assets/teachingbirds/roomba_square.jpg",
show_name: false,
type: "picture-entity",
state_image: {
"Not Today": "/assets/teachingbirds/roomba_bw_square.jpg",
},
entity: "input_select.roomba_mode",
},
{
show_name: false,
type: "picture-entity",
state_image: {
Mail: "/assets/teachingbirds/mailbox_square.jpg",
"Package and mail":
"/assets/teachingbirds/mailbox_square.jpg",
Empty: "/assets/teachingbirds/mailbox_bw_square.jpg",
Package: "/assets/teachingbirds/mailbox_square.jpg",
},
entity: "sensor.mailbox",
},
{
show_name: false,
state_image: {
"Put out": "/assets/teachingbirds/trash_square.jpg",
"Take in": "/assets/teachingbirds/trash_square.jpg",
},
type: "picture-entity",
image: "/assets/teachingbirds/trash_bear_bw_square.jpg",
entity: "sensor.trash_status",
},
],
type: "horizontal-stack",
},
{
cards: [
{
state_image: {
Idle: "/assets/teachingbirds/washer_square.jpg",
Running: "/assets/teachingbirds/laundry_running_square.jpg",
Clean: "/assets/teachingbirds/laundry_clean_2_square.jpg",
},
entity: "input_select.washing_machine_status",
type: "picture-entity",
show_name: false,
name: "Washer",
},
{
state_image: {
Idle: "/assets/teachingbirds/dryer_square.jpg",
Running: "/assets/teachingbirds/clothes_drying_square.jpg",
Clean: "/assets/teachingbirds/folded_clothes_square.jpg",
},
entity: "input_select.dryer_status",
type: "picture-entity",
show_name: false,
name: "Dryer",
},
{
image: "/assets/teachingbirds/guests_square.jpg",
type: "picture-entity",
show_name: false,
tap_action: {
action: "toggle",
},
entity: "input_boolean.guest_mode",
},
{
image: "/assets/teachingbirds/cleaning_square.jpg",
type: "picture-entity",
show_name: false,
tap_action: {
action: "toggle",
},
entity: "input_boolean.cleaning_day",
},
],
type: "horizontal-stack",
},
],
type: "vertical-stack",
},
{
type: "vertical-stack",
cards: [
{
cards: [
{
graph: "line",
type: "sensor",
entity: "sensor.temperature_bedroom",
},
{
graph: "line",
type: "sensor",
name: "S's room",
entity: "sensor.temperature_stefan",
},
],
type: "picture-elements",
type: "horizontal-stack",
},
{
image: "/assets/teachingbirds/background_square.png",
elements: [
cards: [
{
style: {
"--mdc-icon-size": "100%",
top: "50%",
left: "50%",
},
type: "icon",
tap_action: {
action: "navigate",
navigation_path: "/lovelace/home_info",
},
icon: "mdi:car",
graph: "line",
type: "sensor",
entity: "sensor.temperature_passage",
},
{
graph: "line",
type: "sensor",
name: "Laundry",
entity: "sensor.temperature_downstairs_bathroom",
},
],
type: "picture-elements",
},
{
show_name: false,
type: "picture-entity",
name: "Alarm",
image: "/assets/teachingbirds/House_square.jpg",
entity: "alarm_control_panel.house",
},
{
name: "Roomba",
image: "/assets/teachingbirds/roomba_square.jpg",
show_name: false,
type: "picture-entity",
state_image: {
"Not Today": "/assets/teachingbirds/roomba_bw_square.jpg",
},
entity: "input_select.roomba_mode",
},
{
show_name: false,
type: "picture-entity",
state_image: {
Mail: "/assets/teachingbirds/mailbox_square.jpg",
"Package and mail": "/assets/teachingbirds/mailbox_square.jpg",
Empty: "/assets/teachingbirds/mailbox_bw_square.jpg",
Package: "/assets/teachingbirds/mailbox_square.jpg",
},
entity: "sensor.mailbox",
},
{
show_name: false,
state_image: {
"Put out": "/assets/teachingbirds/trash_square.jpg",
"Take in": "/assets/teachingbirds/trash_square.jpg",
},
type: "picture-entity",
image: "/assets/teachingbirds/trash_bear_bw_square.jpg",
entity: "sensor.trash_status",
},
{
state_image: {
Idle: "/assets/teachingbirds/washer_square.jpg",
Running: "/assets/teachingbirds/laundry_running_square.jpg",
Clean: "/assets/teachingbirds/laundry_clean_2_square.jpg",
},
entity: "input_select.washing_machine_status",
type: "picture-entity",
show_name: false,
name: "Washer",
},
{
state_image: {
Idle: "/assets/teachingbirds/dryer_square.jpg",
Running: "/assets/teachingbirds/clothes_drying_square.jpg",
Clean: "/assets/teachingbirds/folded_clothes_square.jpg",
},
entity: "input_select.dryer_status",
type: "picture-entity",
show_name: false,
name: "Dryer",
},
{
image: "/assets/teachingbirds/guests_square.jpg",
type: "picture-entity",
show_name: false,
tap_action: {
action: "toggle",
},
entity: "input_boolean.guest_mode",
},
{
image: "/assets/teachingbirds/cleaning_square.jpg",
type: "picture-entity",
show_name: false,
tap_action: {
action: "toggle",
},
entity: "input_boolean.cleaning_day",
},
],
},
{
type: "grid",
columns: 2,
cards: [
{
graph: "line",
type: "sensor",
entity: "sensor.temperature_bedroom",
},
{
graph: "line",
type: "sensor",
name: "S's room",
entity: "sensor.temperature_stefan",
},
{
graph: "line",
type: "sensor",
entity: "sensor.temperature_passage",
},
{
graph: "line",
type: "sensor",
name: "Laundry",
entity: "sensor.temperature_downstairs_bathroom",
type: "horizontal-stack",
},
],
},

View File

@@ -9,5 +9,5 @@ export interface DemoConfig {
authorUrl: string;
lovelace: (localize: LocalizeFunc) => LovelaceConfig;
entities: (localize: LocalizeFunc) => Entity[];
theme: () => Record<string, string> | null;
theme: () => { [key: string]: string } | null;
}

View File

@@ -7,5 +7,7 @@ import "./ha-demo";
/* polyfill for paper-dropdown */
setTimeout(() => {
import("web-animations-js/web-animations-next-lite.min");
import(
/* webpackChunkName: "polyfill-web-animations-next" */ "web-animations-js/web-animations-next-lite.min"
);
}, 1000);

View File

@@ -6,11 +6,4 @@ export const mockTemplate = (hass: MockHomeAssistant) => {
body: { message: "Template dev tool does not work in the demo." },
})
);
hass.mockWS("render_template", (msg, onChange) => {
onChange!({
result: msg.template,
listeners: { all: false, domains: [], entities: [], time: false },
});
return () => {};
});
};

Binary file not shown.

Before

Width:  |  Height:  |  Size: 94 KiB

View File

@@ -21,16 +21,15 @@ class DemoCard extends PolymerElement {
}
pre {
width: 400px;
margin: 0 16px;
margin: 16px;
overflow: auto;
color: var(--primary-text-color);
}
@media only screen and (max-width: 800px) {
.root {
flex-direction: column;
}
pre {
margin: 16px 0;
margin-left: 0;
}
}
</style>

View File

@@ -5,16 +5,11 @@ import { PolymerElement } from "@polymer/polymer/polymer-element";
import "../../../src/components/ha-switch";
import "../../../src/components/ha-formfield";
import "./demo-card";
import { applyThemesOnElement } from "../../../src/common/dom/apply_themes_on_element";
class DemoCards extends PolymerElement {
static get template() {
return html`
<style>
#container {
min-height: calc(100vh - 128px);
background: var(--primary-background-color);
}
.cards {
display: flex;
flex-wrap: wrap;
@@ -29,9 +24,6 @@ class DemoCards extends PolymerElement {
.filters {
margin-left: 60px;
}
ha-formfield {
margin-right: 16px;
}
</style>
<app-toolbar>
<div class="filters">
@@ -39,21 +31,16 @@ class DemoCards extends PolymerElement {
<ha-switch checked="[[_showConfig]]" on-change="_showConfigToggled">
</ha-switch>
</ha-formfield>
<ha-formfield label="Dark theme">
<ha-switch on-change="_darkThemeToggled"> </ha-switch>
</ha-formfield>
</div>
</app-toolbar>
<div id="container">
<div class="cards">
<template is="dom-repeat" items="[[configs]]">
<demo-card
config="[[item]]"
show-config="[[_showConfig]]"
hass="[[hass]]"
></demo-card>
</template>
</div>
<div class="cards">
<template is="dom-repeat" items="[[configs]]">
<demo-card
config="[[item]]"
show-config="[[_showConfig]]"
hass="[[hass]]"
></demo-card>
</template>
</div>
`;
}
@@ -72,12 +59,6 @@ class DemoCards extends PolymerElement {
_showConfigToggled(ev) {
this._showConfig = ev.target.checked;
}
_darkThemeToggled(ev) {
applyThemesOnElement(this.$.container, { themes: {} }, "default", {
dark: ev.target.checked,
});
}
}
customElements.define("demo-cards", DemoCards);

View File

@@ -3,7 +3,7 @@ import { html } from "@polymer/polymer/lib/utils/html-tag";
import { PolymerElement } from "@polymer/polymer/polymer-element";
import "../../../src/components/ha-card";
import "../../../src/state-summary/state-card-content";
import "../../../src/dialogs/more-info/more-info-content";
import "./more-info-content";
class DemoMoreInfo extends PolymerElement {
static get template() {
@@ -16,19 +16,21 @@ class DemoMoreInfo extends PolymerElement {
ha-card {
width: 333px;
padding: 20px 24px;
}
state-card-content {
display: block;
margin-bottom: 16px;
padding: 16px;
}
more-info-content {
padding: 0 16px;
}
pre {
width: 400px;
margin: 0 16px;
margin: 16px;
overflow: auto;
color: var(--primary-text-color);
}
@media only screen and (max-width: 800px) {
@@ -36,7 +38,7 @@ class DemoMoreInfo extends PolymerElement {
flex-direction: column;
}
pre {
margin: 16px 0;
margin-left: 0;
}
}
</style>

View File

@@ -0,0 +1,73 @@
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/dialogs/more-info/state_more_info_control";
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);

View File

@@ -6,9 +6,7 @@ export const createMediaPlayerEntities = () => [
media_content_type: "music",
media_title: "I Wanna Be A Hippy (Flamman & Abraxas Radio Mix)",
media_artist: "Technohead",
// Pause + Seek + Volume Set + Volume Mute + Previous Track + Next Track + Play Media +
// Select Source + Stop + Clear + Play + Shuffle Set + Browse Media
supported_features: 195135,
supported_features: 64063,
entity_picture: "/images/album_cover_2.jpg",
media_duration: 300,
media_position: 50,
@@ -16,15 +14,12 @@ export const createMediaPlayerEntities = () => [
// 23 seconds in
new Date().getTime() - 23000
).toISOString(),
volume_level: 0.5,
}),
getEntity("media_player", "music_playing", "playing", {
friendly_name: "Playing The Music",
media_content_type: "music",
media_title: "I Wanna Be A Hippy (Flamman & Abraxas Radio Mix)",
media_artist: "Technohead",
// Pause + Seek + Volume Set + Volume Mute + Previous Track + Next Track + Play Media +
// Select Source + Stop + Clear + Play + Shuffle Set
supported_features: 64063,
entity_picture: "/images/album_cover.jpg",
media_duration: 300,
@@ -33,7 +28,6 @@ export const createMediaPlayerEntities = () => [
// 23 seconds in
new Date().getTime() - 23000
).toISOString(),
volume_level: 0.5,
}),
getEntity("media_player", "stream_playing", "playing", {
friendly_name: "Playing the Stream",
@@ -41,125 +35,50 @@ export const createMediaPlayerEntities = () => [
media_title: "Epic sax guy 10 hours",
app_name: "YouTube",
entity_picture: "/images/frenck.jpg",
// Pause + Next Track + Play + Browse Media
supported_features: 147489,
supported_features: 33,
}),
getEntity("media_player", "stream_paused", "paused", {
friendly_name: "Paused the Stream",
media_content_type: "movie",
media_title: "Epic sax guy 10 hours",
app_name: "YouTube",
entity_picture: "/images/frenck.jpg",
// Pause + Next Track + Play
supported_features: 16417,
}),
getEntity("media_player", "stream_playing_previous", "playing", {
friendly_name: 'Playing the Stream (with "previous" support)',
media_content_type: "movie",
media_title: "Epic sax guy 10 hours",
app_name: "YouTube",
entity_picture: "/images/frenck.jpg",
// Pause + Previous Track + Play
supported_features: 16401,
}),
getEntity("media_player", "tv_playing", "playing", {
friendly_name: "Playing non-skip TV Show",
getEntity("media_player", "living_room", "playing", {
friendly_name: "Pause, No skip, tvshow",
media_content_type: "tvshow",
media_title: "Chapter 1",
media_series_title: "House of Cards",
app_name: "Netflix",
entity_picture: "/images/netflix.jpg",
// Pause
supported_features: 1,
}),
getEntity("media_player", "sonos_idle", "idle", {
friendly_name: "Sonos Idle",
// Pause + Seek + Volume Set + Volume Mute + Previous Track + Next Track + Play Media +
// Select Source + Stop + Clear + Play + Shuffle Set
supported_features: 64063,
volume_level: 0.33,
is_volume_muted: true,
}),
getEntity("media_player", "idle_browse_media", "idle", {
friendly_name: "Idle waiting for Browse Media (e.g. Spotify)",
// Pause + Seek + Volume Set + Previous Track + Next Track + Play Media +
// Select Source + Play + Shuffle Set + Browse Media
supported_features: 182839,
volume_level: 0.79,
}),
getEntity("media_player", "theater_off", "off", {
getEntity("media_player", "theater", "off", {
friendly_name: "TV Off",
// On + Off + Play + Next + Pause
supported_features: 16801,
}),
getEntity("media_player", "theater_on", "on", {
friendly_name: "TV On",
// On + Off + Play + Next + Pause
supported_features: 16801,
}),
getEntity("media_player", "theater_off_static", "off", {
friendly_name: "TV Off (cannot be switched on)",
// Off + Next + Pause
supported_features: 289,
}),
getEntity("media_player", "theater_on_static", "on", {
friendly_name: "TV On (cannot be switched off)",
// On + Next + Pause
supported_features: 161,
}),
getEntity("media_player", "android_cast", "playing", {
friendly_name: "Casting App (no supported features)",
friendly_name: "Casting App",
media_title: "Android Screen Casting",
app_name: "Screen Mirroring",
}),
getEntity("media_player", "image_display", "playing", {
friendly_name: "Digital Picture Frame",
media_content_type: "image",
media_title: "Famous Painting",
media_artist: "Famous Artist",
entity_picture: "/images/sunflowers.jpg",
// On + Off + Browse Media
supported_features: 131456,
// supported_features: 21437,
}),
getEntity("media_player", "unavailable", "unavailable", {
friendly_name: "Player Unavailable",
// Pause + Volume Set + Volume Mute + Previous Track + Next Track +
// Play Media + Stop + Play
supported_features: 21437,
}),
getEntity("media_player", "unknown", "unknown", {
friendly_name: "Player Unknown",
// Pause + Volume Set + Volume Mute + Previous Track + Next Track +
// Play Media + Stop + Play
supported_features: 21437,
}),
getEntity("media_player", "playing", "playing", {
friendly_name: "Player Playing (no Pause support)",
// Volume Set + Volume Mute + Previous Track + Next Track +
// Play Media + Stop + Play
supported_features: 21436,
volume_level: 1,
}),
getEntity("media_player", "idle", "idle", {
friendly_name: "Player Idle",
// Pause + Volume Set + Volume Mute + Previous Track + Next Track +
// Play Media + Stop + Play
supported_features: 21437,
volume_level: 0,
}),
getEntity("media_player", "receiver_on", "on", {
source_list: ["AirPlay", "Blu-Ray", "TV", "USB", "iPod (USB)"],
volume_level: 0.63,
is_volume_muted: false,
source: "TV",
friendly_name: "Receiver (selectable sources)",
// Volume Set + Volume Mute + On + Off + Select Source + Play + Sound Mode
friendly_name: "Receiver",
supported_features: 84364,
}),
getEntity("media_player", "receiver_off", "off", {
source_list: ["AirPlay", "Blu-Ray", "TV", "USB", "iPod (USB)"],
friendly_name: "Receiver (selectable sources)",
// Volume Set + Volume Mute + On + Off + Select Source + Play + Sound Mode
friendly_name: "Receiver",
supported_features: 84364,
}),
];

View File

@@ -15,10 +15,6 @@ const ENTITIES = [
getEntity("alarm_control_panel", "unavailable", "unavailable", {
friendly_name: "Alarm",
}),
getEntity("alarm_control_panel", "alarm_code", "disarmed", {
friendly_name: "Alarm",
code_format: "number",
}),
];
const CONFIGS = [
@@ -34,14 +30,7 @@ const CONFIGS = [
config: `
- type: alarm-panel
entity: alarm_control_panel.alarm_armed
name: My Alarm
`,
},
{
heading: "Code Example",
config: `
- type: alarm-panel
entity: alarm_control_panel.alarm_code
title: My Alarm
`,
},
{
@@ -73,7 +62,13 @@ const CONFIGS = [
class DemoAlarmPanelEntity extends PolymerElement {
static get template() {
return html` <demo-cards id="demos" configs="[[_configs]]"></demo-cards> `;
return html`
<demo-cards
id="demos"
hass="[[hass]]"
configs="[[_configs]]"
></demo-cards>
`;
}
static get properties() {
@@ -82,17 +77,14 @@ class DemoAlarmPanelEntity extends PolymerElement {
type: Object,
value: CONFIGS,
},
hass: Object,
};
}
public ready() {
super.ready();
this._setupDemo();
}
private async _setupDemo() {
const hass = provideHass(this.$.demos);
await hass.updateTranslations(null, "en");
hass.updateTranslations(null, "en");
hass.addEntities(ENTITIES);
}
}

View File

@@ -55,7 +55,13 @@ const CONFIGS = [
class DemoConditional extends PolymerElement {
static get template() {
return html` <demo-cards id="demos" configs="[[_configs]]"></demo-cards> `;
return html`
<demo-cards
id="demos"
hass="[[hass]]"
configs="[[_configs]]"
></demo-cards>
`;
}
static get properties() {
@@ -64,6 +70,7 @@ class DemoConditional extends PolymerElement {
type: Object,
value: CONFIGS,
},
hass: Object,
};
}

View File

@@ -20,10 +20,10 @@ const CONFIGS = [
`,
},
{
heading: "With Name (defined in card)",
heading: "With Name",
config: `
- type: button
name: Custom Name
name: Bedroom
entity: light.bed_light
`,
},
@@ -32,7 +32,7 @@ const CONFIGS = [
config: `
- type: button
entity: light.bed_light
icon: mdi:tools
icon: mdi:hotel
`,
},
{
@@ -71,7 +71,13 @@ const CONFIGS = [
class DemoButtonEntity extends PolymerElement {
static get template() {
return html` <demo-cards id="demos" configs="[[_configs]]"></demo-cards> `;
return html`
<demo-cards
id="demos"
hass="[[hass]]"
configs="[[_configs]]"
></demo-cards>
`;
}
static get properties() {
@@ -80,6 +86,7 @@ class DemoButtonEntity extends PolymerElement {
type: Object,
value: CONFIGS,
},
hass: Object,
};
}
@@ -91,4 +98,4 @@ class DemoButtonEntity extends PolymerElement {
}
}
customElements.define("demo-hui-entity-button-card", DemoButtonEntity);
customElements.define("demo-hui-button-card", DemoButtonEntity);

View File

@@ -7,10 +7,7 @@ import "../components/demo-cards";
const ENTITIES = [
getEntity("sensor", "brightness", "12", {}),
getEntity("sensor", "brightness_medium", "53", {}),
getEntity("sensor", "brightness_high", "87", {}),
getEntity("plant", "bonsai", "ok", {}),
getEntity("sensor", "not_working", "unavailable", {}),
getEntity("sensor", "outside_humidity", "54", {
unit_of_measurement: "%",
}),
@@ -23,10 +20,16 @@ const CONFIGS = [
{
heading: "Basic example",
config: `
- type: gauge
entity: sensor.brightness
`,
},
{
heading: "With title",
config: `
- type: gauge
title: Humidity
entity: sensor.outside_humidity
name: Outside Humidity
`,
},
{
@@ -35,7 +38,6 @@ const CONFIGS = [
- type: gauge
entity: sensor.outside_temperature
unit_of_measurement: C
name: Outside Temperature
`,
},
{
@@ -43,45 +45,19 @@ const CONFIGS = [
config: `
- type: gauge
entity: sensor.brightness
name: Brightness Low
severity:
red: 75
red: 32
green: 0
yellow: 50
yellow: 23
`,
},
{
heading: "Setting Severity Levels",
config: `
- type: gauge
entity: sensor.brightness_medium
name: Brightness Medium
severity:
red: 75
green: 0
yellow: 50
`,
},
{
heading: "Setting Severity Levels",
config: `
- type: gauge
entity: sensor.brightness_high
name: Brightness High
severity:
red: 75
green: 0
yellow: 50
`,
},
{
heading: "Setting Min (0) and Max (15) Values",
heading: "Setting Min and Max Values",
config: `
- type: gauge
entity: sensor.brightness
name: Brightness
min: 0
max: 15
max: 38
`,
},
{
@@ -98,13 +74,6 @@ const CONFIGS = [
entity: plant.bonsai
`,
},
{
heading: "Unavailable entity",
config: `
- type: gauge
entity: sensor.not_working
`,
},
];
class DemoGaugeEntity extends PolymerElement {

View File

@@ -8,43 +8,29 @@ import "../components/demo-cards";
const ENTITIES = [
getEntity("light", "bed_light", "on", {
friendly_name: "Bed Light",
brightness: 255,
brightness: 130,
}),
getEntity("light", "dim_on", "on", {
friendly_name: "Dining Room",
supported_features: 1,
brightness: 100,
}),
getEntity("light", "dim_off", "off", {
friendly_name: "Dining Room",
getEntity("light", "dim", "off", {
supported_features: 1,
}),
getEntity("light", "unavailable", "unavailable", {
friendly_name: "Lost Light",
supported_features: 1,
}),
];
const CONFIGS = [
{
heading: "Switchable Light",
heading: "Basic example",
config: `
- type: light
entity: light.bed_light
`,
},
{
heading: "Dimmable Light On",
heading: "Dim",
config: `
- type: light
entity: light.dim_on
`,
},
{
heading: "Dimmable Light Off",
config: `
- type: light
entity: light.dim_off
entity: light.dim
`,
},
{

View File

@@ -163,7 +163,13 @@ const CONFIGS = [
class DemoMap extends PolymerElement {
static get template() {
return html` <demo-cards id="demos" configs="[[_configs]]"></demo-cards> `;
return html`
<demo-cards
id="demos"
hass="[[hass]]"
configs="[[_configs]]"
></demo-cards>
`;
}
static get properties() {
@@ -172,6 +178,7 @@ class DemoMap extends PolymerElement {
type: Object,
value: CONFIGS,
},
hass: Object,
};
}

View File

@@ -1,8 +1,6 @@
import { html } from "@polymer/polymer/lib/utils/html-tag";
/* eslint-plugin-disable lit */
import { PolymerElement } from "@polymer/polymer/polymer-element";
import { mockTemplate } from "../../../demo/src/stubs/template";
import { provideHass } from "../../../src/fake_data/provide_hass";
import "../components/demo-cards";
const CONFIGS = [
@@ -256,7 +254,7 @@ const CONFIGS = [
class DemoMarkdown extends PolymerElement {
static get template() {
return html` <demo-cards id="demos" configs="[[_configs]]"></demo-cards> `;
return html` <demo-cards configs="[[_configs]]"></demo-cards> `;
}
static get properties() {
@@ -267,12 +265,6 @@ class DemoMarkdown extends PolymerElement {
},
};
}
public ready() {
super.ready();
const hass = provideHass(this.$.demos);
mockTemplate(hass);
}
}
customElements.define("demo-hui-markdown-card", DemoMarkdown);

View File

@@ -7,61 +7,40 @@ import { createMediaPlayerEntities } from "../data/media_players";
const CONFIGS = [
{
heading: "Paused Music",
heading: "Paused music",
config: `
- type: media-control
entity: media_player.music_paused
`,
},
{
heading: "Playing Music",
heading: "Playing music",
config: `
- type: media-control
entity: media_player.music_playing
`,
},
{
heading: "Playing Stream",
heading: "Playing stream",
config: `
- type: media-control
entity: media_player.stream_playing
`,
},
{
heading: "Paused Stream",
heading: "Pause, No skip, tvshow",
config: `
- type: media-control
entity: media_player.stream_paused
entity: media_player.living_room
`,
},
{
heading: 'Playing Stream (with "previous" support)',
config: `
- type: media-control
entity: media_player.stream_playing_previous
`,
},
{
heading: "Playing non-skip TV Show",
config: `
- type: media-control
entity: media_player.tv_playing
`,
},
{
heading: "Screen Casting",
heading: "Screen casting",
config: `
- type: media-control
entity: media_player.android_cast
`,
},
{
heading: "Digital Picture Frame",
config: `
- type: media-control
entity: media_player.image_display
`,
},
{
heading: "Sonos Idle",
config: `
@@ -69,53 +48,11 @@ const CONFIGS = [
entity: media_player.sonos_idle
`,
},
{
heading: "Idle waiting for Browse Media",
config: `
- type: media-control
entity: media_player.idle_browse_media
`,
},
{
heading: "Player Off",
config: `
- type: media-control
entity: media_player.theater_off
`,
},
{
heading: "Player On",
config: `
- type: media-control
entity: media_player.theater_on
`,
},
{
heading: "Player Off (cannot be switched on)",
config: `
- type: media-control
entity: media_player.theater_off_static
`,
},
{
heading: "Player On (cannot be switched off)",
config: `
- type: media-control
entity: media_player.theater_on_static
`,
},
{
heading: "Player Idle",
config: `
- type: media-control
entity: media_player.idle
`,
},
{
heading: "Player Playing",
config: `
- type: media-control
entity: media_player.playing
entity: media_player.theater
`,
},
{
@@ -133,14 +70,14 @@ const CONFIGS = [
`,
},
{
heading: "Receiver On (selectable sources)",
heading: "Receiver On",
config: `
- type: media-control
entity: media_player.receiver_on
`,
},
{
heading: "Receiver Off (selectable sources)",
heading: "Receiver Off",
config: `
- type: media-control
entity: media_player.receiver_off
@@ -150,7 +87,13 @@ const CONFIGS = [
class DemoHuiMediControlCard extends PolymerElement {
static get template() {
return html` <demo-cards id="demos" configs="[[_configs]]"></demo-cards> `;
return html`
<demo-cards
id="demos"
hass="[[hass]]"
configs="[[_configs]]"
></demo-cards>
`;
}
static get properties() {
@@ -159,6 +102,7 @@ class DemoHuiMediControlCard extends PolymerElement {
type: Object,
value: CONFIGS,
},
hass: Object,
};
}

View File

@@ -12,52 +12,36 @@ const CONFIGS = [
- type: entities
entities:
- entity: media_player.music_paused
name: Paused Music
name: Paused music
- entity: media_player.music_playing
name: Playing Music
name: Playing music
- entity: media_player.stream_playing
name: Playing Stream
- entity: media_player.stream_paused
name: Paused Stream
- entity: media_player.stream_playing_previous
name: Playing Stream (with "previous" support)
- entity: media_player.tv_playing
name: Playing non-skip TV Show
name: Paused, no play
- entity: media_player.living_room
name: Pause, No skip, tvshow
- entity: media_player.android_cast
name: Screen casting
- entity: media_player.image_display
name: Digital Picture Frame
- entity: media_player.sonos_idle
name: Sonos Idle
- entity: media_player.idle_browse_media
name: Idle waiting for Browse Media
- entity: media_player.theater_off
name: Chromcast Idle
- entity: media_player.theater
name: Player Off
- entity: media_player.theater_on
name: Player On
- entity: media_player.theater_off_static
name: Player Off (cannot be switched on)
- entity: media_player.theater_on_static
name: Player On (cannot be switched off)
- entity: media_player.idle
name: Player Idle
- entity: media_player.playing
name: Player Playing
- entity: media_player.unavailable
name: Player Unavailable
- entity: media_player.unknown
name: Player Unknown
- entity: media_player.receiver_on
name: Receiver On (selectable sources)
- entity: media_player.receiver_off
name: Receiver Off (selectable sources)
`,
},
];
class DemoHuiMediaPlayerRows extends PolymerElement {
static get template() {
return html` <demo-cards id="demos" configs="[[_configs]]"></demo-cards> `;
return html`
<demo-cards
id="demos"
hass="[[hass]]"
configs="[[_configs]]"
></demo-cards>
`;
}
static get properties() {
@@ -66,6 +50,7 @@ class DemoHuiMediaPlayerRows extends PolymerElement {
type: Object,
value: CONFIGS,
},
hass: Object,
};
}

View File

@@ -1,7 +1,6 @@
import { html } from "@polymer/polymer/lib/utils/html-tag";
/* eslint-plugin-disable lit */
import { PolymerElement } from "@polymer/polymer/polymer-element";
import { mockHistory } from "../../../demo/src/stubs/history";
import { getEntity } from "../../../src/fake_data/entity";
import { provideHass } from "../../../src/fake_data/provide_hass";
import "../components/demo-cards";
@@ -37,10 +36,6 @@ const ENTITIES = [
battery: 71,
friendly_name: "Home Boy",
}),
getEntity("sensor", "illumination", "23", {
friendly_name: "Illumination",
unit_of_measurement: "lx",
}),
];
const CONFIGS = [
@@ -94,42 +89,6 @@ const CONFIGS = [
entity: light.bed_light
`,
},
{
heading: "Default Grid",
config: `
- type: grid
cards:
- type: entity
entity: light.kitchen_lights
- type: entity
entity: light.bed_light
- type: entity
entity: device_tracker.demo_paulus
- type: sensor
entity: sensor.illumination
graph: line
- type: entity
entity: device_tracker.demo_anne_therese
`,
},
{
heading: "Non-square Grid with 2 columns",
config: `
- type: grid
columns: 2
square: false
cards:
- type: entity
entity: light.kitchen_lights
- type: entity
entity: light.bed_light
- type: entity
entity: device_tracker.demo_paulus
- type: sensor
entity: sensor.illumination
graph: line
`,
},
];
class DemoStack extends PolymerElement {
@@ -151,7 +110,6 @@ class DemoStack extends PolymerElement {
const hass = provideHass(this.$.demos);
hass.updateTranslations(null, "en");
hass.addEntities(ENTITIES);
mockHistory(hass);
}
}

View File

@@ -6,7 +6,7 @@ import { SUPPORT_BRIGHTNESS } from "../../../src/data/light";
import { getEntity } from "../../../src/fake_data/entity";
import { provideHass } from "../../../src/fake_data/provide_hass";
import "../components/demo-more-infos";
import "../../../src/dialogs/more-info/more-info-content";
import "../components/more-info-content";
const ENTITIES = [
getEntity("light", "bed_light", "on", {
@@ -40,12 +40,8 @@ class DemoMoreInfoLight extends PolymerElement {
public ready() {
super.ready();
this._setupDemo();
}
private async _setupDemo() {
const hass = provideHass(this);
await hass.updateTranslations(null, "en");
hass.updateTranslations(null, "en");
hass.addEntities(ENTITIES);
}
}

View File

@@ -20,47 +20,48 @@ class HaGallery extends PolymerElement {
static get template() {
return html`
<style include="iron-positioning ha-style">
:host {
-ms-user-select: initial;
-webkit-user-select: initial;
-moz-user-select: initial;
}
app-header-layout {
min-height: 100vh;
}
ha-icon-button.invisible {
visibility: hidden;
}
:host {
-ms-user-select: initial;
-webkit-user-select: initial;
-moz-user-select: initial;
}
app-header-layout {
min-height: 100vh;
}
ha-icon-button.invisible {
visibility: hidden;
}
.pickers {
display: flex;
flex-wrap: wrap;
justify-content: center;
align-items: start;
}
.pickers {
display: flex;
flex-wrap: wrap;
justify-content: center;
align-items: start;
}
.pickers ha-card {
width: 400px;
display: block;
margin: 16px 8px;
}
.pickers ha-card {
width: 400px;
display: block;
margin: 16px 8px;
}
.pickers ha-card:last-child {
margin-bottom: 16px;
}
.pickers ha-card:last-child {
margin-bottom: 16px;
}
.intro {
margin: -1em 0;
}
.intro {
margin: -1em 0;
}
p a {
color: var(--primary-color);
}
p a {
color: var(--primary-color);
}
a {
color: var(--primary-text-color);
text-decoration: none;
}
a {
color: var(--primary-text-color);
text-decoration: none;
}
</style>
<app-header-layout>
@@ -69,42 +70,32 @@ class HaGallery extends PolymerElement {
<ha-icon-button
icon="hass:arrow-left"
on-click="_backTapped"
class$="[[_computeHeaderButtonClass(_demo)]]"
class$='[[_computeHeaderButtonClass(_demo)]]'
></ha-icon-button>
<div main-title>
[[_withDefault(_demo, "Home Assistant Gallery")]]
</div>
<div main-title>[[_withDefault(_demo, "Home Assistant Gallery")]]</div>
</app-toolbar>
</app-header>
<div class="content">
<div id="demo"></div>
<template is="dom-if" if="[[!_demo]]">
<div class="pickers">
<ha-card header="Lovelace Card Demos">
<div class="card-content intro">
<div class='content'>
<div id='demo'></div>
<template is='dom-if' if='[[!_demo]]'>
<div class='pickers'>
<ha-card header="Lovelace card demos">
<div class='card-content intro'>
<p>
Lovelace has many different cards. Each card allows the user
to tell a different story about what is going on in their
house. These cards are very customizable, as no household is
the same.
Lovelace has many different cards. Each card allows the user to tell a different story about what is going on in their house. These cards are very customizable, as no household is the same.
</p>
<p>
This gallery helps our developers and designers to see all
the different states that each card can be in.
This gallery helps our developers and designers to see all the different states that each card can be in.
</p>
<p>
Check
<a href="https://www.home-assistant.io/lovelace"
>the official website</a
>
for instructions on how to get started with Lovelace.
Check <a href='https://www.home-assistant.io/lovelace'>the official website</a> for instructions on how to get started with Lovelace.</a>.
</p>
</div>
<template is="dom-repeat" items="[[_lovelaceDemos]]">
<a href="#[[item]]">
<template is='dom-repeat' items='[[_lovelaceDemos]]'>
<a href='#[[item]]'>
<paper-item>
<paper-item-body>{{ item }}</paper-item-body>
<ha-icon icon="hass:chevron-right"></ha-icon>
@@ -113,14 +104,14 @@ class HaGallery extends PolymerElement {
</template>
</ha-card>
<ha-card header="More Info Demos">
<div class="card-content intro">
<ha-card header="More Info demos">
<div class='card-content intro'>
<p>
More info screens show up when an entity is clicked.
</p>
</div>
<template is="dom-repeat" items="[[_moreInfoDemos]]">
<a href="#[[item]]">
<template is='dom-repeat' items='[[_moreInfoDemos]]'>
<a href='#[[item]]'>
<paper-item>
<paper-item-body>{{ item }}</paper-item-body>
<ha-icon icon="hass:chevron-right"></ha-icon>
@@ -129,14 +120,14 @@ class HaGallery extends PolymerElement {
</template>
</ha-card>
<ha-card header="Util Demos">
<div class="card-content intro">
<ha-card header="Util demos">
<div class='card-content intro'>
<p>
Test pages for our utility functions.
</p>
</div>
<template is="dom-repeat" items="[[_utilDemos]]">
<a href="#[[item]]">
<template is='dom-repeat' items='[[_utilDemos]]'>
<a href='#[[item]]'>
<paper-item>
<paper-item-body>{{ item }}</paper-item-body>
<ha-icon icon="hass:chevron-right"></ha-icon>
@@ -148,10 +139,7 @@ class HaGallery extends PolymerElement {
</template>
</div>
</app-header-layout>
<notification-manager
hass="[[_fakeHass]]"
id="notifications"
></notification-manager>
<notification-manager hass=[[_fakeHass]] id='notifications'></notification-manager>
`;
}

View File

@@ -27,8 +27,6 @@ declare global {
}
}
const MAX_FILE_SIZE = 1 * 1024 * 1024 * 1024; // 1GB
@customElement("hassio-upload-snapshot")
export class HassioUploadSnapshot extends LitElement {
public hass!: HomeAssistant;
@@ -53,20 +51,6 @@ export class HassioUploadSnapshot extends LitElement {
private async _uploadFile(ev) {
const file = ev.detail.files[0];
if (file.size > MAX_FILE_SIZE) {
showAlertDialog(this, {
title: "Snapshot file is too big",
text: html`The maximum allowed filesize is 1GB.<br />
<a
href="https://www.home-assistant.io/hassio/haos_common_tasks/#restoring-a-snapshot-on-a-new-install"
target="_blank"
>Have a look here on how to restore it.</a
>`,
confirmText: "ok",
});
return;
}
if (!["application/x-tar"].includes(file.type)) {
showAlertDialog(this, {
title: "Unsupported file format",

View File

@@ -12,7 +12,7 @@ import { atLeastVersion } from "../../../src/common/config/version";
import { navigate } from "../../../src/common/navigate";
import { compare } from "../../../src/common/string/compare";
import "../../../src/components/ha-card";
import { Supervisor } from "../../../src/data/supervisor/supervisor";
import { HassioAddonInfo } from "../../../src/data/hassio/addon";
import { haStyle } from "../../../src/resources/styles";
import { HomeAssistant } from "../../../src/types";
import "../components/hassio-card-content";
@@ -22,14 +22,14 @@ import { hassioStyle } from "../resources/hassio-style";
class HassioAddons extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public supervisor!: Supervisor;
@property({ attribute: false }) public addons?: HassioAddonInfo[];
protected render(): TemplateResult {
return html`
<div class="content">
<h1>Add-ons</h1>
<div class="card-group">
${!this.supervisor.supervisor.addons?.length
${!this.addons?.length
? html`
<ha-card>
<div class="card-content">
@@ -41,7 +41,7 @@ class HassioAddons extends LitElement {
</div>
</ha-card>
`
: this.supervisor.supervisor.addons
: this.addons
.sort((a, b) => compare(a.name, b.name))
.map(
(addon) => html`

View File

@@ -7,7 +7,11 @@ import {
property,
TemplateResult,
} from "lit-element";
import { Supervisor } from "../../../src/data/supervisor/supervisor";
import { HassioHassOSInfo } from "../../../src/data/hassio/host";
import {
HassioHomeAssistantInfo,
HassioSupervisorInfo,
} from "../../../src/data/hassio/supervisor";
import "../../../src/layouts/hass-tabs-subpage";
import { haStyle } from "../../../src/resources/styles";
import { HomeAssistant, Route } from "../../../src/types";
@@ -19,12 +23,16 @@ import "./hassio-update";
class HassioDashboard extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public supervisor!: Supervisor;
@property({ type: Boolean }) public narrow!: boolean;
@property({ attribute: false }) public route!: Route;
@property({ attribute: false }) public supervisorInfo!: HassioSupervisorInfo;
@property({ attribute: false }) public hassInfo!: HassioHomeAssistantInfo;
@property({ attribute: false }) public hassOsInfo!: HassioHassOSInfo;
protected render(): TemplateResult {
return html`
<hass-tabs-subpage
@@ -39,11 +47,13 @@ class HassioDashboard extends LitElement {
<div class="content">
<hassio-update
.hass=${this.hass}
.supervisor=${this.supervisor}
.hassInfo=${this.hassInfo}
.supervisorInfo=${this.supervisorInfo}
.hassOsInfo=${this.hassOsInfo}
></hassio-update>
<hassio-addons
.hass=${this.hass}
.supervisor=${this.supervisor}
.addons=${this.supervisorInfo.addons}
></hassio-addons>
</div>
</hass-tabs-subpage>

View File

@@ -23,7 +23,6 @@ import {
HassioHomeAssistantInfo,
HassioSupervisorInfo,
} from "../../../src/data/hassio/supervisor";
import { Supervisor } from "../../../src/data/supervisor/supervisor";
import {
showAlertDialog,
showConfirmationDialog,
@@ -36,20 +35,31 @@ import { hassioStyle } from "../resources/hassio-style";
export class HassioUpdate extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public supervisor!: Supervisor;
@property({ attribute: false }) public hassInfo?: HassioHomeAssistantInfo;
private _pendingUpdates = memoizeOne((supervisor: Supervisor): number => {
return Object.keys(supervisor).filter(
(value) => supervisor[value].update_available
).length;
});
@property({ attribute: false }) public hassOsInfo?: HassioHassOSInfo;
@property({ attribute: false }) public supervisorInfo?: HassioSupervisorInfo;
private _pendingUpdates = memoizeOne(
(
core?: HassioHomeAssistantInfo,
supervisor?: HassioSupervisorInfo,
os?: HassioHassOSInfo
): number => {
return [core, supervisor, os].filter(
(value) => !!value && value?.update_available
).length;
}
);
protected render(): TemplateResult {
if (!this.supervisor) {
return html``;
}
const updatesAvailable = this._pendingUpdates(
this.hassInfo,
this.supervisorInfo,
this.hassOsInfo
);
const updatesAvailable = this._pendingUpdates(this.supervisor);
if (!updatesAvailable) {
return html``;
}
@@ -64,26 +74,26 @@ export class HassioUpdate extends LitElement {
<div class="card-group">
${this._renderUpdateCard(
"Home Assistant Core",
this.supervisor.core,
this.hassInfo!,
"hassio/homeassistant/update",
`https://${
this.supervisor.core.version_latest.includes("b") ? "rc" : "www"
this.hassInfo?.version_latest.includes("b") ? "rc" : "www"
}.home-assistant.io/latest-release-notes/`
)}
${this._renderUpdateCard(
"Supervisor",
this.supervisor.supervisor,
this.supervisorInfo!,
"hassio/supervisor/update",
`https://github.com//home-assistant/hassio/releases/tag/${
this.supervisor.supervisor.version_latest
this.supervisorInfo!.version_latest
}`
)}
${this.supervisor.host.features.includes("hassos")
${this.hassOsInfo
? this._renderUpdateCard(
"Operating System",
this.supervisor.os,
this.hassOsInfo,
"hassio/os/update",
`https://github.com//home-assistant/hassos/releases/tag/${this.supervisor.os.version_latest}`
`https://github.com//home-assistant/hassos/releases/tag/${this.hassOsInfo.version_latest}`
)
: ""}
</div>

View File

@@ -11,7 +11,10 @@ export const showHassioMarkdownDialog = (
): void => {
fireEvent(element, "show-dialog", {
dialogTag: "dialog-hassio-markdown",
dialogImport: () => import("./dialog-hassio-markdown"),
dialogImport: () =>
import(
/* webpackChunkName: "dialog-hassio-markdown" */ "./dialog-hassio-markdown"
),
dialogParams,
});
};

View File

@@ -1,7 +1,5 @@
import "@material/mwc-button/mwc-button";
import "@material/mwc-icon-button";
import "@material/mwc-list/mwc-list";
import "@material/mwc-list/mwc-list-item";
import "@material/mwc-tab";
import "@material/mwc-tab-bar";
import { mdiClose } from "@mdi/js";
@@ -18,22 +16,18 @@ import {
} from "lit-element";
import { cache } from "lit-html/directives/cache";
import { fireEvent } from "../../../../src/common/dom/fire_event";
import "../../../../src/components/ha-chips";
import "../../../../src/components/ha-circular-progress";
import "../../../../src/components/ha-dialog";
import "../../../../src/components/ha-expansion-panel";
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 {
AccessPoints,
accesspointScan,
NetworkInterface,
updateNetworkInterface,
WifiConfiguration,
} from "../../../../src/data/hassio/network";
import {
showAlertDialog,
@@ -44,51 +38,54 @@ import { haStyleDialog } from "../../../../src/resources/styles";
import type { HomeAssistant } from "../../../../src/types";
import { HassioNetworkDialogParams } from "./show-dialog-network";
const IP_VERSIONS = ["ipv4", "ipv6"];
@customElement("dialog-hassio-network")
export class DialogHassioNetwork extends LitElement
implements HassDialog<HassioNetworkDialogParams> {
@property({ attribute: false }) public hass!: HomeAssistant;
@internalProperty() private _accessPoints?: AccessPoints;
@internalProperty() private _curTabIndex = 0;
@internalProperty() private _dirty = false;
@internalProperty() private _interface?: NetworkInterface;
@internalProperty() private _interfaces!: NetworkInterface[];
@internalProperty() private _prosessing = false;
@internalProperty() private _params?: HassioNetworkDialogParams;
@internalProperty() private _processing = false;
@internalProperty() private _network!: {
interface: string;
data: NetworkInterface;
}[];
@internalProperty() private _scanning = false;
@internalProperty() private _curTabIndex = 0;
@internalProperty() private _wifiConfiguration?: WifiConfiguration;
@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._interfaces = params.network.interfaces.sort((a, b) => {
return a.primary > b.primary ? -1 : 1;
});
this._interface = { ...this._interfaces[this._curTabIndex] };
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._processing = false;
this._prosessing = false;
fireEvent(this, "dialog-closed", { dialog: this.localName });
}
protected render(): TemplateResult {
if (!this._params || !this._interface) {
if (!this._params || !this._network) {
return html``;
}
@@ -110,11 +107,11 @@ export class DialogHassioNetwork extends LitElement
<ha-svg-icon .path=${mdiClose}></ha-svg-icon>
</mwc-icon-button>
</ha-header-bar>
${this._interfaces.length > 1
${this._network.length > 1
? html` <mwc-tab-bar
.activeIndex=${this._curTabIndex}
@MDCTabBar:activated=${this._handleTabActivated}
>${this._interfaces.map(
>${this._network.map(
(device) =>
html`<mwc-tab
.id=${device.interface}
@@ -132,302 +129,81 @@ export class DialogHassioNetwork extends LitElement
private _renderTab() {
return html` <div class="form container">
${IP_VERSIONS.map((version) =>
this._interface![version] ? this._renderIPConfiguration(version) : ""
)}
${this._interface?.type === "wireless"
? html`
<ha-expansion-panel header="Wi-Fi" outlined>
${this._interface?.wifi?.ssid
? html`<p>Connected to: ${this._interface?.wifi?.ssid}</p>`
: ""}
<mwc-button
class="scan"
@click=${this._scanForAP}
.disabled=${this._scanning}
>
${this._scanning
? html`<ha-circular-progress active size="small">
</ha-circular-progress>`
: "Scan for accesspoints"}
</mwc-button>
${this._accessPoints &&
this._accessPoints.accesspoints &&
this._accessPoints.accesspoints.length !== 0
? html`
<mwc-list>
${this._accessPoints.accesspoints
.filter((ap) => ap.ssid)
.map(
(ap) =>
html`
<mwc-list-item
twoline
@click=${this._selectAP}
.activated=${ap.ssid ===
this._wifiConfiguration?.ssid}
.ap=${ap}
>
<span>${ap.ssid}</span>
<span slot="secondary">
${ap.mac} - Strength: ${ap.signal}
</span>
</mwc-list-item>
`
)}
</mwc-list>
`
: ""}
${this._wifiConfiguration
? html`
<div class="radio-row">
<ha-formfield label="open">
<ha-radio
@change=${this._handleRadioValueChangedAp}
.ap=${this._wifiConfiguration}
value="open"
name="auth"
.checked=${this._wifiConfiguration.auth ===
undefined ||
this._wifiConfiguration.auth === "open"}
>
</ha-radio>
</ha-formfield>
<ha-formfield label="wep">
<ha-radio
@change=${this._handleRadioValueChangedAp}
.ap=${this._wifiConfiguration}
value="wep"
name="auth"
.checked=${this._wifiConfiguration.auth === "wep"}
>
</ha-radio>
</ha-formfield>
<ha-formfield label="wpa-psk">
<ha-radio
@change=${this._handleRadioValueChangedAp}
.ap=${this._wifiConfiguration}
value="wpa-psk"
name="auth"
.checked=${this._wifiConfiguration.auth ===
"wpa-psk"}
>
</ha-radio>
</ha-formfield>
</div>
${this._wifiConfiguration.auth === "wpa-psk" ||
this._wifiConfiguration.auth === "wep"
? html`
<paper-input
class="flex-auto"
type="password"
id="psk"
label="Password"
version="wifi"
@value-changed=${this
._handleInputValueChangedWifi}
>
</paper-input>
`
: ""}
`
: ""}
</ha-expansion-panel>
`
: ""}
${this._dirty
? html`<div class="warning">
If you are changing the Wi-Fi, IP or gateway addresses, you might
lose the connection!
</div>`
: ""}
</div>
<div class="buttons">
<mwc-button label="close" @click=${this.closeDialog}> </mwc-button>
<mwc-button @click=${this._updateNetwork} .disabled=${!this._dirty}>
${this._processing
? html`<ha-circular-progress active size="small">
</ha-circular-progress>`
: "Save"}
</mwc-button>
</div>`;
}
private _selectAP(event) {
this._wifiConfiguration = event.currentTarget.ap;
this._dirty = true;
}
private async _scanForAP() {
if (!this._interface) {
return;
}
this._scanning = true;
try {
this._accessPoints = await accesspointScan(
this.hass,
this._interface.interface
);
} catch (err) {
showAlertDialog(this, {
title: "Failed to scan for accesspoints",
text: extractApiErrorMessage(err),
});
} finally {
this._scanning = false;
}
}
private _renderIPConfiguration(version: string) {
return html`
<ha-expansion-panel
.header=${`IPv${version.charAt(version.length - 1)}`}
outlined
>
<div class="radio-row">
<ha-formfield label="DHCP">
<ha-radio
@change=${this._handleRadioValueChanged}
.version=${version}
value="auto"
name="${version}method"
.checked=${this._interface![version]?.method === "auto"}
>
</ha-radio>
</ha-formfield>
<ha-formfield label="Static">
<ha-radio
@change=${this._handleRadioValueChanged}
.version=${version}
value="static"
name="${version}method"
.checked=${this._interface![version]?.method === "static"}
>
</ha-radio>
</ha-formfield>
<ha-formfield label="Disabled" class="warning">
<ha-radio
@change=${this._handleRadioValueChanged}
.version=${version}
value="disabled"
name="${version}method"
.checked=${this._interface![version]?.method === "disabled"}
>
</ha-radio>
</ha-formfield>
</div>
${this._interface![version].method === "static"
? html`
<paper-input
<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="address"
id="ip_address"
label="IP address/Netmask"
.version=${version}
.value=${this._toString(this._interface![version].address)}
.value="${this._device!.data.ip_address}"
@value-changed=${this._handleInputValueChanged}
>
</paper-input>
></paper-input>
<paper-input
class="flex-auto"
id="gateway"
label="Gateway address"
.version=${version}
.value=${this._interface![version].gateway}
.value="${this._device!.data.gateway}"
@value-changed=${this._handleInputValueChanged}
>
</paper-input>
></paper-input>
<paper-input
class="flex-auto"
id="nameservers"
label="DNS servers"
.version=${version}
.value=${this._toString(this._interface![version].nameservers)}
.value="${this._device!.data.nameservers as string}"
@value-changed=${this._handleInputValueChanged}
>
</paper-input>
`
></paper-input>
NB!: If you are changing IP or gateway addresses, you might lose
the connection.`
: ""}
</ha-expansion-panel>
`;
}
_toArray(data: string | string[]): string[] {
if (Array.isArray(data)) {
if (data && typeof data[0] === "string") {
data = data[0];
}
}
if (!data) {
return [];
}
if (typeof data === "string") {
return data.replace(/ /g, "").split(",");
}
return data;
}
_toString(data: string | string[]): string {
if (!data) {
return "";
}
if (Array.isArray(data)) {
return data.join(", ");
}
return data;
</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._processing = true;
let interfaceOptions: Partial<NetworkInterface> = {};
IP_VERSIONS.forEach((version) => {
interfaceOptions[version] = {
method: this._interface![version]?.method || "auto",
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(","),
};
if (this._interface![version]?.method === "static") {
interfaceOptions[version] = {
...interfaceOptions[version],
address: this._toArray(this._interface![version]?.address),
gateway: this._interface![version]?.gateway,
nameservers: this._toArray(this._interface![version]?.nameservers),
};
}
});
if (this._wifiConfiguration) {
interfaceOptions = {
...interfaceOptions,
wifi: {
ssid: this._wifiConfiguration.ssid,
mode: this._wifiConfiguration.mode,
auth: this._wifiConfiguration.auth || "open",
},
};
if (interfaceOptions.wifi!.auth !== "open") {
interfaceOptions.wifi = {
...interfaceOptions.wifi,
psk: this._wifiConfiguration.psk,
};
}
}
interfaceOptions.enabled =
this._wifiConfiguration !== undefined ||
interfaceOptions.ipv4?.method !== "disabled" ||
interfaceOptions.ipv6?.method !== "disabled";
try {
await updateNetworkInterface(
this.hass,
this._interface!.interface,
interfaceOptions
);
await updateNetworkInterface(this.hass, this._device!.interface, options);
} catch (err) {
showAlertDialog(this, {
title: "Failed to change network settings",
text: extractApiErrorMessage(err),
});
this._processing = false;
this._prosessing = false;
return;
}
this._params?.loadData();
@@ -443,73 +219,40 @@ export class DialogHassioNetwork extends LitElement
dismissText: "no",
});
if (!confirm) {
this.requestUpdate("_interface");
this.requestUpdate("_device");
return;
}
}
this._curTabIndex = ev.detail.index;
this._interface = { ...this._interfaces[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 any).value as "disabled" | "auto" | "static";
const version = (ev.target as any).version as "ipv4" | "ipv6";
const value = (ev.target as HaRadio).value as "dhcp" | "static";
if (
!value ||
!this._interface ||
this._interface[version]!.method === value
) {
if (!value || !this._device || this._device!.data.method === value) {
return;
}
this._dirty = true;
this._interface[version]!.method = value;
this.requestUpdate("_interface");
}
private _handleRadioValueChangedAp(ev: CustomEvent): void {
const value = ((ev.target as any).value as string) as
| "open"
| "wep"
| "wpa-psk";
this._wifiConfiguration!.auth = value;
this._dirty = true;
this.requestUpdate("_wifiConfiguration");
this._device!.data.method = value;
this.requestUpdate("_device");
}
private _handleInputValueChanged(ev: CustomEvent): void {
const value: string | null | undefined = (ev.target as PaperInputElement)
.value;
const version = (ev.target as any).version as "ipv4" | "ipv6";
const id = (ev.target as PaperInputElement).id;
if (
!value ||
!this._interface ||
this._toString(this._interface[version]![id]) === this._toString(value)
) {
if (!value || !this._device || this._device.data[id] === value) {
return;
}
this._dirty = true;
this._interface[version]![id] = value;
}
private _handleInputValueChangedWifi(ev: CustomEvent): void {
const value: string | null | undefined = (ev.target as PaperInputElement)
.value;
const id = (ev.target as PaperInputElement).id;
if (
!value ||
!this._wifiConfiguration ||
this._wifiConfiguration![id] === value
) {
return;
}
this._dirty = true;
this._wifiConfiguration![id] = value;
this._device.data[id] = value;
}
static get styles(): CSSResult[] {
@@ -556,16 +299,12 @@ export class DialogHassioNetwork extends LitElement
--mdc-theme-primary: var(--error-color);
}
mwc-button.scan {
margin-left: 8px;
}
:host([rtl]) app-toolbar {
direction: rtl;
text-align: right;
}
.container {
padding: 0 8px 4px;
padding: 20px 24px;
}
.form {
margin-bottom: 53px;
@@ -583,24 +322,6 @@ export class DialogHassioNetwork extends LitElement
padding-bottom: max(env(safe-area-inset-bottom), 8px);
background-color: var(--mdc-theme-surface, #fff);
}
.warning {
color: var(--error-color);
--primary-color: var(--error-color);
}
div.warning {
margin: 12px 4px -12px;
}
ha-expansion-panel {
--expansion-panel-summary-padding: 0 16px;
margin: 4px 0;
}
paper-input {
padding: 0 14px;
}
mwc-list-item {
--mdc-list-side-padding: 10px;
}
`,
];
}

View File

@@ -13,7 +13,10 @@ export const showNetworkDialog = (
): void => {
fireEvent(element, "show-dialog", {
dialogTag: "dialog-hassio-network",
dialogImport: () => import("./dialog-hassio-network"),
dialogImport: () =>
import(
/* webpackChunkName: "dialog-hassio-network" */ "./dialog-hassio-network"
),
dialogParams,
});
};

View File

@@ -4,7 +4,10 @@ import "./dialog-hassio-registries";
export const showRegistriesDialog = (element: HTMLElement): void => {
fireEvent(element, "show-dialog", {
dialogTag: "dialog-hassio-registries",
dialogImport: () => import("./dialog-hassio-registries"),
dialogImport: () =>
import(
/* webpackChunkName: "dialog-hassio-registries" */ "./dialog-hassio-registries"
),
dialogParams: {},
});
};

View File

@@ -13,7 +13,10 @@ export const showRepositoriesDialog = (
): void => {
fireEvent(element, "show-dialog", {
dialogTag: "dialog-hassio-repositories",
dialogImport: () => import("./dialog-hassio-repositories"),
dialogImport: () =>
import(
/* webpackChunkName: "dialog-hassio-repositories" */ "./dialog-hassio-repositories"
),
dialogParams,
});
};

View File

@@ -109,7 +109,7 @@ class HassioSnapshotDialog extends LitElement {
return html``;
}
return html`
<ha-dialog open @closing=${this._closeDialog} .heading=${true}>
<ha-dialog open stacked @closing=${this._closeDialog} .heading=${true}>
<div slot="heading">
<ha-header-bar>
<span slot="title">
@@ -191,37 +191,47 @@ class HassioSnapshotDialog extends LitElement {
: ""}
${this._error ? html` <p class="error">Error: ${this._error}</p> ` : ""}
<div class="button-row" slot="primaryAction">
<mwc-button @click=${this._partialRestoreClicked}>
<ha-svg-icon .path=${mdiHistory} class="icon"></ha-svg-icon>
Restore Selected
</mwc-button>
${!this._onboarding
? html`
<mwc-button @click=${this._deleteClicked}>
<ha-svg-icon .path=${mdiDelete} class="icon warning">
</ha-svg-icon>
<span class="warning">Delete Snapshot</span>
</mwc-button>
`
: ""}
</div>
<div class="button-row" slot="secondaryAction">
${this._snapshot.type === "full"
? html`
<mwc-button @click=${this._fullRestoreClicked}>
<ha-svg-icon .path=${mdiHistory} class="icon"></ha-svg-icon>
Restore Everything
</mwc-button>
`
: ""}
${!this._onboarding
? html`<mwc-button @click=${this._downloadClicked}>
<ha-svg-icon .path=${mdiDownload} class="icon"></ha-svg-icon>
Download Snapshot
</mwc-button>`
: ""}
</div>
<div>Actions:</div>
${!this._onboarding
? html`<mwc-button
@click=${this._downloadClicked}
slot="primaryAction"
>
<ha-svg-icon .path=${mdiDownload} class="icon"></ha-svg-icon>
Download Snapshot
</mwc-button>`
: ""}
<mwc-button
@click=${this._partialRestoreClicked}
slot="secondaryAction"
>
<ha-svg-icon .path=${mdiHistory} class="icon"></ha-svg-icon>
Restore Selected
</mwc-button>
${this._snapshot.type === "full"
? html`
<mwc-button
@click=${this._fullRestoreClicked}
slot="secondaryAction"
>
<ha-svg-icon .path=${mdiHistory} class="icon"></ha-svg-icon>
Wipe &amp; restore
</mwc-button>
`
: ""}
${!this._onboarding
? html`<mwc-button
@click=${this._deleteClicked}
slot="secondaryAction"
>
<ha-svg-icon
.path=${mdiDelete}
class="icon warning"
></ha-svg-icon>
<span class="warning">Delete Snapshot</span>
</mwc-button>`
: ""}
</ha-dialog>
`;
}
@@ -235,14 +245,6 @@ class HassioSnapshotDialog extends LitElement {
display: block;
margin: 4px;
}
mwc-button ha-svg-icon {
margin-right: 4px;
}
.button-row {
display: grid;
gap: 8px;
margin-right: 8px;
}
.details {
color: var(--secondary-text-color);
}
@@ -250,6 +252,10 @@ class HassioSnapshotDialog extends LitElement {
.error {
color: var(--error-color);
}
.buttons {
display: flex;
flex-direction: column;
}
.buttons li {
list-style-type: none;
}

View File

@@ -12,7 +12,10 @@ export const showHassioSnapshotDialog = (
): void => {
fireEvent(element, "show-dialog", {
dialogTag: "dialog-hassio-snapshot",
dialogImport: () => import("./dialog-hassio-snapshot"),
dialogImport: () =>
import(
/* webpackChunkName: "dialog-hassio-snapshot" */ "./dialog-hassio-snapshot"
),
dialogParams,
});
};

View File

@@ -13,7 +13,10 @@ export const showSnapshotUploadDialog = (
): void => {
fireEvent(element, "show-dialog", {
dialogTag: "dialog-hassio-snapshot-upload",
dialogImport: () => import("./dialog-hassio-snapshot-upload"),
dialogImport: () =>
import(
/* webpackChunkName: "dialog-hassio-snapshot-upload" */ "./dialog-hassio-snapshot-upload"
),
dialogParams,
});
};

View File

@@ -1,22 +1,29 @@
import { html, PropertyValues, customElement, property } from "lit-element";
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 { applyThemesOnElement } from "../../src/common/dom/apply_themes_on_element";
import { fireEvent } from "../../src/common/dom/fire_event";
import { makeDialogManager } from "../../src/dialogs/make-dialog-manager";
import { atLeastVersion } from "../../src/common/config/version";
import { SupervisorBaseElement } from "./supervisor-base-element";
@customElement("hassio-main")
export class HassioMain extends SupervisorBaseElement {
export class HassioMain extends urlSyncMixin(ProvideHassLitMixin(LitElement)) {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public panel!: HassioPanelInfo;
@property() public panel!: HassioPanelInfo;
@property({ type: Boolean }) public narrow!: boolean;
@property() public narrow!: boolean;
@property({ attribute: false }) public route?: Route;
@property() public route?: Route;
protected firstUpdated(changedProps: PropertyValues) {
super.firstUpdated(changedProps);
@@ -70,13 +77,9 @@ export class HassioMain extends SupervisorBaseElement {
}
protected render() {
if (!this.supervisor || !this.hass) {
return html``;
}
return html`
<hassio-router
.hass=${this.hass}
.supervisor=${this.supervisor}
.route=${this.route}
.panel=${this.panel}
.narrow=${this.narrow}

View File

@@ -1,5 +1,10 @@
import { customElement, property } from "lit-element";
import { Supervisor } from "../../src/data/supervisor/supervisor";
import { HassioHassOSInfo, HassioHostInfo } from "../../src/data/hassio/host";
import {
HassioHomeAssistantInfo,
HassioSupervisorInfo,
HassioInfo,
} from "../../src/data/hassio/supervisor";
import {
HassRouterPage,
RouterOptions,
@@ -16,12 +21,20 @@ import "./system/hassio-system";
class HassioPanelRouter extends HassRouterPage {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public supervisor!: Supervisor;
@property({ attribute: false }) public route!: Route;
@property({ type: Boolean }) public narrow!: boolean;
@property({ attribute: false }) public supervisorInfo?: HassioSupervisorInfo;
@property({ attribute: false }) public hassioInfo!: HassioInfo;
@property({ attribute: false }) public hostInfo?: HassioHostInfo;
@property({ attribute: false }) public hassInfo?: HassioHomeAssistantInfo;
@property({ attribute: false }) public hassOsInfo!: HassioHassOSInfo;
protected routerOptions: RouterOptions = {
routes: {
dashboard: {
@@ -41,9 +54,13 @@ class HassioPanelRouter extends HassRouterPage {
protected updatePageEl(el) {
el.hass = this.hass;
el.supervisor = this.supervisor;
el.route = this.route;
el.narrow = this.narrow;
el.supervisorInfo = this.supervisorInfo;
el.hassioInfo = this.hassioInfo;
el.hostInfo = this.hostInfo;
el.hassInfo = this.hassInfo;
el.hassOsInfo = this.hassOsInfo;
}
}

View File

@@ -1,13 +1,18 @@
import {
css,
CSSResult,
customElement,
html,
LitElement,
property,
TemplateResult,
css,
CSSResult,
} from "lit-element";
import { Supervisor } from "../../src/data/supervisor/supervisor";
import { HassioHassOSInfo, HassioHostInfo } from "../../src/data/hassio/host";
import {
HassioHomeAssistantInfo,
HassioSupervisorInfo,
HassioInfo,
} from "../../src/data/hassio/supervisor";
import { HomeAssistant, Route } from "../../src/types";
import "./hassio-panel-router";
@@ -15,19 +20,34 @@ import "./hassio-panel-router";
class HassioPanel extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public supervisor!: Supervisor;
@property({ type: Boolean }) public narrow!: boolean;
@property({ attribute: false }) public route!: Route;
@property({ attribute: false }) public supervisorInfo!: HassioSupervisorInfo;
@property({ attribute: false }) public hassioInfo!: HassioInfo;
@property({ attribute: false }) public hostInfo!: HassioHostInfo;
@property({ attribute: false }) public hassInfo!: HassioHomeAssistantInfo;
@property({ attribute: false }) public hassOsInfo!: HassioHassOSInfo;
protected render(): TemplateResult {
if (!this.supervisorInfo) {
return html``;
}
return html`
<hassio-panel-router
.hass=${this.hass}
.supervisor=${this.supervisor}
.route=${this.route}
.hass=${this.hass}
.narrow=${this.narrow}
.supervisorInfo=${this.supervisorInfo}
.hassioInfo=${this.hassioInfo}
.hostInfo=${this.hostInfo}
.hassInfo=${this.hassInfo}
.hassOsInfo=${this.hassOsInfo}
></hassio-panel-router>
`;
}

View File

@@ -1,6 +1,24 @@
import { customElement, property } from "lit-element";
import { HassioPanelInfo } from "../../src/data/hassio/supervisor";
import { Supervisor } from "../../src/data/supervisor/supervisor";
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,
@@ -14,11 +32,9 @@ import "./hassio-panel";
class HassioRouter extends HassRouterPage {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public supervisor!: Supervisor;
@property() public panel!: HassioPanelInfo;
@property({ attribute: false }) public panel!: HassioPanelInfo;
@property({ type: Boolean }) public narrow!: boolean;
@property() public narrow!: boolean;
protected routerOptions: RouterOptions = {
// Hass.io has a page with tabs, so we route all non-matching routes to it.
@@ -35,22 +51,47 @@ class HassioRouter extends HassRouterPage {
system: "dashboard",
addon: {
tag: "hassio-addon-dashboard",
load: () => import("./addon-view/hassio-addon-dashboard"),
load: () =>
import(
/* webpackChunkName: "hassio-addon-dashboard" */ "./addon-view/hassio-addon-dashboard"
),
},
ingress: {
tag: "hassio-ingress-view",
load: () => import("./ingress-view/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.supervisor = this.supervisor;
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") {
@@ -61,12 +102,45 @@ class HassioRouter extends HassRouterPage {
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 {

View File

@@ -13,10 +13,7 @@ import {
fetchHassioAddonInfo,
HassioAddonDetails,
} from "../../../src/data/hassio/addon";
import {
createHassioSession,
validateHassioSession,
} from "../../../src/data/hassio/ingress";
import { createHassioSession } from "../../../src/data/hassio/supervisor";
import "../../../src/layouts/hass-loading-screen";
import "../../../src/layouts/hass-subpage";
import { HomeAssistant, Route } from "../../../src/types";
@@ -38,17 +35,6 @@ class HassioIngressView extends LitElement {
@property({ type: Boolean })
public narrow = false;
private _sessionKeepAlive?: number;
public disconnectedCallback() {
super.disconnectedCallback();
if (this._sessionKeepAlive) {
clearInterval(this._sessionKeepAlive);
this._sessionKeepAlive = undefined;
}
}
protected render(): TemplateResult {
if (!this._addon) {
return html` <hass-loading-screen></hass-loading-screen> `;
@@ -58,7 +44,6 @@ class HassioIngressView extends LitElement {
if (!this.ingressPanel) {
return html`<hass-subpage
.hass=${this.hass}
.header=${this._addon.name}
.narrow=${this.narrow}
>
@@ -98,7 +83,10 @@ class HassioIngressView extends LitElement {
}
private async _fetchData(addonSlug: string) {
const createSessionPromise = createHassioSession(this.hass);
const createSessionPromise = createHassioSession(this.hass).then(
() => true,
() => false
);
let addon;
@@ -131,11 +119,7 @@ class HassioIngressView extends LitElement {
return;
}
let session;
try {
session = await createSessionPromise;
} catch (err) {
if (!(await createSessionPromise)) {
await showAlertDialog(this, {
text: "Unable to create an Ingress session",
title: addon.name,
@@ -144,17 +128,6 @@ class HassioIngressView extends LitElement {
return;
}
if (this._sessionKeepAlive) {
clearInterval(this._sessionKeepAlive);
}
this._sessionKeepAlive = window.setInterval(async () => {
try {
await validateHassioSession(this.hass, session);
} catch (err) {
session = await createHassioSession(this.hass);
}
}, 60000);
this._addon = addon;
}

View File

@@ -26,6 +26,7 @@ import {
TemplateResult,
} from "lit-element";
import { atLeastVersion } from "../../../src/common/config/version";
import { fireEvent } from "../../../src/common/dom/fire_event";
import "../../../src/components/buttons/ha-progress-button";
import "../../../src/components/ha-button-menu";
import "../../../src/components/ha-card";
@@ -40,7 +41,7 @@ import {
HassioSnapshot,
reloadHassioSnapshots,
} from "../../../src/data/hassio/snapshot";
import { Supervisor } from "../../../src/data/supervisor/supervisor";
import { HassioSupervisorInfo } from "../../../src/data/hassio/supervisor";
import "../../../src/layouts/hass-tabs-subpage";
import { PolymerChangedEvent } from "../../../src/polymer-types";
import { haStyle } from "../../../src/resources/styles";
@@ -66,7 +67,7 @@ class HassioSnapshots extends LitElement {
@property({ attribute: false }) public route!: Route;
@property({ attribute: false }) public supervisor!: Supervisor;
@property({ attribute: false }) public supervisorInfo!: HassioSupervisorInfo;
@internalProperty() private _snapshotName = "";
@@ -265,7 +266,7 @@ class HassioSnapshots extends LitElement {
protected updated(changedProps: PropertyValues) {
if (changedProps.has("supervisorInfo")) {
this._addonList = this.supervisor.supervisor.addons
this._addonList = this.supervisorInfo.addons
.map((addon) => ({
slug: addon.slug,
name: addon.name,
@@ -371,6 +372,7 @@ class HassioSnapshots extends LitElement {
await createHassioPartialSnapshot(this.hass, data);
}
this._updateSnapshots();
fireEvent(this, "hass-api-called", { success: true, response: null });
} catch (err) {
this._error = extractApiErrorMessage(err);
}

View File

@@ -1,69 +0,0 @@
import { LitElement, property, PropertyValues } from "lit-element";
import {
fetchHassioHassOsInfo,
fetchHassioHostInfo,
} from "../../src/data/hassio/host";
import { fetchNetworkInfo } from "../../src/data/hassio/network";
import { fetchHassioResolution } from "../../src/data/hassio/resolution";
import {
fetchHassioHomeAssistantInfo,
fetchHassioInfo,
fetchHassioSupervisorInfo,
} from "../../src/data/hassio/supervisor";
import { Supervisor } from "../../src/data/supervisor/supervisor";
import { ProvideHassLitMixin } from "../../src/mixins/provide-hass-lit-mixin";
import { urlSyncMixin } from "../../src/state/url-sync-mixin";
declare global {
interface HASSDomEvents {
"supervisor-update": Partial<Supervisor>;
}
}
export class SupervisorBaseElement extends urlSyncMixin(
ProvideHassLitMixin(LitElement)
) {
@property({ attribute: false }) public supervisor?: Supervisor;
protected _updateSupervisor(obj: Partial<Supervisor>): void {
this.supervisor = { ...this.supervisor!, ...obj };
}
protected firstUpdated(changedProps: PropertyValues): void {
super.firstUpdated(changedProps);
this._initSupervisor();
this.addEventListener("supervisor-update", (ev) =>
this._updateSupervisor(ev.detail)
);
}
private async _initSupervisor(): Promise<void> {
const [
supervisor,
host,
core,
info,
os,
network,
resolution,
] = await Promise.all([
fetchHassioSupervisorInfo(this.hass),
fetchHassioHostInfo(this.hass),
fetchHassioHomeAssistantInfo(this.hass),
fetchHassioInfo(this.hass),
fetchHassioHassOsInfo(this.hass),
fetchNetworkInfo(this.hass),
fetchHassioResolution(this.hass),
]);
this.supervisor = {
supervisor,
host,
core,
info,
os,
network,
resolution,
};
}
}

View File

@@ -8,12 +8,12 @@ import {
CSSResult,
customElement,
html,
internalProperty,
LitElement,
property,
TemplateResult,
} from "lit-element";
import memoizeOne from "memoize-one";
import { fireEvent } from "../../../src/common/dom/fire_event";
import "../../../src/components/buttons/ha-progress-button";
import "../../../src/components/ha-button-menu";
import "../../../src/components/ha-card";
@@ -27,6 +27,8 @@ import {
changeHostOptions,
configSyncOS,
fetchHassioHostInfo,
HassioHassOSInfo,
HassioHostInfo as HassioHostInfoType,
rebootHost,
shutdownHost,
updateOS,
@@ -35,7 +37,7 @@ import {
fetchNetworkInfo,
NetworkInfo,
} from "../../../src/data/hassio/network";
import { Supervisor } from "../../../src/data/supervisor/supervisor";
import { HassioInfo } from "../../../src/data/hassio/supervisor";
import {
showAlertDialog,
showConfirmationDialog,
@@ -51,22 +53,28 @@ import { hassioStyle } from "../resources/hassio-style";
class HassioHostInfo extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public supervisor!: Supervisor;
@property() public hostInfo!: HassioHostInfoType;
@property({ attribute: false }) public hassioInfo!: HassioInfo;
@property({ attribute: false }) public hassOsInfo!: HassioHassOSInfo;
@internalProperty() public _networkInfo?: NetworkInfo;
protected render(): TemplateResult | void {
const primaryIpAddress = this.supervisor.host.features.includes("network")
? this._primaryIpAddress(this.supervisor.network!)
const primaryIpAddress = this.hostInfo.features.includes("network")
? this._primaryIpAddress(this._networkInfo!)
: "";
return html`
<ha-card header="Host System">
<div class="card-content">
${this.supervisor.host.features.includes("hostname")
${this.hostInfo.features.includes("hostname")
? html`<ha-settings-row>
<span slot="heading">
Hostname
</span>
<span slot="description">
${this.supervisor.host.hostname}
${this.hostInfo.hostname}
</span>
<mwc-button
title="Change the hostname"
@@ -76,7 +84,7 @@ class HassioHostInfo extends LitElement {
</mwc-button>
</ha-settings-row>`
: ""}
${this.supervisor.host.features.includes("network")
${this.hostInfo.features.includes("network")
? html` <ha-settings-row>
<span slot="heading">
IP Address
@@ -98,9 +106,10 @@ class HassioHostInfo extends LitElement {
Operating System
</span>
<span slot="description">
${this.supervisor.host.operating_system}
${this.hostInfo.operating_system}
</span>
${this.supervisor.os.update_available
${this.hostInfo.features.includes("hassos") &&
this.hassOsInfo.update_available
? html`
<ha-progress-button
title="Update the host OS"
@@ -111,29 +120,29 @@ class HassioHostInfo extends LitElement {
`
: ""}
</ha-settings-row>
${!this.supervisor.host.features.includes("hassos")
${!this.hostInfo.features.includes("hassos")
? html`<ha-settings-row>
<span slot="heading">
Docker version
</span>
<span slot="description">
${this.supervisor.info.docker}
${this.hassioInfo.docker}
</span>
</ha-settings-row>`
: ""}
${this.supervisor.host.deployment
${this.hostInfo.deployment
? html`<ha-settings-row>
<span slot="heading">
Deployment
</span>
<span slot="description">
${this.supervisor.host.deployment}
${this.hostInfo.deployment}
</span>
</ha-settings-row>`
: ""}
</div>
<div class="card-actions">
${this.supervisor.host.features.includes("reboot")
${this.hostInfo.features.includes("reboot")
? html`
<ha-progress-button
title="Reboot the host OS"
@@ -144,7 +153,7 @@ class HassioHostInfo extends LitElement {
</ha-progress-button>
`
: ""}
${this.supervisor.host.features.includes("shutdown")
${this.hostInfo.features.includes("shutdown")
? html`
<ha-progress-button
title="Shutdown the host OS"
@@ -166,7 +175,7 @@ class HassioHostInfo extends LitElement {
<mwc-list-item title="Show a list of hardware">
Hardware
</mwc-list-item>
${this.supervisor.host.features.includes("hassos")
${this.hostInfo.features.includes("hassos")
? html`<mwc-list-item
title="Load HassOS configs or updates from USB"
>
@@ -184,10 +193,12 @@ class HassioHostInfo extends LitElement {
}
private _primaryIpAddress = memoizeOne((network_info: NetworkInfo) => {
if (!network_info || !network_info.interfaces) {
if (!network_info) {
return "";
}
return network_info.interfaces.find((a) => a.primary)?.ipv4?.address![0];
return Object.keys(network_info?.interfaces)
.map((device) => network_info.interfaces[device])
.find((device) => device.primary)?.ip_address;
});
private async _handleMenuAction(ev: CustomEvent<ActionDetail>) {
@@ -305,13 +316,13 @@ class HassioHostInfo extends LitElement {
private async _changeNetworkClicked(): Promise<void> {
showNetworkDialog(this, {
network: this.supervisor.network!,
network: this._networkInfo!,
loadData: () => this._loadData(),
});
}
private async _changeHostnameClicked(): Promise<void> {
const curHostname: string = this.supervisor.host.hostname;
const curHostname: string = this.hostInfo.hostname;
const hostname = await showPromptDialog(this, {
title: "Change Hostname",
inputLabel: "Please enter a new hostname:",
@@ -322,8 +333,7 @@ class HassioHostInfo extends LitElement {
if (hostname && hostname !== curHostname) {
try {
await changeHostOptions(this.hass, { hostname });
const host = await fetchHassioHostInfo(this.hass);
fireEvent(this, "supervisor-update", { host });
this.hostInfo = await fetchHassioHostInfo(this.hass);
} catch (err) {
showAlertDialog(this, {
title: "Setting hostname failed",
@@ -336,8 +346,7 @@ class HassioHostInfo extends LitElement {
private async _importFromUSB(): Promise<void> {
try {
await configSyncOS(this.hass);
const host = await fetchHassioHostInfo(this.hass);
fireEvent(this, "supervisor-update", { host });
this.hostInfo = await fetchHassioHostInfo(this.hass);
} catch (err) {
showAlertDialog(this, {
title: "Failed to import from USB",
@@ -347,8 +356,7 @@ class HassioHostInfo extends LitElement {
}
private async _loadData(): Promise<void> {
const network = await fetchNetworkInfo(this.hass);
fireEvent(this, "supervisor-update", { network });
this._networkInfo = await fetchNetworkInfo(this.hass);
}
static get styles(): CSSResult[] {

View File

@@ -13,15 +13,16 @@ import "../../../src/components/ha-card";
import "../../../src/components/ha-settings-row";
import "../../../src/components/ha-switch";
import { extractApiErrorMessage } from "../../../src/data/hassio/common";
import { HassioHostInfo as HassioHostInfoType } from "../../../src/data/hassio/host";
import { fetchHassioResolution } from "../../../src/data/hassio/resolution";
import {
fetchHassioSupervisorInfo,
HassioSupervisorInfo as HassioSupervisorInfoType,
reloadSupervisor,
restartSupervisor,
setSupervisorOption,
SupervisorOptions,
updateSupervisor,
} from "../../../src/data/hassio/supervisor";
import { Supervisor } from "../../../src/data/supervisor/supervisor";
import {
showAlertDialog,
showConfirmationDialog,
@@ -31,7 +32,7 @@ import { HomeAssistant } from "../../../src/types";
import { documentationUrl } from "../../../src/util/documentation-url";
import { hassioStyle } from "../resources/hassio-style";
const UNSUPPORTED_REASON = {
const ISSUES = {
container: {
title: "Containers known to cause issues",
url: "/more-info/unsupported/container",
@@ -45,10 +46,6 @@ const UNSUPPORTED_REASON = {
title: "Docker Version",
url: "/more-info/unsupported/docker_version",
},
job_conditions: {
title: "Ignored job conditions",
url: "/more-info/unsupported/job_conditions",
},
lxc: { title: "LXC", url: "/more-info/unsupported/lxc" },
network_manager: {
title: "Network Manager",
@@ -62,30 +59,14 @@ const UNSUPPORTED_REASON = {
systemd: { title: "Systemd", url: "/more-info/unsupported/systemd" },
};
const UNHEALTHY_REASON = {
privileged: {
title: "Supervisor is not privileged",
url: "/more-info/unsupported/privileged",
},
supervisor: {
title: "Supervisor was not able to update",
url: "/more-info/unhealthy/supervisor",
},
setup: {
title: "Setup of the Supervisor failed",
url: "/more-info/unhealthy/setup",
},
docker: {
title: "The Docker environment is not working properly",
url: "/more-info/unhealthy/docker",
},
};
@customElement("hassio-supervisor-info")
class HassioSupervisorInfo extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public supervisor!: Supervisor;
@property({ attribute: false })
public supervisorInfo!: HassioSupervisorInfoType;
@property() public hostInfo!: HassioHostInfoType;
protected render(): TemplateResult | void {
return html`
@@ -96,7 +77,7 @@ class HassioSupervisorInfo extends LitElement {
Version
</span>
<span slot="description">
${this.supervisor.supervisor.version}
${this.supervisorInfo.version}
</span>
</ha-settings-row>
<ha-settings-row>
@@ -104,9 +85,9 @@ class HassioSupervisorInfo extends LitElement {
Newest Version
</span>
<span slot="description">
${this.supervisor.supervisor.version_latest}
${this.supervisorInfo.version_latest}
</span>
${this.supervisor.supervisor.update_available
${this.supervisorInfo.update_available
? html`
<ha-progress-button
title="Update the supervisor"
@@ -122,9 +103,9 @@ class HassioSupervisorInfo extends LitElement {
Channel
</span>
<span slot="description">
${this.supervisor.supervisor.channel}
${this.supervisorInfo.channel}
</span>
${this.supervisor.supervisor.channel === "beta"
${this.supervisorInfo.channel === "beta"
? html`
<ha-progress-button
@click=${this._toggleBeta}
@@ -133,7 +114,7 @@ class HassioSupervisorInfo extends LitElement {
Leave beta channel
</ha-progress-button>
`
: this.supervisor.supervisor.channel === "stable"
: this.supervisorInfo.channel === "stable"
? html`
<ha-progress-button
@click=${this._toggleBeta}
@@ -145,7 +126,7 @@ class HassioSupervisorInfo extends LitElement {
: ""}
</ha-settings-row>
${this.supervisor.supervisor.supported
${this.supervisorInfo?.supported
? html` <ha-settings-row three-line>
<span slot="heading">
Share Diagnostics
@@ -162,7 +143,7 @@ class HassioSupervisorInfo extends LitElement {
</div>
<ha-switch
haptic
.checked=${this.supervisor.supervisor.diagnostics}
.checked=${this.supervisorInfo.diagnostics}
@change=${this._toggleDiagnostics}
></ha-switch>
</ha-settings-row>`
@@ -176,33 +157,14 @@ class HassioSupervisorInfo extends LitElement {
Learn more
</button>
</div>`}
${!this.supervisor.supervisor.healthy
? html`<div class="error">
Your installtion is running in an unhealthy state.
<button
class="link"
title="Learn more about why your system is marked as unhealthy"
@click=${this._unhealthyDialog}
>
Learn more
</button>
</div>`
: ""}
</div>
<div class="card-actions">
<ha-progress-button
@click=${this._supervisorReload}
title="Reload parts of the Supervisor"
title="Reload parts of the supervisor"
>
Reload
</ha-progress-button>
<ha-progress-button
class="warning"
@click=${this._supervisorRestart}
title="Restart the Supervisor"
>
Restart
</ha-progress-button>
</div>
</ha-card>
`;
@@ -212,7 +174,7 @@ class HassioSupervisorInfo extends LitElement {
const button = ev.currentTarget as any;
button.progress = true;
if (this.supervisor.supervisor.channel === "stable") {
if (this.supervisorInfo.channel === "stable") {
const confirmed = await showConfirmationDialog(this, {
title: "WARNING",
text: html` Beta releases are for testers and early adopters and can
@@ -241,19 +203,18 @@ class HassioSupervisorInfo extends LitElement {
try {
const data: Partial<SupervisorOptions> = {
channel:
this.supervisor.supervisor.channel === "stable" ? "beta" : "stable",
channel: this.supervisorInfo.channel === "stable" ? "beta" : "stable",
};
await setSupervisorOption(this.hass, data);
await this._reloadSupervisor();
await reloadSupervisor(this.hass);
fireEvent(this, "hass-api-called", { success: true, response: null });
} catch (err) {
showAlertDialog(this, {
title: "Failed to set supervisor option",
text: extractApiErrorMessage(err),
});
} finally {
button.progress = false;
}
button.progress = false;
}
private async _supervisorReload(ev: CustomEvent): Promise<void> {
@@ -261,37 +222,15 @@ class HassioSupervisorInfo extends LitElement {
button.progress = true;
try {
await this._reloadSupervisor();
await reloadSupervisor(this.hass);
this.supervisorInfo = await fetchHassioSupervisorInfo(this.hass);
} catch (err) {
showAlertDialog(this, {
title: "Failed to reload the supervisor",
text: extractApiErrorMessage(err),
});
} finally {
button.progress = false;
}
}
private async _reloadSupervisor(): Promise<void> {
await reloadSupervisor(this.hass);
const supervisor = await fetchHassioSupervisorInfo(this.hass);
fireEvent(this, "supervisor-update", { supervisor });
}
private async _supervisorRestart(ev: CustomEvent): Promise<void> {
const button = ev.currentTarget as any;
button.progress = true;
try {
await restartSupervisor(this.hass);
} catch (err) {
showAlertDialog(this, {
title: "Failed to restart the supervisor",
text: extractApiErrorMessage(err),
});
} finally {
button.progress = false;
}
button.progress = false;
}
private async _supervisorUpdate(ev: CustomEvent): Promise<void> {
@@ -300,7 +239,7 @@ class HassioSupervisorInfo extends LitElement {
const confirmed = await showConfirmationDialog(this, {
title: "Update Supervisor",
text: `Are you sure you want to update supervisor to version ${this.supervisor.supervisor.version_latest}?`,
text: `Are you sure you want to update supervisor to version ${this.supervisorInfo.version_latest}?`,
confirmText: "update",
dismissText: "cancel",
});
@@ -317,9 +256,8 @@ class HassioSupervisorInfo extends LitElement {
title: "Failed to update the supervisor",
text: extractApiErrorMessage(err),
});
} finally {
button.progress = false;
}
button.progress = false;
}
private async _diagnosticsInformationDialog(): Promise<void> {
@@ -338,53 +276,22 @@ class HassioSupervisorInfo extends LitElement {
}
private async _unsupportedDialog(): Promise<void> {
const resolution = await fetchHassioResolution(this.hass);
await showAlertDialog(this, {
title: "You are running an unsupported installation",
text: html`Below is a list of issues found with your installation, click
on the links to learn how you can resolve the issues. <br /><br />
<ul>
${this.supervisor.resolution.unsupported.map(
${resolution.unsupported.map(
(issue) => html`
<li>
${UNSUPPORTED_REASON[issue]
${ISSUES[issue]
? html`<a
href="${documentationUrl(
this.hass,
UNSUPPORTED_REASON[issue].url
)}"
href="${documentationUrl(this.hass, ISSUES[issue].url)}"
target="_blank"
rel="noreferrer"
>
${UNSUPPORTED_REASON[issue].title}
</a>`
: issue}
</li>
`
)}
</ul>`,
});
}
private async _unhealthyDialog(): Promise<void> {
await showAlertDialog(this, {
title: "Your installation is unhealthy",
text: html`Running an unhealthy installation will cause issues. Below is a
list of issues found with your installation, click on the links to learn
how you can resolve the issues. <br /><br />
<ul>
${this.supervisor.resolution.unhealthy.map(
(issue) => html`
<li>
${UNHEALTHY_REASON[issue]
? html`<a
href="${documentationUrl(
this.hass,
UNHEALTHY_REASON[issue].url
)}"
target="_blank"
rel="noreferrer"
>
${UNHEALTHY_REASON[issue].title}
${ISSUES[issue].title}
</a>`
: issue}
</li>
@@ -397,7 +304,7 @@ class HassioSupervisorInfo extends LitElement {
private async _toggleDiagnostics(): Promise<void> {
try {
const data: SupervisorOptions = {
diagnostics: !this.supervisor.supervisor?.diagnostics,
diagnostics: !this.supervisorInfo?.diagnostics,
};
await setSupervisorOption(this.hass, data);
} catch (err) {

View File

@@ -19,7 +19,6 @@ import "../../../src/components/ha-card";
import "../../../src/components/ha-settings-row";
import { fetchHassioStats, HassioStats } from "../../../src/data/hassio/common";
import { HassioHostInfo } from "../../../src/data/hassio/host";
import { Supervisor } from "../../../src/data/supervisor/supervisor";
import { haStyle } from "../../../src/resources/styles";
import { HomeAssistant } from "../../../src/types";
import { bytesToString } from "../../../src/util/bytes-to-string";
@@ -33,7 +32,7 @@ import { hassioStyle } from "../resources/hassio-style";
class HassioSystemMetrics extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public supervisor!: Supervisor;
@property() public hostInfo!: HassioHostInfo;
@internalProperty() private _supervisorMetrics?: HassioStats;
@@ -65,8 +64,8 @@ class HassioSystemMetrics extends LitElement {
},
{
description: "Used Space",
value: this._getUsedSpace(this.supervisor.host),
tooltip: `${this.supervisor.host.disk_used} GB/${this.supervisor.host.disk_total} GB`,
value: this._getUsedSpace(this.hostInfo),
tooltip: `${this.hostInfo.disk_used} GB/${this.hostInfo.disk_total} GB`,
},
];

View File

@@ -7,7 +7,14 @@ import {
property,
TemplateResult,
} from "lit-element";
import { Supervisor } from "../../../src/data/supervisor/supervisor";
import {
HassioHassOSInfo,
HassioHostInfo,
} from "../../../src/data/hassio/host";
import {
HassioInfo,
HassioSupervisorInfo,
} from "../../../src/data/hassio/supervisor";
import "../../../src/layouts/hass-tabs-subpage";
import { haStyle } from "../../../src/resources/styles";
import { HomeAssistant, Route } from "../../../src/types";
@@ -22,12 +29,18 @@ import "./hassio-system-metrics";
class HassioSystem extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public supervisor!: Supervisor;
@property({ type: Boolean }) public narrow!: boolean;
@property({ attribute: false }) public route!: Route;
@property() public supervisorInfo!: HassioSupervisorInfo;
@property({ attribute: false }) public hassioInfo!: HassioInfo;
@property() public hostInfo!: HassioHostInfo;
@property({ attribute: false }) public hassOsInfo!: HassioHassOSInfo;
protected render(): TemplateResult | void {
return html`
<hass-tabs-subpage
@@ -43,15 +56,18 @@ class HassioSystem extends LitElement {
<div class="card-group">
<hassio-supervisor-info
.hass=${this.hass}
.supervisor=${this.supervisor}
.hostInfo=${this.hostInfo}
.supervisorInfo=${this.supervisorInfo}
></hassio-supervisor-info>
<hassio-host-info
.hass=${this.hass}
.supervisor=${this.supervisor}
.hassioInfo=${this.hassioInfo}
.hostInfo=${this.hostInfo}
.hassOsInfo=${this.hassOsInfo}
></hassio-host-info>
<hassio-system-metrics
.hass=${this.hass}
.supervisor=${this.supervisor}
.hostInfo=${this.hostInfo}
></hassio-system-metrics>
</div>
<hassio-supervisor-log .hass=${this.hass}></hassio-supervisor-log>

View File

@@ -83,9 +83,6 @@
"@types/sortablejs": "^1.10.6",
"@vaadin/vaadin-combo-box": "^5.0.10",
"@vaadin/vaadin-date-picker": "^4.0.7",
"@vibrant/color": "^3.2.1-alpha.1",
"@vibrant/core": "^3.2.1-alpha.1",
"@vibrant/quantizer-mmcq": "^3.2.1-alpha.1",
"@vue/web-component-wrapper": "^1.2.0",
"@webcomponents/webcomponentsjs": "^2.2.7",
"chart.js": "~2.8.0",
@@ -93,6 +90,7 @@
"codemirror": "^5.49.0",
"comlink": "^4.3.0",
"core-js": "^3.6.5",
"cpx": "^1.5.0",
"cropperjs": "^1.5.7",
"deep-clone-simple": "^1.1.1",
"deep-freeze": "^0.0.1",
@@ -112,7 +110,7 @@
"marked": "^1.1.1",
"mdn-polyfills": "^5.16.0",
"memoize-one": "^5.0.2",
"node-vibrant": "3.2.1-alpha.1",
"node-vibrant": "^3.1.6",
"proxy-polyfill": "^0.3.1",
"punycode": "^2.1.1",
"qrcode": "^1.4.4",
@@ -121,10 +119,7 @@
"roboto-fontface": "^0.10.0",
"sortablejs": "^1.10.2",
"superstruct": "^0.10.12",
"tinykeys": "^1.1.1",
"unfetch": "^4.1.0",
"vis-data": "^7.1.1",
"vis-network": "^8.5.4",
"vue": "^2.6.11",
"vue2-daterange-picker": "^0.5.1",
"web-animations-js": "^2.3.2",
@@ -144,11 +139,9 @@
"@babel/plugin-proposal-optional-chaining": "^7.11.0",
"@babel/plugin-syntax-dynamic-import": "^7.8.3",
"@babel/plugin-syntax-import-meta": "^7.10.4",
"@babel/plugin-syntax-top-level-await": "^7.10.4",
"@babel/preset-env": "^7.11.5",
"@babel/preset-typescript": "^7.10.4",
"@koa/cors": "^3.1.0",
"@open-wc/dev-server-hmr": "^0.0.2",
"@rollup/plugin-babel": "^5.2.1",
"@rollup/plugin-commonjs": "^11.1.0",
"@rollup/plugin-json": "^4.0.3",
"@rollup/plugin-node-resolve": "^7.1.3",
@@ -167,11 +160,8 @@
"@types/webspeechapi": "^0.0.29",
"@typescript-eslint/eslint-plugin": "^4.4.0",
"@typescript-eslint/parser": "^4.4.0",
"@web/dev-server": "^0.0.24",
"@web/dev-server-rollup": "^0.2.11",
"babel-loader": "^8.1.0",
"chai": "^4.2.0",
"cpx": "^1.5.0",
"del": "^4.0.0",
"eslint": "^6.8.0",
"eslint-config-airbnb-typescript": "^7.2.1",
@@ -187,6 +177,7 @@
"gulp": "^4.0.0",
"gulp-foreach": "^0.1.0",
"gulp-json-transform": "^0.4.6",
"gulp-jsonminify": "^1.1.0",
"gulp-merge-json": "^1.3.1",
"gulp-rename": "^2.0.0",
"gulp-zopfli-green": "^3.0.1",
@@ -205,6 +196,7 @@
"raw-loader": "^2.0.0",
"require-dir": "^1.2.0",
"rollup": "^2.8.2",
"rollup-plugin-babel": "^4.4.0",
"rollup-plugin-string": "^3.0.0",
"rollup-plugin-terser": "^5.3.0",
"rollup-plugin-visualizer": "^4.0.4",
@@ -212,15 +204,15 @@
"sinon": "^7.3.1",
"source-map-url": "^0.4.0",
"systemjs": "^6.3.2",
"terser-webpack-plugin": "^5.0.0",
"terser-webpack-plugin": "^3.0.6",
"ts-lit-plugin": "^1.2.1",
"ts-mocha": "^7.0.0",
"typescript": "^4.0.3",
"vinyl-buffer": "^1.0.1",
"vinyl-source-stream": "^2.0.0",
"webpack": "5.1.3",
"webpack-cli": "4.1.0",
"webpack-dev-server": "^3.11.0",
"webpack": "5.0.0-rc.3",
"webpack-cli": "4.0.0-rc.0",
"webpack-dev-server": "^3.10.3",
"webpack-manifest-plugin": "3.0.0-rc.0",
"workbox-build": "^5.1.3"
},

41
polymer.json Normal file
View File

@@ -0,0 +1,41 @@
{
"entrypoint": "index.html",
"shell": "src/entrypoints/app.js",
"fragments": [
"src/panels/config/ha-panel-config.js",
"src/panels/dev-event/ha-panel-dev-event.js",
"src/panels/dev-info/ha-panel-dev-info.js",
"src/panels/dev-mqtt/ha-panel-dev-mqtt.js",
"src/panels/dev-service/ha-panel-dev-service.js",
"src/panels/dev-state/ha-panel-dev-state.js",
"src/panels/dev-template/ha-panel-dev-template.js",
"src/panels/history/ha-panel-history.js",
"src/panels/iframe/ha-panel-iframe.js",
"src/panels/logbook/ha-panel-logbook.js",
"src/panels/map/ha-panel-map.js",
"src/panels/shopping-list/ha-panel-shopping-list.js",
"src/panels/mailbox/ha-panel-mailbox.js",
"hassio/src/entrypoint.js"
],
"sources": ["src/**/*", "!src/translations/*"],
"lint": {
"rules": ["polymer-3"],
"ignoreWarnings": ["could-not-resolve-reference", "could-not-load"],
"filesToIgnore": [
"**/*.html",
"**/src/panels/config/js/**/*.js",
"**/ha-iconset-svg.js",
"**/ha-script-editor.js",
"**/ha-automation-editor.js",
"**/ha-big-calendar.js"
]
},
"builds": [
{
"preset": "es5-bundled"
},
{
"preset": "es6-bundled"
}
]
}

View File

@@ -1,55 +0,0 @@
#!/bin/sh
# Helper to start Home Assistant Core inside the devcontainer
# Stop on errors
set -e
if [ -z "${DEVCONTAINER}" ]; then
echo "This task should only run inside a devcontainer, for local install HA Core in a venv."
exit 1
fi
if [ ! -z "${CODESPACES}" ]; then
WORKSPACE="/root/workspace/frontend"
else
WORKSPACE="/workspaces/frontend"
fi
if [ -z $(which hass) ]; then
echo "Installing Home Asstant core from dev."
python3 -m pip install --upgrade \
colorlog \
git+git://github.com/home-assistant/home-assistant.git@dev
fi
if [ ! -d "${WORKSPACE}/config" ]; then
echo "Creating default configuration."
mkdir -p "${WORKSPACE}/config";
hass --script ensure_config -c config
echo "demo:
logger:
default: info
logs:
homeassistant.components.frontend: debug
" >> "${WORKSPACE}/config/configuration.yaml"
if [ ! -z "${HASSIO}" ]; then
echo "
# frontend:
# development_repo: ${WORKSPACE}
hassio:
development_repo: ${WORKSPACE}" >> "${WORKSPACE}/config/configuration.yaml"
else
echo "
frontend:
development_repo: ${WORKSPACE}
# hassio:
# development_repo: ${WORKSPACE}" >> "${WORKSPACE}/config/configuration.yaml"
fi
fi
hass -c "${WORKSPACE}/config"

View File

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

View File

@@ -18,7 +18,7 @@ import "./ha-auth-flow";
import { extractSearchParamsObject } from "../common/url/search-params";
import punycode from "punycode";
import("./ha-pick-auth-provider");
import(/* webpackChunkName: "pick-auth-provider" */ "./ha-pick-auth-provider");
class HaAuthorize extends litLocalizeLiteMixin(LitElement) {
@property() public clientId?: string;

View File

@@ -1,4 +1,10 @@
import { expandHex } from "./hex";
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);
@@ -8,7 +14,10 @@ const rgb_hex = (component: number): string => {
// Conversion between HEX and RGB
export const hex2rgb = (hex: string): [number, number, number] => {
hex = expandHex(hex);
hex = hex.replace("#", "");
if (hex.length === 3 || hex.length === 4) {
hex = expand_hex(hex);
}
return [
parseInt(hex.substring(0, 2), 16),

View File

@@ -1,24 +0,0 @@
export const expandHex = (hex: string): string => {
hex = hex.replace("#", "");
if (hex.length === 6) return hex;
let result = "";
for (const val of hex) {
result += val + val;
}
return result;
};
// Blend 2 hex colors: c1 is placed over c2, blend is c1's opacity.
export const hexBlend = (c1: string, c2: string, blend = 50): string => {
let color = "";
c1 = expandHex(c1);
c2 = expandHex(c2);
for (let i = 0; i <= 5; i += 2) {
const h1 = parseInt(c1.substr(i, 2), 16);
const h2 = parseInt(c2.substr(i, 2), 16);
let hex = Math.floor(h2 + (h1 - h2) * (blend / 100)).toString(16);
while (hex.length < 2) hex = "0" + hex;
color += hex;
}
return `#${color}`;
};

View File

@@ -22,8 +22,3 @@ export const rgbContrast = (
return (lum2 + 0.05) / (lum1 + 0.05);
};
export const getRGBContrastRatio = (
rgb1: [number, number, number],
rgb2: [number, number, number]
) => Math.round((rgbContrast(rgb1, rgb2) + Number.EPSILON) * 100) / 100;

View File

@@ -1,18 +0,0 @@
import { isComponentLoaded } from "./is_component_loaded";
import { PageNavigation } from "../../layouts/hass-tabs-subpage";
import { HomeAssistant } from "../../types";
export const canShowPage = (hass: HomeAssistant, page: PageNavigation) => {
return (
(isCore(page) || isLoadedIntegration(hass, page)) &&
!hideAdvancedPage(hass, page)
);
};
const isLoadedIntegration = (hass: HomeAssistant, page: PageNavigation) =>
!page.component || isComponentLoaded(hass, page.component);
const isCore = (page: PageNavigation) => page.core;
const isAdvancedPage = (page: PageNavigation) => page.advancedOnly;
const userWantsAdvanced = (hass: HomeAssistant) => hass.userData?.showAdvanced;
const hideAdvancedPage = (hass: HomeAssistant, page: PageNavigation) =>
isAdvancedPage(page) && !userWantsAdvanced(hass);

View File

@@ -1,6 +1,5 @@
import { Theme } from "../../data/ws-themes";
import { darkStyles, derivedStyles } from "../../resources/styles";
import type { HomeAssistant } from "../../types";
import { HomeAssistant, Theme } from "../../types";
import {
hex2rgb,
lab2hex,
@@ -8,16 +7,15 @@ import {
rgb2hex,
rgb2lab,
} from "../color/convert-color";
import { hexBlend } from "../color/hex";
import { labBrighten, labDarken } from "../color/lab";
import { rgbContrast } from "../color/rgb";
interface ProcessedTheme {
keys: { [key: string]: "" };
styles: Record<string, string>;
styles: { [key: string]: string };
}
let PROCESSED_THEMES: Record<string, ProcessedTheme> = {};
let PROCESSED_THEMES: { [key: string]: ProcessedTheme } = {};
/**
* Apply a theme to an element by setting the CSS variables on it.
@@ -39,13 +37,6 @@ export const applyThemesOnElement = (
if (themeOptions.dark) {
cacheKey = `${cacheKey}__dark`;
themeRules = darkStyles;
if (themeOptions.primaryColor) {
themeRules["app-header-background-color"] = hexBlend(
themeOptions.primaryColor,
"#121212",
8
);
}
}
if (themeOptions.primaryColor) {
cacheKey = `${cacheKey}__primary_${themeOptions.primaryColor}`;

View File

@@ -1,7 +1,7 @@
import { directive, NodePart, Part } from "lit-html";
export const dynamicElement = directive(
(tag: string, properties?: Record<string, any>) => (part: Part): void => {
(tag: string, properties?: { [key: string]: any }) => (part: Part): void => {
if (!(part instanceof NodePart)) {
throw new Error(
"dynamicElementDirective can only be used in content bindings"

View File

@@ -13,12 +13,13 @@ export const setupLeafletMap = async (
throw new Error("Cannot setup Leaflet map on disconnected element");
}
// eslint-disable-next-line
const Leaflet = ((await import("leaflet")) as any)
.default as LeafletModuleType;
const Leaflet = ((await import(
/* webpackChunkName: "leaflet" */ "leaflet"
)) as any).default as LeafletModuleType;
Leaflet.Icon.Default.imagePath = "/static/images/leaflet/images/";
if (draw) {
await import("leaflet-draw");
await import(/* webpackChunkName: "leaflet-draw" */ "leaflet-draw");
}
const map = Leaflet.map(mapElement);

View File

@@ -5,7 +5,6 @@ import { formatDateTime } from "../datetime/format_date_time";
import { formatTime } from "../datetime/format_time";
import { LocalizeFunc } from "../translations/localize";
import { computeStateDomain } from "./compute_state_domain";
import { formatNumber } from "../string/format_number";
export const computeStateDisplay = (
localize: LocalizeFunc,
@@ -20,9 +19,7 @@ export const computeStateDisplay = (
}
if (stateObj.attributes.unit_of_measurement) {
return `${formatNumber(compareState, language)} ${
stateObj.attributes.unit_of_measurement
}`;
return `${compareState} ${stateObj.attributes.unit_of_measurement}`;
}
const domain = computeStateDomain(stateObj);

View File

@@ -43,7 +43,6 @@ export const coverIcon = (state?: string, stateObj?: HassEntity): string => {
}
case "blind":
case "curtain":
case "shade":
switch (state) {
case "opening":
return "hass:arrow-up-box";
@@ -78,25 +77,3 @@ export const coverIcon = (state?: string, stateObj?: HassEntity): string => {
return "hass:window-open";
}
};
export const computeOpenIcon = (stateObj: HassEntity): string => {
switch (stateObj.attributes.device_class) {
case "awning":
case "door":
case "gate":
return "hass:arrow-expand-horizontal";
default:
return "hass:arrow-up";
}
};
export const computeCloseIcon = (stateObj: HassEntity): string => {
switch (stateObj.attributes.device_class) {
case "awning":
case "door":
case "gate":
return "hass:arrow-collapse-horizontal";
default:
return "hass:arrow-down";
}
};

View File

@@ -77,11 +77,6 @@ export const domainIcon = (
return "hass:calendar";
}
break;
case "sun":
return stateObj?.state === "above_horizon"
? FIXED_DOMAIN_ICONS[domain]
: "hass:weather-night";
}
if (domain in FIXED_DOMAIN_ICONS) {

View File

@@ -1,5 +1,5 @@
import { HassEntities } from "home-assistant-js-websocket";
import type { GroupEntity } from "../../data/group";
import { GroupEntity } from "../../types";
import { DEFAULT_VIEW_ENTITY_ID } from "../const";
// Return an ordered array of available views

View File

@@ -1,5 +1,5 @@
import { HassEntities } from "home-assistant-js-websocket";
import { GroupEntity } from "../../data/group";
import { GroupEntity } from "../../types";
export const getGroupEntities = (
entities: HassEntities,

View File

@@ -1,5 +1,5 @@
import { HassEntities } from "home-assistant-js-websocket";
import { GroupEntity } from "../../data/group";
import { GroupEntity } from "../../types";
import { computeDomain } from "./compute_domain";
import { getGroupEntities } from "./get_group_entities";

View File

@@ -1,5 +1,5 @@
import { HassEntities } from "home-assistant-js-websocket";
import { GroupEntity } from "../../data/group";
import { GroupEntity } from "../../types";
import { computeDomain } from "./compute_domain";
// Split a collection into a list of groups and a 'rest' list of ungrouped

View File

@@ -1,132 +0,0 @@
import Vibrant from "node-vibrant/lib/browser";
import MMCQ from "@vibrant/quantizer-mmcq";
import { BasicPipeline } from "@vibrant/core/lib/pipeline";
import { Swatch, Vec3 } from "@vibrant/color";
import { getRGBContrastRatio } from "../color/rgb";
const CONTRAST_RATIO = 4.5;
// How much the total diff between 2 RGB colors can be
// to be considered similar.
const COLOR_SIMILARITY_THRESHOLD = 150;
// For debug purposes, is being tree shaken.
const DEBUG_COLOR = __DEV__ && false;
const logColor = (
color: Swatch,
label = `${color.hex} - ${color.population}`
) =>
// eslint-disable-next-line no-console
console.log(
`%c${label}`,
`color: ${color.bodyTextColor}; background-color: ${color.hex}`
);
const customGenerator = (colors: Swatch[]) => {
colors.sort((colorA, colorB) => colorB.population - colorA.population);
const backgroundColor = colors[0];
let foregroundColor: Vec3 | undefined;
const contrastRatios = new Map<string, number>();
const approvedContrastRatio = (hex: string, rgb: Swatch["rgb"]) => {
if (!contrastRatios.has(hex)) {
contrastRatios.set(hex, getRGBContrastRatio(backgroundColor.rgb, rgb));
}
return contrastRatios.get(hex)! > CONTRAST_RATIO;
};
// We take each next color and find one that has better contrast.
for (let i = 1; i < colors.length && foregroundColor === undefined; i++) {
// If this color matches, score, take it.
if (approvedContrastRatio(colors[i].hex, colors[i].rgb)) {
if (DEBUG_COLOR) {
logColor(colors[i], "PICKED");
}
foregroundColor = colors[i].rgb;
break;
}
// This color has the wrong contrast ratio, but it is the right color.
// Let's find similar colors that might have the right contrast ratio
const currentColor = colors[i];
if (DEBUG_COLOR) {
logColor(colors[i], "Finding similar color with better contrast");
}
for (let j = i + 1; j < colors.length; j++) {
const compareColor = colors[j];
// difference. 0 is same, 765 max difference
const diffScore =
Math.abs(currentColor.rgb[0] - compareColor.rgb[0]) +
Math.abs(currentColor.rgb[1] - compareColor.rgb[1]) +
Math.abs(currentColor.rgb[2] - compareColor.rgb[2]);
if (DEBUG_COLOR) {
logColor(colors[j], `${colors[j].hex} - ${diffScore}`);
}
if (diffScore > COLOR_SIMILARITY_THRESHOLD) {
continue;
}
if (approvedContrastRatio(compareColor.hex, compareColor.rgb)) {
if (DEBUG_COLOR) {
logColor(compareColor, "PICKED");
}
foregroundColor = compareColor.rgb;
break;
}
}
}
if (foregroundColor === undefined) {
foregroundColor =
// @ts-expect-error
backgroundColor.getYiq() < 200 ? [255, 255, 255] : [0, 0, 0];
}
if (DEBUG_COLOR) {
// eslint-disable-next-line no-console
console.log();
// eslint-disable-next-line no-console
console.log(
"%cPicked colors",
`color: ${foregroundColor}; background-color: ${backgroundColor.hex}; font-weight: bold; padding: 16px;`
);
colors.forEach((color) => logColor(color));
// eslint-disable-next-line no-console
console.log();
}
return {
foreground: new Swatch(foregroundColor, 0),
background: backgroundColor,
};
};
Vibrant.use(
new BasicPipeline().filter
.register(
"default",
(r: number, g: number, b: number, a: number) =>
a >= 125 && !(r > 250 && g > 250 && b > 250)
)
.quantizer.register("mmcq", MMCQ)
// Our generator has different output
// @ts-expect-error
.generator.register("default", customGenerator)
);
export const extractColors = (url: string, downsampleColors = 16) =>
new Vibrant(url, {
colorCount: downsampleColors,
})
.getPalette()
.then(({ foreground, background }) => {
return { background: background!, foreground: foreground! };
});

View File

@@ -59,7 +59,7 @@ export const fuzzyFilterSort: FuzzyFilterSort = (filter, items) => {
: fuzzySequentialMatch(filter, item.text);
return item;
})
.filter((item) => item.score !== undefined && item.score > 0)
.filter((item) => item.score === undefined || item.score > 0)
.sort(({ score: scoreA = 0 }, { score: scoreB = 0 }) =>
scoreA > scoreB ? -1 : scoreA < scoreB ? 1 : 0
);

View File

@@ -1,54 +0,0 @@
/**
* Formats a number based on the specified language with thousands separator(s) and decimal character for better legibility.
*
* @param num The number to format
* @param language The language to use when formatting the number
*/
export const formatNumber = (
num: string | number,
language: string,
options?: Intl.NumberFormatOptions
): string => {
// Polyfill for Number.isNaN, which is more reliable than the global isNaN()
Number.isNaN =
Number.isNaN ||
function isNaN(input) {
return typeof input === "number" && isNaN(input);
};
if (!Number.isNaN(Number(num)) && Intl) {
return new Intl.NumberFormat(
language,
getDefaultFormatOptions(num, options)
).format(Number(num));
}
return num.toString();
};
/**
* Generates default options for Intl.NumberFormat
* @param num The number to be formatted
* @param options The Intl.NumberFormatOptions that should be included in the returned options
*/
const getDefaultFormatOptions = (
num: string | number,
options?: Intl.NumberFormatOptions
): Intl.NumberFormatOptions => {
const defaultOptions: Intl.NumberFormatOptions = options || {};
if (typeof num !== "string") {
return defaultOptions;
}
// Keep decimal trailing zeros if they are present in a string numeric value
if (
!options ||
(!options.minimumFractionDigits && !options.maximumFractionDigits)
) {
const digits = num.indexOf(".") > -1 ? num.split(".")[1].length : 0;
defaultOptions.minimumFractionDigits = digits;
defaultOptions.maximumFractionDigits = digits;
}
return defaultOptions;
};

View File

@@ -13,12 +13,9 @@ export interface FormatsType {
time: FormatType;
}
let polyfillLoaded = !shouldPolyfill();
const polyfillProm = polyfillLoaded
? undefined
: import("@formatjs/intl-pluralrules/polyfill-locales").then(() => {
polyfillLoaded = true;
});
if (shouldPolyfill()) {
await import("@formatjs/intl-pluralrules/polyfill-locales");
}
/**
* Adapted from Polymer app-localize-behavior.
@@ -41,16 +38,12 @@ const polyfillProm = polyfillLoaded
* }
*/
export const computeLocalize = async (
export const computeLocalize = (
cache: any,
language: string,
resources: Resources,
formats?: FormatsType
): Promise<LocalizeFunc> => {
if (!polyfillLoaded) {
await polyfillProm;
}
): LocalizeFunc => {
// Everytime any of the parameters change, invalidate the strings cache.
cache._localizationCache = {};
@@ -102,7 +95,7 @@ export const computeLocalize = async (
export const localizeKey = (
localize: LocalizeFunc,
key: string,
placeholders?: Record<string, string>
placeholders?: { [key: string]: string }
) => {
const args: [string, ...string[]] = [key];
if (placeholders) {

View File

@@ -1,4 +1,4 @@
export const extractSearchParamsObject = (): Record<string, string> => {
export const extractSearchParamsObject = (): { [key: string]: string } => {
const query = {};
const searchParams = new URLSearchParams(location.search);
for (const [key, value] of searchParams.entries()) {

View File

@@ -1,8 +0,0 @@
export const copyToClipboard = (str) => {
const el = document.createElement("textarea");
el.value = str;
document.body.appendChild(el);
el.select();
document.execCommand("copy");
document.body.removeChild(el);
};

View File

@@ -1,5 +1,5 @@
import "@material/mwc-icon-button/mwc-icon-button";
import "@material/mwc-button/mwc-button";
import "../ha-icon-button";
import "@polymer/paper-input/paper-input";
import "@polymer/paper-item/paper-item";
import "@polymer/paper-item/paper-item-body";
@@ -38,8 +38,6 @@ import { SubscribeMixin } from "../../mixins/subscribe-mixin";
import { PolymerChangedEvent } from "../../polymer-types";
import { HomeAssistant } from "../../types";
import "./ha-devices-picker";
import "../ha-svg-icon";
import { mdiClose, mdiMenuDown, mdiMenuUp } from "@mdi/js";
interface DevicesByArea {
[areaId: string]: AreaDevices;
@@ -64,7 +62,7 @@ const rowRenderer = (
margin: -10px 0;
padding: 0;
}
mwc-icon-button {
ha-icon-button {
float: right;
}
.devices {
@@ -326,34 +324,36 @@ export class HaAreaDevicesPicker extends SubscribeMixin(LitElement) {
autocorrect="off"
spellcheck="false"
>
<div class="suffix" slot="suffix">
${this.value
? html`<mwc-icon-button
class="clear-button"
.label=${this.hass.localize(
${this.value
? html`
<ha-icon-button
aria-label=${this.hass.localize(
"ui.components.device-picker.clear"
)}
slot="suffix"
class="clear-button"
icon="hass:close"
@click=${this._clearValue}
no-ripple
>
<ha-svg-icon .path=${mdiClose}></ha-svg-icon>
</mwc-icon-button> `
: ""}
${areas.length > 0
? html`
<mwc-icon-button
.label=${this.hass.localize(
"ui.components.device-picker.show_devices"
)}
class="toggle-button"
>
<ha-svg-icon
.path=${this._opened ? mdiMenuUp : mdiMenuDown}
></ha-svg-icon>
</mwc-icon-button>
`
: ""}
</div>
Clear
</ha-icon-button>
`
: ""}
${areas.length > 0
? html`
<ha-icon-button
aria-label=${this.hass.localize(
"ui.components.device-picker.show_devices"
)}
slot="suffix"
class="toggle-button"
.icon=${this._opened ? "hass:menu-up" : "hass:menu-down"}
>
Toggle
</ha-icon-button>
`
: ""}
</paper-input>
</vaadin-combo-box-light>
<mwc-button @click=${this._switchPicker}
@@ -409,12 +409,10 @@ export class HaAreaDevicesPicker extends SubscribeMixin(LitElement) {
static get styles(): CSSResult {
return css`
.suffix {
display: flex;
}
mwc-icon-button {
--mdc-icon-button-size: 24px;
padding: 0px 2px;
paper-input > ha-icon-button {
width: 24px;
height: 24px;
padding: 2px;
color: var(--secondary-text-color);
}
[hidden] {

View File

@@ -42,10 +42,6 @@ interface Device {
id: string;
}
export type HaDevicePickerDeviceFilterFunc = (
device: DeviceRegistryEntry
) => boolean;
const rowRenderer = (root: HTMLElement, _owner, model: { item: Device }) => {
if (!root.firstElementChild) {
root.innerHTML = `
@@ -106,8 +102,6 @@ export class HaDevicePicker extends SubscribeMixin(LitElement) {
@property({ type: Array, attribute: "include-device-classes" })
public includeDeviceClasses?: string[];
@property() public deviceFilter?: HaDevicePickerDeviceFilterFunc;
@property({ type: Boolean })
private _opened?: boolean;
@@ -118,8 +112,7 @@ export class HaDevicePicker extends SubscribeMixin(LitElement) {
entities: EntityRegistryEntry[],
includeDomains: this["includeDomains"],
excludeDomains: this["excludeDomains"],
includeDeviceClasses: this["includeDeviceClasses"],
deviceFilter: this["deviceFilter"]
includeDeviceClasses: this["includeDeviceClasses"]
): Device[] => {
if (!devices.length) {
return [];
@@ -187,14 +180,6 @@ export class HaDevicePicker extends SubscribeMixin(LitElement) {
});
}
if (deviceFilter) {
inputDevices = inputDevices.filter(
(device) =>
// We always want to include the device of the current value
device.id === this.value || deviceFilter!(device)
);
}
const outputDevices = inputDevices.map((device) => {
return {
id: device.id,
@@ -239,8 +224,7 @@ export class HaDevicePicker extends SubscribeMixin(LitElement) {
this.entities,
this.includeDomains,
this.excludeDomains,
this.includeDeviceClasses,
this.deviceFilter
this.includeDeviceClasses
);
return html`
<vaadin-combo-box-light

View File

@@ -88,7 +88,6 @@ class HaChartBase extends mixinBehaviors(
.chartTooltip .beforeBody {
text-align: center;
font-weight: 300;
word-break: break-all;
}
.chartLegend li {
display: inline-block;
@@ -230,7 +229,9 @@ class HaChartBase extends mixinBehaviors(
}
if (scriptsLoaded === null) {
scriptsLoaded = import("../../resources/ha-chart-scripts.js");
scriptsLoaded = import(
/* webpackChunkName: "load_chart" */ "../../resources/ha-chart-scripts.js"
);
}
scriptsLoaded.then((ChartModule) => {
this.ChartClass = ChartModule.default;

View File

@@ -19,7 +19,6 @@ import { PolymerChangedEvent } from "../../polymer-types";
import { HomeAssistant } from "../../types";
import "../ha-svg-icon";
import "./state-badge";
import { formatAttributeName } from "../../util/hass-attributes-util";
import "@material/mwc-icon-button/mwc-icon-button";
export type HaEntityPickerEntityFilterFunc = (entityId: HassEntity) => boolean;
@@ -36,9 +35,7 @@ const rowRenderer = (root: HTMLElement, _owner, model: { item: string }) => {
<paper-item></paper-item>
`;
}
root.querySelector("paper-item")!.textContent = formatAttributeName(
model.item
);
root.querySelector("paper-item")!.textContent = model.item;
};
@customElement("ha-entity-attribute-picker")
@@ -95,7 +92,7 @@ class HaEntityAttributePicker extends LitElement {
this.hass.localize(
"ui.components.entity.entity-attribute-picker.attribute"
)}
.value=${this._value ? formatAttributeName(this._value) : ""}
.value=${this._value}
.disabled=${this.disabled || !this.entityId}
class="input"
autocapitalize="none"
@@ -143,7 +140,7 @@ class HaEntityAttributePicker extends LitElement {
}
private get _value() {
return this.value;
return this.value || "";
}
private _openedChanged(ev: PolymerChangedEvent<boolean>) {

View File

@@ -95,8 +95,7 @@ export class HaEntityPicker extends LitElement {
@property() public entityFilter?: HaEntityPickerEntityFilterFunc;
@property({ type: Boolean, attribute: "hide-clear-icon" })
public hideClearIcon = false;
@property({ type: Boolean }) public hideClearIcon = false;
@property({ type: Boolean }) private _opened = false;

View File

@@ -0,0 +1,25 @@
import { html } from "@polymer/polymer/lib/utils/html-tag";
/* eslint-plugin-disable lit */
import { PolymerElement } from "@polymer/polymer/polymer-element";
import { stateIcon } from "../../common/entity/state_icon";
import "../ha-icon";
class HaStateIcon extends PolymerElement {
static get template() {
return html` <ha-icon icon="[[computeIcon(stateObj)]]"></ha-icon> `;
}
static get properties() {
return {
stateObj: {
type: Object,
},
};
}
computeIcon(stateObj) {
return stateIcon(stateObj);
}
}
customElements.define("ha-state-icon", HaStateIcon);

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