Compare commits

..

1 Commits

Author SHA1 Message Date
Paulus Schoutsen
3717e94814 Switch onboarding and auth to dynamic import 2020-10-12 14:10:15 +00:00
396 changed files with 7379 additions and 18632 deletions

View File

@@ -75,7 +75,6 @@
"object-curly-newline": 0,
"default-case": 0,
"wc/no-self-class": 0,
"no-shadow": 0,
"@typescript-eslint/camelcase": 0,
"@typescript-eslint/ban-ts-comment": 0,
"@typescript-eslint/no-use-before-define": 0,
@@ -83,8 +82,7 @@
"@typescript-eslint/no-explicit-any": 0,
"@typescript-eslint/no-unused-vars": 0,
"@typescript-eslint/explicit-function-return-type": 0,
"@typescript-eslint/explicit-module-boundary-types": 0,
"@typescript-eslint/no-shadow": ["error"]
"@typescript-eslint/explicit-module-boundary-types": 0
},
"plugins": ["disable", "import", "lit", "prettier", "@typescript-eslint"],
"processor": "disable/disable"

View File

@@ -0,0 +1,26 @@
---
name: Request a feature for the UI, Frontend or Lovelace
about: Request an new feature for the Home Assistant frontend.
labels: feature request
---
<!--
DO NOT DELETE ANY TEXT from this template!
Otherwise, your request may be closed without comment.
-->
## The request
<!--
Describe to our maintainers, the feature you would like to be added.
Please be clear and concise and, if possible, provide a screenshot or mockup.
-->
## The alternatives
<!--
Are you currently using, or have you considered alternatives?
If so, could you please describe those?
-->
## Additional information

View File

@@ -1,8 +1,5 @@
blank_issues_enabled: false
contact_links:
- name: Request a feature for the UI, Frontend or Lovelace
url: https://github.com/home-assistant/frontend/discussions/category_choices
about: Request an new feature for the Home Assistant frontend.
- name: Report a bug that is NOT related to the UI, Frontend or Lovelace
url: https://github.com/home-assistant/core/issues
about: This is the issue tracker for our frontend. Please report other issues with the backend repository.

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.

2
.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

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"
},
]
}

137
.vscode/tasks.json vendored
View File

@@ -1,137 +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 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
}
},
]
}

View File

@@ -26,4 +26,4 @@ A complete guide can be found at the following [link](https://www.home-assistant
Home Assistant is open-source and Apache 2 licensed. Feel free to browse the repository, learn and reuse parts in your own projects.
We use [BrowserStack](https://www.browserstack.com) to test Home Assistant on a large variety of devices.
We use [BrowserStack](https://www.browserstack.com) to test Home Assistant on a large variation of devices.

View File

@@ -54,10 +54,7 @@ module.exports.babelOptions = ({ latestBuild }) => ({
presets: [
!latestBuild && [
require("@babel/preset-env").default,
{
useBuiltIns: "entry",
corejs: "3.6",
},
{ modules: false, useBuiltIns: "entry", corejs: 3 },
],
require("@babel/preset-typescript").default,
].filter(Boolean),
@@ -69,7 +66,7 @@ module.exports.babelOptions = ({ latestBuild }) => ({
],
// Only support the syntax, Webpack will handle it.
"@babel/plugin-syntax-import-meta",
"@babel/plugin-syntax-dynamic-import",
"@babel/syntax-dynamic-import",
"@babel/plugin-proposal-optional-chaining",
"@babel/plugin-proposal-nullish-coalescing-operator",
[

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");
@@ -300,6 +301,7 @@ gulp.task("build-flattened-translations", function () {
return flatten(data);
})
)
.pipe(minify())
.pipe(
rename((filePath) => {
if (filePath.dirname === "core") {

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,10 +33,7 @@ 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 handler = (done) => (err, stats) => {
@@ -56,12 +45,12 @@ const handler = (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();
}

View File

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

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,
@@ -44,7 +29,7 @@ const createWebpackConfig = ({
module: {
rules: [
{
test: /\.m?js$|\.ts$/,
test: /\.js$|\.ts$/,
exclude: bundle.babelExclude(),
use: {
loader: "babel-loader",
@@ -60,8 +45,10 @@ const createWebpackConfig = ({
optimization: {
minimizer: [
new TerserPlugin({
cache: true,
parallel: true,
extractComments: true,
sourceMap: true,
terserOptions: bundle.terserOptions(latestBuild),
}),
],
@@ -110,17 +97,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"],
},
@@ -131,22 +108,6 @@ const createWebpackConfig = ({
}
return `${chunk.name}.${chunk.hash.substr(0, 8)}.js`;
},
environment: {
// The environment supports arrow functions ('() => { ... }').
arrowFunction: latestBuild,
// The environment supports BigInt as literal (123n).
bigIntLiteral: false,
// The environment supports const and let for variable declarations.
const: latestBuild,
// The environment supports destructuring ('{ a, b } = obj').
destructuring: latestBuild,
// The environment supports an async import() function to import EcmaScript modules.
dynamicImport: latestBuild,
// The environment supports 'for of' iteration ('for (const x of array) { ... }').
forOf: latestBuild,
// The environment supports ECMAScript Module syntax to import ECMAScript modules (import ... from '...').
module: latestBuild,
},
chunkFilename:
isProdBuild && !isStatsBuild
? "chunk.[chunkhash].js"

View File

@@ -30,7 +30,7 @@ class HcLayout extends LitElement {
<ha-card>
<div class="layout">
<img class="hero" src="/images/google-nest-hub.png" />
<h1 class="card-header">
<div class="card-header">
Home Assistant Cast${this.subtitle ? ` ${this.subtitle}` : ""}
${this.auth
? html`
@@ -44,7 +44,7 @@ class HcLayout extends LitElement {
</div>
`
: ""}
</h1>
</div>
<slot></slot>
</div>
</ha-card>

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

@@ -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

@@ -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,12 +16,15 @@ 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 {

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
`,
},
{
@@ -94,12 +83,8 @@ class DemoAlarmPanelEntity extends PolymerElement {
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

@@ -98,4 +98,4 @@ class DemoButtonEntity extends PolymerElement {
}
}
customElements.define("demo-hui-entity-button-card", DemoButtonEntity);
customElements.define("demo-hui-button-card", DemoButtonEntity);

View File

@@ -8,7 +8,6 @@ import "../components/demo-cards";
const ENTITIES = [
getEntity("sensor", "brightness", "12", {}),
getEntity("plant", "bonsai", "ok", {}),
getEntity("sensor", "not_working", "unavailable", {}),
getEntity("sensor", "outside_humidity", "54", {
unit_of_measurement: "%",
}),
@@ -75,13 +74,6 @@ const CONFIGS = [
entity: plant.bonsai
`,
},
{
heading: "Unavailable entity",
config: `
- type: gauge
entity: sensor.not_working
`,
},
];
class DemoGaugeEntity extends PolymerElement {

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

View File

@@ -12,45 +12,23 @@ 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)
`,
},
];

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

@@ -23,9 +23,9 @@ import { hassioStyle } from "../resources/hassio-style";
class HassioAddonRepositoryEl extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public repo!: HassioAddonRepository;
@property() public repo!: HassioAddonRepository;
@property({ attribute: false }) public addons!: HassioAddonInfo[];
@property() public addons!: HassioAddonInfo[];
@property() public filter!: string;
@@ -78,18 +78,18 @@ class HassioAddonRepositoryEl extends LitElement {
.title=${addon.name}
.description=${addon.description}
.available=${addon.available}
.icon=${addon.installed && addon.update_available
.icon=${addon.installed && addon.installed !== addon.version
? mdiArrowUpBoldCircle
: mdiPuzzle}
.iconTitle=${addon.installed
? addon.update_available
? addon.installed !== addon.version
? "New version available"
: "Add-on is installed"
: addon.available
? "Add-on is not installed"
: "Add-on is not available on your system"}
.iconClass=${addon.installed
? addon.update_available
? addon.installed !== addon.version
? "update"
: "installed"
: !addon.available
@@ -104,7 +104,7 @@ class HassioAddonRepositoryEl extends LitElement {
: undefined}
.showTopbar=${addon.installed || !addon.available}
.topbarClass=${addon.installed
? addon.update_available
? addon.installed !== addon.version
? "update"
: "installed"
: !addon.available

View File

@@ -11,7 +11,6 @@ import {
PropertyValues,
} from "lit-element";
import { html, TemplateResult } from "lit-html";
import { atLeastVersion } from "../../../src/common/config/version";
import "../../../src/common/search/search-input";
import "../../../src/components/ha-button-menu";
import "../../../src/components/ha-svg-icon";
@@ -25,7 +24,6 @@ import { extractApiErrorMessage } from "../../../src/data/hassio/common";
import "../../../src/layouts/hass-loading-screen";
import "../../../src/layouts/hass-tabs-subpage";
import { HomeAssistant, Route } from "../../../src/types";
import { showRegistriesDialog } from "../dialogs/registries/show-dialog-registries";
import { showRepositoriesDialog } from "../dialogs/repositories/show-dialog-repositories";
import { supervisorTabs } from "../hassio-tabs";
import "./hassio-addon-repository";
@@ -115,12 +113,6 @@ class HassioAddonStore extends LitElement {
<mwc-list-item>
Reload
</mwc-list-item>
${this.hass.userData?.showAdvanced &&
atLeastVersion(this.hass.config.version, 0, 117)
? html`<mwc-list-item>
Registries
</mwc-list-item>`
: ""}
</ha-button-menu>
${repos.length === 0
? html`<hass-loading-screen no-toolbar></hass-loading-screen>`
@@ -165,9 +157,6 @@ class HassioAddonStore extends LitElement {
case 1:
this.refreshData();
break;
case 2:
this._manageRegistries();
break;
}
}
@@ -184,10 +173,6 @@ class HassioAddonStore extends LitElement {
});
}
private async _manageRegistries() {
showRegistriesDialog(this);
}
private async _loadData() {
try {
const addonsInfo = await fetchHassioAddonsInfo(this.hass);

View File

@@ -39,11 +39,13 @@ class HassioAddonConfig extends LitElement {
@property({ type: Boolean }) private _configHasChanged = false;
@property({ type: Boolean }) private _valid = true;
@query("ha-yaml-editor", true) private _editor!: HaYamlEditor;
protected render(): TemplateResult {
const editor = this._editor;
// If editor not rendered, don't show the error.
const valid = editor ? editor.isValid : true;
return html`
<h1>${this.addon.name}</h1>
<ha-card header="Configuration">
@@ -52,7 +54,7 @@ class HassioAddonConfig extends LitElement {
@value-changed=${this._configChanged}
></ha-yaml-editor>
${this._error ? html` <div class="errors">${this._error}</div> ` : ""}
${this._valid ? "" : html` <div class="errors">Invalid YAML</div> `}
${valid ? "" : html` <div class="errors">Invalid YAML</div> `}
</div>
<div class="card-actions">
<ha-progress-button class="warning" @click=${this._resetTapped}>
@@ -60,7 +62,7 @@ class HassioAddonConfig extends LitElement {
</ha-progress-button>
<ha-progress-button
@click=${this._saveTapped}
.disabled=${!this._configHasChanged || !this._valid}
.disabled=${!this._configHasChanged || !valid}
>
Save
</ha-progress-button>
@@ -76,9 +78,9 @@ class HassioAddonConfig extends LitElement {
}
}
private _configChanged(ev): void {
private _configChanged(): void {
this._configHasChanged = true;
this._valid = ev.detail.isValid;
this.requestUpdate();
}
private async _resetTapped(ev: CustomEvent): Promise<void> {

View File

@@ -135,7 +135,7 @@ class HassioAddonInfo extends LitElement {
protected render(): TemplateResult {
return html`
${this.addon.update_available
${this._computeUpdateAvailable
? html`
<ha-card header="Update available! 🎉">
<div class="card-content">
@@ -178,7 +178,7 @@ class HassioAddonInfo extends LitElement {
${!this.addon.protected
? html`
<ha-card class="warning">
<h1 class="card-header">Warning: Protection mode is disabled!</h1>
<div class="card-header">Warning: Protection mode is disabled!</div>
<div class="card-content">
Protection mode on this add-on is disabled! This gives the add-on full access to the entire system, which adds security risks, and could damage your system when used incorrectly. Only disable the protection mode if you know, need AND trust the source of this add-on.
</div>
@@ -609,6 +609,15 @@ class HassioAddonInfo extends LitElement {
return this.addon?.state === "started";
}
private get _computeUpdateAvailable(): boolean | "" {
return (
this.addon &&
!this.addon.detached &&
this.addon.version &&
this.addon.version !== this.addon.version_latest
);
}
private get _pathWebui(): string | null {
return (
this.addon.webui &&

View File

@@ -50,7 +50,7 @@ class HassioCardContent extends LitElement {
`
: html`
<ha-svg-icon
class=${this.iconClass!}
class=${this.iconClass}
.path=${this.icon}
.title=${this.iconTitle}
></ha-svg-icon>

View File

@@ -1,3 +1,4 @@
import "../../../src/components/ha-file-upload";
import "@material/mwc-icon-button/mwc-icon-button";
import { mdiFolderUpload } from "@mdi/js";
import "@polymer/iron-input/iron-input";
@@ -11,15 +12,13 @@ import {
} from "lit-element";
import { fireEvent } from "../../../src/common/dom/fire_event";
import "../../../src/components/ha-circular-progress";
import "../../../src/components/ha-file-upload";
import "../../../src/components/ha-svg-icon";
import { extractApiErrorMessage } from "../../../src/data/hassio/common";
import {
HassioSnapshot,
uploadSnapshot,
} from "../../../src/data/hassio/snapshot";
import { showAlertDialog } from "../../../src/dialogs/generic/show-dialog-box";
import { HomeAssistant } from "../../../src/types";
import { showAlertDialog } from "../../../src/dialogs/generic/show-dialog-box";
declare global {
interface HASSDomEvents {
@@ -66,7 +65,7 @@ export class HassioUploadSnapshot extends LitElement {
} catch (err) {
showAlertDialog(this, {
title: "Upload failed",
text: extractApiErrorMessage(err),
text: err.toString(),
confirmText: "ok",
});
} finally {

View File

@@ -52,21 +52,22 @@ class HassioAddons extends LitElement {
.title=${addon.name}
.description=${addon.description}
available
.showTopbar=${addon.update_available}
.showTopbar=${addon.installed !== addon.version}
topbarClass="update"
.icon=${addon.update_available!
.icon=${addon.installed !== addon.version
? mdiArrowUpBoldCircle
: mdiPuzzle}
.iconTitle=${addon.state !== "started"
? "Add-on is stopped"
: addon.update_available!
: addon.installed !== addon.version
? "New version available"
: "Add-on is running"}
.iconClass=${addon.update_available
.iconClass=${addon.installed &&
addon.installed !== addon.version
? addon.state === "started"
? "update"
: "update stopped"
: addon.state === "started"
: addon.installed && addon.state === "started"
? "running"
: "stopped"}
.iconImage=${atLeastVersion(

View File

@@ -5,11 +5,11 @@ import {
CSSResult,
customElement,
html,
internalProperty,
LitElement,
property,
TemplateResult,
} from "lit-element";
import memoizeOne from "memoize-one";
import "../../../src/components/buttons/ha-progress-button";
import "../../../src/components/ha-card";
import "../../../src/components/ha-svg-icon";
@@ -35,30 +35,29 @@ import { hassioStyle } from "../resources/hassio-style";
export class HassioUpdate extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public hassInfo?: HassioHomeAssistantInfo;
@property({ attribute: false }) public hassInfo: HassioHomeAssistantInfo;
@property({ attribute: false }) public hassOsInfo?: HassioHassOSInfo;
@property({ attribute: false }) public supervisorInfo?: HassioSupervisorInfo;
@property() public supervisorInfo: HassioSupervisorInfo;
private _pendingUpdates = memoizeOne(
(
core?: HassioHomeAssistantInfo,
supervisor?: HassioSupervisorInfo,
os?: HassioHassOSInfo
): number => {
return [core, supervisor, os].filter(
(value) => !!value && value?.update_available
).length;
}
);
@internalProperty() private _error?: string;
protected render(): TemplateResult {
const updatesAvailable = this._pendingUpdates(
const updatesAvailable: number = [
this.hassInfo,
this.supervisorInfo,
this.hassOsInfo
);
this.hassOsInfo,
].filter((value) => {
return (
!!value &&
(value.version_latest
? value.version !== value.version_latest
: value.version_latest
? value.version !== value.version_latest
: false)
);
}).length;
if (!updatesAvailable) {
return html``;
@@ -66,6 +65,9 @@ export class HassioUpdate extends LitElement {
return html`
<div class="content">
${this._error
? html` <div class="error">Error: ${this._error}</div> `
: ""}
<h1>
${updatesAvailable > 1
? "Updates Available 🎉"
@@ -74,24 +76,26 @@ export class HassioUpdate extends LitElement {
<div class="card-group">
${this._renderUpdateCard(
"Home Assistant Core",
this.hassInfo!,
this.hassInfo.version,
this.hassInfo.version_latest,
"hassio/homeassistant/update",
`https://${
this.hassInfo?.version_latest.includes("b") ? "rc" : "www"
}.home-assistant.io/latest-release-notes/`
this.hassInfo.version_latest.includes("b") ? "rc" : "www"
}.home-assistant.io/latest-release-notes/`,
mdiHomeAssistant
)}
${this._renderUpdateCard(
"Supervisor",
this.supervisorInfo!,
this.supervisorInfo.version,
this.supervisorInfo.version_latest,
"hassio/supervisor/update",
`https://github.com//home-assistant/hassio/releases/tag/${
this.supervisorInfo!.version_latest
}`
`https://github.com//home-assistant/hassio/releases/tag/${this.supervisorInfo.version_latest}`
)}
${this.hassOsInfo
? this._renderUpdateCard(
"Operating System",
this.hassOsInfo,
this.hassOsInfo.version,
this.hassOsInfo.version_latest,
"hassio/os/update",
`https://github.com//home-assistant/hassos/releases/tag/${this.hassOsInfo.version_latest}`
)
@@ -103,22 +107,28 @@ export class HassioUpdate extends LitElement {
private _renderUpdateCard(
name: string,
object: HassioHomeAssistantInfo | HassioSupervisorInfo | HassioHassOSInfo,
curVersion: string,
lastVersion: string,
apiPath: string,
releaseNotesUrl: string
releaseNotesUrl: string,
icon?: string
): TemplateResult {
if (!object.update_available) {
if (!lastVersion || lastVersion === curVersion) {
return html``;
}
return html`
<ha-card>
<div class="card-content">
<div class="icon">
<ha-svg-icon .path=${mdiHomeAssistant}></ha-svg-icon>
</div>
<div class="update-heading">${name} ${object.version_latest}</div>
${icon
? html`
<div class="icon">
<ha-svg-icon .path=${icon}></ha-svg-icon>
</div>
`
: ""}
<div class="update-heading">${name} ${lastVersion}</div>
<div class="warning">
You are currently running version ${object.version}
You are currently running version ${curVersion}
</div>
</div>
<div class="card-actions">
@@ -128,7 +138,7 @@ export class HassioUpdate extends LitElement {
<ha-progress-button
.apiPath=${apiPath}
.name=${name}
.version=${object.version_latest}
.version=${lastVersion}
@click=${this._confirmUpdate}
>
Update

View File

@@ -1,245 +0,0 @@
import "@material/mwc-button/mwc-button";
import "@material/mwc-icon-button/mwc-icon-button";
import "@material/mwc-list/mwc-list-item";
import { mdiDelete } from "@mdi/js";
import { PaperInputElement } from "@polymer/paper-input/paper-input";
import {
css,
CSSResult,
customElement,
html,
internalProperty,
LitElement,
property,
TemplateResult,
} from "lit-element";
import "../../../../src/components/ha-circular-progress";
import { createCloseHeading } from "../../../../src/components/ha-dialog";
import "../../../../src/components/ha-svg-icon";
import { extractApiErrorMessage } from "../../../../src/data/hassio/common";
import {
addHassioDockerRegistry,
fetchHassioDockerRegistries,
removeHassioDockerRegistry,
} from "../../../../src/data/hassio/docker";
import { showAlertDialog } from "../../../../src/dialogs/generic/show-dialog-box";
import { haStyle, haStyleDialog } from "../../../../src/resources/styles";
import type { HomeAssistant } from "../../../../src/types";
@customElement("dialog-hassio-registries")
class HassioRegistriesDialog extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) private _registries?: {
registry: string;
username: string;
}[];
@internalProperty() private _registry?: string;
@internalProperty() private _username?: string;
@internalProperty() private _password?: string;
@internalProperty() private _opened = false;
@internalProperty() private _addingRegistry = false;
protected render(): TemplateResult {
return html`
<ha-dialog
.open=${this._opened}
@closing=${this.closeDialog}
scrimClickAction
escapeKeyAction
.heading=${createCloseHeading(
this.hass,
this._addingRegistry
? "Add New Docker Registry"
: "Manage Docker Registries"
)}
>
<div class="form">
${this._addingRegistry
? html`
<paper-input
@value-changed=${this._inputChanged}
class="flex-auto"
name="registry"
label="Registry"
required
auto-validate
></paper-input>
<paper-input
@value-changed=${this._inputChanged}
class="flex-auto"
name="username"
label="Username"
required
auto-validate
></paper-input>
<paper-input
@value-changed=${this._inputChanged}
class="flex-auto"
name="password"
label="Password"
type="password"
required
auto-validate
></paper-input>
<mwc-button
?disabled=${Boolean(
!this._registry || !this._username || !this._password
)}
@click=${this._addNewRegistry}
>
Add registry
</mwc-button>
`
: html`${this._registries?.length
? this._registries.map((entry) => {
return html`
<mwc-list-item class="option" hasMeta twoline>
<span>${entry.registry}</span>
<span slot="secondary"
>Username: ${entry.username}</span
>
<mwc-icon-button
.entry=${entry}
title="Remove"
slot="meta"
@click=${this._removeRegistry}
>
<ha-svg-icon .path=${mdiDelete}></ha-svg-icon>
</mwc-icon-button>
</mwc-list-item>
`;
})
: html`
<mwc-list-item>
<span>No registries configured</span>
</mwc-list-item>
`}
<mwc-button @click=${this._addRegistry}>
Add new registry
</mwc-button> `}
</div>
</ha-dialog>
`;
}
private _inputChanged(ev: Event) {
const target = ev.currentTarget as PaperInputElement;
this[`_${target.name}`] = target.value;
}
public async showDialog(_dialogParams: any): Promise<void> {
this._opened = true;
await this._loadRegistries();
await this.updateComplete;
}
public closeDialog(): void {
this._addingRegistry = false;
this._opened = false;
}
public focus(): void {
this.updateComplete.then(() =>
(this.shadowRoot?.querySelector(
"[dialogInitialFocus]"
) as HTMLElement)?.focus()
);
}
private async _loadRegistries(): Promise<void> {
const registries = await fetchHassioDockerRegistries(this.hass);
this._registries = Object.keys(registries!.registries).map((key) => ({
registry: key,
username: registries.registries[key].username,
}));
}
private _addRegistry(): void {
this._addingRegistry = true;
}
private async _addNewRegistry(): Promise<void> {
const data = {};
data[this._registry!] = {
username: this._username,
password: this._password,
};
try {
await addHassioDockerRegistry(this.hass, data);
await this._loadRegistries();
this._addingRegistry = false;
} catch (err) {
showAlertDialog(this, {
title: "Failed to add registry",
text: extractApiErrorMessage(err),
});
}
}
private async _removeRegistry(ev: Event): Promise<void> {
const entry = (ev.currentTarget as any).entry;
try {
await removeHassioDockerRegistry(this.hass, entry.registry);
await this._loadRegistries();
} catch (err) {
showAlertDialog(this, {
title: "Failed to remove registry",
text: extractApiErrorMessage(err),
});
}
}
static get styles(): CSSResult[] {
return [
haStyle,
haStyleDialog,
css`
ha-dialog.button-left {
--justify-action-buttons: flex-start;
}
paper-icon-item {
cursor: pointer;
}
.form {
color: var(--primary-text-color);
}
.option {
border: 1px solid var(--divider-color);
border-radius: 4px;
margin-top: 4px;
}
mwc-button {
margin-left: 8px;
}
mwc-icon-button {
color: var(--error-color);
margin: -10px;
}
mwc-list-item {
cursor: default;
}
mwc-list-item span[slot="secondary"] {
color: var(--secondary-text-color);
}
ha-paper-dropdown-menu {
display: block;
}
`,
];
}
}
declare global {
interface HTMLElementTagNameMap {
"dialog-hassio-registries": HassioRegistriesDialog;
}
}

View File

@@ -1,13 +0,0 @@
import { fireEvent } from "../../../../src/common/dom/fire_event";
import "./dialog-hassio-registries";
export const showRegistriesDialog = (element: HTMLElement): void => {
fireEvent(element, "show-dialog", {
dialogTag: "dialog-hassio-registries",
dialogImport: () =>
import(
/* webpackChunkName: "dialog-hassio-registries" */ "./dialog-hassio-registries"
),
dialogParams: {},
});
};

View File

@@ -1,7 +1,6 @@
import "@material/mwc-button";
import { mdiClose, mdiDelete, mdiDownload, mdiHistory } from "@mdi/js";
import "@polymer/paper-checkbox/paper-checkbox";
import type { PaperCheckboxElement } from "@polymer/paper-checkbox/paper-checkbox";
import { PaperCheckboxElement } from "@polymer/paper-checkbox/paper-checkbox";
import "@polymer/paper-input/paper-input";
import {
css,

View File

@@ -25,13 +25,13 @@ class HassioPanelRouter extends HassRouterPage {
@property({ type: Boolean }) public narrow!: boolean;
@property({ attribute: false }) public supervisorInfo?: HassioSupervisorInfo;
@property({ attribute: false }) public supervisorInfo: HassioSupervisorInfo;
@property({ attribute: false }) public hassioInfo!: HassioInfo;
@property({ attribute: false }) public hostInfo?: HassioHostInfo;
@property({ attribute: false }) public hostInfo: HassioHostInfo;
@property({ attribute: false }) public hassInfo?: HassioHomeAssistantInfo;
@property({ attribute: false }) public hassInfo: HassioHomeAssistantInfo;
@property({ attribute: false }) public hassOsInfo!: HassioHassOSInfo;

View File

@@ -66,15 +66,15 @@ class HassioRouter extends HassRouterPage {
},
};
@internalProperty() private _supervisorInfo?: HassioSupervisorInfo;
@internalProperty() private _supervisorInfo: HassioSupervisorInfo;
@internalProperty() private _hostInfo?: HassioHostInfo;
@internalProperty() private _hostInfo: HassioHostInfo;
@internalProperty() private _hassioInfo?: HassioInfo;
@internalProperty() private _hassOsInfo?: HassioHassOSInfo;
@internalProperty() private _hassInfo?: HassioHomeAssistantInfo;
@internalProperty() private _hassInfo: HassioHomeAssistantInfo;
protected firstUpdated(changedProps: PropertyValues) {
super.firstUpdated(changedProps);

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

@@ -108,8 +108,8 @@ class HassioHostInfo extends LitElement {
<span slot="description">
${this.hostInfo.operating_system}
</span>
${this.hostInfo.features.includes("hassos") &&
this.hassOsInfo.update_available
${this.hostInfo.version !== this.hostInfo.version_latest &&
this.hostInfo.features.includes("hassos")
? html`
<ha-progress-button
title="Update the host OS"

View File

@@ -7,21 +7,18 @@ import {
property,
TemplateResult,
} from "lit-element";
import { fireEvent } from "../../../src/common/dom/fire_event";
import "../../../src/components/buttons/ha-progress-button";
import "../../../src/components/ha-card";
import "../../../src/components/ha-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,
setSupervisorOption,
SupervisorOptions,
updateSupervisor,
fetchHassioSupervisorInfo,
} from "../../../src/data/hassio/supervisor";
import {
showAlertDialog,
@@ -29,42 +26,14 @@ import {
} from "../../../src/dialogs/generic/show-dialog-box";
import { haStyle } from "../../../src/resources/styles";
import { HomeAssistant } from "../../../src/types";
import { documentationUrl } from "../../../src/util/documentation-url";
import { hassioStyle } from "../resources/hassio-style";
const ISSUES = {
container: {
title: "Containers known to cause issues",
url: "/more-info/unsupported/container",
},
dbus: { title: "DBUS", url: "/more-info/unsupported/dbus" },
docker_configuration: {
title: "Docker Configuration",
url: "/more-info/unsupported/docker_configuration",
},
docker_version: {
title: "Docker Version",
url: "/more-info/unsupported/docker_version",
},
lxc: { title: "LXC", url: "/more-info/unsupported/lxc" },
network_manager: {
title: "Network Manager",
url: "/more-info/unsupported/network_manager",
},
os: { title: "Operating System", url: "/more-info/unsupported/os" },
privileged: {
title: "Supervisor is not privileged",
url: "/more-info/unsupported/privileged",
},
systemd: { title: "Systemd", url: "/more-info/unsupported/systemd" },
};
import { extractApiErrorMessage } from "../../../src/data/hassio/common";
@customElement("hassio-supervisor-info")
class HassioSupervisorInfo extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false })
public supervisorInfo!: HassioSupervisorInfoType;
@property() public supervisorInfo!: HassioSupervisorInfoType;
@property() public hostInfo!: HassioHostInfoType;
@@ -87,7 +56,7 @@ class HassioSupervisorInfo extends LitElement {
<span slot="description">
${this.supervisorInfo.version_latest}
</span>
${this.supervisorInfo.update_available
${this.supervisorInfo.version !== this.supervisorInfo.version_latest
? html`
<ha-progress-button
title="Update the supervisor"
@@ -149,13 +118,18 @@ class HassioSupervisorInfo extends LitElement {
</ha-settings-row>`
: html`<div class="error">
You are running an unsupported installation.
<button
class="link"
<a
href="https://github.com/home-assistant/architecture/blob/master/adr/${this.hostInfo.features.includes(
"hassos"
)
? "0015-home-assistant-os.md"
: "0014-home-assistant-supervised.md"}"
target="_blank"
rel="noreferrer"
title="Learn more about how you can make your system compliant"
@click=${this._unsupportedDialog}
>
Learn more
</button>
Learn More
</a>
</div>`}
</div>
<div class="card-actions">
@@ -207,7 +181,7 @@ class HassioSupervisorInfo extends LitElement {
};
await setSupervisorOption(this.hass, data);
await reloadSupervisor(this.hass);
fireEvent(this, "hass-api-called", { success: true, response: null });
this.supervisorInfo = await fetchHassioSupervisorInfo(this.hass);
} catch (err) {
showAlertDialog(this, {
title: "Failed to set supervisor option",
@@ -275,32 +249,6 @@ 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>
${resolution.unsupported.map(
(issue) => html`
<li>
${ISSUES[issue]
? html`<a
href="${documentationUrl(this.hass, ISSUES[issue].url)}"
target="_blank"
rel="noreferrer"
>
${ISSUES[issue].title}
</a>`
: issue}
</li>
`
)}
</ul>`,
});
}
private async _toggleDiagnostics(): Promise<void> {
try {
const data: SupervisorOptions = {

View File

@@ -21,11 +21,11 @@ import { fetchHassioStats, HassioStats } from "../../../src/data/hassio/common";
import { HassioHostInfo } from "../../../src/data/hassio/host";
import { haStyle } from "../../../src/resources/styles";
import { HomeAssistant } from "../../../src/types";
import { bytesToString } from "../../../src/util/bytes-to-string";
import {
getValueInPercentage,
roundWithOneDecimal,
} from "../../../src/util/calculate";
import { bytesToString } from "../../../src/util/bytes-to-string";
import { hassioStyle } from "../resources/hassio-style";
@customElement("hassio-system-metrics")
@@ -65,7 +65,9 @@ class HassioSystemMetrics extends LitElement {
{
description: "Used Space",
value: this._getUsedSpace(this.hostInfo),
tooltip: `${this.hostInfo.disk_used} GB/${this.hostInfo.disk_total} GB`,
tooltip: `${
this.hostInfo.disk_used
} GB/${this.hostInfo.disk_total} GB`,
},
];

View File

@@ -22,8 +22,7 @@
"author": "Paulus Schoutsen <Paulus@PaulusSchoutsen.nl> (http://paulusschoutsen.nl)",
"license": "Apache-2.0",
"dependencies": {
"@formatjs/intl-getcanonicallocales": "^1.4.6",
"@formatjs/intl-pluralrules": "^3.4.10",
"@formatjs/intl-pluralrules": "^1.5.8",
"@fullcalendar/common": "5.1.0",
"@fullcalendar/core": "5.1.0",
"@fullcalendar/daygrid": "5.1.0",
@@ -78,7 +77,7 @@
"@polymer/paper-toast": "^3.0.1",
"@polymer/paper-tooltip": "^3.0.1",
"@polymer/polymer": "3.1.0",
"@thomasloven/round-slider": "0.5.2",
"@thomasloven/round-slider": "0.5.0",
"@types/chromecast-caf-sender": "^1.0.3",
"@types/sortablejs": "^1.10.6",
"@vaadin/vaadin-combo-box": "^5.0.10",
@@ -110,7 +109,7 @@
"marked": "^1.1.1",
"mdn-polyfills": "^5.16.0",
"memoize-one": "^5.0.2",
"node-vibrant": "^3.1.6",
"node-vibrant": "^3.1.5",
"proxy-polyfill": "^0.3.1",
"punycode": "^2.1.1",
"qrcode": "^1.4.4",
@@ -119,7 +118,6 @@
"roboto-fontface": "^0.10.0",
"sortablejs": "^1.10.2",
"superstruct": "^0.10.12",
"tinykeys": "^1.1.1",
"unfetch": "^4.1.0",
"vue": "^2.6.11",
"vue2-daterange-picker": "^0.5.1",
@@ -177,6 +175,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",
@@ -203,15 +202,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"
},

View File

@@ -13,6 +13,7 @@
"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"
],

Binary file not shown.

Before

Width:  |  Height:  |  Size: 30 KiB

View File

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

View File

@@ -1,4 +1,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

@@ -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

@@ -7,7 +7,6 @@ import {
rgb2hex,
rgb2lab,
} from "../color/convert-color";
import { hexBlend } from "../color/hex";
import { labBrighten, labDarken } from "../color/lab";
import { rgbContrast } from "../color/rgb";
@@ -38,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

@@ -10,7 +10,10 @@ export const dynamicElement = directive(
let element = part.value as HTMLElement | undefined;
if (tag === element?.localName) {
if (
element !== undefined &&
tag.toUpperCase() === (element as HTMLElement).tagName
) {
if (properties) {
Object.entries(properties).forEach(([key, value]) => {
element![key] = value;

View File

@@ -23,7 +23,7 @@ export const binarySensorIcon = (state?: string, stateObj?: HassEntity) => {
case "problem":
case "safety":
case "smoke":
return is_off ? "hass:check-circle" : "hass:alert-circle";
return is_off ? "hass:shield-check" : "hass:alert";
case "heat":
return is_off ? "hass:thermometer" : "hass:fire";
case "light":

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 { numberFormat } from "../string/number-format";
export const computeStateDisplay = (
localize: LocalizeFunc,
@@ -20,9 +19,7 @@ export const computeStateDisplay = (
}
if (stateObj.attributes.unit_of_measurement) {
return `${numberFormat(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

@@ -51,14 +51,18 @@ class SearchInput extends LitElement {
@value-changed=${this._filterInputChanged}
.noLabelFloat=${this.noLabelFloat}
>
<slot name="prefix" slot="prefix">
<ha-svg-icon class="prefix" .path=${mdiMagnify}></ha-svg-icon>
</slot>
<ha-svg-icon
.path=${mdiMagnify}
slot="prefix"
class="prefix"
></ha-svg-icon>
${this.filter &&
html`
<mwc-icon-button
slot="suffix"
class="suffix"
@click=${this._clearSearch}
alt="Clear"
title="Clear"
>
<ha-svg-icon .path=${mdiClose}></ha-svg-icon>

View File

@@ -1,244 +0,0 @@
// MIT License
// Copyright (c) 2015 - present Microsoft Corporation
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
// Names from https://blog.codinghorror.com/ascii-pronunciation-rules-for-programmers/
/**
* An inlined enum containing useful character codes (to be used with String.charCodeAt).
* Please leave the const keyword such that it gets inlined when compiled to JavaScript!
*/
export enum CharCode {
Null = 0,
/**
* The `\b` character.
*/
Backspace = 8,
/**
* The `\t` character.
*/
Tab = 9,
/**
* The `\n` character.
*/
LineFeed = 10,
/**
* The `\r` character.
*/
CarriageReturn = 13,
Space = 32,
/**
* The `!` character.
*/
ExclamationMark = 33,
/**
* The `"` character.
*/
DoubleQuote = 34,
/**
* The `#` character.
*/
Hash = 35,
/**
* The `$` character.
*/
DollarSign = 36,
/**
* The `%` character.
*/
PercentSign = 37,
/**
* The `&` character.
*/
Ampersand = 38,
/**
* The `'` character.
*/
SingleQuote = 39,
/**
* The `(` character.
*/
OpenParen = 40,
/**
* The `)` character.
*/
CloseParen = 41,
/**
* The `*` character.
*/
Asterisk = 42,
/**
* The `+` character.
*/
Plus = 43,
/**
* The `,` character.
*/
Comma = 44,
/**
* The `-` character.
*/
Dash = 45,
/**
* The `.` character.
*/
Period = 46,
/**
* The `/` character.
*/
Slash = 47,
Digit0 = 48,
Digit1 = 49,
Digit2 = 50,
Digit3 = 51,
Digit4 = 52,
Digit5 = 53,
Digit6 = 54,
Digit7 = 55,
Digit8 = 56,
Digit9 = 57,
/**
* The `:` character.
*/
Colon = 58,
/**
* The `;` character.
*/
Semicolon = 59,
/**
* The `<` character.
*/
LessThan = 60,
/**
* The `=` character.
*/
Equals = 61,
/**
* The `>` character.
*/
GreaterThan = 62,
/**
* The `?` character.
*/
QuestionMark = 63,
/**
* The `@` character.
*/
AtSign = 64,
A = 65,
B = 66,
C = 67,
D = 68,
E = 69,
F = 70,
G = 71,
H = 72,
I = 73,
J = 74,
K = 75,
L = 76,
M = 77,
N = 78,
O = 79,
P = 80,
Q = 81,
R = 82,
S = 83,
T = 84,
U = 85,
V = 86,
W = 87,
X = 88,
Y = 89,
Z = 90,
/**
* The `[` character.
*/
OpenSquareBracket = 91,
/**
* The `\` character.
*/
Backslash = 92,
/**
* The `]` character.
*/
CloseSquareBracket = 93,
/**
* The `^` character.
*/
Caret = 94,
/**
* The `_` character.
*/
Underline = 95,
/**
* The ``(`)`` character.
*/
BackTick = 96,
a = 97,
b = 98,
c = 99,
d = 100,
e = 101,
f = 102,
g = 103,
h = 104,
i = 105,
j = 106,
k = 107,
l = 108,
m = 109,
n = 110,
o = 111,
p = 112,
q = 113,
r = 114,
s = 115,
t = 116,
u = 117,
v = 118,
w = 119,
x = 120,
y = 121,
z = 122,
/**
* The `{` character.
*/
OpenCurlyBrace = 123,
/**
* The `|` character.
*/
Pipe = 124,
/**
* The `}` character.
*/
CloseCurlyBrace = 125,
/**
* The `~` character.
*/
Tilde = 126,
}

View File

@@ -1,463 +0,0 @@
/* eslint-disable no-console */
// MIT License
// Copyright (c) 2015 - present Microsoft Corporation
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
import { CharCode } from "./char-code";
const _debug = false;
export interface Match {
start: number;
end: number;
}
const _maxLen = 128;
function initTable() {
const table: number[][] = [];
const row: number[] = [0];
for (let i = 1; i <= _maxLen; i++) {
row.push(-i);
}
for (let i = 0; i <= _maxLen; i++) {
const thisRow = row.slice(0);
thisRow[0] = -i;
table.push(thisRow);
}
return table;
}
function isSeparatorAtPos(value: string, index: number): boolean {
if (index < 0 || index >= value.length) {
return false;
}
const code = value.charCodeAt(index);
switch (code) {
case CharCode.Underline:
case CharCode.Dash:
case CharCode.Period:
case CharCode.Space:
case CharCode.Slash:
case CharCode.Backslash:
case CharCode.SingleQuote:
case CharCode.DoubleQuote:
case CharCode.Colon:
case CharCode.DollarSign:
return true;
default:
return false;
}
}
function isWhitespaceAtPos(value: string, index: number): boolean {
if (index < 0 || index >= value.length) {
return false;
}
const code = value.charCodeAt(index);
switch (code) {
case CharCode.Space:
case CharCode.Tab:
return true;
default:
return false;
}
}
function isUpperCaseAtPos(pos: number, word: string, wordLow: string): boolean {
return word[pos] !== wordLow[pos];
}
function isPatternInWord(
patternLow: string,
patternPos: number,
patternLen: number,
wordLow: string,
wordPos: number,
wordLen: number
): boolean {
while (patternPos < patternLen && wordPos < wordLen) {
if (patternLow[patternPos] === wordLow[wordPos]) {
patternPos += 1;
}
wordPos += 1;
}
return patternPos === patternLen; // pattern must be exhausted
}
enum Arrow {
Top = 0b1,
Diag = 0b10,
Left = 0b100,
}
/**
* A tuple of three values.
* 0. the score
* 1. the matches encoded as bitmask (2^53)
* 2. the offset at which matching started
*/
export type FuzzyScore = [number, number, number];
interface FilterGlobals {
_matchesCount: number;
_topMatch2: number;
_topScore: number;
_wordStart: number;
_firstMatchCanBeWeak: boolean;
_table: number[][];
_scores: number[][];
_arrows: Arrow[][];
}
function initGlobals(): FilterGlobals {
return {
_matchesCount: 0,
_topMatch2: 0,
_topScore: 0,
_wordStart: 0,
_firstMatchCanBeWeak: false,
_table: initTable(),
_scores: initTable(),
_arrows: <Arrow[][]>initTable(),
};
}
export function fuzzyScore(
pattern: string,
patternLow: string,
patternStart: number,
word: string,
wordLow: string,
wordStart: number,
firstMatchCanBeWeak: boolean
): FuzzyScore | undefined {
const globals = initGlobals();
const patternLen = pattern.length > _maxLen ? _maxLen : pattern.length;
const wordLen = word.length > _maxLen ? _maxLen : word.length;
if (
patternStart >= patternLen ||
wordStart >= wordLen ||
patternLen - patternStart > wordLen - wordStart
) {
return undefined;
}
// Run a simple check if the characters of pattern occur
// (in order) at all in word. If that isn't the case we
// stop because no match will be possible
if (
!isPatternInWord(
patternLow,
patternStart,
patternLen,
wordLow,
wordStart,
wordLen
)
) {
return undefined;
}
let row = 1;
let column = 1;
let patternPos = patternStart;
let wordPos = wordStart;
let hasStrongFirstMatch = false;
// There will be a match, fill in tables
for (
row = 1, patternPos = patternStart;
patternPos < patternLen;
row++, patternPos++
) {
for (
column = 1, wordPos = wordStart;
wordPos < wordLen;
column++, wordPos++
) {
const score = _doScore(
pattern,
patternLow,
patternPos,
patternStart,
word,
wordLow,
wordPos
);
if (patternPos === patternStart && score > 1) {
hasStrongFirstMatch = true;
}
globals._scores[row][column] = score;
const diag =
globals._table[row - 1][column - 1] + (score > 1 ? 1 : score);
const top = globals._table[row - 1][column] + -1;
const left = globals._table[row][column - 1] + -1;
if (left >= top) {
// left or diag
if (left > diag) {
globals._table[row][column] = left;
globals._arrows[row][column] = Arrow.Left;
} else if (left === diag) {
globals._table[row][column] = left;
globals._arrows[row][column] = Arrow.Left || Arrow.Diag;
} else {
globals._table[row][column] = diag;
globals._arrows[row][column] = Arrow.Diag;
}
} else if (top > diag) {
globals._table[row][column] = top;
globals._arrows[row][column] = Arrow.Top;
} else if (top === diag) {
globals._table[row][column] = top;
globals._arrows[row][column] = Arrow.Top || Arrow.Diag;
} else {
globals._table[row][column] = diag;
globals._arrows[row][column] = Arrow.Diag;
}
}
}
if (_debug) {
printTables(pattern, patternStart, word, wordStart, globals);
}
if (!hasStrongFirstMatch && !firstMatchCanBeWeak) {
return undefined;
}
globals._matchesCount = 0;
globals._topScore = -100;
globals._wordStart = wordStart;
globals._firstMatchCanBeWeak = firstMatchCanBeWeak;
_findAllMatches2(
row - 1,
column - 1,
patternLen === wordLen ? 1 : 0,
0,
false,
globals
);
if (globals._matchesCount === 0) {
return undefined;
}
return [globals._topScore, globals._topMatch2, wordStart];
}
function _doScore(
pattern: string,
patternLow: string,
patternPos: number,
patternStart: number,
word: string,
wordLow: string,
wordPos: number
) {
if (patternLow[patternPos] !== wordLow[wordPos]) {
return -1;
}
if (wordPos === patternPos - patternStart) {
// common prefix: `foobar <-> foobaz`
// ^^^^^
if (pattern[patternPos] === word[wordPos]) {
return 7;
}
return 5;
}
if (
isUpperCaseAtPos(wordPos, word, wordLow) &&
(wordPos === 0 || !isUpperCaseAtPos(wordPos - 1, word, wordLow))
) {
// hitting upper-case: `foo <-> forOthers`
// ^^ ^
if (pattern[patternPos] === word[wordPos]) {
return 7;
}
return 5;
}
if (
isSeparatorAtPos(wordLow, wordPos) &&
(wordPos === 0 || !isSeparatorAtPos(wordLow, wordPos - 1))
) {
// hitting a separator: `. <-> foo.bar`
// ^
return 5;
}
if (
isSeparatorAtPos(wordLow, wordPos - 1) ||
isWhitespaceAtPos(wordLow, wordPos - 1)
) {
// post separator: `foo <-> bar_foo`
// ^^^
return 5;
}
return 1;
}
function printTable(
table: number[][],
pattern: string,
patternLen: number,
word: string,
wordLen: number
): string {
function pad(s: string, n: number, _pad = " ") {
while (s.length < n) {
s = _pad + s;
}
return s;
}
let ret = ` | |${word
.split("")
.map((c) => pad(c, 3))
.join("|")}\n`;
for (let i = 0; i <= patternLen; i++) {
if (i === 0) {
ret += " |";
} else {
ret += `${pattern[i - 1]}|`;
}
ret +=
table[i]
.slice(0, wordLen + 1)
.map((n) => pad(n.toString(), 3))
.join("|") + "\n";
}
return ret;
}
function printTables(
pattern: string,
patternStart: number,
word: string,
wordStart: number,
globals: FilterGlobals
): void {
pattern = pattern.substr(patternStart);
word = word.substr(wordStart);
console.log(
printTable(globals._table, pattern, pattern.length, word, word.length)
);
console.log(
printTable(globals._arrows, pattern, pattern.length, word, word.length)
);
console.log(
printTable(globals._scores, pattern, pattern.length, word, word.length)
);
}
function _findAllMatches2(
row: number,
column: number,
total: number,
matches: number,
lastMatched: boolean,
globals: FilterGlobals
): void {
if (globals._matchesCount >= 10 || total < -25) {
// stop when having already 10 results, or
// when a potential alignment as already 5 gaps
return;
}
let simpleMatchCount = 0;
while (row > 0 && column > 0) {
const score = globals._scores[row][column];
const arrow = globals._arrows[row][column];
if (arrow === Arrow.Left) {
// left -> no match, skip a word character
column -= 1;
if (lastMatched) {
total -= 5; // new gap penalty
} else if (matches !== 0) {
total -= 1; // gap penalty after first match
}
lastMatched = false;
simpleMatchCount = 0;
} else if (arrow && Arrow.Diag) {
if (arrow && Arrow.Left) {
// left
_findAllMatches2(
row,
column - 1,
matches !== 0 ? total - 1 : total, // gap penalty after first match
matches,
lastMatched,
globals
);
}
// diag
total += score;
row -= 1;
column -= 1;
lastMatched = true;
// match -> set a 1 at the word pos
matches += 2 ** (column + globals._wordStart);
// count simple matches and boost a row of
// simple matches when they yield in a
// strong match.
if (score === 1) {
simpleMatchCount += 1;
if (row === 0 && !globals._firstMatchCanBeWeak) {
// when the first match is a weak
// match we discard it
return;
}
} else {
// boost
total += 1 + simpleMatchCount * (score - 1);
simpleMatchCount = 0;
}
} else {
return;
}
}
total -= column >= 3 ? 9 : column * 3; // late start penalty
// dynamically keep track of the current top score
// and insert the current best score at head, the rest at tail
globals._matchesCount += 1;
if (total > globals._topScore) {
globals._topScore = total;
globals._topMatch2 = matches;
}
}
// #endregion

View File

@@ -1,66 +0,0 @@
import { fuzzyScore } from "./filter";
/**
* Determine whether a sequence of letters exists in another string,
* in that order, allowing for skipping. Ex: "chdr" exists in "chandelier")
*
* @param {string} filter - Sequence of letters to check for
* @param {string} word - Word to check for sequence
*
* @return {number} Score representing how well the word matches the filter. Return of 0 means no match.
*/
export const fuzzySequentialMatch = (filter: string, ...words: string[]) => {
let topScore = 0;
for (const word of words) {
const scores = fuzzyScore(
filter,
filter.toLowerCase(),
0,
word,
word.toLowerCase(),
0,
true
);
if (!scores) {
continue;
}
// The VS Code implementation of filter treats a score of "0" as just barely a match
// But we will typically use this matcher in a .filter(), which interprets 0 as a failure.
// By shifting all scores up by 1, we allow "0" matches, while retaining score precedence
const score = scores[0] + 1;
if (score > topScore) {
topScore = score;
}
}
return topScore;
};
export interface ScorableTextItem {
score?: number;
text: string;
altText?: string;
}
type FuzzyFilterSort = <T extends ScorableTextItem>(
filter: string,
items: T[]
) => T[];
export const fuzzyFilterSort: FuzzyFilterSort = (filter, items) => {
return items
.map((item) => {
item.score = item.altText
? fuzzySequentialMatch(filter, item.text, item.altText)
: fuzzySequentialMatch(filter, item.text);
return item;
})
.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,22 +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 numberFormat = (
num: string | number,
language: string
): string => {
// Polyfill for Number.isNaN, which is more reliable that 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).format(Number(num));
}
return num.toString();
};

View File

@@ -0,0 +1,29 @@
/**
* Determine whether a sequence of letters exists in another string,
* in that order, allowing for skipping. Ex: "chdr" exists in "chandelier")
*
* filter => sequence of letters
* word => Word to check for sequence
*
* return true if word contains sequence. Otherwise false.
*/
export const fuzzySequentialMatch = (filter: string, word: string) => {
if (filter === "") {
return true;
}
for (let i = 0; i <= filter.length; i++) {
const pos = word.indexOf(filter[0]);
if (pos < 0) {
return false;
}
const newWord = word.substring(pos + 1);
const newFilter = filter.substring(1);
return fuzzySequentialMatch(newFilter, newWord);
}
return true;
};

View File

@@ -1,5 +1,4 @@
import IntlMessageFormat from "intl-messageformat";
import { shouldPolyfill } from "@formatjs/intl-pluralrules/should-polyfill";
import { Resources } from "../../types";
export type LocalizeFunc = (key: string, ...args: any[]) => string;
@@ -13,12 +12,9 @@ export interface FormatsType {
time: FormatType;
}
let polyfillLoaded = !shouldPolyfill();
const polyfillProm = polyfillLoaded
? undefined
: import("@formatjs/intl-pluralrules/polyfill-locales").then(() => {
polyfillLoaded = true;
});
if (!Intl.PluralRules) {
import("@formatjs/intl-pluralrules/polyfill-locales");
}
/**
* Adapted from Polymer app-localize-behavior.
@@ -41,16 +37,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 = {};

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

@@ -94,8 +94,6 @@ export class HaDataTable extends LitElement {
@property({ type: Boolean }) public selectable = false;
@property({ type: Boolean }) public clickable = false;
@property({ type: Boolean }) public hasFab = false;
@property({ type: Boolean, attribute: "auto-height" })
@@ -329,13 +327,12 @@ export class HaDataTable extends LitElement {
<div
aria-rowindex=${index}
role="row"
.rowId=${row[this.id]}
.rowId="${row[this.id]}"
@click=${this._handleRowClick}
class="mdc-data-table__row ${classMap({
"mdc-data-table__row--selected": this._checkedRows.includes(
String(row[this.id])
),
clickable: this.clickable,
})}"
aria-selected=${ifDefined(
this._checkedRows.includes(String(row[this.id]))
@@ -353,7 +350,6 @@ export class HaDataTable extends LitElement {
<ha-checkbox
class="mdc-data-table__row-checkbox"
@change=${this._handleRowCheckboxClick}
.rowId=${row[this.id]}
.disabled=${row.selectable === false}
.checked=${this._checkedRows.includes(
String(row[this.id])
@@ -461,7 +457,9 @@ export class HaDataTable extends LitElement {
);
private _handleHeaderClick(ev: Event) {
const columnId = (ev.currentTarget as any).columnId;
const columnId = ((ev.target as HTMLElement).closest(
".mdc-data-table__header-cell"
) as any).columnId;
if (!this.columns[columnId].sortable) {
return;
}
@@ -495,8 +493,8 @@ export class HaDataTable extends LitElement {
}
private _handleRowCheckboxClick(ev: Event) {
const checkbox = ev.currentTarget as HaCheckbox;
const rowId = (checkbox as any).rowId;
const checkbox = ev.target as HaCheckbox;
const rowId = (checkbox.closest(".mdc-data-table__row") as any).rowId;
if (checkbox.checked) {
if (this._checkedRows.includes(rowId)) {
@@ -514,7 +512,7 @@ export class HaDataTable extends LitElement {
if (target.tagName === "HA-CHECKBOX") {
return;
}
const rowId = (ev.currentTarget as any).rowId;
const rowId = (target.closest(".mdc-data-table__row") as any).rowId;
fireEvent(this, "row-click", { id: rowId }, { bubbles: false });
}
@@ -888,9 +886,6 @@ export class HaDataTable extends LitElement {
.forceLTR {
direction: ltr;
}
.clickable {
cursor: pointer;
}
`;
}
}

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

@@ -9,17 +9,9 @@ import { HaDeviceAutomationPicker } from "./ha-device-automation-picker";
@customElement("ha-device-action-picker")
class HaDeviceActionPicker extends HaDeviceAutomationPicker<DeviceAction> {
protected get NO_AUTOMATION_TEXT() {
return this.hass.localize(
"ui.panel.config.devices.automation.actions.no_actions"
);
}
protected NO_AUTOMATION_TEXT = "No actions";
protected get UNKNOWN_AUTOMATION_TEXT() {
return this.hass.localize(
"ui.panel.config.devices.automation.actions.unknown_action"
);
}
protected UNKNOWN_AUTOMATION_TEXT = "Unknown action";
constructor() {
super(

View File

@@ -33,24 +33,16 @@ export abstract class HaDeviceAutomationPicker<
@property() public value?: T;
protected NO_AUTOMATION_TEXT = "No automations";
protected UNKNOWN_AUTOMATION_TEXT = "Unknown automation";
@internalProperty() private _automations: T[] = [];
// Trigger an empty render so we start with a clean DOM.
// paper-listbox does not like changing things around.
@internalProperty() private _renderEmpty = false;
protected get NO_AUTOMATION_TEXT() {
return this.hass.localize(
"ui.panel.config.devices.automation.actions.no_actions"
);
}
protected get UNKNOWN_AUTOMATION_TEXT() {
return this.hass.localize(
"ui.panel.config.devices.automation.actions.unknown_action"
);
}
private _localizeDeviceAutomation: (
hass: HomeAssistant,
automation: T

View File

@@ -11,17 +11,9 @@ import { HaDeviceAutomationPicker } from "./ha-device-automation-picker";
class HaDeviceConditionPicker extends HaDeviceAutomationPicker<
DeviceCondition
> {
protected get NO_AUTOMATION_TEXT() {
return this.hass.localize(
"ui.panel.config.devices.automation.conditions.no_conditions"
);
}
protected NO_AUTOMATION_TEXT = "No conditions";
protected get UNKNOWN_AUTOMATION_TEXT() {
return this.hass.localize(
"ui.panel.config.devices.automation.conditions.unknown_condition"
);
}
protected UNKNOWN_AUTOMATION_TEXT = "Unknown condition";
constructor() {
super(

View File

@@ -9,17 +9,9 @@ import { HaDeviceAutomationPicker } from "./ha-device-automation-picker";
@customElement("ha-device-trigger-picker")
class HaDeviceTriggerPicker extends HaDeviceAutomationPicker<DeviceTrigger> {
protected get NO_AUTOMATION_TEXT() {
return this.hass.localize(
"ui.panel.config.devices.automation.triggers.no_triggers"
);
}
protected NO_AUTOMATION_TEXT = "No triggers";
protected get UNKNOWN_AUTOMATION_TEXT() {
return this.hass.localize(
"ui.panel.config.devices.automation.triggers.unknown_trigger"
);
}
protected UNKNOWN_AUTOMATION_TEXT = "Unknown trigger";
constructor() {
super(

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;
@@ -279,7 +278,7 @@ class HaChartBase extends mixinBehaviors(
this.set(["tooltip", "title"], title);
if (tooltip.beforeBody) {
this.set(["tooltip", "beforeBody"], tooltip.beforeBody.join("\n"));
this.set(["tooltip", "beforeBody"], tooltip.beforeBody.join("\n"));
}
const bodyLines = tooltip.body.map((n) => n.lines);

View File

@@ -1,5 +1,3 @@
import "@material/mwc-icon-button/mwc-icon-button";
import { mdiClose, mdiMenuDown, mdiMenuUp } from "@mdi/js";
import "@polymer/paper-input/paper-input";
import "@polymer/paper-item/paper-item";
import "@vaadin/vaadin-combo-box/theme/material/vaadin-combo-box-light";
@@ -15,13 +13,10 @@ import {
query,
TemplateResult,
} from "lit-element";
import memoizeOne from "memoize-one";
import { fireEvent } from "../../common/dom/fire_event";
import { computeDomain } from "../../common/entity/compute_domain";
import { PolymerChangedEvent } from "../../polymer-types";
import { HomeAssistant } from "../../types";
import { formatAttributeName } from "../../util/hass-attributes-util";
import "../ha-svg-icon";
import "../ha-icon-button";
import "./state-badge";
export type HaEntityPickerEntityFilterFunc = (entityId: HassEntity) => boolean;
@@ -38,44 +33,7 @@ const rowRenderer = (root: HTMLElement, _owner, model: { item: string }) => {
<paper-item></paper-item>
`;
}
root.querySelector("paper-item")!.textContent = formatAttributeName(
model.item
);
};
const SELECTABLE_ATTRIBUTES: { [key: string]: string[] } = {
light: ["brightness"],
climate: [
"current_temperature",
"fan_mode",
"preset_mode",
"swing_mode",
"temperature",
"current_hundity",
"humidity",
"hvac_action",
],
fan: ["speed"],
air_quality: [
"nitrogen_oxide",
"particulate_matter_10",
"particulate_matter_2_5",
],
cover: ["current_position", "current_tilt_position"],
device_tracker: ["battery"],
humidifier: ["humidty"],
media_player: ["media_title"],
vacuum: ["battery_level", "status"],
water_heater: ["current_temperature", "temperature", "operation_mode"],
weather: [
"temperature",
"humidity",
"ozone",
"pressure",
"wind_bearing",
"wind_speed",
"visibility",
],
root.querySelector("paper-item")!.textContent = model.item;
};
@customElement("ha-entity-attribute-picker")
@@ -105,8 +63,9 @@ class HaEntityAttributePicker extends LitElement {
protected updated(changedProps: PropertyValues) {
if (changedProps.has("_opened") && this._opened) {
(this._comboBox as any).items = this.entityId
? this._selectableAttributes(this.entityId)
const state = this.entityId ? this.hass.states[this.entityId] : undefined;
(this._comboBox as any).items = state
? Object.keys(state.attributes)
: [];
}
}
@@ -130,7 +89,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"
@@ -138,58 +97,45 @@ class HaEntityAttributePicker extends LitElement {
autocorrect="off"
spellcheck="false"
>
<div class="suffix" slot="suffix">
${this.value
? html`
<mwc-icon-button
.label=${this.hass.localize(
"ui.components.entity.entity-picker.clear"
)}
class="clear-button"
tabindex="-1"
@click=${this._clearValue}
no-ripple
>
<ha-svg-icon .path=${mdiClose}></ha-svg-icon>
</mwc-icon-button>
`
: ""}
${this.value
? html`
<ha-icon-button
aria-label=${this.hass.localize(
"ui.components.entity.entity-picker.clear"
)}
slot="suffix"
class="clear-button"
icon="hass:close"
@click=${this._clearValue}
no-ripple
>
Clear
</ha-icon-button>
`
: ""}
<mwc-icon-button
.label=${this.hass.localize(
"ui.components.entity.entity-attribute-picker.show_attributes"
)}
class="toggle-button"
tabindex="-1"
>
<ha-svg-icon
.path=${this._opened ? mdiMenuUp : mdiMenuDown}
></ha-svg-icon>
</mwc-icon-button>
</div>
<ha-icon-button
aria-label=${this.hass.localize(
"ui.components.entity.entity-attribute-picker.show_attributes"
)}
slot="suffix"
class="toggle-button"
.icon=${this._opened ? "hass:menu-up" : "hass:menu-down"}
>
Toggle
</ha-icon-button>
</paper-input>
</vaadin-combo-box-light>
`;
}
private _selectableAttributes = memoizeOne((entity: string) => {
const stateObj = this.hass.states[entity];
if (!stateObj) {
return [];
}
return Object.keys(stateObj.attributes).filter((attr) =>
SELECTABLE_ATTRIBUTES[computeDomain(entity)].includes(attr)
);
});
private _clearValue(ev: Event) {
ev.stopPropagation();
this._setValue("");
}
private get _value() {
return this.value;
return this.value || "";
}
private _openedChanged(ev: PolymerChangedEvent<boolean>) {
@@ -213,10 +159,7 @@ class HaEntityAttributePicker extends LitElement {
static get styles(): CSSResult {
return css`
.suffix {
display: flex;
}
mwc-icon-button {
paper-input > ha-icon-button {
--mdc-icon-button-size: 24px;
padding: 0px 2px;
color: var(--secondary-text-color);

View File

@@ -1,5 +1,3 @@
import "@material/mwc-icon-button/mwc-icon-button";
import { mdiClose, mdiMenuDown, mdiMenuUp } from "@mdi/js";
import "@polymer/paper-input/paper-input";
import "@polymer/paper-item/paper-icon-item";
import "@polymer/paper-item/paper-item-body";
@@ -22,7 +20,7 @@ import { computeDomain } from "../../common/entity/compute_domain";
import { computeStateName } from "../../common/entity/compute_state_name";
import { PolymerChangedEvent } from "../../polymer-types";
import { HomeAssistant } from "../../types";
import "../ha-svg-icon";
import "../ha-icon-button";
import "./state-badge";
export type HaEntityPickerEntityFilterFunc = (entityId: HassEntity) => boolean;
@@ -103,8 +101,6 @@ export class HaEntityPicker extends LitElement {
private _initedStates = false;
private _states: HassEntity[] = [];
private _getStates = memoizeOne(
(
_opened: boolean,
@@ -170,7 +166,7 @@ export class HaEntityPicker extends LitElement {
protected updated(changedProps: PropertyValues) {
if (!this._initedStates || (changedProps.has("_opened") && this._opened)) {
this._states = this._getStates(
const states = this._getStates(
this._opened,
this.hass,
this.includeDomains,
@@ -178,7 +174,7 @@ export class HaEntityPicker extends LitElement {
this.entityFilter,
this.includeDeviceClasses
);
(this._comboBox as any).filteredItems = this._states;
(this._comboBox as any).items = states;
this._initedStates = true;
}
}
@@ -196,7 +192,6 @@ export class HaEntityPicker extends LitElement {
.renderer=${rowRenderer}
@opened-changed=${this._openedChanged}
@value-changed=${this._valueChanged}
@filter-changed=${this._filterChanged}
>
<paper-input
.autofocus=${this.autofocus}
@@ -211,35 +206,35 @@ export class HaEntityPicker extends LitElement {
autocorrect="off"
spellcheck="false"
>
<div class="suffix" slot="suffix">
${this.value && !this.hideClearIcon
? html`
<mwc-icon-button
.label=${this.hass.localize(
"ui.components.entity.entity-picker.clear"
)}
class="clear-button"
tabindex="-1"
@click=${this._clearValue}
no-ripple
>
<ha-svg-icon .path=${mdiClose}></ha-svg-icon>
</mwc-icon-button>
`
: ""}
${this.value && !this.hideClearIcon
? html`
<ha-icon-button
aria-label=${this.hass.localize(
"ui.components.entity.entity-picker.clear"
)}
slot="suffix"
class="clear-button"
icon="hass:close"
tabindex="-1"
@click=${this._clearValue}
no-ripple
>
Clear
</ha-icon-button>
`
: ""}
<mwc-icon-button
.label=${this.hass.localize(
"ui.components.entity.entity-picker.show_entities"
)}
class="toggle-button"
tabindex="-1"
>
<ha-svg-icon
.path=${this._opened ? mdiMenuUp : mdiMenuDown}
></ha-svg-icon>
</mwc-icon-button>
</div>
<ha-icon-button
aria-label=${this.hass.localize(
"ui.components.entity.entity-picker.show_entities"
)}
slot="suffix"
class="toggle-button"
.icon=${this._opened ? "hass:menu-up" : "hass:menu-down"}
tabindex="-1"
>
Toggle
</ha-icon-button>
</paper-input>
</vaadin-combo-box-light>
`;
@@ -265,15 +260,6 @@ export class HaEntityPicker extends LitElement {
}
}
private _filterChanged(ev: CustomEvent): void {
const filterString = ev.detail.value.toLowerCase();
(this._comboBox as any).filteredItems = this._states.filter(
(state) =>
state.entity_id.toLowerCase().includes(filterString) ||
computeStateName(state).toLowerCase().includes(filterString)
);
}
private _setValue(value: string) {
this.value = value;
setTimeout(() => {
@@ -284,10 +270,7 @@ export class HaEntityPicker extends LitElement {
static get styles(): CSSResult {
return css`
.suffix {
display: flex;
}
mwc-icon-button {
paper-input > ha-icon-button {
--mdc-icon-button-size: 24px;
padding: 0px 2px;
color: var(--secondary-text-color);

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);

View File

@@ -11,14 +11,11 @@ import {
} from "lit-element";
import { ifDefined } from "lit-html/directives/if-defined";
import { styleMap } from "lit-html/directives/style-map";
import { computeActiveState } from "../../common/entity/compute_active_state";
import { computeStateDomain } from "../../common/entity/compute_state_domain";
import { stateIcon } from "../../common/entity/state_icon";
import { iconColorCSS } from "../../common/style/icon_color_css";
import type { HomeAssistant } from "../../types";
import "../ha-icon";
export class StateBadge extends LitElement {
@@ -40,13 +37,7 @@ export class StateBadge extends LitElement {
protected render(): TemplateResult {
const stateObj = this.stateObj;
if (!stateObj) {
return html`<div class="missing">
<ha-icon icon="hass:alert"></ha-icon>
</div>`;
}
if (!this._showIcon) {
if (!stateObj || !this._showIcon) {
return html``;
}
@@ -149,9 +140,6 @@ export class StateBadge extends LitElement {
ha-icon {
transition: color 0.3s ease-in-out, filter 0.3s ease-in-out;
}
.missing {
color: #fce588;
}
${iconColorCSS}
`;

View File

@@ -0,0 +1,124 @@
import { html } from "@polymer/polymer/lib/utils/html-tag";
import "@polymer/paper-tooltip/paper-tooltip";
/* eslint-plugin-disable lit */
import LocalizeMixin from "../../mixins/localize-mixin";
import { PolymerElement } from "@polymer/polymer/polymer-element";
import { computeStateName } from "../../common/entity/compute_state_name";
import { computeRTL } from "../../common/util/compute_rtl";
import "../ha-relative-time";
import "./state-badge";
class StateInfo extends LocalizeMixin(PolymerElement) {
static get template() {
return html`
${this.styleTemplate} ${this.stateBadgeTemplate} ${this.infoTemplate}
`;
}
static get styleTemplate() {
return html`
<style>
:host {
@apply --paper-font-body1;
min-width: 120px;
white-space: nowrap;
}
state-badge {
float: left;
}
:host([rtl]) state-badge {
float: right;
}
.info {
margin-left: 56px;
}
:host([rtl]) .info {
margin-right: 56px;
margin-left: 0;
text-align: right;
}
.name {
@apply --paper-font-common-nowrap;
color: var(--primary-text-color);
line-height: 40px;
}
.name[in-dialog],
:host([secondary-line]) .name {
line-height: 20px;
}
.time-ago,
.extra-info,
.extra-info > * {
@apply --paper-font-common-nowrap;
color: var(--secondary-text-color);
}
</style>
`;
}
static get stateBadgeTemplate() {
return html` <state-badge state-obj="[[stateObj]]"></state-badge> `;
}
static get infoTemplate() {
return html`
<div class="info">
<div class="name" in-dialog$="[[inDialog]]">
[[computeStateName(stateObj)]]
</div>
<template is="dom-if" if="[[inDialog]]">
<div class="time-ago">
<ha-relative-time
id="last_changed"
hass="[[hass]]"
datetime="[[stateObj.last_changed]]"
></ha-relative-time>
<paper-tooltip animation-delay="0" for="last_changed">
[[localize('ui.dialogs.more_info_control.last_updated')]]:
<ha-relative-time
hass="[[hass]]"
datetime="[[stateObj.last_updated]]"
></ha-relative-time>
</paper-tooltip>
</div>
</template>
<template is="dom-if" if="[[!inDialog]]">
<div class="extra-info"><slot> </slot></div>
</template>
</div>
`;
}
static get properties() {
return {
hass: Object,
stateObj: Object,
inDialog: {
type: Boolean,
value: () => false,
},
rtl: {
type: Boolean,
reflectToAttribute: true,
computed: "computeRTL(hass)",
},
};
}
computeStateName(stateObj) {
return computeStateName(stateObj);
}
computeRTL(hass) {
return computeRTL(hass);
}
}
customElements.define("state-info", StateInfo);

View File

@@ -1,158 +0,0 @@
import "@polymer/paper-tooltip/paper-tooltip";
import {
css,
CSSResult,
customElement,
html,
LitElement,
property,
TemplateResult,
} from "lit-element";
import type { HassEntity } from "home-assistant-js-websocket";
import { computeStateName } from "../../common/entity/compute_state_name";
import { computeRTL } from "../../common/util/compute_rtl";
import type { HomeAssistant } from "../../types";
import "../ha-relative-time";
import "./state-badge";
@customElement("state-info")
class StateInfo extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public stateObj?: HassEntity;
@property({ type: Boolean }) public inDialog = false;
// property used only in css
@property({ type: Boolean, reflect: true }) public rtl = false;
protected render(): TemplateResult {
if (!this.hass || !this.stateObj) {
return html``;
}
return html`<state-badge
.stateObj=${this.stateObj}
.stateColor=${true}
></state-badge>
<div class="info">
<div class="name" .inDialog=${this.inDialog}>
${computeStateName(this.stateObj)}
</div>
${this.inDialog
? html`<div class="time-ago">
<ha-relative-time
id="last_changed"
.hass=${this.hass}
.datetime=${this.stateObj.last_changed}
></ha-relative-time>
<paper-tooltip animation-delay="0" for="last_changed">
<div>
<div class="row">
<span class="column-name">
${this.hass.localize(
"ui.dialogs.more_info_control.last_changed"
)}:
</span>
<ha-relative-time
.hass=${this.hass}
.datetime=${this.stateObj.last_changed}
></ha-relative-time>
</div>
<div class="row">
<span>
${this.hass.localize(
"ui.dialogs.more_info_control.last_updated"
)}:
</span>
<ha-relative-time
.hass=${this.hass}
.datetime=${this.stateObj.last_updated}
></ha-relative-time>
</div>
</div>
</paper-tooltip>
</div>`
: html`<div class="extra-info"><slot> </slot></div>`}
</div>`;
}
protected updated(changedProps) {
super.updated(changedProps);
if (!changedProps.has("hass")) {
return;
}
const oldHass = changedProps.get("hass") as HomeAssistant | undefined;
if (!oldHass || oldHass.language !== this.hass.language) {
this.rtl = computeRTL(this.hass);
}
}
static get styles(): CSSResult {
return css`
:host {
@apply --paper-font-body1;
min-width: 120px;
white-space: nowrap;
}
state-badge {
float: left;
}
:host([rtl]) state-badge {
float: right;
}
.info {
margin-left: 56px;
}
:host([rtl]) .info {
margin-right: 56px;
margin-left: 0;
text-align: right;
}
.name {
@apply --paper-font-common-nowrap;
color: var(--primary-text-color);
line-height: 40px;
}
.name[in-dialog],
:host([secondary-line]) .name {
line-height: 20px;
}
.time-ago,
.extra-info,
.extra-info > * {
@apply --paper-font-common-nowrap;
color: var(--secondary-text-color);
}
.row {
display: flex;
flex-direction: row;
flex-wrap: no-wrap;
width: 100%;
justify-content: space-between;
margin: 0 2px 4px 0;
}
.row:last-child {
margin-bottom: 0px;
}
`;
}
}
declare global {
interface HTMLElementTagNameMap {
"state-info": StateInfo;
}
}

View File

@@ -9,9 +9,7 @@ import {
TemplateResult,
} from "lit-element";
import { until } from "lit-html/directives/until";
import hassAttributeUtil, {
formatAttributeName,
} from "../util/hass-attributes-util";
import hassAttributeUtil from "../util/hass-attributes-util";
let jsYamlPromise: Promise<typeof import("js-yaml")>;
@@ -36,7 +34,7 @@ class HaAttributes extends LitElement {
(attribute) => html`
<div class="data-entry">
<div class="key">
${formatAttributeName(attribute)}
${attribute.replace(/_/g, " ").replace("id", "ID")}
</div>
<div class="value">
${this.formatAttribute(attribute)}
@@ -63,16 +61,15 @@ class HaAttributes extends LitElement {
justify-content: space-between;
}
.data-entry .value {
max-width: 50%;
max-width: 200px;
overflow-wrap: break-word;
text-align: right;
}
.key {
flex-grow: 1;
.key:first-letter {
text-transform: capitalize;
}
.attribution {
color: var(--secondary-text-color);
text-align: center;
text-align: right;
}
pre {
font-family: inherit;

View File

@@ -50,12 +50,9 @@ export class HaCard extends LitElement {
font-family: var(--ha-card-header-font-family, inherit);
font-size: var(--ha-card-header-font-size, 24px);
letter-spacing: -0.012em;
line-height: 48px;
padding: 12px 16px 16px;
line-height: 32px;
padding: 24px 16px 16px;
display: block;
margin-block-start: 0px;
margin-block-end: 0px;
font-weight: normal;
}
:host ::slotted(.card-content:not(:first-child)),
@@ -78,7 +75,7 @@ export class HaCard extends LitElement {
protected render(): TemplateResult {
return html`
${this.header
? html`<h1 class="card-header">${this.header}</h1>`
? html` <div class="card-header">${this.header}</div> `
: html``}
<slot></slot>
`;

View File

@@ -11,7 +11,7 @@ export class HaCircularProgress extends CircularProgress {
public alt = "Loading";
@property()
public size: "tiny" | "small" | "medium" | "large" = "medium";
public size: "small" | "medium" | "large" = "medium";
// @ts-ignore
public set density(_) {
@@ -20,8 +20,6 @@ export class HaCircularProgress extends CircularProgress {
public get density() {
switch (this.size) {
case "tiny":
return -8;
case "small":
return -5;
case "medium":

View File

@@ -0,0 +1,131 @@
import { html } from "@polymer/polymer/lib/utils/html-tag";
/* eslint-plugin-disable lit */
import { PolymerElement } from "@polymer/polymer/polymer-element";
import { CLIMATE_PRESET_NONE } from "../data/climate";
import LocalizeMixin from "../mixins/localize-mixin";
/*
* @appliesMixin LocalizeMixin
*/
class HaClimateState extends LocalizeMixin(PolymerElement) {
static get template() {
return html`
<style>
:host {
display: flex;
flex-direction: column;
justify-content: center;
white-space: nowrap;
}
.target {
color: var(--primary-text-color);
}
.current {
color: var(--secondary-text-color);
}
.state-label {
font-weight: bold;
text-transform: capitalize;
}
.unit {
display: inline-block;
direction: ltr;
}
</style>
<div class="target">
<template is="dom-if" if="[[_hasKnownState(stateObj.state)]]">
<span class="state-label">
[[_localizeState(localize, stateObj)]]
<template is="dom-if" if="[[_renderPreset(stateObj.attributes)]]">
- [[_localizePreset(localize, stateObj.attributes.preset_mode)]]
</template>
</span>
</template>
<div class="unit">[[computeTarget(hass, stateObj)]]</div>
</div>
<template is="dom-if" if="[[currentStatus]]">
<div class="current">
[[localize('ui.card.climate.currently')]]:
<div class="unit">[[currentStatus]]</div>
</div>
</template>
`;
}
static get properties() {
return {
hass: Object,
stateObj: Object,
currentStatus: {
type: String,
computed: "computeCurrentStatus(hass, stateObj)",
},
};
}
computeCurrentStatus(hass, stateObj) {
if (!hass || !stateObj) return null;
if (stateObj.attributes.current_temperature != null) {
return `${stateObj.attributes.current_temperature} ${hass.config.unit_system.temperature}`;
}
if (stateObj.attributes.current_humidity != null) {
return `${stateObj.attributes.current_humidity} %`;
}
return null;
}
computeTarget(hass, stateObj) {
if (!hass || !stateObj) return null;
// We're using "!= null" on purpose so that we match both null and undefined.
if (
stateObj.attributes.target_temp_low != null &&
stateObj.attributes.target_temp_high != null
) {
return `${stateObj.attributes.target_temp_low}-${stateObj.attributes.target_temp_high} ${hass.config.unit_system.temperature}`;
}
if (stateObj.attributes.temperature != null) {
return `${stateObj.attributes.temperature} ${hass.config.unit_system.temperature}`;
}
if (
stateObj.attributes.target_humidity_low != null &&
stateObj.attributes.target_humidity_high != null
) {
return `${stateObj.attributes.target_humidity_low}-${stateObj.attributes.target_humidity_high}%`;
}
if (stateObj.attributes.humidity != null) {
return `${stateObj.attributes.humidity} %`;
}
return "";
}
_hasKnownState(state) {
return state !== "unknown";
}
_localizeState(localize, stateObj) {
const stateString = localize(`component.climate.state._.${stateObj.state}`);
return stateObj.attributes.hvac_action
? `${localize(
`state_attributes.climate.hvac_action.${stateObj.attributes.hvac_action}`
)} (${stateString})`
: stateString;
}
_localizePreset(localize, preset) {
return localize(`state_attributes.climate.preset_mode.${preset}`) || preset;
}
_renderPreset(attributes) {
return (
attributes.preset_mode && attributes.preset_mode !== CLIMATE_PRESET_NONE
);
}
}
customElements.define("ha-climate-state", HaClimateState);

View File

@@ -1,139 +0,0 @@
import {
css,
CSSResult,
customElement,
html,
LitElement,
property,
TemplateResult,
} from "lit-element";
import { HassEntity } from "home-assistant-js-websocket";
import { CLIMATE_PRESET_NONE } from "../data/climate";
import type { HomeAssistant } from "../types";
@customElement("ha-climate-state")
class HaClimateState extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public stateObj!: HassEntity;
protected render(): TemplateResult {
const currentStatus = this._computeCurrentStatus();
return html`<div class="target">
${this.stateObj.state !== "unknown"
? html`<span class="state-label">
${this._localizeState()}
${this.stateObj.attributes.preset_mode &&
this.stateObj.attributes.preset_mode !== CLIMATE_PRESET_NONE
? html`-
${this.hass.localize(
`state_attributes.climate.preset_mode.${this.stateObj.attributes.preset_mode}`
) || this.stateObj.attributes.preset_mode}`
: ""}
</span>`
: ""}
<div class="unit">${this._computeTarget()}</div>
</div>
${currentStatus
? html`<div class="current">
${this.hass.localize("ui.card.climate.currently")}:
<div class="unit">${currentStatus}</div>
</div>`
: ""}`;
}
private _computeCurrentStatus(): string | undefined {
if (!this.hass || !this.stateObj) {
return undefined;
}
if (this.stateObj.attributes.current_temperature != null) {
return `${this.stateObj.attributes.current_temperature} ${this.hass.config.unit_system.temperature}`;
}
if (this.stateObj.attributes.current_humidity != null) {
return `${this.stateObj.attributes.current_humidity} %`;
}
return undefined;
}
private _computeTarget(): string {
if (!this.hass || !this.stateObj) {
return "";
}
if (
this.stateObj.attributes.target_temp_low != null &&
this.stateObj.attributes.target_temp_high != null
) {
return `${this.stateObj.attributes.target_temp_low}-${this.stateObj.attributes.target_temp_high} ${this.hass.config.unit_system.temperature}`;
}
if (this.stateObj.attributes.temperature != null) {
return `${this.stateObj.attributes.temperature} ${this.hass.config.unit_system.temperature}`;
}
if (
this.stateObj.attributes.target_humidity_low != null &&
this.stateObj.attributes.target_humidity_high != null
) {
return `${this.stateObj.attributes.target_humidity_low}-${this.stateObj.attributes.target_humidity_high}%`;
}
if (this.stateObj.attributes.humidity != null) {
return `${this.stateObj.attributes.humidity} %`;
}
return "";
}
private _localizeState(): string {
const stateString = this.hass.localize(
`component.climate.state._.${this.stateObj.state}`
);
return this.stateObj.attributes.hvac_action
? `${this.hass.localize(
`state_attributes.climate.hvac_action.${this.stateObj.attributes.hvac_action}`
)} (${stateString})`
: stateString;
}
static get styles(): CSSResult {
return css`
:host {
display: flex;
flex-direction: column;
justify-content: center;
white-space: nowrap;
}
.target {
color: var(--primary-text-color);
}
.current {
color: var(--secondary-text-color);
}
.state-label {
font-weight: bold;
text-transform: capitalize;
}
.unit {
display: inline-block;
direction: ltr;
}
`;
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-climate-state": HaClimateState;
}
}

View File

@@ -81,7 +81,6 @@ export class HaCodeEditor extends UpdatingElement {
protected firstUpdated(changedProps: PropertyValues): void {
super.firstUpdated(changedProps);
this._blockKeyboardShortcuts();
this._load();
}
@@ -233,10 +232,6 @@ export class HaCodeEditor extends UpdatingElement {
this.codemirror!.on("changes", () => this._onChange());
}
private _blockKeyboardShortcuts() {
this.addEventListener("keydown", (ev) => ev.stopPropagation());
}
private _onChange(): void {
const newValue = this.value;
if (newValue === this._value) {

View File

@@ -0,0 +1,126 @@
import "./ha-icon-button";
import { html } from "@polymer/polymer/lib/utils/html-tag";
/* eslint-plugin-disable lit */
import { PolymerElement } from "@polymer/polymer/polymer-element";
import { UNAVAILABLE } from "../data/entity";
import CoverEntity from "../util/cover-model";
class HaCoverControls extends PolymerElement {
static get template() {
return html`
<style>
.state {
white-space: nowrap;
}
[invisible] {
visibility: hidden !important;
}
</style>
<div class="state">
<ha-icon-button
aria-label="Open cover"
icon="[[computeOpenIcon(stateObj)]]"
on-click="onOpenTap"
invisible$="[[!entityObj.supportsOpen]]"
disabled="[[computeOpenDisabled(stateObj, entityObj)]]"
></ha-icon-button>
<ha-icon-button
aria-label="Stop the cover from moving"
icon="hass:stop"
on-click="onStopTap"
invisible$="[[!entityObj.supportsStop]]"
disabled="[[computeStopDisabled(stateObj)]]"
></ha-icon-button>
<ha-icon-button
aria-label="Close cover"
icon="[[computeCloseIcon(stateObj)]]"
on-click="onCloseTap"
invisible$="[[!entityObj.supportsClose]]"
disabled="[[computeClosedDisabled(stateObj, entityObj)]]"
></ha-icon-button>
</div>
`;
}
static get properties() {
return {
hass: {
type: Object,
},
stateObj: {
type: Object,
},
entityObj: {
type: Object,
computed: "computeEntityObj(hass, stateObj)",
},
};
}
computeEntityObj(hass, stateObj) {
return new CoverEntity(hass, stateObj);
}
computeOpenIcon(stateObj) {
switch (stateObj.attributes.device_class) {
case "awning":
case "door":
case "gate":
return "hass:arrow-expand-horizontal";
default:
return "hass:arrow-up";
}
}
computeCloseIcon(stateObj) {
switch (stateObj.attributes.device_class) {
case "awning":
case "door":
case "gate":
return "hass:arrow-collapse-horizontal";
default:
return "hass:arrow-down";
}
}
computeStopDisabled(stateObj) {
if (stateObj.state === UNAVAILABLE) {
return true;
}
return false;
}
computeOpenDisabled(stateObj, entityObj) {
if (stateObj.state === UNAVAILABLE) {
return true;
}
const assumedState = stateObj.attributes.assumed_state === true;
return (entityObj.isFullyOpen || entityObj.isOpening) && !assumedState;
}
computeClosedDisabled(stateObj, entityObj) {
if (stateObj.state === UNAVAILABLE) {
return true;
}
const assumedState = stateObj.attributes.assumed_state === true;
return (entityObj.isFullyClosed || entityObj.isClosing) && !assumedState;
}
onOpenTap(ev) {
ev.stopPropagation();
this.entityObj.openCover();
}
onCloseTap(ev) {
ev.stopPropagation();
this.entityObj.closeCover();
}
onStopTap(ev) {
ev.stopPropagation();
this.entityObj.stopCover();
}
}
customElements.define("ha-cover-controls", HaCoverControls);

View File

@@ -1,135 +0,0 @@
import {
css,
CSSResult,
customElement,
html,
internalProperty,
LitElement,
property,
PropertyValues,
TemplateResult,
} from "lit-element";
import { classMap } from "lit-html/directives/class-map";
import type { HassEntity } from "home-assistant-js-websocket";
import type { HomeAssistant } from "../types";
import { UNAVAILABLE } from "../data/entity";
import CoverEntity from "../util/cover-model";
import "./ha-icon-button";
import { computeCloseIcon, computeOpenIcon } from "../common/entity/cover_icon";
@customElement("ha-cover-controls")
class HaCoverControls extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public stateObj!: HassEntity;
@internalProperty() private _entityObj?: CoverEntity;
protected updated(changedProperties: PropertyValues): void {
super.updated(changedProperties);
if (changedProperties.has("stateObj")) {
this._entityObj = new CoverEntity(this.hass, this.stateObj);
}
}
protected render(): TemplateResult {
if (!this._entityObj) {
return html``;
}
return html`
<div class="state">
<ha-icon-button
class=${classMap({
hidden: !this._entityObj.supportsOpen,
})}
.label=${this.hass.localize(
"ui.dialogs.more_info_control.open_cover"
)}
.icon=${computeOpenIcon(this.stateObj)}
@click=${this._onOpenTap}
.disabled=${this._computeOpenDisabled()}
></ha-icon-button>
<ha-icon-button
class=${classMap({
hidden: !this._entityObj.supportsStop,
})}
.label=${this.hass.localize(
"ui.dialogs.more_info_control.stop_cover"
)}
icon="hass:stop"
@click=${this._onStopTap}
.disabled=${this.stateObj.state === UNAVAILABLE}
></ha-icon-button>
<ha-icon-button
class=${classMap({
hidden: !this._entityObj.supportsClose,
})}
.label=${this.hass.localize(
"ui.dialogs.more_info_control.close_cover"
)}
.icon=${computeCloseIcon(this.stateObj)}
@click=${this._onCloseTap}
.disabled=${this._computeClosedDisabled()}
></ha-icon-button>
</div>
`;
}
private _computeOpenDisabled(): boolean {
if (this.stateObj.state === UNAVAILABLE) {
return true;
}
const assumedState = this.stateObj.attributes.assumed_state === true;
return (
(this._entityObj.isFullyOpen || this._entityObj.isOpening) &&
!assumedState
);
}
private _computeClosedDisabled(): boolean {
if (this.stateObj.state === UNAVAILABLE) {
return true;
}
const assumedState = this.stateObj.attributes.assumed_state === true;
return (
(this._entityObj.isFullyClosed || this._entityObj.isClosing) &&
!assumedState
);
}
private _onOpenTap(ev): void {
ev.stopPropagation();
this._entityObj.openCover();
}
private _onCloseTap(ev): void {
ev.stopPropagation();
this._entityObj.closeCover();
}
private _onStopTap(ev): void {
ev.stopPropagation();
this._entityObj.stopCover();
}
static get styles(): CSSResult {
return css`
.state {
white-space: nowrap;
}
.hidden {
visibility: hidden !important;
}
`;
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-cover-controls": HaCoverControls;
}
}

View File

@@ -0,0 +1,106 @@
import "@polymer/iron-flex-layout/iron-flex-layout-classes";
import "./ha-icon-button";
import { html } from "@polymer/polymer/lib/utils/html-tag";
/* eslint-plugin-disable lit */
import { PolymerElement } from "@polymer/polymer/polymer-element";
import { UNAVAILABLE } from "../data/entity";
import CoverEntity from "../util/cover-model";
class HaCoverTiltControls extends PolymerElement {
static get template() {
return html`
<style include="iron-flex"></style>
<style>
:host {
white-space: nowrap;
}
[invisible] {
visibility: hidden !important;
}
</style>
<ha-icon-button
aria-label="Open cover tilt"
icon="hass:arrow-top-right"
on-click="onOpenTiltTap"
title="Open tilt"
invisible$="[[!entityObj.supportsOpenTilt]]"
disabled="[[computeOpenDisabled(stateObj, entityObj)]]"
></ha-icon-button>
<ha-icon-button
aria-label="Stop cover from moving"
icon="hass:stop"
on-click="onStopTiltTap"
invisible$="[[!entityObj.supportsStopTilt]]"
disabled="[[computeStopDisabled(stateObj)]]"
title="Stop tilt"
></ha-icon-button>
<ha-icon-button
aria-label="Close cover tilt"
icon="hass:arrow-bottom-left"
on-click="onCloseTiltTap"
title="Close tilt"
invisible$="[[!entityObj.supportsCloseTilt]]"
disabled="[[computeClosedDisabled(stateObj, entityObj)]]"
></ha-icon-button>
`;
}
static get properties() {
return {
hass: {
type: Object,
},
stateObj: {
type: Object,
},
entityObj: {
type: Object,
computed: "computeEntityObj(hass, stateObj)",
},
};
}
computeEntityObj(hass, stateObj) {
return new CoverEntity(hass, stateObj);
}
computeStopDisabled(stateObj) {
if (stateObj.state === UNAVAILABLE) {
return true;
}
return false;
}
computeOpenDisabled(stateObj, entityObj) {
if (stateObj.state === UNAVAILABLE) {
return true;
}
const assumedState = stateObj.attributes.assumed_state === true;
return entityObj.isFullyOpenTilt && !assumedState;
}
computeClosedDisabled(stateObj, entityObj) {
if (stateObj.state === UNAVAILABLE) {
return true;
}
const assumedState = stateObj.attributes.assumed_state === true;
return entityObj.isFullyClosedTilt && !assumedState;
}
onOpenTiltTap(ev) {
ev.stopPropagation();
this.entityObj.openCoverTilt();
}
onCloseTiltTap(ev) {
ev.stopPropagation();
this.entityObj.closeCoverTilt();
}
onStopTiltTap(ev) {
ev.stopPropagation();
this.entityObj.stopCoverTilt();
}
}
customElements.define("ha-cover-tilt-controls", HaCoverTiltControls);

View File

@@ -1,122 +0,0 @@
import { HassEntity } from "home-assistant-js-websocket";
import {
LitElement,
property,
internalProperty,
CSSResult,
css,
customElement,
TemplateResult,
html,
PropertyValues,
} from "lit-element";
import { classMap } from "lit-html/directives/class-map";
import { UNAVAILABLE } from "../data/entity";
import { HomeAssistant } from "../types";
import CoverEntity from "../util/cover-model";
import "./ha-icon-button";
@customElement("ha-cover-tilt-controls")
class HaCoverTiltControls extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) stateObj!: HassEntity;
@internalProperty() private _entityObj?: CoverEntity;
protected updated(changedProperties: PropertyValues): void {
super.updated(changedProperties);
if (changedProperties.has("stateObj")) {
this._entityObj = new CoverEntity(this.hass, this.stateObj);
}
}
protected render(): TemplateResult {
if (!this._entityObj) {
return html``;
}
return html` <ha-icon-button
class=${classMap({
invisible: !this._entityObj.supportsStop,
})}
label=${this.hass.localize(
"ui.dialogs.more_info_control.open_tilt_cover"
)}
icon="hass:arrow-top-right"
@click=${this._onOpenTiltTap}
.disabled=${this._computeOpenDisabled()}
></ha-icon-button>
<ha-icon-button
class=${classMap({
invisible: !this._entityObj.supportsStop,
})}
label=${this.hass.localize("ui.dialogs.more_info_control.stop_cover")}
icon="hass:stop"
@click=${this._onStopTiltTap}
.disabled=${this.stateObj.state === UNAVAILABLE}
></ha-icon-button>
<ha-icon-button
class=${classMap({
invisible: !this._entityObj.supportsStop,
})}
label=${this.hass.localize(
"ui.dialogs.more_info_control.open_tilt_cover"
)}
icon="hass:arrow-bottom-left"
@click=${this._onCloseTiltTap}
.disabled=${this._computeClosedDisabled()}
></ha-icon-button>`;
}
private _computeOpenDisabled(): boolean {
if (this.stateObj.state === UNAVAILABLE) {
return true;
}
const assumedState = this.stateObj.attributes.assumed_state === true;
return this._entityObj.isFullyOpenTilt && !assumedState;
}
private _computeClosedDisabled(): boolean {
if (this.stateObj.state === UNAVAILABLE) {
return true;
}
const assumedState = this.stateObj.attributes.assumed_state === true;
return this._entityObj.isFullyClosedTilt && !assumedState;
}
private _onOpenTiltTap(ev): void {
ev.stopPropagation();
this._entityObj.openCoverTilt();
}
private _onCloseTiltTap(ev): void {
ev.stopPropagation();
this._entityObj.closeCoverTilt();
}
private _onStopTiltTap(ev): void {
ev.stopPropagation();
this._entityObj.stopCoverTilt();
}
static get styles(): CSSResult {
return css`
:host {
white-space: nowrap;
}
.invisible {
visibility: hidden !important;
}
`;
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-cover-tilt-controls": HaCoverTiltControls;
}
}

View File

@@ -1,70 +1,127 @@
import "@vaadin/vaadin-date-picker/theme/material/vaadin-date-picker";
import "@polymer/paper-input/paper-input";
import type { PaperInputElement } from "@polymer/paper-input/paper-input";
import {
css,
customElement,
html,
LitElement,
property,
TemplateResult,
} from "lit-element";
const VaadinDatePicker = customElements.get("vaadin-date-picker");
@customElement("ha-date-input")
export class HaDateInput extends LitElement {
@property() public year?: string;
export class HaDateInput extends VaadinDatePicker {
constructor() {
super();
@property() public month?: string;
this.i18n.formatDate = this._formatISODate;
this.i18n.parseDate = this._parseISODate;
}
@property() public day?: string;
ready() {
super.ready();
const styleEl = document.createElement("style");
styleEl.innerHTML = `
@property({ type: Boolean }) public disabled = false;
static get styles() {
return css`
:host {
width: 12ex;
margin-top: -6px;
--material-body-font-size: 16px;
--_material-text-field-input-line-background-color: var(--primary-text-color);
--_material-text-field-input-line-opacity: 1;
--material-primary-color: var(--primary-text-color);
display: block;
font-family: var(--paper-font-common-base_-_font-family);
-webkit-font-smoothing: var(
--paper-font-common-base_-_-webkit-font-smoothing
);
}
paper-input {
width: 30px;
text-align: center;
--paper-input-container-shared-input-style_-_-webkit-appearance: textfield;
--paper-input-container-input_-_-moz-appearance: textfield;
--paper-input-container-shared-input-style_-_appearance: textfield;
--paper-input-container-input-webkit-spinner_-_-webkit-appearance: none;
--paper-input-container-input-webkit-spinner_-_margin: 0;
--paper-input-container-input-webkit-spinner_-_display: none;
}
paper-input#year {
width: 50px;
}
.date-input-wrap {
display: flex;
flex-direction: row;
}
`;
this.shadowRoot.appendChild(styleEl);
this._inputElement.querySelector("[part='toggle-button']").style.display =
"none";
}
private _formatISODate(d) {
return [
("0000" + String(d.year)).slice(-4),
("0" + String(d.month + 1)).slice(-2),
("0" + String(d.day)).slice(-2),
].join("-");
protected render(): TemplateResult {
return html`
<div class="date-input-wrap">
<paper-input
id="year"
type="number"
.value=${this.year}
@change=${this._formatYear}
maxlength="4"
max="9999"
min="0"
.disabled=${this.disabled}
no-label-float
>
<span suffix="" slot="suffix">-</span>
</paper-input>
<paper-input
id="month"
type="number"
.value=${this.month}
@change=${this._formatMonth}
maxlength="2"
max="12"
min="1"
.disabled=${this.disabled}
no-label-float
>
<span suffix="" slot="suffix">-</span>
</paper-input>
<paper-input
id="day"
type="number"
.value=${this.day}
@change=${this._formatDay}
maxlength="2"
max="31"
min="1"
.disabled=${this.disabled}
no-label-float
>
</paper-input>
</div>
`;
}
private _parseISODate(text) {
const parts = text.split("-");
const today = new Date();
let date;
let month = today.getMonth();
let year = today.getFullYear();
if (parts.length === 3) {
year = parseInt(parts[0]);
if (parts[0].length < 3 && year >= 0) {
year += year < 50 ? 2000 : 1900;
}
month = parseInt(parts[1]) - 1;
date = parseInt(parts[2]);
} else if (parts.length === 2) {
month = parseInt(parts[0]) - 1;
date = parseInt(parts[1]);
} else if (parts.length === 1) {
date = parseInt(parts[0]);
}
private _formatYear() {
const yearElement = this.shadowRoot!.getElementById(
"year"
) as PaperInputElement;
this.year = yearElement.value!;
}
if (date !== undefined) {
return { day: date, month, year };
}
return undefined;
private _formatMonth() {
const monthElement = this.shadowRoot!.getElementById(
"month"
) as PaperInputElement;
this.month = ("0" + monthElement.value!).slice(-2);
}
private _formatDay() {
const dayElement = this.shadowRoot!.getElementById(
"day"
) as PaperInputElement;
this.day = ("0" + dayElement.value!).slice(-2);
}
get value() {
return `${this.year}-${this.month}-${this.day}`;
}
}
customElements.define("ha-date-input", HaDateInput as any);
declare global {
interface HTMLElementTagNameMap {
"ha-date-input": HaDateInput;

View File

@@ -40,7 +40,6 @@ export class HaDialog extends MwcDialog {
.mdc-dialog {
--mdc-dialog-scroll-divider-color: var(--divider-color);
z-index: var(--dialog-z-index, 7);
backdrop-filter: var(--dialog-backdrop-filter, none);
}
.mdc-dialog__actions {
justify-content: var(--justify-action-buttons, flex-end);

View File

@@ -1,119 +0,0 @@
import {
css,
CSSResult,
customElement,
html,
LitElement,
property,
query,
TemplateResult,
} from "lit-element";
import { fireEvent } from "../common/dom/fire_event";
import "./ha-svg-icon";
import { mdiChevronDown } from "@mdi/js";
import { classMap } from "lit-html/directives/class-map";
@customElement("ha-expansion-panel")
class HaExpansionPanel extends LitElement {
@property({ type: Boolean, reflect: true }) expanded = false;
@property({ type: Boolean, reflect: true }) outlined = false;
@query(".container") private _container!: HTMLDivElement;
protected render(): TemplateResult {
return html`
<div class="summary" @click=${this._toggleContainer}>
<slot name="title"></slot>
<ha-svg-icon
.path=${mdiChevronDown}
class="summary-icon ${classMap({ expanded: this.expanded })}"
></ha-svg-icon>
</div>
<div
class="container ${classMap({ expanded: this.expanded })}"
@transitionend=${this._handleTransitionEnd}
>
<slot></slot>
</div>
`;
}
private _handleTransitionEnd() {
this._container.style.removeProperty("height");
}
private _toggleContainer(): void {
const scrollHeight = this._container.scrollHeight;
this._container.style.height = `${scrollHeight}px`;
if (this.expanded) {
setTimeout(() => {
this._container.style.height = "0px";
}, 0);
}
this.expanded = !this.expanded;
fireEvent(this, "expanded-changed", { expanded: this.expanded });
}
static get styles(): CSSResult {
return css`
:host {
display: block;
}
:host([outlined]) {
box-shadow: none;
border-width: 1px;
border-style: solid;
border-color: var(
--ha-card-border-color,
var(--divider-color, #e0e0e0)
);
border-radius: var(--ha-card-border-radius, 4px);
}
.summary {
display: flex;
padding: 0px 16px;
min-height: 48px;
align-items: center;
cursor: pointer;
overflow: hidden;
}
.summary-icon {
transition: transform 150ms cubic-bezier(0.4, 0, 0.2, 1);
margin-left: auto;
}
.summary-icon.expanded {
transform: rotate(180deg);
}
.container {
overflow: hidden;
transition: height 300ms cubic-bezier(0.4, 0, 0.2, 1);
height: 0px;
}
.container.expanded {
height: auto;
}
`;
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-expansion-panel": HaExpansionPanel;
}
// for fire event
interface HASSDomEvents {
"expanded-changed": {
expanded: boolean;
};
}
}

View File

@@ -38,8 +38,7 @@ class HaHLSPlayer extends LitElement {
@property({ type: Boolean, attribute: "allow-exoplayer" })
public allowExoPlayer = false;
// don't cache this, as we remove it on disconnects
@query("video") private _videoEl!: HTMLVideoElement;
@query("video", true) private _videoEl!: HTMLVideoElement;
@internalProperty() private _attached = false;
@@ -104,6 +103,7 @@ class HaHLSPlayer extends LitElement {
private async _startHls(): Promise<void> {
const videoEl = this._videoEl;
const playlist_url = this.url.replace("master_playlist", "playlist");
const useExoPlayerPromise = this._getUseExoPlayer();
const masterPlaylistPromise = fetch(this.url);
@@ -125,30 +125,13 @@ class HaHLSPlayer extends LitElement {
}
this._useExoPlayer = await useExoPlayerPromise;
const masterPlaylist = await (await masterPlaylistPromise).text();
// Parse playlist assuming it is a master playlist. Match group 1 is whether hevc, match group 2 is regular playlist url
// See https://tools.ietf.org/html/rfc8216 for HLS spec details
const playlistRegexp = /#EXT-X-STREAM-INF:.*?(?:CODECS=".*?(?<isHevc>hev1|hvc1)?\..*?".*?)?(?:\n|\r\n)(?<streamUrl>.+)/g;
const match = playlistRegexp.exec(masterPlaylist);
const matchTwice = playlistRegexp.exec(masterPlaylist);
// Get the regular playlist url from the input (master) playlist, falling back to the input playlist if necessary
// This avoids the player having to load and parse the master playlist again before loading the regular playlist
let playlist_url: string;
if (match !== null && matchTwice === null) {
// Only send the regular playlist url if we match exactly once
playlist_url = new URL(match.groups!.streamUrl, this.url).href;
} else {
playlist_url = this.url;
let hevcRegexp: RegExp;
let masterPlaylist: string;
if (this._useExoPlayer) {
hevcRegexp = /CODECS=".*?((hev1)|(hvc1))\..*?"/;
masterPlaylist = await (await masterPlaylistPromise).text();
}
// If codec is HEVC and ExoPlayer is supported, use ExoPlayer.
if (
this._useExoPlayer &&
match !== null &&
match.groups!.isHevc !== undefined
) {
if (this._useExoPlayer && hevcRegexp!.test(masterPlaylist!)) {
this._renderHLSExoPlayer(playlist_url);
} else if (hls.isSupported()) {
this._renderHLSPolyfill(videoEl, hls, playlist_url);
@@ -171,9 +154,6 @@ class HaHLSPlayer extends LitElement {
}
private _resizeExoPlayer = () => {
if (!this._videoEl) {
return;
}
const rect = this._videoEl.getBoundingClientRect();
this.hass!.auth.external!.fireMessage({
type: "exoplayer/resize",

View File

@@ -9,16 +9,11 @@ import {
import { mdiArrowLeft, mdiArrowRight } from "@mdi/js";
import "@material/mwc-icon-button/mwc-icon-button";
import "./ha-svg-icon";
import { HomeAssistant } from "../types";
@customElement("ha-icon-button-arrow-next")
export class HaIconButtonArrowNext extends LitElement {
@property({ attribute: false }) public hass?: HomeAssistant;
@property({ type: Boolean }) public disabled = false;
@property() public label?: string;
@internalProperty() private _icon = mdiArrowRight;
public connectedCallback() {
@@ -34,10 +29,7 @@ export class HaIconButtonArrowNext extends LitElement {
}
protected render(): TemplateResult {
return html`<mwc-icon-button
.disabled=${this.disabled}
.label=${this.label || this.hass?.localize("ui.common.next") || "Next"}
>
return html`<mwc-icon-button .disabled=${this.disabled}>
<ha-svg-icon .path=${this._icon}></ha-svg-icon>
</mwc-icon-button> `;
}

View File

@@ -9,16 +9,11 @@ import {
import { mdiArrowLeft, mdiArrowRight } from "@mdi/js";
import "@material/mwc-icon-button/mwc-icon-button";
import "./ha-svg-icon";
import { HomeAssistant } from "../types";
@customElement("ha-icon-button-arrow-prev")
export class HaIconButtonArrowPrev extends LitElement {
@property({ attribute: false }) public hass?: HomeAssistant;
@property({ type: Boolean }) public disabled = false;
@property() public label?: string;
@internalProperty() private _icon = mdiArrowLeft;
public connectedCallback() {
@@ -34,14 +29,9 @@ export class HaIconButtonArrowPrev extends LitElement {
}
protected render(): TemplateResult {
return html`
<mwc-icon-button
.disabled=${this.disabled}
.label=${this.label || this.hass?.localize("ui.common.back") || "Back"}
>
<ha-svg-icon .path=${this._icon}></ha-svg-icon>
</mwc-icon-button>
`;
return html`<mwc-icon-button .disabled=${this.disabled}>
<ha-svg-icon .path=${this._icon}></ha-svg-icon>
</mwc-icon-button> `;
}
}

View File

@@ -9,16 +9,11 @@ import {
import { mdiChevronRight, mdiChevronLeft } from "@mdi/js";
import "@material/mwc-icon-button";
import "./ha-svg-icon";
import { HomeAssistant } from "../types";
@customElement("ha-icon-button-next")
export class HaIconButtonNext extends LitElement {
@property({ attribute: false }) public hass?: HomeAssistant;
@property({ type: Boolean }) public disabled = false;
@property() public label?: string;
@internalProperty() private _icon = mdiChevronRight;
public connectedCallback() {
@@ -34,14 +29,9 @@ export class HaIconButtonNext extends LitElement {
}
protected render(): TemplateResult {
return html`
<mwc-icon-button
.disabled=${this.disabled}
.label=${this.label || this.hass?.localize("ui.common.next") || "Next"}
>
<ha-svg-icon .path=${this._icon}></ha-svg-icon>
</mwc-icon-button>
`;
return html`<mwc-icon-button .disabled=${this.disabled}>
<ha-svg-icon .path=${this._icon}></ha-svg-icon>
</mwc-icon-button> `;
}
}

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