mirror of
https://github.com/home-assistant/frontend.git
synced 2025-08-31 20:10:27 +00:00
Compare commits
131 Commits
20191119.6
...
remove-lig
Author | SHA1 | Date | |
---|---|---|---|
![]() |
c1eabeb29f | ||
![]() |
5ff8fe68ba | ||
![]() |
a2a039ebc5 | ||
![]() |
1064aed1b0 | ||
![]() |
7025592e8e | ||
![]() |
4966354b62 | ||
![]() |
68d6faf4af | ||
![]() |
e3346483b9 | ||
![]() |
e8fb79e5ce | ||
![]() |
d612162ab1 | ||
![]() |
86f8ef3a70 | ||
![]() |
0e43435362 | ||
![]() |
aaefe0b09f | ||
![]() |
bc731a9dc3 | ||
![]() |
da25701dca | ||
![]() |
21ae483dc9 | ||
![]() |
38b6e9ca10 | ||
![]() |
d31245866c | ||
![]() |
4e08d8f3b3 | ||
![]() |
1e717ab33e | ||
![]() |
995fb4974e | ||
![]() |
ffb76132f8 | ||
![]() |
acba3af54b | ||
![]() |
40ac456937 | ||
![]() |
5c32413bf7 | ||
![]() |
22792c70c5 | ||
![]() |
a8ed87298a | ||
![]() |
b15270dfe2 | ||
![]() |
58ad949bc8 | ||
![]() |
adce40de56 | ||
![]() |
0f487ae4bf | ||
![]() |
2848e3a63b | ||
![]() |
5a172a64c5 | ||
![]() |
433aa16ea6 | ||
![]() |
50cb8cf3cc | ||
![]() |
4e5406b27b | ||
![]() |
80eb80619a | ||
![]() |
bf71b3a869 | ||
![]() |
ff270c4b7d | ||
![]() |
5415068917 | ||
![]() |
357a67c00d | ||
![]() |
cbe4269320 | ||
![]() |
fbd5185ce2 | ||
![]() |
a33cf97e2c | ||
![]() |
7e7da26543 | ||
![]() |
79058e893b | ||
![]() |
2eb548bb74 | ||
![]() |
08baf8a757 | ||
![]() |
f02fa6a94b | ||
![]() |
2ed6d0e73c | ||
![]() |
35d9b2ac3c | ||
![]() |
18d09c6f04 | ||
![]() |
70b81de49d | ||
![]() |
f0808c1f54 | ||
![]() |
e779f0747e | ||
![]() |
bdd18775c3 | ||
![]() |
711d51c022 | ||
![]() |
1b0d8bba29 | ||
![]() |
2988cc512f | ||
![]() |
a2f8e5f3e7 | ||
![]() |
680bf06a4b | ||
![]() |
ff0b1881e2 | ||
![]() |
de653e1f7b | ||
![]() |
bb41170765 | ||
![]() |
0ed2bc93aa | ||
![]() |
04770f8ee2 | ||
![]() |
15a2790b9f | ||
![]() |
83880791b1 | ||
![]() |
4dca3289f6 | ||
![]() |
083a3ebfc4 | ||
![]() |
6117c4e989 | ||
![]() |
609763e658 | ||
![]() |
2c57ab60f1 | ||
![]() |
dd17a153d2 | ||
![]() |
c2d551bb7c | ||
![]() |
e0b1921108 | ||
![]() |
fcf39ceb96 | ||
![]() |
3cc979a077 | ||
![]() |
9972973774 | ||
![]() |
20ae32bc26 | ||
![]() |
a29892023b | ||
![]() |
b283fec482 | ||
![]() |
e0116a8236 | ||
![]() |
d1990a4bac | ||
![]() |
cbba1849e2 | ||
![]() |
43393d1647 | ||
![]() |
b47ee1051c | ||
![]() |
393adacc9e | ||
![]() |
073428849e | ||
![]() |
e6ac0258e3 | ||
![]() |
d7e7798a55 | ||
![]() |
2557414b11 | ||
![]() |
f7065fbce9 | ||
![]() |
016564eee9 | ||
![]() |
ff3087c39c | ||
![]() |
239438ee5d | ||
![]() |
5458cda31f | ||
![]() |
36f49e66fd | ||
![]() |
2bafd38ea8 | ||
![]() |
73b3262491 | ||
![]() |
808cde033f | ||
![]() |
fa8f6b7b91 | ||
![]() |
94c120cdb1 | ||
![]() |
7b2be54f8f | ||
![]() |
4b56db5255 | ||
![]() |
93165c9111 | ||
![]() |
caa604d5ca | ||
![]() |
e7e9e2cf85 | ||
![]() |
daa04e9973 | ||
![]() |
5355269f5d | ||
![]() |
2665a75250 | ||
![]() |
8a39d18323 | ||
![]() |
b8a026397b | ||
![]() |
bd5fe302eb | ||
![]() |
de0f1b2b65 | ||
![]() |
defaa2b276 | ||
![]() |
60efe00a1f | ||
![]() |
fe93b993db | ||
![]() |
f6afc92d3c | ||
![]() |
e4c635c855 | ||
![]() |
8ef15c50b4 | ||
![]() |
81588469b8 | ||
![]() |
70a920af3c | ||
![]() |
1329e60c89 | ||
![]() |
ea9e8cc392 | ||
![]() |
0acd41b7f0 | ||
![]() |
85ca73db84 | ||
![]() |
444cbd00d9 | ||
![]() |
6edf23b91f | ||
![]() |
1249c0eea9 | ||
![]() |
3133118870 |
27
.github/ISSUE_TEMPLATE/bug_report.md
vendored
27
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@@ -41,7 +41,32 @@ Provide details about what browser (and version) you are seeing the issue in. An
|
||||
**Description of problem:**
|
||||
|
||||
<!--
|
||||
Explain what the issue is, and how things should look/behave. If possible provide a screenshot with a description.
|
||||
Explain what the issue is, and what is the current behaviour. If possible provide a screenshot with a description.
|
||||
-->
|
||||
|
||||
**Expected behaviour:**
|
||||
|
||||
<!--
|
||||
Explain how things should look/behave. If possible provide a screenshot with a description.
|
||||
-->
|
||||
|
||||
**Relevant config:**
|
||||
|
||||
<!--
|
||||
Give the config of both the integration that is used, the Lovelace config, scene, automation or otherwise relevant configuration.
|
||||
-->
|
||||
|
||||
**Steps to reproduce this problem:**
|
||||
|
||||
<!--
|
||||
Sum up all steps that are necessary to reproduce this bug.
|
||||
For example:
|
||||
1. Add a climate integration
|
||||
2. Navigate to Lovelace
|
||||
3. Click more info of the climate entity
|
||||
4. Set the hvac action to heat
|
||||
5. Set the temperature higher than the current temperature
|
||||
6. Set the hvac action to cool
|
||||
-->
|
||||
|
||||
**Javascript errors shown in the web inspector (if applicable):**
|
||||
|
27
.github/lock.yml
vendored
Normal file
27
.github/lock.yml
vendored
Normal 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
|
@@ -13,15 +13,6 @@ script:
|
||||
- npm run test
|
||||
# - xvfb-run wct --module-resolution=node --npm
|
||||
# - 'if [ "${TRAVIS_PULL_REQUEST}" = "false" ]; then wct --module-resolution=node --npm --plugin sauce; fi'
|
||||
services:
|
||||
- docker
|
||||
before_deploy:
|
||||
- "docker pull lokalise/lokalise-cli@sha256:2198814ebddfda56ee041a4b427521757dd57f75415ea9693696a64c550cef21"
|
||||
deploy:
|
||||
provider: script
|
||||
script: script/travis_deploy
|
||||
"on":
|
||||
branch: master
|
||||
dist: trusty
|
||||
addons:
|
||||
sauce_connect: true
|
||||
|
@@ -2,9 +2,9 @@
|
||||
|
||||
This is the repository for the official [Home Assistant](https://home-assistant.io) frontend.
|
||||
|
||||
[](https://home-assistant.io/demo/)
|
||||
[](https://demo.home-assistant.io/)
|
||||
|
||||
- [View demo of the Polymer frontend](https://home-assistant.io/demo/)
|
||||
- [View demo of Home Assistant](https://demo.home-assistant.io/)
|
||||
- [More information about Home Assistant](https://home-assistant.io)
|
||||
- [Frontend development instructions](https://developers.home-assistant.io/docs/en/frontend_index.html)
|
||||
|
||||
@@ -31,3 +31,5 @@ It is possible to compile the project and/or run commands in the development env
|
||||
## License
|
||||
|
||||
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 variation of devices.
|
||||
|
70
azure-pipelines-translation.yml
Normal file
70
azure-pipelines-translation.yml
Normal file
@@ -0,0 +1,70 @@
|
||||
# https://dev.azure.com/home-assistant
|
||||
|
||||
trigger:
|
||||
batch: true
|
||||
branches:
|
||||
include:
|
||||
- dev
|
||||
paths:
|
||||
include:
|
||||
- translations/en.json
|
||||
pr: none
|
||||
schedules:
|
||||
- cron: "30 0 * * *"
|
||||
displayName: "translation update"
|
||||
branches:
|
||||
include:
|
||||
- dev
|
||||
always: true
|
||||
variables:
|
||||
- group: translation
|
||||
resources:
|
||||
repositories:
|
||||
- repository: azure
|
||||
type: github
|
||||
name: 'home-assistant/ci-azure'
|
||||
endpoint: 'home-assistant'
|
||||
|
||||
|
||||
jobs:
|
||||
|
||||
- job: 'Upload'
|
||||
pool:
|
||||
vmImage: 'ubuntu-latest'
|
||||
steps:
|
||||
- task: NodeTool@0
|
||||
displayName: 'Use Node 12.x'
|
||||
inputs:
|
||||
versionSpec: '12.x'
|
||||
- script: |
|
||||
export LOKALISE_TOKEN="$(lokaliseToken)"
|
||||
export AZURE_BRANCH="$(Build.SourceBranchName)"
|
||||
|
||||
./script/translations_upload_base
|
||||
displayName: 'Upload Translation'
|
||||
|
||||
- job: 'Download'
|
||||
dependsOn:
|
||||
- 'Upload'
|
||||
condition: or(eq(variables['Build.Reason'], 'Schedule'), eq(variables['Build.Reason'], 'Manual'))
|
||||
pool:
|
||||
vmImage: 'ubuntu-latest'
|
||||
steps:
|
||||
- task: NodeTool@0
|
||||
displayName: 'Use Node 12.x'
|
||||
inputs:
|
||||
versionSpec: '12.x'
|
||||
- template: templates/azp-step-git-init.yaml@azure
|
||||
- script: |
|
||||
export LOKALISE_TOKEN="$(lokaliseToken)"
|
||||
export AZURE_BRANCH="$(Build.SourceBranchName)"
|
||||
|
||||
npm install
|
||||
./script/translations_download
|
||||
displayName: 'Download Translation'
|
||||
- script: |
|
||||
git checkout dev
|
||||
git add translation
|
||||
git commit -am "[ci skip] Translation update"
|
||||
git push
|
||||
displayName: 'Update translation'
|
@@ -33,6 +33,7 @@ module.exports.babelLoaderConfig = ({ latestBuild }) => {
|
||||
pragma: "h",
|
||||
},
|
||||
],
|
||||
"@babel/plugin-proposal-optional-chaining",
|
||||
[
|
||||
require("@babel/plugin-proposal-decorators").default,
|
||||
{ decoratorsBeforeExport: true },
|
||||
|
@@ -91,7 +91,7 @@ const createWebpackConfig = ({
|
||||
),
|
||||
].filter(Boolean),
|
||||
resolve: {
|
||||
extensions: [".ts", ".js", ".json", ".tsx"],
|
||||
extensions: [".ts", ".js", ".json"],
|
||||
alias: {
|
||||
react: "preact-compat",
|
||||
"react-dom": "preact-compat",
|
||||
|
@@ -39,6 +39,7 @@ class HcLovelace extends LitElement {
|
||||
mode: "storage",
|
||||
language: "en",
|
||||
saveConfig: async () => undefined,
|
||||
deleteConfig: async () => undefined,
|
||||
setEditMode: () => undefined,
|
||||
};
|
||||
return this.lovelaceConfig.views[index].panel
|
||||
|
@@ -175,9 +175,9 @@ export class HcMain extends HassElement {
|
||||
} catch (err) {
|
||||
// Generate a Lovelace config.
|
||||
this._unsubLovelace = () => undefined;
|
||||
const {
|
||||
generateLovelaceConfigFromHass,
|
||||
} = await import("../../../../src/panels/lovelace/common/generate-lovelace-config");
|
||||
const { generateLovelaceConfigFromHass } = await import(
|
||||
"../../../../src/panels/lovelace/common/generate-lovelace-config"
|
||||
);
|
||||
this._handleNewLovelaceConfig(
|
||||
await generateLovelaceConfigFromHass(this.hass!)
|
||||
);
|
||||
|
@@ -53,7 +53,7 @@ class CardModder extends LitElement {
|
||||
for (var k in this._config.style) {
|
||||
if (window.cardTools.hasTemplate(this._config.style[k]))
|
||||
this.templated.push(k);
|
||||
this.card.style.setProperty(k, '');
|
||||
this.card.style.setProperty(k, "");
|
||||
target.style.setProperty(
|
||||
k,
|
||||
window.cardTools.parseTemplate(this._config.style[k])
|
||||
|
@@ -12,5 +12,7 @@ import "./resources/hademo-icons";
|
||||
|
||||
/* polyfill for paper-dropdown */
|
||||
setTimeout(() => {
|
||||
import(/* webpackChunkName: "polyfill-web-animations-next" */ "web-animations-js/web-animations-next-lite.min");
|
||||
import(
|
||||
/* webpackChunkName: "polyfill-web-animations-next" */ "web-animations-js/web-animations-next-lite.min"
|
||||
);
|
||||
}, 1000);
|
||||
|
@@ -65,74 +65,79 @@ const generateHistory = (state, deltas) => {
|
||||
const incrementalUnits = ["clients", "queries", "ads"];
|
||||
|
||||
export const mockHistory = (mockHass: MockHomeAssistant) => {
|
||||
mockHass.mockAPI(new RegExp("history/period/.+"), (
|
||||
hass,
|
||||
// @ts-ignore
|
||||
method,
|
||||
path,
|
||||
// @ts-ignore
|
||||
parameters
|
||||
) => {
|
||||
const params = parseQuery<HistoryQueryParams>(path.split("?")[1]);
|
||||
const entities = params.filter_entity_id.split(",");
|
||||
mockHass.mockAPI(
|
||||
new RegExp("history/period/.+"),
|
||||
(
|
||||
hass,
|
||||
// @ts-ignore
|
||||
method,
|
||||
path,
|
||||
// @ts-ignore
|
||||
parameters
|
||||
) => {
|
||||
const params = parseQuery<HistoryQueryParams>(path.split("?")[1]);
|
||||
const entities = params.filter_entity_id.split(",");
|
||||
|
||||
const results: HassEntity[][] = [];
|
||||
const results: HassEntity[][] = [];
|
||||
|
||||
for (const entityId of entities) {
|
||||
const state = hass.states[entityId];
|
||||
for (const entityId of entities) {
|
||||
const state = hass.states[entityId];
|
||||
|
||||
if (!state) {
|
||||
continue;
|
||||
}
|
||||
if (!state) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!state.attributes.unit_of_measurement) {
|
||||
results.push(generateHistory(state, [state.state]));
|
||||
continue;
|
||||
}
|
||||
if (!state.attributes.unit_of_measurement) {
|
||||
results.push(generateHistory(state, [state.state]));
|
||||
continue;
|
||||
}
|
||||
|
||||
const numberState = Number(state.state);
|
||||
const numberState = Number(state.state);
|
||||
|
||||
if (isNaN(numberState)) {
|
||||
// tslint:disable-next-line
|
||||
console.log(
|
||||
"Ignoring state with unparsable state but with a unit",
|
||||
entityId,
|
||||
state
|
||||
if (isNaN(numberState)) {
|
||||
// tslint:disable-next-line
|
||||
console.log(
|
||||
"Ignoring state with unparsable state but with a unit",
|
||||
entityId,
|
||||
state
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
const statesToGenerate = 15;
|
||||
let genFunc;
|
||||
|
||||
if (incrementalUnits.includes(state.attributes.unit_of_measurement)) {
|
||||
let initial = Math.floor(
|
||||
numberState * 0.4 + numberState * Math.random() * 0.2
|
||||
);
|
||||
const diff = Math.max(
|
||||
1,
|
||||
Math.floor((numberState - initial) / statesToGenerate)
|
||||
);
|
||||
genFunc = () => {
|
||||
initial += diff;
|
||||
return Math.min(numberState, initial);
|
||||
};
|
||||
} else {
|
||||
const diff = Math.floor(
|
||||
numberState * (numberState > 80 ? 0.05 : 0.5)
|
||||
);
|
||||
genFunc = () =>
|
||||
numberState - diff + Math.floor(Math.random() * 2 * diff);
|
||||
}
|
||||
|
||||
results.push(
|
||||
generateHistory(
|
||||
{
|
||||
entity_id: state.entity_id,
|
||||
attributes: state.attributes,
|
||||
},
|
||||
Array.from({ length: statesToGenerate }, genFunc)
|
||||
)
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
const statesToGenerate = 15;
|
||||
let genFunc;
|
||||
|
||||
if (incrementalUnits.includes(state.attributes.unit_of_measurement)) {
|
||||
let initial = Math.floor(
|
||||
numberState * 0.4 + numberState * Math.random() * 0.2
|
||||
);
|
||||
const diff = Math.max(
|
||||
1,
|
||||
Math.floor((numberState - initial) / statesToGenerate)
|
||||
);
|
||||
genFunc = () => {
|
||||
initial += diff;
|
||||
return Math.min(numberState, initial);
|
||||
};
|
||||
} else {
|
||||
const diff = Math.floor(numberState * (numberState > 80 ? 0.05 : 0.5));
|
||||
genFunc = () =>
|
||||
numberState - diff + Math.floor(Math.random() * 2 * diff);
|
||||
}
|
||||
|
||||
results.push(
|
||||
generateHistory(
|
||||
{
|
||||
entity_id: state.entity_id,
|
||||
attributes: state.attributes,
|
||||
},
|
||||
Array.from({ length: statesToGenerate }, genFunc)
|
||||
)
|
||||
);
|
||||
return results;
|
||||
}
|
||||
return results;
|
||||
});
|
||||
);
|
||||
};
|
||||
|
@@ -12,9 +12,10 @@ export const mockLovelace = (
|
||||
localizePromise: Promise<LocalizeFunc>
|
||||
) => {
|
||||
hass.mockWS("lovelace/config", () =>
|
||||
Promise.all([selectedDemoConfig, localizePromise]).then(
|
||||
([config, localize]) => config.lovelace(localize)
|
||||
)
|
||||
Promise.all([
|
||||
selectedDemoConfig,
|
||||
localizePromise,
|
||||
]).then(([config, localize]) => config.lovelace(localize))
|
||||
);
|
||||
|
||||
hass.mockWS("lovelace/config/save", () => Promise.resolve());
|
||||
|
@@ -44,9 +44,7 @@ class HassioAddonAudio extends EventsMixin(PolymerElement) {
|
||||
selected="{{selectedInput}}"
|
||||
>
|
||||
<template is="dom-repeat" items="[[inputDevices]]">
|
||||
<paper-item device\$="[[item.device]]"
|
||||
>[[item.name]]</paper-item
|
||||
>
|
||||
<paper-item device$="[[item.device]]">[[item.name]]</paper-item>
|
||||
</template>
|
||||
</paper-listbox>
|
||||
</paper-dropdown-menu>
|
||||
@@ -57,9 +55,7 @@ class HassioAddonAudio extends EventsMixin(PolymerElement) {
|
||||
selected="{{selectedOutput}}"
|
||||
>
|
||||
<template is="dom-repeat" items="[[outputDevices]]">
|
||||
<paper-item device\$="[[item.device]]"
|
||||
>[[item.name]]</paper-item
|
||||
>
|
||||
<paper-item device$="[[item.device]]">[[item.name]]</paper-item>
|
||||
</template>
|
||||
</paper-listbox>
|
||||
</paper-dropdown-menu>
|
||||
|
@@ -373,19 +373,21 @@ class HassioAddonInfo extends EventsMixin(PolymerElement) {
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
<div class="state">
|
||||
<div>
|
||||
Protection mode
|
||||
<span>
|
||||
<iron-icon icon="hassio:information"></iron-icon>
|
||||
<paper-tooltip>Grant the add-on elevated system access.</paper-tooltip>
|
||||
</span>
|
||||
<template is="dom-if" if="[[_computeUsesProtectedOptions(addon)]]">
|
||||
<div class="state">
|
||||
<div>
|
||||
Protection mode
|
||||
<span>
|
||||
<iron-icon icon="hassio:information"></iron-icon>
|
||||
<paper-tooltip>Grant the add-on elevated system access.</paper-tooltip>
|
||||
</span>
|
||||
</div>
|
||||
<ha-switch
|
||||
on-change="protectionToggled"
|
||||
checked="[[addon.protected]]"
|
||||
></ha-switch>
|
||||
</div>
|
||||
<ha-switch
|
||||
on-change="protectionToggled"
|
||||
checked="[[addon.protected]]"
|
||||
></ha-switch>
|
||||
</div>
|
||||
</template>
|
||||
</template>
|
||||
</div>
|
||||
<div class="card-actions">
|
||||
@@ -569,7 +571,10 @@ class HassioAddonInfo extends EventsMixin(PolymerElement) {
|
||||
openChangelog() {
|
||||
this.hass
|
||||
.callApi("get", `hassio/addons/${this.addonSlug}/changelog`)
|
||||
.then((resp) => resp, () => "Error getting changelog")
|
||||
.then(
|
||||
(resp) => resp,
|
||||
() => "Error getting changelog"
|
||||
)
|
||||
.then((content) => {
|
||||
showHassioMarkdownDialog(this, {
|
||||
title: "Changelog",
|
||||
@@ -607,6 +612,10 @@ class HassioAddonInfo extends EventsMixin(PolymerElement) {
|
||||
return !addon.ingress || !this._computeHA92plus(hass);
|
||||
}
|
||||
|
||||
_computeUsesProtectedOptions(addon) {
|
||||
return addon.docker_api || addon.full_access || addon.host_pid;
|
||||
}
|
||||
|
||||
_computeHA92plus(hass) {
|
||||
const [major, minor] = hass.config.version.split(".", 2);
|
||||
return Number(major) > 0 || (major === "0" && Number(minor) >= 92);
|
||||
|
@@ -74,9 +74,7 @@ export class HassioUpdate extends LitElement {
|
||||
this.supervisorInfo.version,
|
||||
this.supervisorInfo.last_version,
|
||||
"hassio/supervisor/update",
|
||||
`https://github.com//home-assistant/hassio/releases/tag/${
|
||||
this.supervisorInfo.last_version
|
||||
}`
|
||||
`https://github.com//home-assistant/hassio/releases/tag/${this.supervisorInfo.last_version}`
|
||||
)}
|
||||
${this.hassOsInfo
|
||||
? this._renderUpdateCard(
|
||||
@@ -84,9 +82,7 @@ export class HassioUpdate extends LitElement {
|
||||
this.hassOsInfo.version,
|
||||
this.hassOsInfo.version_latest,
|
||||
"hassio/hassos/update",
|
||||
`https://github.com//home-assistant/hassos/releases/tag/${
|
||||
this.hassOsInfo.version_latest
|
||||
}`
|
||||
`https://github.com//home-assistant/hassos/releases/tag/${this.hassOsInfo.version_latest}`
|
||||
)
|
||||
: ""}
|
||||
</div>
|
||||
|
@@ -12,7 +12,9 @@ export const showHassioMarkdownDialog = (
|
||||
fireEvent(element, "show-dialog", {
|
||||
dialogTag: "dialog-hassio-markdown",
|
||||
dialogImport: () =>
|
||||
import(/* webpackChunkName: "dialog-hassio-markdown" */ "./dialog-hassio-markdown"),
|
||||
import(
|
||||
/* webpackChunkName: "dialog-hassio-markdown" */ "./dialog-hassio-markdown"
|
||||
),
|
||||
dialogParams,
|
||||
});
|
||||
};
|
||||
|
@@ -12,7 +12,9 @@ export const showHassioSnapshotDialog = (
|
||||
fireEvent(element, "show-dialog", {
|
||||
dialogTag: "dialog-hassio-snapshot",
|
||||
dialogImport: () =>
|
||||
import(/* webpackChunkName: "dialog-hassio-snapshot" */ "./dialog-hassio-snapshot"),
|
||||
import(
|
||||
/* webpackChunkName: "dialog-hassio-snapshot" */ "./dialog-hassio-snapshot"
|
||||
),
|
||||
dialogParams,
|
||||
});
|
||||
};
|
||||
|
@@ -27,6 +27,7 @@ import { makeDialogManager } from "../../src/dialogs/make-dialog-manager";
|
||||
import { ProvideHassLitMixin } from "../../src/mixins/provide-hass-lit-mixin";
|
||||
// Don't codesplit it, that way the dashboard always loads fast.
|
||||
import "./hassio-pages-with-tabs";
|
||||
import { navigate } from "../../src/common/navigate";
|
||||
|
||||
// The register callback of the IronA11yKeysBehavior inside paper-icon-button
|
||||
// is not called, causing _keyBindings to be uninitiliazed for paper-icon-button,
|
||||
@@ -56,12 +57,16 @@ class HassioMain extends ProvideHassLitMixin(HassRouterPage) {
|
||||
addon: {
|
||||
tag: "hassio-addon-view",
|
||||
load: () =>
|
||||
import(/* webpackChunkName: "hassio-addon-view" */ "./addon-view/hassio-addon-view"),
|
||||
import(
|
||||
/* webpackChunkName: "hassio-addon-view" */ "./addon-view/hassio-addon-view"
|
||||
),
|
||||
},
|
||||
ingress: {
|
||||
tag: "hassio-ingress-view",
|
||||
load: () =>
|
||||
import(/* webpackChunkName: "hassio-ingress-view" */ "./ingress-view/hassio-ingress-view"),
|
||||
import(
|
||||
/* webpackChunkName: "hassio-ingress-view" */ "./ingress-view/hassio-ingress-view"
|
||||
),
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -161,14 +166,20 @@ class HassioMain extends ProvideHassLitMixin(HassRouterPage) {
|
||||
}),
|
||||
]);
|
||||
if (!addon.ingress_url) {
|
||||
throw new Error("Add-on does not support Ingress");
|
||||
alert("Add-on does not support Ingress");
|
||||
return;
|
||||
}
|
||||
if (addon.state !== "started") {
|
||||
alert("Add-on is not running. Please start it first");
|
||||
navigate(this, `/hassio/addon/${addon.slug}`, true);
|
||||
return;
|
||||
}
|
||||
location.assign(addon.ingress_url);
|
||||
// await a promise that doesn't resolve, so we show the loading screen
|
||||
// while we load the next page.
|
||||
await new Promise(() => undefined);
|
||||
} catch (err) {
|
||||
alert(`Unable to open ingress connection `);
|
||||
alert("Unable to open ingress connection");
|
||||
}
|
||||
}
|
||||
|
||||
|
48
package.json
48
package.json
@@ -8,7 +8,7 @@
|
||||
"version": "1.0.0",
|
||||
"scripts": {
|
||||
"build": "script/build_frontend",
|
||||
"lint": "eslint src hassio/src gallery/src && tslint 'src/**/*.ts' 'src/**/*.tsx' 'hassio/src/**/*.ts' 'gallery/src/**/*.ts' 'cast/src/**/*.ts' 'test-mocha/**/*.ts' && tsc",
|
||||
"lint": "eslint src hassio/src gallery/src && tslint 'src/**/*.ts' 'hassio/src/**/*.ts' 'gallery/src/**/*.ts' 'cast/src/**/*.ts' 'test-mocha/**/*.ts' && tsc",
|
||||
"mocha": "node_modules/.bin/ts-mocha -p test-mocha/tsconfig.test.json --opts test-mocha/mocha.opts",
|
||||
"test": "npm run lint && npm run mocha",
|
||||
"docker_build": "sh ./script/docker_run.sh build $npm_package_version",
|
||||
@@ -19,13 +19,14 @@
|
||||
"dependencies": {
|
||||
"@material/chips": "^3.2.0",
|
||||
"@material/data-table": "^3.2.0",
|
||||
"@material/mwc-base": "^0.8.0",
|
||||
"@material/mwc-button": "^0.8.0",
|
||||
"@material/mwc-checkbox": "^0.8.0",
|
||||
"@material/mwc-fab": "^0.8.0",
|
||||
"@material/mwc-ripple": "^0.8.0",
|
||||
"@material/mwc-switch": "^0.8.0",
|
||||
"@mdi/svg": "4.5.95",
|
||||
"@material/mwc-base": "^0.10.0",
|
||||
"@material/mwc-button": "^0.10.0",
|
||||
"@material/mwc-checkbox": "^0.10.0",
|
||||
"@material/mwc-dialog": "^0.10.0",
|
||||
"@material/mwc-fab": "^0.10.0",
|
||||
"@material/mwc-ripple": "^0.10.0",
|
||||
"@material/mwc-switch": "^0.10.0",
|
||||
"@mdi/svg": "4.7.95",
|
||||
"@polymer/app-layout": "^3.0.2",
|
||||
"@polymer/app-localize-behavior": "^3.0.1",
|
||||
"@polymer/app-route": "^3.0.2",
|
||||
@@ -68,8 +69,8 @@
|
||||
"@polymer/paper-tooltip": "^3.0.1",
|
||||
"@polymer/polymer": "3.1.0",
|
||||
"@thomasloven/round-slider": "0.3.7",
|
||||
"@vaadin/vaadin-combo-box": "^4.2.8",
|
||||
"@vaadin/vaadin-date-picker": "^3.3.3",
|
||||
"@vaadin/vaadin-combo-box": "^5.0.6",
|
||||
"@vaadin/vaadin-date-picker": "^4.0.3",
|
||||
"@webcomponents/shadycss": "^1.9.0",
|
||||
"@webcomponents/webcomponentsjs": "^2.2.7",
|
||||
"chart.js": "~2.8.0",
|
||||
@@ -88,6 +89,7 @@
|
||||
"leaflet": "^1.4.0",
|
||||
"lit-element": "^2.2.1",
|
||||
"lit-html": "^1.1.0",
|
||||
"lit-virtualizer": "^0.4.2",
|
||||
"marked": "^0.6.1",
|
||||
"mdn-polyfills": "^5.16.0",
|
||||
"memoize-one": "^5.0.2",
|
||||
@@ -104,15 +106,16 @@
|
||||
"xss": "^1.0.6"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.4.0",
|
||||
"@babel/plugin-external-helpers": "^7.2.0",
|
||||
"@babel/plugin-proposal-class-properties": "^7.4.0",
|
||||
"@babel/plugin-proposal-decorators": "^7.4.0",
|
||||
"@babel/plugin-proposal-object-rest-spread": "^7.4.0",
|
||||
"@babel/plugin-syntax-dynamic-import": "^7.2.0",
|
||||
"@babel/plugin-transform-react-jsx": "^7.3.0",
|
||||
"@babel/preset-env": "^7.4.2",
|
||||
"@babel/preset-typescript": "^7.4.0",
|
||||
"@babel/core": "^7.7.4",
|
||||
"@babel/plugin-external-helpers": "^7.7.4",
|
||||
"@babel/plugin-proposal-class-properties": "^7.7.4",
|
||||
"@babel/plugin-proposal-decorators": "^7.7.4",
|
||||
"@babel/plugin-proposal-object-rest-spread": "^7.7.4",
|
||||
"@babel/plugin-proposal-optional-chaining": "^7.7.4",
|
||||
"@babel/plugin-syntax-dynamic-import": "^7.7.4",
|
||||
"@babel/plugin-transform-react-jsx": "^7.7.4",
|
||||
"@babel/preset-env": "^7.7.4",
|
||||
"@babel/preset-typescript": "^7.7.4",
|
||||
"@types/chai": "^4.1.7",
|
||||
"@types/chromecast-caf-receiver": "^3.0.12",
|
||||
"@types/chromecast-caf-sender": "^1.0.1",
|
||||
@@ -154,18 +157,18 @@
|
||||
"merge-stream": "^1.0.1",
|
||||
"mocha": "^6.0.2",
|
||||
"parse5": "^5.1.0",
|
||||
"prettier": "^1.16.4",
|
||||
"prettier": "^1.19.1",
|
||||
"raw-loader": "^2.0.0",
|
||||
"reify": "^0.18.1",
|
||||
"require-dir": "^1.2.0",
|
||||
"sinon": "^7.3.1",
|
||||
"terser-webpack-plugin": "^1.2.3",
|
||||
"ts-mocha": "^6.0.0",
|
||||
"tslint": "^5.14.0",
|
||||
"tslint": "^5.20.1",
|
||||
"tslint-config-prettier": "^1.18.0",
|
||||
"tslint-eslint-rules": "^5.4.0",
|
||||
"tslint-plugin-prettier": "^2.0.1",
|
||||
"typescript": "^3.6.3",
|
||||
"typescript": "^3.7.2",
|
||||
"web-component-tester": "^6.9.2",
|
||||
"webpack": "^4.40.2",
|
||||
"webpack-cli": "^3.3.9",
|
||||
@@ -178,7 +181,6 @@
|
||||
"_comment_2": "Fix in https://github.com/Polymer/polymer/pull/5569",
|
||||
"resolutions": {
|
||||
"@webcomponents/webcomponentsjs": "^2.2.10",
|
||||
"@vaadin/vaadin-lumo-styles": "^1.4.2",
|
||||
"@polymer/polymer": "3.1.0",
|
||||
"lit-html": "^1.1.2"
|
||||
},
|
||||
|
@@ -26,8 +26,8 @@ LANG_ISO=en
|
||||
|
||||
CURRENT_BRANCH=$(git rev-parse --abbrev-ref HEAD)
|
||||
|
||||
if [ "${CURRENT_BRANCH-}" != "master" ] && [ "${TRAVIS_BRANCH-}" != "master" ] ; then
|
||||
echo "Please only run the translations upload script from a clean checkout of master."
|
||||
if [ "${CURRENT_BRANCH-}" != "dev" ] && [ "${AZURE_BRANCH-}" != "dev" ] ; then
|
||||
echo "Please only run the translations upload script from a clean checkout of dev."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
|
@@ -1,11 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# Safe bash settings
|
||||
# -e Exit on command fail
|
||||
# -u Exit on unset variable
|
||||
# -o pipefail Exit if piped command has error code
|
||||
set -eu -o pipefail
|
||||
|
||||
cd "$(dirname "$0")/.."
|
||||
|
||||
script/translations_upload_base
|
2
setup.py
2
setup.py
@@ -2,7 +2,7 @@ from setuptools import setup, find_packages
|
||||
|
||||
setup(
|
||||
name="home-assistant-frontend",
|
||||
version="20191119.6",
|
||||
version="20200108.0",
|
||||
description="The Home Assistant frontend",
|
||||
url="https://github.com/home-assistant/home-assistant-polymer",
|
||||
author="The Home Assistant Authors",
|
||||
|
@@ -98,9 +98,7 @@ class HaAuthFlow extends litLocalizeLiteMixin(LitElement) {
|
||||
<ha-markdown
|
||||
allowsvg
|
||||
.content=${this.localize(
|
||||
`ui.panel.page-authorize.form.providers.${
|
||||
step.handler[0]
|
||||
}.abort.${step.reason}`
|
||||
`ui.panel.page-authorize.form.providers.${step.handler[0]}.abort.${step.reason}`
|
||||
)}
|
||||
></ha-markdown>
|
||||
`;
|
||||
@@ -229,9 +227,7 @@ class HaAuthFlow extends litLocalizeLiteMixin(LitElement) {
|
||||
}
|
||||
|
||||
private _computeStepDescription(step: DataEntryFlowStepForm) {
|
||||
const resourceKey = `ui.panel.page-authorize.form.providers.${
|
||||
step.handler[0]
|
||||
}.step.${step.step_id}.description`;
|
||||
const resourceKey = `ui.panel.page-authorize.form.providers.${step.handler[0]}.step.${step.step_id}.description`;
|
||||
const args: string[] = [];
|
||||
const placeholders = step.description_placeholders || {};
|
||||
Object.keys(placeholders).forEach((key) => {
|
||||
@@ -245,9 +241,7 @@ class HaAuthFlow extends litLocalizeLiteMixin(LitElement) {
|
||||
// Returns a callback for ha-form to calculate labels per schema object
|
||||
return (schema) =>
|
||||
this.localize(
|
||||
`ui.panel.page-authorize.form.providers.${step.handler[0]}.step.${
|
||||
step.step_id
|
||||
}.data.${schema.name}`
|
||||
`ui.panel.page-authorize.form.providers.${step.handler[0]}.step.${step.step_id}.data.${schema.name}`
|
||||
);
|
||||
}
|
||||
|
||||
@@ -255,9 +249,7 @@ class HaAuthFlow extends litLocalizeLiteMixin(LitElement) {
|
||||
// Returns a callback for ha-form to calculate error messages
|
||||
return (error) =>
|
||||
this.localize(
|
||||
`ui.panel.page-authorize.form.providers.${
|
||||
step.handler[0]
|
||||
}.error.${error}`
|
||||
`ui.panel.page-authorize.form.providers.${step.handler[0]}.error.${error}`
|
||||
);
|
||||
}
|
||||
|
||||
|
@@ -11,7 +11,9 @@ import "./ha-auth-flow";
|
||||
import { AuthProvider, fetchAuthProviders } from "../data/auth";
|
||||
import { registerServiceWorker } from "../util/register-service-worker";
|
||||
|
||||
import(/* webpackChunkName: "pick-auth-provider" */ "../auth/ha-pick-auth-provider");
|
||||
import(
|
||||
/* webpackChunkName: "pick-auth-provider" */ "../auth/ha-pick-auth-provider"
|
||||
);
|
||||
|
||||
interface QueryParams {
|
||||
client_id?: string;
|
||||
|
@@ -10,11 +10,11 @@ function toLocaleDateStringSupportsOptions() {
|
||||
return false;
|
||||
}
|
||||
|
||||
export default (toLocaleDateStringSupportsOptions()
|
||||
export default toLocaleDateStringSupportsOptions()
|
||||
? (dateObj: Date, locales: string) =>
|
||||
dateObj.toLocaleDateString(locales, {
|
||||
year: "numeric",
|
||||
month: "long",
|
||||
day: "numeric",
|
||||
})
|
||||
: (dateObj: Date) => fecha.format(dateObj, "mediumDate"));
|
||||
: (dateObj: Date) => fecha.format(dateObj, "mediumDate");
|
||||
|
@@ -10,7 +10,7 @@ function toLocaleStringSupportsOptions() {
|
||||
return false;
|
||||
}
|
||||
|
||||
export default (toLocaleStringSupportsOptions()
|
||||
export default toLocaleStringSupportsOptions()
|
||||
? (dateObj: Date, locales: string) =>
|
||||
dateObj.toLocaleString(locales, {
|
||||
year: "numeric",
|
||||
@@ -19,4 +19,4 @@ export default (toLocaleStringSupportsOptions()
|
||||
hour: "numeric",
|
||||
minute: "2-digit",
|
||||
})
|
||||
: (dateObj: Date) => fecha.format(dateObj, "haDateTime"));
|
||||
: (dateObj: Date) => fecha.format(dateObj, "haDateTime");
|
||||
|
@@ -10,10 +10,10 @@ function toLocaleTimeStringSupportsOptions() {
|
||||
return false;
|
||||
}
|
||||
|
||||
export default (toLocaleTimeStringSupportsOptions()
|
||||
export default toLocaleTimeStringSupportsOptions()
|
||||
? (dateObj: Date, locales: string) =>
|
||||
dateObj.toLocaleTimeString(locales, {
|
||||
hour: "numeric",
|
||||
minute: "2-digit",
|
||||
})
|
||||
: (dateObj: Date) => fecha.format(dateObj, "shortTime"));
|
||||
: (dateObj: Date) => fecha.format(dateObj, "shortTime");
|
||||
|
@@ -60,7 +60,7 @@ export const applyThemesOnElement = (
|
||||
element.updateStyles(styles);
|
||||
} else if (window.ShadyCSS) {
|
||||
// implement updateStyles() method of Polymer elements
|
||||
window.ShadyCSS.styleSubtree(/** @type {!HTMLElement} */ (element), styles);
|
||||
window.ShadyCSS.styleSubtree(/** @type {!HTMLElement} */ element, styles);
|
||||
}
|
||||
|
||||
if (!updateMeta) {
|
||||
|
33
src/common/dom/dynamic-element-directive.ts
Normal file
33
src/common/dom/dynamic-element-directive.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
import { directive, Part, NodePart } from "lit-html";
|
||||
|
||||
export const dynamicElement = directive(
|
||||
(tag: string, properties?: { [key: string]: any }) => (part: Part): void => {
|
||||
if (!(part instanceof NodePart)) {
|
||||
throw new Error(
|
||||
"dynamicContentDirective can only be used in content bindings"
|
||||
);
|
||||
}
|
||||
|
||||
let element = part.value as HTMLElement | undefined;
|
||||
|
||||
if (
|
||||
element !== undefined &&
|
||||
tag.toUpperCase() === (element as HTMLElement).tagName
|
||||
) {
|
||||
if (properties) {
|
||||
Object.entries(properties).forEach(([key, value]) => {
|
||||
element![key] = value;
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
element = document.createElement(tag);
|
||||
if (properties) {
|
||||
Object.entries(properties).forEach(([key, value]) => {
|
||||
element![key] = value;
|
||||
});
|
||||
}
|
||||
part.setValue(element);
|
||||
}
|
||||
);
|
@@ -11,7 +11,9 @@ export const setupLeafletMap = async (
|
||||
throw new Error("Cannot setup Leaflet map on disconnected element");
|
||||
}
|
||||
// tslint:disable-next-line
|
||||
const Leaflet = (await import(/* webpackChunkName: "leaflet" */ "leaflet")) as LeafletModuleType;
|
||||
const Leaflet = (await import(
|
||||
/* webpackChunkName: "leaflet" */ "leaflet"
|
||||
)) as LeafletModuleType;
|
||||
Leaflet.Icon.Default.imagePath = "/static/images/leaflet/images/";
|
||||
|
||||
const map = Leaflet.map(mapElement);
|
||||
|
@@ -1,25 +0,0 @@
|
||||
// interface OnChangeComponent {
|
||||
// props: {
|
||||
// index: number;
|
||||
// onChange(index: number, data: object);
|
||||
// };
|
||||
// }
|
||||
|
||||
// export function onChangeEvent(this: OnChangeComponent, prop, ev) {
|
||||
export function onChangeEvent(this: any, prop, ev) {
|
||||
const origData = this.props[prop];
|
||||
|
||||
if (ev.target.value === origData[ev.target.name]) {
|
||||
return;
|
||||
}
|
||||
|
||||
const data = { ...origData };
|
||||
|
||||
if (ev.target.value) {
|
||||
data[ev.target.name] = ev.target.value;
|
||||
} else {
|
||||
delete data[ev.target.name];
|
||||
}
|
||||
|
||||
this.props.onChange(this.props.index, data);
|
||||
}
|
@@ -1,9 +0,0 @@
|
||||
import { render } from "preact";
|
||||
|
||||
export default function unmount(mountEl) {
|
||||
render(
|
||||
// @ts-ignore
|
||||
() => null,
|
||||
mountEl
|
||||
);
|
||||
}
|
@@ -14,7 +14,11 @@ import "@material/mwc-button";
|
||||
|
||||
@customElement("search-input")
|
||||
class SearchInput extends LitElement {
|
||||
@property() private filter?: string;
|
||||
@property() public filter?: string;
|
||||
|
||||
public focus() {
|
||||
this.shadowRoot!.querySelector("paper-input")!.focus();
|
||||
}
|
||||
|
||||
protected render(): TemplateResult | void {
|
||||
return html`
|
||||
|
@@ -15,6 +15,7 @@ class HaCallServiceButton extends EventsMixin(PolymerElement) {
|
||||
id="progress"
|
||||
progress="[[progress]]"
|
||||
on-click="buttonTapped"
|
||||
tabindex="0"
|
||||
><slot></slot
|
||||
></ha-progress-button>
|
||||
`;
|
||||
|
@@ -6,8 +6,9 @@ import {
|
||||
MDCDataTableFoundation,
|
||||
} from "@material/data-table";
|
||||
|
||||
import { classMap } from "lit-html/directives/class-map";
|
||||
|
||||
import {
|
||||
BaseElement,
|
||||
html,
|
||||
query,
|
||||
queryAll,
|
||||
@@ -15,10 +16,11 @@ import {
|
||||
css,
|
||||
customElement,
|
||||
property,
|
||||
classMap,
|
||||
TemplateResult,
|
||||
PropertyValues,
|
||||
} from "@material/mwc-base/base-element";
|
||||
} from "lit-element";
|
||||
|
||||
import { BaseElement } from "@material/mwc-base/base-element";
|
||||
|
||||
// eslint-disable-next-line import/no-webpack-loader-syntax
|
||||
// @ts-ignore
|
||||
@@ -73,7 +75,7 @@ export interface DataTableSortColumnData {
|
||||
export interface DataTableColumnData extends DataTableSortColumnData {
|
||||
title: string;
|
||||
type?: "numeric" | "icon";
|
||||
template?: <T>(data: any, row: T) => TemplateResult;
|
||||
template?: <T>(data: any, row: T) => TemplateResult | string;
|
||||
}
|
||||
|
||||
export interface DataTableRowData {
|
||||
@@ -86,11 +88,11 @@ export class HaDataTable extends BaseElement {
|
||||
@property({ type: Array }) public data: DataTableRowData[] = [];
|
||||
@property({ type: Boolean }) public selectable = false;
|
||||
@property({ type: String }) public id = "id";
|
||||
@property({ type: String }) public filter = "";
|
||||
protected mdcFoundation!: MDCDataTableFoundation;
|
||||
protected readonly mdcFoundationClass = MDCDataTableFoundation;
|
||||
@query(".mdc-data-table") protected mdcRoot!: HTMLElement;
|
||||
@queryAll(".mdc-data-table__row") protected rowElements!: HTMLElement[];
|
||||
@query("#header-checkbox") private _headerCheckbox!: HaCheckbox;
|
||||
@property({ type: Boolean }) private _filterable = false;
|
||||
@property({ type: Boolean }) private _headerChecked = false;
|
||||
@property({ type: Boolean }) private _headerIndeterminate = false;
|
||||
@@ -106,13 +108,19 @@ export class HaDataTable extends BaseElement {
|
||||
private _worker: any | undefined;
|
||||
|
||||
private _debounceSearch = debounce(
|
||||
(ev) => {
|
||||
this._filter = ev.detail.value;
|
||||
(value: string) => {
|
||||
this._filter = value;
|
||||
},
|
||||
200,
|
||||
false
|
||||
);
|
||||
|
||||
public clearSelection(): void {
|
||||
this._headerChecked = false;
|
||||
this._headerIndeterminate = false;
|
||||
this.mdcFoundation.handleHeaderRowCheckboxChange();
|
||||
}
|
||||
|
||||
protected firstUpdated() {
|
||||
super.firstUpdated();
|
||||
this._worker = sortFilterWorker();
|
||||
@@ -144,6 +152,10 @@ export class HaDataTable extends BaseElement {
|
||||
this._sortColumns = clonedColumns;
|
||||
}
|
||||
|
||||
if (properties.has("filter")) {
|
||||
this._debounceSearch(this.filter);
|
||||
}
|
||||
|
||||
if (
|
||||
properties.has("data") ||
|
||||
properties.has("columns") ||
|
||||
@@ -157,14 +169,18 @@ export class HaDataTable extends BaseElement {
|
||||
|
||||
protected render() {
|
||||
return html`
|
||||
${this._filterable
|
||||
? html`
|
||||
<search-input
|
||||
@value-changed=${this._handleSearchChange}
|
||||
></search-input>
|
||||
`
|
||||
: ""}
|
||||
<div class="mdc-data-table">
|
||||
<slot name="header">
|
||||
${this._filterable
|
||||
? html`
|
||||
<div class="table-header">
|
||||
<search-input
|
||||
@value-changed=${this._handleSearchChange}
|
||||
></search-input>
|
||||
</div>
|
||||
`
|
||||
: ""}
|
||||
</slot>
|
||||
<table class="mdc-data-table__table">
|
||||
<thead>
|
||||
<tr class="mdc-data-table__header-row">
|
||||
@@ -176,7 +192,6 @@ export class HaDataTable extends BaseElement {
|
||||
scope="col"
|
||||
>
|
||||
<ha-checkbox
|
||||
id="header-checkbox"
|
||||
class="mdc-data-table__row-checkbox"
|
||||
@change=${this._handleHeaderRowCheckboxChange}
|
||||
.indeterminate=${this._headerIndeterminate}
|
||||
@@ -240,7 +255,9 @@ export class HaDataTable extends BaseElement {
|
||||
<ha-checkbox
|
||||
class="mdc-data-table__row-checkbox"
|
||||
@change=${this._handleRowCheckboxChange}
|
||||
.checked=${this._checkedRows.includes(row[this.id])}
|
||||
.checked=${this._checkedRows.includes(
|
||||
String(row[this.id])
|
||||
)}
|
||||
>
|
||||
</ha-checkbox>
|
||||
</td>
|
||||
@@ -370,9 +387,10 @@ export class HaDataTable extends BaseElement {
|
||||
});
|
||||
}
|
||||
|
||||
private _handleHeaderRowCheckboxChange() {
|
||||
this._headerChecked = this._headerCheckbox.checked;
|
||||
this._headerIndeterminate = this._headerCheckbox.indeterminate;
|
||||
private _handleHeaderRowCheckboxChange(ev: Event) {
|
||||
const checkbox = ev.target as HaCheckbox;
|
||||
this._headerChecked = checkbox.checked;
|
||||
this._headerIndeterminate = checkbox.indeterminate;
|
||||
this.mdcFoundation.handleHeaderRowCheckboxChange();
|
||||
}
|
||||
|
||||
@@ -385,20 +403,26 @@ export class HaDataTable extends BaseElement {
|
||||
}
|
||||
|
||||
private _handleRowClick(ev: Event) {
|
||||
const rowId = (ev.target as HTMLElement)
|
||||
.closest("tr")!
|
||||
.getAttribute("data-row-id")!;
|
||||
const target = ev.target as HTMLElement;
|
||||
if (target.tagName === "HA-CHECKBOX") {
|
||||
return;
|
||||
}
|
||||
const rowId = target.closest("tr")!.getAttribute("data-row-id")!;
|
||||
fireEvent(this, "row-click", { id: rowId }, { bubbles: false });
|
||||
}
|
||||
|
||||
private _setRowChecked(rowId: string, checked: boolean) {
|
||||
if (checked && !this._checkedRows.includes(rowId)) {
|
||||
this._checkedRows = [...this._checkedRows, rowId];
|
||||
} else if (!checked) {
|
||||
const index = this._checkedRows.indexOf(rowId);
|
||||
if (index !== -1) {
|
||||
this._checkedRows.splice(index, 1);
|
||||
if (checked) {
|
||||
if (this._checkedRows.includes(rowId)) {
|
||||
return;
|
||||
}
|
||||
this._checkedRows = [...this._checkedRows, rowId];
|
||||
} else {
|
||||
const index = this._checkedRows.indexOf(rowId);
|
||||
if (index === -1) {
|
||||
return;
|
||||
}
|
||||
this._checkedRows.splice(index, 1);
|
||||
}
|
||||
fireEvent(this, "selection-changed", {
|
||||
id: rowId,
|
||||
@@ -407,7 +431,7 @@ export class HaDataTable extends BaseElement {
|
||||
}
|
||||
|
||||
private _handleSearchChange(ev: CustomEvent): void {
|
||||
this._debounceSearch(ev);
|
||||
this._debounceSearch(ev.detail.value);
|
||||
}
|
||||
|
||||
static get styles(): CSSResult {
|
||||
@@ -587,6 +611,9 @@ export class HaDataTable extends BaseElement {
|
||||
.mdc-data-table__header-cell:hover.not-sorted ha-icon {
|
||||
left: 0px;
|
||||
}
|
||||
.table-header {
|
||||
border-bottom: 1px solid rgba(var(--rgb-primary-text-color), 0.12);
|
||||
}
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
412
src/components/device/ha-area-devices-picker.ts
Normal file
412
src/components/device/ha-area-devices-picker.ts
Normal file
@@ -0,0 +1,412 @@
|
||||
import "@polymer/paper-input/paper-input";
|
||||
import "@polymer/paper-item/paper-item";
|
||||
import "@polymer/paper-item/paper-item-body";
|
||||
import "@vaadin/vaadin-combo-box/theme/material/vaadin-combo-box-light";
|
||||
import "@polymer/paper-listbox/paper-listbox";
|
||||
import memoizeOne from "memoize-one";
|
||||
import {
|
||||
LitElement,
|
||||
TemplateResult,
|
||||
html,
|
||||
css,
|
||||
CSSResult,
|
||||
customElement,
|
||||
property,
|
||||
PropertyValues,
|
||||
} from "lit-element";
|
||||
import { UnsubscribeFunc } from "home-assistant-js-websocket";
|
||||
import { SubscribeMixin } from "../../mixins/subscribe-mixin";
|
||||
import "./ha-devices-picker";
|
||||
|
||||
import { HomeAssistant } from "../../types";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import {
|
||||
DeviceRegistryEntry,
|
||||
subscribeDeviceRegistry,
|
||||
} from "../../data/device_registry";
|
||||
import { compare } from "../../common/string/compare";
|
||||
import { PolymerChangedEvent } from "../../polymer-types";
|
||||
import {
|
||||
AreaRegistryEntry,
|
||||
subscribeAreaRegistry,
|
||||
} from "../../data/area_registry";
|
||||
import { DeviceEntityLookup } from "../../panels/config/devices/ha-devices-data-table";
|
||||
import {
|
||||
EntityRegistryEntry,
|
||||
subscribeEntityRegistry,
|
||||
} from "../../data/entity_registry";
|
||||
import { computeDomain } from "../../common/entity/compute_domain";
|
||||
|
||||
interface DevicesByArea {
|
||||
[areaId: string]: AreaDevices;
|
||||
}
|
||||
|
||||
interface AreaDevices {
|
||||
id?: string;
|
||||
name: string;
|
||||
devices: string[];
|
||||
}
|
||||
|
||||
const rowRenderer = (
|
||||
root: HTMLElement,
|
||||
_owner,
|
||||
model: { item: AreaDevices }
|
||||
) => {
|
||||
if (!root.firstElementChild) {
|
||||
root.innerHTML = `
|
||||
<style>
|
||||
paper-item {
|
||||
width: 100%;
|
||||
margin: -10px 0;
|
||||
padding: 0;
|
||||
}
|
||||
paper-icon-button {
|
||||
float: right;
|
||||
}
|
||||
.devices {
|
||||
display: none;
|
||||
}
|
||||
.devices.visible {
|
||||
display: block;
|
||||
}
|
||||
</style>
|
||||
<paper-item>
|
||||
<paper-item-body two-line="">
|
||||
<div class='name'>[[item.name]]</div>
|
||||
<div secondary>[[item.devices.length]] devices</div>
|
||||
</paper-item-body>
|
||||
</paper-item>
|
||||
`;
|
||||
}
|
||||
root.querySelector(".name")!.textContent = model.item.name!;
|
||||
root.querySelector(
|
||||
"[secondary]"
|
||||
)!.textContent = `${model.item.devices.length.toString()} devices`;
|
||||
};
|
||||
|
||||
@customElement("ha-area-devices-picker")
|
||||
export class HaAreaDevicesPicker extends SubscribeMixin(LitElement) {
|
||||
@property() public hass!: HomeAssistant;
|
||||
@property() public label?: string;
|
||||
@property() public value?: string;
|
||||
@property() public area?: string;
|
||||
@property() public devices?: string[];
|
||||
/**
|
||||
* Show only devices with entities from specific domains.
|
||||
* @type {Array}
|
||||
* @attr include-domains
|
||||
*/
|
||||
@property({ type: Array, attribute: "include-domains" })
|
||||
public includeDomains?: string[];
|
||||
/**
|
||||
* Show no devices with entities of these domains.
|
||||
* @type {Array}
|
||||
* @attr exclude-domains
|
||||
*/
|
||||
@property({ type: Array, attribute: "exclude-domains" })
|
||||
public excludeDomains?: string[];
|
||||
/**
|
||||
* Show only deviced with entities of these device classes.
|
||||
* @type {Array}
|
||||
* @attr include-device-classes
|
||||
*/
|
||||
@property({ type: Array, attribute: "include-device-classes" })
|
||||
public includeDeviceClasses?: string[];
|
||||
@property({ type: Boolean })
|
||||
private _opened?: boolean;
|
||||
@property() private _areaPicker = true;
|
||||
@property() private _devices?: DeviceRegistryEntry[];
|
||||
@property() private _areas?: AreaRegistryEntry[];
|
||||
@property() private _entities?: EntityRegistryEntry[];
|
||||
private _selectedDevices: string[] = [];
|
||||
private _filteredDevices: DeviceRegistryEntry[] = [];
|
||||
|
||||
private _getDevices = memoizeOne(
|
||||
(
|
||||
devices: DeviceRegistryEntry[],
|
||||
areas: AreaRegistryEntry[],
|
||||
entities: EntityRegistryEntry[],
|
||||
includeDomains: this["includeDomains"],
|
||||
excludeDomains: this["excludeDomains"],
|
||||
includeDeviceClasses: this["includeDeviceClasses"]
|
||||
): AreaDevices[] => {
|
||||
if (!devices.length) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const deviceEntityLookup: DeviceEntityLookup = {};
|
||||
for (const entity of entities) {
|
||||
if (!entity.device_id) {
|
||||
continue;
|
||||
}
|
||||
if (!(entity.device_id in deviceEntityLookup)) {
|
||||
deviceEntityLookup[entity.device_id] = [];
|
||||
}
|
||||
deviceEntityLookup[entity.device_id].push(entity);
|
||||
}
|
||||
|
||||
let inputDevices = [...devices];
|
||||
|
||||
if (includeDomains) {
|
||||
inputDevices = inputDevices.filter((device) => {
|
||||
const devEntities = deviceEntityLookup[device.id];
|
||||
if (!devEntities || !devEntities.length) {
|
||||
return false;
|
||||
}
|
||||
return deviceEntityLookup[device.id].some((entity) =>
|
||||
includeDomains.includes(computeDomain(entity.entity_id))
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
if (excludeDomains) {
|
||||
inputDevices = inputDevices.filter((device) => {
|
||||
const devEntities = deviceEntityLookup[device.id];
|
||||
if (!devEntities || !devEntities.length) {
|
||||
return true;
|
||||
}
|
||||
return entities.every(
|
||||
(entity) =>
|
||||
!excludeDomains.includes(computeDomain(entity.entity_id))
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
if (includeDeviceClasses) {
|
||||
inputDevices = inputDevices.filter((device) => {
|
||||
const devEntities = deviceEntityLookup[device.id];
|
||||
if (!devEntities || !devEntities.length) {
|
||||
return false;
|
||||
}
|
||||
return deviceEntityLookup[device.id].some((entity) => {
|
||||
const stateObj = this.hass.states[entity.entity_id];
|
||||
if (!stateObj) {
|
||||
return false;
|
||||
}
|
||||
return (
|
||||
stateObj.attributes.device_class &&
|
||||
includeDeviceClasses.includes(stateObj.attributes.device_class)
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
this._filteredDevices = inputDevices;
|
||||
|
||||
const areaLookup: { [areaId: string]: AreaRegistryEntry } = {};
|
||||
for (const area of areas) {
|
||||
areaLookup[area.area_id] = area;
|
||||
}
|
||||
|
||||
const devicesByArea: DevicesByArea = {};
|
||||
|
||||
for (const device of inputDevices) {
|
||||
const areaId = device.area_id;
|
||||
if (areaId) {
|
||||
if (!(areaId in devicesByArea)) {
|
||||
devicesByArea[areaId] = {
|
||||
id: areaId,
|
||||
name: areaLookup[areaId].name,
|
||||
devices: [],
|
||||
};
|
||||
}
|
||||
devicesByArea[areaId].devices.push(device.id);
|
||||
}
|
||||
}
|
||||
|
||||
const sorted = Object.keys(devicesByArea)
|
||||
.sort((a, b) =>
|
||||
compare(devicesByArea[a].name || "", devicesByArea[b].name || "")
|
||||
)
|
||||
.map((key) => devicesByArea[key]);
|
||||
|
||||
return sorted;
|
||||
}
|
||||
);
|
||||
|
||||
public hassSubscribe(): UnsubscribeFunc[] {
|
||||
return [
|
||||
subscribeDeviceRegistry(this.hass.connection!, (devices) => {
|
||||
this._devices = devices;
|
||||
}),
|
||||
subscribeAreaRegistry(this.hass.connection!, (areas) => {
|
||||
this._areas = areas;
|
||||
}),
|
||||
subscribeEntityRegistry(this.hass.connection!, (entities) => {
|
||||
this._entities = entities;
|
||||
}),
|
||||
];
|
||||
}
|
||||
|
||||
protected updated(changedProps: PropertyValues) {
|
||||
if (changedProps.has("area") && this.area) {
|
||||
this._areaPicker = true;
|
||||
this.value = this.area;
|
||||
} else if (changedProps.has("devices") && this.devices) {
|
||||
this._areaPicker = false;
|
||||
const filteredDeviceIds = this._filteredDevices.map(
|
||||
(device) => device.id
|
||||
);
|
||||
const selectedDevices = this.devices.filter((device) =>
|
||||
filteredDeviceIds.includes(device)
|
||||
);
|
||||
this._setValue(selectedDevices);
|
||||
}
|
||||
}
|
||||
|
||||
protected render(): TemplateResult | void {
|
||||
if (!this._devices || !this._areas || !this._entities) {
|
||||
return;
|
||||
}
|
||||
const areas = this._getDevices(
|
||||
this._devices,
|
||||
this._areas,
|
||||
this._entities,
|
||||
this.includeDomains,
|
||||
this.excludeDomains,
|
||||
this.includeDeviceClasses
|
||||
);
|
||||
if (!this._areaPicker || areas.length === 0) {
|
||||
return html`
|
||||
<ha-devices-picker
|
||||
@value-changed=${this._devicesPicked}
|
||||
.hass=${this.hass}
|
||||
.includeDomains=${this.includeDomains}
|
||||
.includeDeviceClasses=${this.includeDeviceClasses}
|
||||
.value=${this._selectedDevices}
|
||||
.pickDeviceLabel=${`Add ${this.label} device`}
|
||||
.pickedDeviceLabel=${`${this.label} device`}
|
||||
></ha-devices-picker>
|
||||
${areas.length > 0
|
||||
? html`
|
||||
<mwc-button @click=${this._switchPicker}
|
||||
>Choose an area</mwc-button
|
||||
>
|
||||
`
|
||||
: ""}
|
||||
`;
|
||||
}
|
||||
return html`
|
||||
<vaadin-combo-box-light
|
||||
item-value-path="id"
|
||||
item-id-path="id"
|
||||
item-label-path="name"
|
||||
.items=${areas}
|
||||
.value=${this._value}
|
||||
.renderer=${rowRenderer}
|
||||
@opened-changed=${this._openedChanged}
|
||||
@value-changed=${this._areaPicked}
|
||||
>
|
||||
<paper-input
|
||||
.label=${this.label === undefined && this.hass
|
||||
? this.hass.localize("ui.components.device-picker.device")
|
||||
: `${this.label} in area`}
|
||||
class="input"
|
||||
autocapitalize="none"
|
||||
autocomplete="off"
|
||||
autocorrect="off"
|
||||
spellcheck="false"
|
||||
>
|
||||
${this.value
|
||||
? html`
|
||||
<paper-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
|
||||
>
|
||||
Clear
|
||||
</paper-icon-button>
|
||||
`
|
||||
: ""}
|
||||
${areas.length > 0
|
||||
? html`
|
||||
<paper-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
|
||||
</paper-icon-button>
|
||||
`
|
||||
: ""}
|
||||
</paper-input>
|
||||
</vaadin-combo-box-light>
|
||||
<mwc-button @click=${this._switchPicker}
|
||||
>Choose individual devices</mwc-button
|
||||
>
|
||||
`;
|
||||
}
|
||||
|
||||
private _clearValue(ev: Event) {
|
||||
ev.stopPropagation();
|
||||
this._setValue([]);
|
||||
}
|
||||
|
||||
private get _value() {
|
||||
return this.value || [];
|
||||
}
|
||||
|
||||
private _openedChanged(ev: PolymerChangedEvent<boolean>) {
|
||||
this._opened = ev.detail.value;
|
||||
}
|
||||
|
||||
private async _switchPicker() {
|
||||
this._areaPicker = !this._areaPicker;
|
||||
}
|
||||
|
||||
private async _areaPicked(ev: PolymerChangedEvent<string>) {
|
||||
const value = ev.detail.value;
|
||||
let selectedDevices = [];
|
||||
const target = ev.target as any;
|
||||
if (target.selectedItem) {
|
||||
selectedDevices = target.selectedItem.devices;
|
||||
}
|
||||
|
||||
if (value !== this._value || this._selectedDevices !== selectedDevices) {
|
||||
this._setValue(selectedDevices, value);
|
||||
}
|
||||
}
|
||||
|
||||
private _devicesPicked(ev: CustomEvent) {
|
||||
ev.stopPropagation();
|
||||
const selectedDevices = ev.detail.value;
|
||||
this._setValue(selectedDevices);
|
||||
}
|
||||
|
||||
private _setValue(selectedDevices: string[], value = "") {
|
||||
this.value = value;
|
||||
this._selectedDevices = selectedDevices;
|
||||
setTimeout(() => {
|
||||
fireEvent(this, "value-changed", { value: selectedDevices });
|
||||
fireEvent(this, "change");
|
||||
}, 0);
|
||||
}
|
||||
|
||||
static get styles(): CSSResult {
|
||||
return css`
|
||||
paper-input > paper-icon-button {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
padding: 2px;
|
||||
color: var(--secondary-text-color);
|
||||
}
|
||||
[hidden] {
|
||||
display: none;
|
||||
}
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-area-devices-picker": HaAreaDevicesPicker;
|
||||
}
|
||||
}
|
@@ -176,6 +176,7 @@ export abstract class HaDeviceAutomationPicker<
|
||||
this.value = automation;
|
||||
setTimeout(() => {
|
||||
fireEvent(this, "change");
|
||||
fireEvent(this, "value-changed", { value: automation });
|
||||
}, 0);
|
||||
}
|
||||
|
||||
|
@@ -1,7 +1,7 @@
|
||||
import "@polymer/paper-input/paper-input";
|
||||
import "@polymer/paper-item/paper-item";
|
||||
import "@polymer/paper-item/paper-item-body";
|
||||
import "@vaadin/vaadin-combo-box/vaadin-combo-box-light";
|
||||
import "@vaadin/vaadin-combo-box/theme/material/vaadin-combo-box-light";
|
||||
import "@polymer/paper-listbox/paper-listbox";
|
||||
import memoizeOne from "memoize-one";
|
||||
import {
|
||||
|
127
src/components/device/ha-devices-picker.ts
Normal file
127
src/components/device/ha-devices-picker.ts
Normal file
@@ -0,0 +1,127 @@
|
||||
import {
|
||||
LitElement,
|
||||
TemplateResult,
|
||||
property,
|
||||
html,
|
||||
customElement,
|
||||
} from "lit-element";
|
||||
import "@polymer/paper-icon-button/paper-icon-button-light";
|
||||
|
||||
import { HomeAssistant } from "../../types";
|
||||
import { PolymerChangedEvent } from "../../polymer-types";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
|
||||
import "./ha-device-picker";
|
||||
|
||||
@customElement("ha-devices-picker")
|
||||
class HaDevicesPicker extends LitElement {
|
||||
@property() public hass?: HomeAssistant;
|
||||
@property() public value?: string[];
|
||||
/**
|
||||
* Show entities from specific domains.
|
||||
* @type {string}
|
||||
* @attr include-domains
|
||||
*/
|
||||
@property({ type: Array, attribute: "include-domains" })
|
||||
public includeDomains?: string[];
|
||||
/**
|
||||
* Show no entities of these domains.
|
||||
* @type {Array}
|
||||
* @attr exclude-domains
|
||||
*/
|
||||
@property({ type: Array, attribute: "exclude-domains" })
|
||||
public excludeDomains?: string[];
|
||||
@property({ attribute: "picked-device-label" })
|
||||
@property({ type: Array, attribute: "include-device-classes" })
|
||||
public includeDeviceClasses?: string[];
|
||||
public pickedDeviceLabel?: string;
|
||||
@property({ attribute: "pick-device-label" }) public pickDeviceLabel?: string;
|
||||
|
||||
protected render(): TemplateResult | void {
|
||||
if (!this.hass) {
|
||||
return;
|
||||
}
|
||||
|
||||
const currentDevices = this._currentDevices;
|
||||
return html`
|
||||
${currentDevices.map(
|
||||
(entityId) => html`
|
||||
<div>
|
||||
<ha-device-picker
|
||||
allow-custom-entity
|
||||
.curValue=${entityId}
|
||||
.hass=${this.hass}
|
||||
.includeDomains=${this.includeDomains}
|
||||
.excludeDomains=${this.excludeDomains}
|
||||
.includeDeviceClasses=${this.includeDeviceClasses}
|
||||
.value=${entityId}
|
||||
.label=${this.pickedDeviceLabel}
|
||||
@value-changed=${this._deviceChanged}
|
||||
></ha-device-picker>
|
||||
</div>
|
||||
`
|
||||
)}
|
||||
<div>
|
||||
<ha-device-picker
|
||||
.hass=${this.hass}
|
||||
.includeDomains=${this.includeDomains}
|
||||
.excludeDomains=${this.excludeDomains}
|
||||
.includeDeviceClasses=${this.includeDeviceClasses}
|
||||
.label=${this.pickDeviceLabel}
|
||||
@value-changed=${this._addDevice}
|
||||
></ha-device-picker>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
private get _currentDevices() {
|
||||
return this.value || [];
|
||||
}
|
||||
|
||||
private async _updateDevices(devices) {
|
||||
fireEvent(this, "value-changed", {
|
||||
value: devices,
|
||||
});
|
||||
|
||||
this.value = devices;
|
||||
}
|
||||
|
||||
private _deviceChanged(event: PolymerChangedEvent<string>) {
|
||||
event.stopPropagation();
|
||||
const curValue = (event.currentTarget as any).curValue;
|
||||
const newValue = event.detail.value;
|
||||
if (newValue === curValue || newValue !== "") {
|
||||
return;
|
||||
}
|
||||
if (newValue === "") {
|
||||
this._updateDevices(
|
||||
this._currentDevices.filter((dev) => dev !== curValue)
|
||||
);
|
||||
} else {
|
||||
this._updateDevices(
|
||||
this._currentDevices.map((dev) => (dev === curValue ? newValue : dev))
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private async _addDevice(event: PolymerChangedEvent<string>) {
|
||||
event.stopPropagation();
|
||||
const toAdd = event.detail.value;
|
||||
(event.currentTarget as any).value = "";
|
||||
if (!toAdd) {
|
||||
return;
|
||||
}
|
||||
const currentDevices = this._currentDevices;
|
||||
if (currentDevices.includes(toAdd)) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._updateDevices([...currentDevices, toAdd]);
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-devices-picker": HaDevicesPicker;
|
||||
}
|
||||
}
|
@@ -215,7 +215,9 @@ class HaChartBase extends mixinBehaviors(
|
||||
}
|
||||
|
||||
if (scriptsLoaded === null) {
|
||||
scriptsLoaded = import(/* webpackChunkName: "load_chart" */ "../../resources/ha-chart-scripts.js");
|
||||
scriptsLoaded = import(
|
||||
/* webpackChunkName: "load_chart" */ "../../resources/ha-chart-scripts.js"
|
||||
);
|
||||
}
|
||||
scriptsLoaded.then((ChartModule) => {
|
||||
this.ChartClass = ChartModule.default;
|
||||
|
@@ -2,7 +2,7 @@ import "@polymer/paper-icon-button/paper-icon-button";
|
||||
import "@polymer/paper-input/paper-input";
|
||||
import "@polymer/paper-item/paper-icon-item";
|
||||
import "@polymer/paper-item/paper-item-body";
|
||||
import "@vaadin/vaadin-combo-box/vaadin-combo-box-light";
|
||||
import "@vaadin/vaadin-combo-box/theme/material/vaadin-combo-box-light";
|
||||
import memoizeOne from "memoize-one";
|
||||
|
||||
import "./state-badge";
|
||||
|
@@ -1,91 +0,0 @@
|
||||
import "@polymer/iron-flex-layout/iron-flex-layout-classes";
|
||||
import { html } from "@polymer/polymer/lib/utils/html-tag";
|
||||
import { PolymerElement } from "@polymer/polymer/polymer-element";
|
||||
|
||||
import hassAttributeUtil from "../util/hass-attributes-util";
|
||||
|
||||
class HaAttributes extends PolymerElement {
|
||||
static get template() {
|
||||
return html`
|
||||
<style include="iron-flex iron-flex-alignment"></style>
|
||||
<style>
|
||||
.data-entry .value {
|
||||
max-width: 200px;
|
||||
overflow-wrap: break-word;
|
||||
}
|
||||
.attribution {
|
||||
color: var(--secondary-text-color);
|
||||
text-align: right;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="layout vertical">
|
||||
<template
|
||||
is="dom-repeat"
|
||||
items="[[computeDisplayAttributes(stateObj, filtersArray)]]"
|
||||
as="attribute"
|
||||
>
|
||||
<div class="data-entry layout justified horizontal">
|
||||
<div class="key">[[formatAttribute(attribute)]]</div>
|
||||
<div class="value">
|
||||
[[formatAttributeValue(stateObj, attribute)]]
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<div class="attribution" hidden$="[[!computeAttribution(stateObj)]]">
|
||||
[[computeAttribution(stateObj)]]
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
static get properties() {
|
||||
return {
|
||||
stateObj: {
|
||||
type: Object,
|
||||
},
|
||||
extraFilters: {
|
||||
type: String,
|
||||
value: "",
|
||||
},
|
||||
filtersArray: {
|
||||
type: Array,
|
||||
computed: "computeFiltersArray(extraFilters)",
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
computeFiltersArray(extraFilters) {
|
||||
return Object.keys(hassAttributeUtil.LOGIC_STATE_ATTRIBUTES).concat(
|
||||
extraFilters ? extraFilters.split(",") : []
|
||||
);
|
||||
}
|
||||
|
||||
computeDisplayAttributes(stateObj, filtersArray) {
|
||||
if (!stateObj) {
|
||||
return [];
|
||||
}
|
||||
return Object.keys(stateObj.attributes).filter(function(key) {
|
||||
return filtersArray.indexOf(key) === -1;
|
||||
});
|
||||
}
|
||||
|
||||
formatAttribute(attribute) {
|
||||
return attribute.replace(/_/g, " ");
|
||||
}
|
||||
|
||||
formatAttributeValue(stateObj, attribute) {
|
||||
var value = stateObj.attributes[attribute];
|
||||
if (value === null) return "-";
|
||||
if (Array.isArray(value)) {
|
||||
return value.join(", ");
|
||||
}
|
||||
return value instanceof Object ? JSON.stringify(value, null, 2) : value;
|
||||
}
|
||||
|
||||
computeAttribution(stateObj) {
|
||||
return stateObj.attributes.attribution;
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define("ha-attributes", HaAttributes);
|
97
src/components/ha-attributes.ts
Normal file
97
src/components/ha-attributes.ts
Normal file
@@ -0,0 +1,97 @@
|
||||
import {
|
||||
property,
|
||||
LitElement,
|
||||
TemplateResult,
|
||||
html,
|
||||
CSSResult,
|
||||
css,
|
||||
customElement,
|
||||
} from "lit-element";
|
||||
import { HassEntity } from "home-assistant-js-websocket";
|
||||
|
||||
import hassAttributeUtil from "../util/hass-attributes-util";
|
||||
|
||||
@customElement("ha-attributes")
|
||||
class HaAttributes extends LitElement {
|
||||
@property() public stateObj?: HassEntity;
|
||||
@property() public extraFilters?: string;
|
||||
|
||||
protected render(): TemplateResult | void {
|
||||
if (!this.stateObj) {
|
||||
return html``;
|
||||
}
|
||||
|
||||
return html`
|
||||
<div>
|
||||
${this.computeDisplayAttributes(
|
||||
Object.keys(hassAttributeUtil.LOGIC_STATE_ATTRIBUTES).concat(
|
||||
this.extraFilters ? this.extraFilters.split(",") : []
|
||||
)
|
||||
).map(
|
||||
(attribute) => html`
|
||||
<div class="data-entry">
|
||||
<div class="key">${attribute.replace(/_/g, " ")}</div>
|
||||
<div class="value">
|
||||
${this.formatAttributeValue(attribute)}
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
)}
|
||||
${this.stateObj.attributes.attribution
|
||||
? html`
|
||||
<div class="attribution">
|
||||
${this.stateObj.attributes.attribution}
|
||||
</div>
|
||||
`
|
||||
: ""}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
static get styles(): CSSResult {
|
||||
return css`
|
||||
.data-entry {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
}
|
||||
.data-entry .value {
|
||||
max-width: 200px;
|
||||
overflow-wrap: break-word;
|
||||
}
|
||||
.attribution {
|
||||
color: var(--secondary-text-color);
|
||||
text-align: right;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
private computeDisplayAttributes(filtersArray: string[]): string[] {
|
||||
if (!this.stateObj) {
|
||||
return [];
|
||||
}
|
||||
return Object.keys(this.stateObj.attributes).filter((key) => {
|
||||
return filtersArray.indexOf(key) === -1;
|
||||
});
|
||||
}
|
||||
|
||||
private formatAttributeValue(attribute: string): string {
|
||||
if (!this.stateObj) {
|
||||
return "-";
|
||||
}
|
||||
const value = this.stateObj.attributes[attribute];
|
||||
if (value === null) {
|
||||
return "-";
|
||||
}
|
||||
if (Array.isArray(value)) {
|
||||
return value.join(", ");
|
||||
}
|
||||
return value instanceof Object ? JSON.stringify(value, null, 2) : value;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-attributes": HaAttributes;
|
||||
}
|
||||
}
|
@@ -122,8 +122,9 @@ class HaCameraStream extends LitElement {
|
||||
|
||||
private async _startHls(): Promise<void> {
|
||||
// tslint:disable-next-line
|
||||
const Hls = ((await import(/* webpackChunkName: "hls.js" */ "hls.js")) as any)
|
||||
.default as HLSModule;
|
||||
const Hls = ((await import(
|
||||
/* webpackChunkName: "hls.js" */ "hls.js"
|
||||
)) as any).default as HLSModule;
|
||||
let hlsSupported = Hls.isSupported();
|
||||
const videoEl = this._videoEl;
|
||||
|
||||
|
@@ -72,9 +72,7 @@ class HaClimateState extends LocalizeMixin(PolymerElement) {
|
||||
computeCurrentStatus(hass, stateObj) {
|
||||
if (!hass || !stateObj) return null;
|
||||
if (stateObj.attributes.current_temperature != null) {
|
||||
return `${stateObj.attributes.current_temperature} ${
|
||||
hass.config.unit_system.temperature
|
||||
}`;
|
||||
return `${stateObj.attributes.current_temperature} ${hass.config.unit_system.temperature}`;
|
||||
}
|
||||
if (stateObj.attributes.current_humidity != null) {
|
||||
return `${stateObj.attributes.current_humidity} %`;
|
||||
@@ -89,22 +87,16 @@ class HaClimateState extends LocalizeMixin(PolymerElement) {
|
||||
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}`;
|
||||
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
|
||||
}`;
|
||||
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
|
||||
}%`;
|
||||
return `${stateObj.attributes.target_humidity_low}-${stateObj.attributes.target_humidity_high}%`;
|
||||
}
|
||||
if (stateObj.attributes.humidity != null) {
|
||||
return `${stateObj.attributes.humidity} %`;
|
||||
@@ -121,9 +113,7 @@ class HaClimateState extends LocalizeMixin(PolymerElement) {
|
||||
const stateString = localize(`state.climate.${stateObj.state}`);
|
||||
return stateObj.attributes.hvac_action
|
||||
? `${localize(
|
||||
`state_attributes.climate.hvac_action.${
|
||||
stateObj.attributes.hvac_action
|
||||
}`
|
||||
`state_attributes.climate.hvac_action.${stateObj.attributes.hvac_action}`
|
||||
)} (${stateString})`
|
||||
: stateString;
|
||||
}
|
||||
|
@@ -3,7 +3,7 @@ import "@polymer/paper-input/paper-input";
|
||||
import "@polymer/paper-item/paper-item";
|
||||
import { html } from "@polymer/polymer/lib/utils/html-tag";
|
||||
import { PolymerElement } from "@polymer/polymer/polymer-element";
|
||||
import "@vaadin/vaadin-combo-box/vaadin-combo-box-light";
|
||||
import "@vaadin/vaadin-combo-box/theme/material/vaadin-combo-box-light";
|
||||
|
||||
import { EventsMixin } from "../mixins/events-mixin";
|
||||
|
||||
|
@@ -1,4 +1,5 @@
|
||||
import { classMap, html, customElement } from "@material/mwc-base/base-element";
|
||||
import { classMap } from "lit-html/directives/class-map";
|
||||
import { html, customElement } from "lit-element";
|
||||
import { ripple } from "@material/mwc-ripple/ripple-directive.js";
|
||||
|
||||
import "@material/mwc-fab";
|
||||
|
@@ -3,10 +3,8 @@ import {
|
||||
LitElement,
|
||||
html,
|
||||
property,
|
||||
query,
|
||||
CSSResult,
|
||||
css,
|
||||
PropertyValues,
|
||||
} from "lit-element";
|
||||
|
||||
import "./ha-form-string";
|
||||
@@ -16,6 +14,7 @@ import "./ha-form-boolean";
|
||||
import "./ha-form-select";
|
||||
import "./ha-form-positive_time_period_dict";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import { dynamicElement } from "../../common/dom/dynamic-element-directive";
|
||||
|
||||
export type HaFormSchema =
|
||||
| HaFormStringSchema
|
||||
@@ -100,20 +99,14 @@ export class HaForm extends LitElement implements HaFormElement {
|
||||
@property() public computeError?: (schema: HaFormSchema, error) => string;
|
||||
@property() public computeLabel?: (schema: HaFormSchema) => string;
|
||||
@property() public computeSuffix?: (schema: HaFormSchema) => string;
|
||||
@query("ha-form") private _childForm?: HaForm;
|
||||
@query("#element") private _elementContainer?: HTMLDivElement;
|
||||
|
||||
public focus() {
|
||||
const input = this._childForm
|
||||
? this._childForm
|
||||
: this._elementContainer
|
||||
? this._elementContainer.lastChild
|
||||
: undefined;
|
||||
|
||||
const input =
|
||||
this.shadowRoot!.getElementById("child-form") ||
|
||||
this.shadowRoot!.querySelector("ha-form");
|
||||
if (!input) {
|
||||
return;
|
||||
}
|
||||
|
||||
(input as HTMLElement).focus();
|
||||
}
|
||||
|
||||
@@ -151,40 +144,16 @@ export class HaForm extends LitElement implements HaFormElement {
|
||||
</div>
|
||||
`
|
||||
: ""}
|
||||
<div id="element"></div>
|
||||
${dynamicElement(`ha-form-${this.schema.type}`, {
|
||||
schema: this.schema,
|
||||
data: this.data,
|
||||
label: this._computeLabel(this.schema),
|
||||
suffix: this._computeSuffix(this.schema),
|
||||
id: "child-form",
|
||||
})}
|
||||
`;
|
||||
}
|
||||
|
||||
protected updated(changedProperties: PropertyValues) {
|
||||
const schemaChanged = changedProperties.has("schema");
|
||||
const oldSchema = schemaChanged
|
||||
? changedProperties.get("schema")
|
||||
: undefined;
|
||||
if (
|
||||
!Array.isArray(this.schema) &&
|
||||
schemaChanged &&
|
||||
(!oldSchema || (oldSchema as HaFormSchema).type !== this.schema.type)
|
||||
) {
|
||||
const element = document.createElement(
|
||||
`ha-form-${this.schema.type}`
|
||||
) as HaFormElement;
|
||||
element.schema = this.schema;
|
||||
element.data = this.data;
|
||||
element.label = this._computeLabel(this.schema);
|
||||
element.suffix = this._computeSuffix(this.schema);
|
||||
if (this._elementContainer!.lastChild) {
|
||||
this._elementContainer!.removeChild(this._elementContainer!.lastChild);
|
||||
}
|
||||
this._elementContainer!.appendChild(element);
|
||||
} else if (this._elementContainer && this._elementContainer.lastChild) {
|
||||
const element = this._elementContainer!.lastChild as HaFormElement;
|
||||
element.schema = this.schema;
|
||||
element.data = this.data;
|
||||
element.label = this._computeLabel(this.schema);
|
||||
element.suffix = this._computeSuffix(this.schema);
|
||||
}
|
||||
}
|
||||
|
||||
private _computeLabel(schema: HaFormSchema) {
|
||||
return this.computeLabel
|
||||
? this.computeLabel(schema)
|
||||
|
@@ -520,10 +520,13 @@ class HaSidebar extends LitElement {
|
||||
}
|
||||
|
||||
a {
|
||||
text-decoration: none;
|
||||
color: var(--sidebar-text-color);
|
||||
font-weight: 500;
|
||||
font-size: 14px;
|
||||
text-decoration: none;
|
||||
position: relative;
|
||||
display: block;
|
||||
outline: 0;
|
||||
}
|
||||
|
||||
paper-icon-item {
|
||||
@@ -546,7 +549,8 @@ class HaSidebar extends LitElement {
|
||||
color: var(--sidebar-icon-color);
|
||||
}
|
||||
|
||||
.iron-selected paper-icon-item:before {
|
||||
.iron-selected paper-icon-item::before,
|
||||
a:not(.iron-selected):focus::before {
|
||||
border-radius: 4px;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
@@ -555,11 +559,22 @@ class HaSidebar extends LitElement {
|
||||
left: 0;
|
||||
pointer-events: none;
|
||||
content: "";
|
||||
background-color: var(--sidebar-selected-icon-color);
|
||||
opacity: 0.12;
|
||||
transition: opacity 15ms linear;
|
||||
will-change: opacity;
|
||||
}
|
||||
.iron-selected paper-icon-item::before {
|
||||
background-color: var(--sidebar-selected-icon-color);
|
||||
opacity: 0.12;
|
||||
}
|
||||
a:not(.iron-selected):focus::before {
|
||||
background-color: currentColor;
|
||||
opacity: var(--dark-divider-opacity);
|
||||
margin: 4px 8px;
|
||||
}
|
||||
.iron-selected paper-icon-item:focus::before,
|
||||
.iron-selected:focus paper-icon-item::before {
|
||||
opacity: 0.2;
|
||||
}
|
||||
|
||||
.iron-selected paper-icon-item[pressed]:before {
|
||||
opacity: 0.37;
|
||||
|
@@ -58,14 +58,10 @@ class HaWaterHeaterState extends LocalizeMixin(PolymerElement) {
|
||||
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}`;
|
||||
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
|
||||
}`;
|
||||
return `${stateObj.attributes.temperature} ${hass.config.unit_system.temperature}`;
|
||||
}
|
||||
|
||||
return "";
|
||||
|
96
src/components/ha-yaml-editor.ts
Normal file
96
src/components/ha-yaml-editor.ts
Normal file
@@ -0,0 +1,96 @@
|
||||
import { safeDump, safeLoad } from "js-yaml";
|
||||
import "./ha-code-editor";
|
||||
import { LitElement, property, customElement, html, query } from "lit-element";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import { afterNextRender } from "../common/util/render-status";
|
||||
// tslint:disable-next-line
|
||||
import { HaCodeEditor } from "./ha-code-editor";
|
||||
|
||||
const isEmpty = (obj: object) => {
|
||||
if (typeof obj !== "object") {
|
||||
return false;
|
||||
}
|
||||
for (const key in obj) {
|
||||
if (obj.hasOwnProperty(key)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
@customElement("ha-yaml-editor")
|
||||
export class HaYamlEditor extends LitElement {
|
||||
@property() public value?: any;
|
||||
@property() public isValid = true;
|
||||
@property() public label?: string;
|
||||
@property() private _yaml?: string;
|
||||
@query("ha-code-editor") private _editor?: HaCodeEditor;
|
||||
|
||||
public setValue(value) {
|
||||
try {
|
||||
this._yaml = value && !isEmpty(value) ? safeDump(value) : "";
|
||||
} catch (err) {
|
||||
alert(`There was an error converting to YAML: ${err}`);
|
||||
}
|
||||
afterNextRender(() => {
|
||||
if (this._editor?.codemirror) {
|
||||
this._editor.codemirror.refresh();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
protected firstUpdated() {
|
||||
this.setValue(this.value);
|
||||
}
|
||||
|
||||
protected render() {
|
||||
if (this._yaml === undefined) {
|
||||
return;
|
||||
}
|
||||
return html`
|
||||
${this.label
|
||||
? html`
|
||||
<p>${this.label}</p>
|
||||
`
|
||||
: ""}
|
||||
<ha-code-editor
|
||||
.value=${this._yaml}
|
||||
mode="yaml"
|
||||
.error=${this.isValid === false}
|
||||
@value-changed=${this._onChange}
|
||||
></ha-code-editor>
|
||||
`;
|
||||
}
|
||||
|
||||
private _onChange(ev: CustomEvent) {
|
||||
ev.stopPropagation();
|
||||
const value = ev.detail.value;
|
||||
let parsed;
|
||||
let isValid = true;
|
||||
|
||||
if (value) {
|
||||
try {
|
||||
parsed = safeLoad(value);
|
||||
isValid = true;
|
||||
} catch (err) {
|
||||
// Invalid YAML
|
||||
isValid = false;
|
||||
}
|
||||
} else {
|
||||
parsed = {};
|
||||
}
|
||||
|
||||
this.value = parsed;
|
||||
this.isValid = isValid;
|
||||
|
||||
if (isValid) {
|
||||
fireEvent(this, "value-changed", { value: parsed });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-yaml-editor": HaYamlEditor;
|
||||
}
|
||||
}
|
@@ -4,6 +4,8 @@ import {
|
||||
} from "home-assistant-js-websocket";
|
||||
import { HomeAssistant } from "../types";
|
||||
import { navigate } from "../common/navigate";
|
||||
import { DeviceCondition, DeviceTrigger } from "./device_automation";
|
||||
import { Action } from "./script";
|
||||
|
||||
export interface AutomationEntity extends HassEntityBase {
|
||||
attributes: HassEntityAttributeBase & {
|
||||
@@ -15,11 +17,162 @@ export interface AutomationEntity extends HassEntityBase {
|
||||
export interface AutomationConfig {
|
||||
alias: string;
|
||||
description: string;
|
||||
trigger: any[];
|
||||
condition?: any[];
|
||||
action: any[];
|
||||
trigger: Trigger[];
|
||||
condition?: Condition[];
|
||||
action: Action[];
|
||||
}
|
||||
|
||||
export interface ForDict {
|
||||
hours?: number | string;
|
||||
minutes?: number | string;
|
||||
seconds?: number | string;
|
||||
}
|
||||
|
||||
export interface StateTrigger {
|
||||
platform: "state";
|
||||
entity_id?: string;
|
||||
from?: string | number;
|
||||
to?: string | number;
|
||||
for?: string | number | ForDict;
|
||||
}
|
||||
|
||||
export interface MqttTrigger {
|
||||
platform: "mqtt";
|
||||
topic: string;
|
||||
payload?: string;
|
||||
}
|
||||
|
||||
export interface GeoLocationTrigger {
|
||||
platform: "geo_location";
|
||||
source: "string";
|
||||
zone: "string";
|
||||
event: "enter" | "leave";
|
||||
}
|
||||
|
||||
export interface HassTrigger {
|
||||
platform: "homeassistant";
|
||||
event: "start" | "shutdown";
|
||||
}
|
||||
|
||||
export interface NumericStateTrigger {
|
||||
platform: "numeric_state";
|
||||
entity_id: string;
|
||||
above?: number;
|
||||
below?: number;
|
||||
value_template?: string;
|
||||
for?: string | number | ForDict;
|
||||
}
|
||||
|
||||
export interface SunTrigger {
|
||||
platform: "sun";
|
||||
offset: number;
|
||||
event: "sunrise" | "sunset";
|
||||
}
|
||||
|
||||
export interface TimePatternTrigger {
|
||||
platform: "time_pattern";
|
||||
hours?: number | string;
|
||||
minutes?: number | string;
|
||||
seconds?: number | string;
|
||||
}
|
||||
|
||||
export interface WebhookTrigger {
|
||||
platform: "webhook";
|
||||
webhook_id: string;
|
||||
}
|
||||
|
||||
export interface ZoneTrigger {
|
||||
platform: "zone";
|
||||
entity_id: string;
|
||||
zone: string;
|
||||
event: "enter" | "leave";
|
||||
}
|
||||
|
||||
export interface TimeTrigger {
|
||||
platform: "time";
|
||||
at: string;
|
||||
}
|
||||
|
||||
export interface TemplateTrigger {
|
||||
platform: "template";
|
||||
value_template: string;
|
||||
}
|
||||
|
||||
export interface EventTrigger {
|
||||
platform: "event";
|
||||
event_type: string;
|
||||
event_data: any;
|
||||
}
|
||||
|
||||
export type Trigger =
|
||||
| StateTrigger
|
||||
| MqttTrigger
|
||||
| GeoLocationTrigger
|
||||
| HassTrigger
|
||||
| NumericStateTrigger
|
||||
| SunTrigger
|
||||
| TimePatternTrigger
|
||||
| WebhookTrigger
|
||||
| ZoneTrigger
|
||||
| TimeTrigger
|
||||
| TemplateTrigger
|
||||
| EventTrigger
|
||||
| DeviceTrigger;
|
||||
|
||||
export interface LogicalCondition {
|
||||
condition: "and" | "or";
|
||||
conditions: Condition[];
|
||||
}
|
||||
|
||||
export interface StateCondition {
|
||||
condition: "state";
|
||||
entity_id: string;
|
||||
state: string | number;
|
||||
}
|
||||
|
||||
export interface NumericStateCondition {
|
||||
condition: "numeric_state";
|
||||
entity_id: string;
|
||||
above?: number;
|
||||
below?: number;
|
||||
value_template?: string;
|
||||
}
|
||||
|
||||
export interface SunCondition {
|
||||
condition: "sun";
|
||||
after_offset: number;
|
||||
before_offset: number;
|
||||
after: "sunrise" | "sunset";
|
||||
before: "sunrise" | "sunset";
|
||||
}
|
||||
|
||||
export interface ZoneCondition {
|
||||
condition: "zone";
|
||||
entity_id: string;
|
||||
zone: string;
|
||||
}
|
||||
|
||||
export interface TimeCondition {
|
||||
condition: "time";
|
||||
after: string;
|
||||
before: string;
|
||||
}
|
||||
|
||||
export interface TemplateCondition {
|
||||
condition: "template";
|
||||
value_template: string;
|
||||
}
|
||||
|
||||
export type Condition =
|
||||
| StateCondition
|
||||
| NumericStateCondition
|
||||
| SunCondition
|
||||
| ZoneCondition
|
||||
| TimeCondition
|
||||
| TemplateCondition
|
||||
| DeviceCondition
|
||||
| LogicalCondition;
|
||||
|
||||
export const deleteAutomation = (hass: HomeAssistant, id: string) =>
|
||||
hass.callApi("DELETE", `config/automation/config/${id}`);
|
||||
|
||||
|
@@ -19,9 +19,7 @@ export interface Stream {
|
||||
}
|
||||
|
||||
export const computeMJPEGStreamUrl = (entity: CameraEntity) =>
|
||||
`/api/camera_proxy_stream/${entity.entity_id}?token=${
|
||||
entity.attributes.access_token
|
||||
}`;
|
||||
`/api/camera_proxy_stream/${entity.entity_id}?token=${entity.attributes.access_token}`;
|
||||
|
||||
export const fetchThumbnailUrlWithCache = (
|
||||
hass: HomeAssistant,
|
||||
|
@@ -4,6 +4,8 @@ import { debounce } from "../common/util/debounce";
|
||||
import { getCollection, Connection } from "home-assistant-js-websocket";
|
||||
import { LocalizeFunc } from "../common/translations/localize";
|
||||
|
||||
export const DISCOVERY_SOURCES = ["unignore", "homekit", "ssdp", "zeroconf"];
|
||||
|
||||
export const createConfigFlow = (hass: HomeAssistant, handler: string) =>
|
||||
hass.callApi<DataEntryFlowStep>("POST", "config/config_entries/flow", {
|
||||
handler,
|
||||
@@ -26,6 +28,9 @@ export const handleConfigFlowStep = (
|
||||
data
|
||||
);
|
||||
|
||||
export const ignoreConfigFlow = (hass: HomeAssistant, flowId: string) =>
|
||||
hass.callWS({ type: "config_entries/ignore_flow", flow_id: flowId });
|
||||
|
||||
export const deleteConfigFlow = (hass: HomeAssistant, flowId: string) =>
|
||||
hass.callApi("DELETE", `config/config_entries/flow/${flowId}`);
|
||||
|
||||
|
@@ -3,7 +3,7 @@ import { HomeAssistant } from "../types";
|
||||
interface ProcessResults {
|
||||
card: { [key: string]: { [key: string]: string } };
|
||||
speech: {
|
||||
[SpeechType in "plain" | "ssml"]: { extra_data: any; speech: string }
|
||||
[SpeechType in "plain" | "ssml"]: { extra_data: any; speech: string };
|
||||
};
|
||||
}
|
||||
|
||||
|
@@ -18,7 +18,7 @@ export interface DeviceCondition extends DeviceAutomation {
|
||||
}
|
||||
|
||||
export interface DeviceTrigger extends DeviceAutomation {
|
||||
platform: string;
|
||||
platform: "device";
|
||||
}
|
||||
|
||||
export const fetchDeviceActions = (hass: HomeAssistant, deviceId: string) =>
|
||||
@@ -107,9 +107,7 @@ export const localizeDeviceAutomationAction = (
|
||||
state ? computeStateName(state) : "<unknown>",
|
||||
"subtype",
|
||||
hass.localize(
|
||||
`component.${action.domain}.device_automation.action_subtype.${
|
||||
action.subtype
|
||||
}`
|
||||
`component.${action.domain}.device_automation.action_subtype.${action.subtype}`
|
||||
)
|
||||
);
|
||||
};
|
||||
@@ -122,16 +120,12 @@ export const localizeDeviceAutomationCondition = (
|
||||
? hass.states[condition.entity_id]
|
||||
: undefined;
|
||||
return hass.localize(
|
||||
`component.${condition.domain}.device_automation.condition_type.${
|
||||
condition.type
|
||||
}`,
|
||||
`component.${condition.domain}.device_automation.condition_type.${condition.type}`,
|
||||
"entity_name",
|
||||
state ? computeStateName(state) : "<unknown>",
|
||||
"subtype",
|
||||
hass.localize(
|
||||
`component.${condition.domain}.device_automation.condition_subtype.${
|
||||
condition.subtype
|
||||
}`
|
||||
`component.${condition.domain}.device_automation.condition_subtype.${condition.subtype}`
|
||||
)
|
||||
);
|
||||
};
|
||||
@@ -142,16 +136,12 @@ export const localizeDeviceAutomationTrigger = (
|
||||
) => {
|
||||
const state = trigger.entity_id ? hass.states[trigger.entity_id] : undefined;
|
||||
return hass.localize(
|
||||
`component.${trigger.domain}.device_automation.trigger_type.${
|
||||
trigger.type
|
||||
}`,
|
||||
`component.${trigger.domain}.device_automation.trigger_type.${trigger.type}`,
|
||||
"entity_name",
|
||||
state ? computeStateName(state) : "<unknown>",
|
||||
"subtype",
|
||||
hass.localize(
|
||||
`component.${trigger.domain}.device_automation.trigger_subtype.${
|
||||
trigger.subtype
|
||||
}`
|
||||
`component.${trigger.domain}.device_automation.trigger_subtype.${trigger.subtype}`
|
||||
)
|
||||
);
|
||||
};
|
||||
|
@@ -149,9 +149,7 @@ export const createHassioSession = async (hass: HomeAssistant) => {
|
||||
"POST",
|
||||
"hassio/ingress/session"
|
||||
);
|
||||
document.cookie = `ingress_session=${
|
||||
response.data.session
|
||||
};path=/api/hassio_ingress/`;
|
||||
document.cookie = `ingress_session=${response.data.session};path=/api/hassio_ingress/`;
|
||||
};
|
||||
|
||||
export const reloadHassioAddons = (hass: HomeAssistant) =>
|
||||
|
7
src/data/logbook.ts
Normal file
7
src/data/logbook.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
export interface LogbookEntry {
|
||||
when: string;
|
||||
name: string;
|
||||
message: string;
|
||||
entity_id?: string;
|
||||
domain: string;
|
||||
}
|
@@ -69,6 +69,10 @@ export interface NoActionConfig extends BaseActionConfig {
|
||||
action: "none";
|
||||
}
|
||||
|
||||
export interface CustomActionConfig extends BaseActionConfig {
|
||||
action: "fire-dom-event";
|
||||
}
|
||||
|
||||
export interface BaseActionConfig {
|
||||
confirmation?: ConfirmationRestrictionConfig;
|
||||
}
|
||||
@@ -88,7 +92,8 @@ export type ActionConfig =
|
||||
| NavigateActionConfig
|
||||
| UrlActionConfig
|
||||
| MoreInfoActionConfig
|
||||
| NoActionConfig;
|
||||
| NoActionConfig
|
||||
| CustomActionConfig;
|
||||
|
||||
export const fetchConfig = (
|
||||
conn: Connection,
|
||||
@@ -108,6 +113,11 @@ export const saveConfig = (
|
||||
config,
|
||||
});
|
||||
|
||||
export const deleteConfig = (hass: HomeAssistant): Promise<void> =>
|
||||
hass.callWS({
|
||||
type: "lovelace/config/delete",
|
||||
});
|
||||
|
||||
export const subscribeLovelaceUpdates = (
|
||||
conn: Connection,
|
||||
onChange: () => void
|
||||
|
@@ -18,38 +18,6 @@ export const SCENE_IGNORED_DOMAINS = [
|
||||
"zone",
|
||||
];
|
||||
|
||||
export const SCENE_SAVED_ATTRIBUTES = {
|
||||
light: [
|
||||
"brightness",
|
||||
"color_temp",
|
||||
"effect",
|
||||
"rgb_color",
|
||||
"xy_color",
|
||||
"hs_color",
|
||||
],
|
||||
media_player: [
|
||||
"is_volume_muted",
|
||||
"volume_level",
|
||||
"sound_mode",
|
||||
"source",
|
||||
"media_content_id",
|
||||
"media_content_type",
|
||||
],
|
||||
climate: [
|
||||
"target_temperature",
|
||||
"target_temperature_high",
|
||||
"target_temperature_low",
|
||||
"target_humidity",
|
||||
"fan_mode",
|
||||
"swing_mode",
|
||||
"hvac_mode",
|
||||
"preset_mode",
|
||||
],
|
||||
vacuum: ["cleaning_mode"],
|
||||
fan: ["speed", "current_direction"],
|
||||
water_heather: ["temperature", "operation_mode"],
|
||||
};
|
||||
|
||||
export interface SceneEntity extends HassEntityBase {
|
||||
attributes: HassEntityAttributeBase & { id?: string };
|
||||
}
|
||||
|
@@ -1,5 +1,21 @@
|
||||
import { HomeAssistant } from "../types";
|
||||
import { computeObjectId } from "../common/entity/compute_object_id";
|
||||
import { Condition } from "./automation";
|
||||
import {
|
||||
HassEntityBase,
|
||||
HassEntityAttributeBase,
|
||||
} from "home-assistant-js-websocket";
|
||||
|
||||
export interface ScriptEntity extends HassEntityBase {
|
||||
attributes: HassEntityAttributeBase & {
|
||||
last_triggered: string;
|
||||
};
|
||||
}
|
||||
|
||||
export interface ScriptConfig {
|
||||
alias: string;
|
||||
sequence: Action[];
|
||||
}
|
||||
|
||||
export interface EventAction {
|
||||
event: string;
|
||||
@@ -7,12 +23,40 @@ export interface EventAction {
|
||||
event_data_template?: { [key: string]: any };
|
||||
}
|
||||
|
||||
export interface ServiceAction {
|
||||
service: string;
|
||||
entity_id?: string;
|
||||
data?: { [key: string]: any };
|
||||
}
|
||||
|
||||
export interface DeviceAction {
|
||||
device_id: string;
|
||||
domain: string;
|
||||
entity_id: string;
|
||||
}
|
||||
|
||||
export interface DelayAction {
|
||||
delay: number;
|
||||
}
|
||||
|
||||
export interface SceneAction {
|
||||
scene: string;
|
||||
}
|
||||
|
||||
export interface WaitAction {
|
||||
wait_template: string;
|
||||
timeout?: number;
|
||||
}
|
||||
|
||||
export type Action =
|
||||
| EventAction
|
||||
| DeviceAction
|
||||
| ServiceAction
|
||||
| Condition
|
||||
| DelayAction
|
||||
| SceneAction
|
||||
| WaitAction;
|
||||
|
||||
export const triggerScript = (
|
||||
hass: HomeAssistant,
|
||||
entityId: string,
|
||||
|
@@ -51,6 +51,12 @@ export interface ReadAttributeServiceData {
|
||||
manufacturer?: number;
|
||||
}
|
||||
|
||||
export interface ZHAGroup {
|
||||
name: string;
|
||||
group_id: number;
|
||||
members: ZHADevice[];
|
||||
}
|
||||
|
||||
export const reconfigureNode = (
|
||||
hass: HomeAssistant,
|
||||
ieeeAddress: string
|
||||
@@ -120,6 +126,32 @@ export const unbindDevices = (
|
||||
target_ieee: targetIEEE,
|
||||
});
|
||||
|
||||
export const bindDeviceToGroup = (
|
||||
hass: HomeAssistant,
|
||||
deviceIEEE: string,
|
||||
groupId: number,
|
||||
clusters: Cluster[]
|
||||
): Promise<void> =>
|
||||
hass.callWS({
|
||||
type: "zha/groups/bind",
|
||||
source_ieee: deviceIEEE,
|
||||
group_id: groupId,
|
||||
bindings: clusters,
|
||||
});
|
||||
|
||||
export const unbindDeviceFromGroup = (
|
||||
hass: HomeAssistant,
|
||||
deviceIEEE: string,
|
||||
groupId: number,
|
||||
clusters: Cluster[]
|
||||
): Promise<void> =>
|
||||
hass.callWS({
|
||||
type: "zha/groups/unbind",
|
||||
source_ieee: deviceIEEE,
|
||||
group_id: groupId,
|
||||
bindings: clusters,
|
||||
});
|
||||
|
||||
export const readAttributeValue = (
|
||||
hass: HomeAssistant,
|
||||
data: ReadAttributeServiceData
|
||||
@@ -153,3 +185,66 @@ export const fetchClustersForZhaNode = (
|
||||
type: "zha/devices/clusters",
|
||||
ieee: ieeeAddress,
|
||||
});
|
||||
|
||||
export const fetchGroups = (hass: HomeAssistant): Promise<ZHAGroup[]> =>
|
||||
hass.callWS({
|
||||
type: "zha/groups",
|
||||
});
|
||||
|
||||
export const removeGroups = (
|
||||
hass: HomeAssistant,
|
||||
groupIdsToRemove: number[]
|
||||
): Promise<ZHAGroup[]> =>
|
||||
hass.callWS({
|
||||
type: "zha/group/remove",
|
||||
group_ids: groupIdsToRemove,
|
||||
});
|
||||
|
||||
export const fetchGroup = (
|
||||
hass: HomeAssistant,
|
||||
groupId: number
|
||||
): Promise<ZHAGroup> =>
|
||||
hass.callWS({
|
||||
type: "zha/group",
|
||||
group_id: groupId,
|
||||
});
|
||||
|
||||
export const fetchGroupableDevices = (
|
||||
hass: HomeAssistant
|
||||
): Promise<ZHADevice[]> =>
|
||||
hass.callWS({
|
||||
type: "zha/devices/groupable",
|
||||
});
|
||||
|
||||
export const addMembersToGroup = (
|
||||
hass: HomeAssistant,
|
||||
groupId: number,
|
||||
membersToAdd: string[]
|
||||
): Promise<ZHAGroup> =>
|
||||
hass.callWS({
|
||||
type: "zha/group/members/add",
|
||||
group_id: groupId,
|
||||
members: membersToAdd,
|
||||
});
|
||||
|
||||
export const removeMembersFromGroup = (
|
||||
hass: HomeAssistant,
|
||||
groupId: number,
|
||||
membersToRemove: string[]
|
||||
): Promise<ZHAGroup> =>
|
||||
hass.callWS({
|
||||
type: "zha/group/members/remove",
|
||||
group_id: groupId,
|
||||
members: membersToRemove,
|
||||
});
|
||||
|
||||
export const addGroup = (
|
||||
hass: HomeAssistant,
|
||||
groupName: string,
|
||||
membersToAdd?: string[]
|
||||
): Promise<ZHAGroup> =>
|
||||
hass.callWS({
|
||||
type: "zha/group/add",
|
||||
group_name: groupName,
|
||||
members: membersToAdd,
|
||||
});
|
||||
|
@@ -98,9 +98,7 @@ class DialogConfigEntrySystemOptions extends LitElement {
|
||||
"ui.dialogs.config_entry_system_options.enable_new_entities_description",
|
||||
"integration",
|
||||
this.hass.localize(
|
||||
`component.${
|
||||
this._params.entry.domain
|
||||
}.config.title`
|
||||
`component.${this._params.entry.domain}.config.title`
|
||||
) || this._params.entry.domain
|
||||
)}
|
||||
</p>
|
||||
@@ -117,7 +115,7 @@ class DialogConfigEntrySystemOptions extends LitElement {
|
||||
.disabled=${this._submitting}
|
||||
>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.entity_registry.editor.update"
|
||||
"ui.panel.config.entities.editor.update"
|
||||
)}
|
||||
</mwc-button>
|
||||
</div>
|
||||
|
@@ -10,7 +10,9 @@ export interface ConfigEntrySystemOptionsDialogParams {
|
||||
}
|
||||
|
||||
export const loadConfigEntrySystemOptionsDialog = () =>
|
||||
import(/* webpackChunkName: "config-entry-system-options" */ "./dialog-config-entry-system-options");
|
||||
import(
|
||||
/* webpackChunkName: "config-entry-system-options" */ "./dialog-config-entry-system-options"
|
||||
);
|
||||
|
||||
export const showConfigEntrySystemOptionsDialog = (
|
||||
element: HTMLElement,
|
||||
|
@@ -11,6 +11,7 @@ import {
|
||||
import "@material/mwc-button";
|
||||
import "@polymer/paper-dialog-scrollable/paper-dialog-scrollable";
|
||||
import "@polymer/paper-tooltip/paper-tooltip";
|
||||
import "@polymer/paper-icon-button/paper-icon-button";
|
||||
import "@polymer/paper-spinner/paper-spinner";
|
||||
import { UnsubscribeFunc } from "home-assistant-js-websocket";
|
||||
|
||||
@@ -124,6 +125,7 @@ class DataEntryFlowDialog extends LitElement {
|
||||
<ha-paper-dialog
|
||||
with-backdrop
|
||||
opened
|
||||
modal
|
||||
@opened-changed=${this._openedChanged}
|
||||
>
|
||||
${this._loading || (this._step === null && this._handlers === undefined)
|
||||
@@ -134,53 +136,62 @@ class DataEntryFlowDialog extends LitElement {
|
||||
? // When we are going to next step, we render 1 round of empty
|
||||
// to reset the element.
|
||||
""
|
||||
: this._step === null
|
||||
? // Show handler picker
|
||||
html`
|
||||
<step-flow-pick-handler
|
||||
.flowConfig=${this._params.flowConfig}
|
||||
.hass=${this.hass}
|
||||
.handlers=${this._handlers}
|
||||
.showAdvanced=${this._params.showAdvanced}
|
||||
></step-flow-pick-handler>
|
||||
`
|
||||
: this._step.type === "form"
|
||||
? html`
|
||||
<step-flow-form
|
||||
.flowConfig=${this._params.flowConfig}
|
||||
.step=${this._step}
|
||||
.hass=${this.hass}
|
||||
></step-flow-form>
|
||||
`
|
||||
: this._step.type === "external"
|
||||
? html`
|
||||
<step-flow-external
|
||||
.flowConfig=${this._params.flowConfig}
|
||||
.step=${this._step}
|
||||
.hass=${this.hass}
|
||||
></step-flow-external>
|
||||
`
|
||||
: this._step.type === "abort"
|
||||
? html`
|
||||
<step-flow-abort
|
||||
.flowConfig=${this._params.flowConfig}
|
||||
.step=${this._step}
|
||||
.hass=${this.hass}
|
||||
></step-flow-abort>
|
||||
`
|
||||
: this._devices === undefined || this._areas === undefined
|
||||
? // When it's a create entry result, we will fetch device & area registry
|
||||
html`
|
||||
<step-flow-loading></step-flow-loading>
|
||||
`
|
||||
: html`
|
||||
<step-flow-create-entry
|
||||
.flowConfig=${this._params.flowConfig}
|
||||
.step=${this._step}
|
||||
.hass=${this.hass}
|
||||
.devices=${this._devices}
|
||||
.areas=${this._areas}
|
||||
></step-flow-create-entry>
|
||||
<paper-icon-button
|
||||
aria-label=${this.hass.localize(
|
||||
"ui.panel.config.integrations.config_flow.dismiss"
|
||||
)}
|
||||
icon="hass:close"
|
||||
dialog-dismiss
|
||||
></paper-icon-button>
|
||||
${this._step === null
|
||||
? // Show handler picker
|
||||
html`
|
||||
<step-flow-pick-handler
|
||||
.flowConfig=${this._params.flowConfig}
|
||||
.hass=${this.hass}
|
||||
.handlers=${this._handlers}
|
||||
.showAdvanced=${this._params.showAdvanced}
|
||||
></step-flow-pick-handler>
|
||||
`
|
||||
: this._step.type === "form"
|
||||
? html`
|
||||
<step-flow-form
|
||||
.flowConfig=${this._params.flowConfig}
|
||||
.step=${this._step}
|
||||
.hass=${this.hass}
|
||||
></step-flow-form>
|
||||
`
|
||||
: this._step.type === "external"
|
||||
? html`
|
||||
<step-flow-external
|
||||
.flowConfig=${this._params.flowConfig}
|
||||
.step=${this._step}
|
||||
.hass=${this.hass}
|
||||
></step-flow-external>
|
||||
`
|
||||
: this._step.type === "abort"
|
||||
? html`
|
||||
<step-flow-abort
|
||||
.flowConfig=${this._params.flowConfig}
|
||||
.step=${this._step}
|
||||
.hass=${this.hass}
|
||||
></step-flow-abort>
|
||||
`
|
||||
: this._devices === undefined || this._areas === undefined
|
||||
? // When it's a create entry result, we will fetch device & area registry
|
||||
html`
|
||||
<step-flow-loading></step-flow-loading>
|
||||
`
|
||||
: html`
|
||||
<step-flow-create-entry
|
||||
.flowConfig=${this._params.flowConfig}
|
||||
.step=${this._step}
|
||||
.hass=${this.hass}
|
||||
.devices=${this._devices}
|
||||
.areas=${this._areas}
|
||||
></step-flow-create-entry>
|
||||
`}
|
||||
`}
|
||||
</ha-paper-dialog>
|
||||
`;
|
||||
@@ -318,6 +329,12 @@ class DataEntryFlowDialog extends LitElement {
|
||||
display: block;
|
||||
padding: 0;
|
||||
}
|
||||
paper-icon-button {
|
||||
display: inline-block;
|
||||
padding: 8px;
|
||||
margin: 16px 16px 0 0;
|
||||
float: right;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
@@ -71,9 +71,7 @@ export const showConfigFlowDialog = (
|
||||
|
||||
renderShowFormStepFieldLabel(hass, step, field) {
|
||||
return hass.localize(
|
||||
`component.${step.handler}.config.step.${step.step_id}.data.${
|
||||
field.name
|
||||
}`
|
||||
`component.${step.handler}.config.step.${step.step_id}.data.${field.name}`
|
||||
);
|
||||
},
|
||||
|
||||
|
@@ -79,7 +79,9 @@ export interface DataEntryFlowDialogParams {
|
||||
}
|
||||
|
||||
export const loadDataEntryFlowDialog = () =>
|
||||
import(/* webpackChunkName: "dialog-config-flow" */ "./dialog-data-entry-flow");
|
||||
import(
|
||||
/* webpackChunkName: "dialog-config-flow" */ "./dialog-data-entry-flow"
|
||||
);
|
||||
|
||||
export const showFlowDialog = (
|
||||
element: HTMLElement,
|
||||
|
@@ -54,9 +54,7 @@ export const showOptionsFlowDialog = (
|
||||
|
||||
renderShowFormStepFieldLabel(hass, step, field) {
|
||||
return hass.localize(
|
||||
`component.${configEntry.domain}.options.step.${step.step_id}.data.${
|
||||
field.name
|
||||
}`
|
||||
`component.${configEntry.domain}.options.step.${step.step_id}.data.${field.name}`
|
||||
);
|
||||
},
|
||||
|
||||
|
@@ -109,6 +109,7 @@ class StepFlowForm extends LitElement {
|
||||
|
||||
protected firstUpdated(changedProps: PropertyValues) {
|
||||
super.firstUpdated(changedProps);
|
||||
setTimeout(() => this.shadowRoot!.querySelector("ha-form")!.focus(), 0);
|
||||
this.addEventListener("keypress", (ev) => {
|
||||
if (ev.keyCode === 13) {
|
||||
this._submitStep();
|
||||
|
@@ -19,6 +19,7 @@ import "../../components/ha-icon-next";
|
||||
import "../../common/search/search-input";
|
||||
import { styleMap } from "lit-html/directives/style-map";
|
||||
import { FlowConfig } from "./show-dialog-data-entry-flow";
|
||||
import { configFlowContentStyles } from "./styles";
|
||||
|
||||
interface HandlerObj {
|
||||
name: string;
|
||||
@@ -101,6 +102,14 @@ class StepFlowPickHandler extends LitElement {
|
||||
`;
|
||||
}
|
||||
|
||||
protected firstUpdated(changedProps) {
|
||||
super.firstUpdated(changedProps);
|
||||
setTimeout(
|
||||
() => this.shadowRoot!.querySelector("search-input")!.focus(),
|
||||
0
|
||||
);
|
||||
}
|
||||
|
||||
protected updated(changedProps) {
|
||||
super.updated(changedProps);
|
||||
// Store the width so that when we search, box doesn't jump
|
||||
@@ -125,28 +134,27 @@ class StepFlowPickHandler extends LitElement {
|
||||
});
|
||||
}
|
||||
|
||||
static get styles(): CSSResult {
|
||||
return css`
|
||||
h2 {
|
||||
margin-bottom: 2px;
|
||||
padding-left: 16px;
|
||||
}
|
||||
div {
|
||||
overflow: auto;
|
||||
max-height: 600px;
|
||||
}
|
||||
paper-item {
|
||||
cursor: pointer;
|
||||
}
|
||||
p {
|
||||
text-align: center;
|
||||
padding: 16px;
|
||||
margin: 0;
|
||||
}
|
||||
p > a {
|
||||
color: var(--primary-color);
|
||||
}
|
||||
`;
|
||||
static get styles(): CSSResult[] {
|
||||
return [
|
||||
configFlowContentStyles,
|
||||
css`
|
||||
div {
|
||||
overflow: auto;
|
||||
max-height: 600px;
|
||||
}
|
||||
paper-item {
|
||||
cursor: pointer;
|
||||
}
|
||||
p {
|
||||
text-align: center;
|
||||
padding: 16px;
|
||||
margin: 0;
|
||||
}
|
||||
p > a {
|
||||
color: var(--primary-color);
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -117,9 +117,7 @@ class DialogDeviceRegistryDetail extends LitElement {
|
||||
</paper-dialog-scrollable>
|
||||
<div class="paper-dialog-buttons">
|
||||
<mwc-button @click="${this._updateEntry}">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.entity_registry.editor.update"
|
||||
)}
|
||||
${this.hass.localize("ui.panel.config.entities.editor.update")}
|
||||
</mwc-button>
|
||||
</div>
|
||||
</ha-paper-dialog>
|
||||
|
@@ -12,7 +12,9 @@ export interface DeviceRegistryDetailDialogParams {
|
||||
}
|
||||
|
||||
export const loadDeviceRegistryDetailDialog = () =>
|
||||
import(/* webpackChunkName: "device-registry-detail-dialog" */ "./dialog-device-registry-detail");
|
||||
import(
|
||||
/* webpackChunkName: "device-registry-detail-dialog" */ "./dialog-device-registry-detail"
|
||||
);
|
||||
|
||||
export const showDeviceRegistryDetailDialog = (
|
||||
element: HTMLElement,
|
||||
|
@@ -6,7 +6,9 @@ export interface HaDomainTogglerDialogParams {
|
||||
}
|
||||
|
||||
export const loadDomainTogglerDialog = () =>
|
||||
import(/* webpackChunkName: "dialog-domain-toggler" */ "./dialog-domain-toggler");
|
||||
import(
|
||||
/* webpackChunkName: "dialog-domain-toggler" */ "./dialog-domain-toggler"
|
||||
);
|
||||
|
||||
export const showDomainTogglerDialog = (
|
||||
element: HTMLElement,
|
||||
|
@@ -100,7 +100,7 @@ class MoreInfoClimate extends LitElement {
|
||||
</div>
|
||||
`
|
||||
: ""}
|
||||
${stateObj.attributes.temperature
|
||||
${stateObj.attributes.temperature !== undefined
|
||||
? html`
|
||||
<ha-climate-control
|
||||
.value=${stateObj.attributes.temperature}
|
||||
@@ -112,8 +112,8 @@ class MoreInfoClimate extends LitElement {
|
||||
></ha-climate-control>
|
||||
`
|
||||
: ""}
|
||||
${stateObj.attributes.target_temp_low ||
|
||||
stateObj.attributes.target_temp_high
|
||||
${stateObj.attributes.target_temp_low !== undefined ||
|
||||
stateObj.attributes.target_temp_high !== undefined
|
||||
? html`
|
||||
<ha-climate-control
|
||||
.value=${stateObj.attributes.target_temp_low}
|
||||
|
@@ -2,7 +2,7 @@ import "@polymer/iron-flex-layout/iron-flex-layout-classes";
|
||||
import "@polymer/paper-input/paper-input";
|
||||
import { html } from "@polymer/polymer/lib/utils/html-tag";
|
||||
import { PolymerElement } from "@polymer/polymer/polymer-element";
|
||||
import "@vaadin/vaadin-date-picker/vaadin-date-picker";
|
||||
import "@vaadin/vaadin-date-picker/theme/material/vaadin-date-picker";
|
||||
|
||||
import "../../../components/ha-relative-time";
|
||||
import "../../../components/paper-time-input";
|
||||
|
@@ -1,6 +1,7 @@
|
||||
import "@polymer/app-layout/app-toolbar/app-toolbar";
|
||||
import "@polymer/paper-dialog-scrollable/paper-dialog-scrollable";
|
||||
import "@polymer/paper-icon-button/paper-icon-button";
|
||||
import "@material/mwc-button";
|
||||
import { html } from "@polymer/polymer/lib/utils/html-tag";
|
||||
import { PolymerElement } from "@polymer/polymer/polymer-element";
|
||||
|
||||
@@ -11,18 +12,25 @@ import "../../state-summary/state-card-content";
|
||||
|
||||
import "./controls/more-info-content";
|
||||
|
||||
import { navigate } from "../../common/navigate";
|
||||
import { computeStateName } from "../../common/entity/compute_state_name";
|
||||
import { computeStateDomain } from "../../common/entity/compute_state_domain";
|
||||
import { isComponentLoaded } from "../../common/config/is_component_loaded";
|
||||
import { DOMAINS_MORE_INFO_NO_HISTORY } from "../../common/const";
|
||||
import { EventsMixin } from "../../mixins/events-mixin";
|
||||
import LocalizeMixin from "../../mixins/localize-mixin";
|
||||
import { computeRTL } from "../../common/util/compute_rtl";
|
||||
import { removeEntityRegistryEntry } from "../../data/entity_registry";
|
||||
import { showConfirmationDialog } from "../confirmation/show-dialog-confirmation";
|
||||
|
||||
const DOMAINS_NO_INFO = ["camera", "configurator", "history_graph"];
|
||||
const EDITABLE_DOMAINS_WITH_ID = ["scene", "automation"];
|
||||
const EDITABLE_DOMAINS = ["script"];
|
||||
|
||||
/*
|
||||
* @appliesMixin EventsMixin
|
||||
*/
|
||||
class MoreInfoControls extends EventsMixin(PolymerElement) {
|
||||
class MoreInfoControls extends LocalizeMixin(EventsMixin(PolymerElement)) {
|
||||
static get template() {
|
||||
return html`
|
||||
<style include="ha-style-dialog">
|
||||
@@ -56,6 +64,10 @@ class MoreInfoControls extends EventsMixin(PolymerElement) {
|
||||
padding-bottom: 16px;
|
||||
}
|
||||
|
||||
mwc-button.warning {
|
||||
--mdc-theme-primary: var(--google-red-500);
|
||||
}
|
||||
|
||||
:host([domain="camera"]) paper-dialog-scrollable {
|
||||
margin: 0 -24px -21px;
|
||||
}
|
||||
@@ -68,7 +80,7 @@ class MoreInfoControls extends EventsMixin(PolymerElement) {
|
||||
|
||||
<app-toolbar>
|
||||
<paper-icon-button
|
||||
aria-label="Dismiss dialog"
|
||||
aria-label$="[[localize('ui.dialogs.more_info_control.dismiss')]]"
|
||||
icon="hass:close"
|
||||
dialog-dismiss
|
||||
></paper-icon-button>
|
||||
@@ -77,10 +89,18 @@ class MoreInfoControls extends EventsMixin(PolymerElement) {
|
||||
</div>
|
||||
<template is="dom-if" if="[[canConfigure]]">
|
||||
<paper-icon-button
|
||||
aria-label$="[[localize('ui.dialogs.more_info_control.settings')]]"
|
||||
icon="hass:settings"
|
||||
on-click="_gotoSettings"
|
||||
></paper-icon-button>
|
||||
</template>
|
||||
<template is="dom-if" if="[[_computeEdit(hass, stateObj)]]">
|
||||
<paper-icon-button
|
||||
aria-label$="[[localize('ui.dialogs.more_info_control.edit')]]"
|
||||
icon="hass:pencil"
|
||||
on-click="_gotoEdit"
|
||||
></paper-icon-button>
|
||||
</template>
|
||||
</app-toolbar>
|
||||
|
||||
<template is="dom-if" if="[[_computeShowStateInfo(stateObj)]]" restamp="">
|
||||
@@ -115,6 +135,15 @@ class MoreInfoControls extends EventsMixin(PolymerElement) {
|
||||
state-obj="[[stateObj]]"
|
||||
hass="[[hass]]"
|
||||
></more-info-content>
|
||||
<template
|
||||
is="dom-if"
|
||||
if="[[_computeShowRestored(stateObj)]]"
|
||||
restamp=""
|
||||
>
|
||||
[[localize('ui.dialogs.more_info_control.restored.not_provided')]] <br />
|
||||
[[localize('ui.dialogs.more_info_control.restored.remove_intro')]] <br />
|
||||
<mwc-button class="warning" on-click="_removeEntity">[[localize('ui.dialogs.more_info_control.restored.remove_action')]]</mwc-buttom>
|
||||
</template>
|
||||
</paper-dialog-scrollable>
|
||||
`;
|
||||
}
|
||||
@@ -170,6 +199,10 @@ class MoreInfoControls extends EventsMixin(PolymerElement) {
|
||||
return !stateObj || !DOMAINS_NO_INFO.includes(computeStateDomain(stateObj));
|
||||
}
|
||||
|
||||
_computeShowRestored(stateObj) {
|
||||
return stateObj && stateObj.attributes.restored;
|
||||
}
|
||||
|
||||
_computeShowHistoryComponent(hass, stateObj) {
|
||||
return (
|
||||
hass &&
|
||||
@@ -187,6 +220,16 @@ class MoreInfoControls extends EventsMixin(PolymerElement) {
|
||||
return stateObj ? computeStateName(stateObj) : "";
|
||||
}
|
||||
|
||||
_computeEdit(hass, stateObj) {
|
||||
const domain = this._computeDomain(stateObj);
|
||||
return (
|
||||
stateObj &&
|
||||
hass.user.is_admin &&
|
||||
((EDITABLE_DOMAINS_WITH_ID.includes(domain) && stateObj.attributes.id) ||
|
||||
EDITABLE_DOMAINS.includes(domain))
|
||||
);
|
||||
}
|
||||
|
||||
_stateObjChanged(newVal) {
|
||||
if (!newVal) {
|
||||
return;
|
||||
@@ -200,10 +243,38 @@ class MoreInfoControls extends EventsMixin(PolymerElement) {
|
||||
}
|
||||
}
|
||||
|
||||
_removeEntity() {
|
||||
showConfirmationDialog(this, {
|
||||
title: this.localize(
|
||||
"ui.dialogs.more_info_control.restored.confirm_remove_title"
|
||||
),
|
||||
text: this.localize(
|
||||
"ui.dialogs.more_info_control.restored.confirm_remove_text"
|
||||
),
|
||||
confirmBtnText: this.localize("ui.common.yes"),
|
||||
cancelBtnText: this.localize("ui.common.no"),
|
||||
confirm: () =>
|
||||
removeEntityRegistryEntry(this.hass, this.stateObj.entity_id),
|
||||
});
|
||||
}
|
||||
|
||||
_gotoSettings() {
|
||||
this.fire("more-info-page", { page: "settings" });
|
||||
}
|
||||
|
||||
_gotoEdit() {
|
||||
const domain = this._computeDomain(this.stateObj);
|
||||
navigate(
|
||||
this,
|
||||
`/config/${domain}/edit/${
|
||||
EDITABLE_DOMAINS_WITH_ID.includes(domain)
|
||||
? this.stateObj.attributes.id
|
||||
: this.stateObj.entity_id
|
||||
}`
|
||||
);
|
||||
this.fire("hass-more-info", { entityId: null });
|
||||
}
|
||||
|
||||
_computeRTL(hass) {
|
||||
return computeRTL(hass);
|
||||
}
|
||||
|
@@ -47,6 +47,7 @@ class MoreInfoSettings extends LocalizeMixin(EventsMixin(PolymerElement)) {
|
||||
|
||||
<app-toolbar>
|
||||
<ha-paper-icon-button-arrow-prev
|
||||
aria-label$="[[localize('ui.dialogs.more_info_settings.back')]]"
|
||||
on-click="_backTapped"
|
||||
></ha-paper-icon-button-arrow-prev>
|
||||
<div main-title="">[[_computeStateName(stateObj)]]</div>
|
||||
|
@@ -49,10 +49,10 @@ export class HuiNotificationDrawer extends EventsMixin(
|
||||
text-align: center;
|
||||
}
|
||||
</style>
|
||||
<app-drawer id='drawer' opened="{{open}}" disable-swipe align="start">
|
||||
<app-drawer id="drawer" opened="{{open}}" disable-swipe align="start">
|
||||
<app-toolbar>
|
||||
<div main-title>[[localize('ui.notification_drawer.title')]]</div>
|
||||
<ha-paper-icon-button-prev on-click="_closeDrawer"></paper-icon-button>
|
||||
<ha-paper-icon-button-prev on-click="_closeDrawer" aria-label$="[[localize('ui.notification_drawer.close')]]"></paper-icon-button>
|
||||
</app-toolbar>
|
||||
<div class="notifications">
|
||||
<template is="dom-if" if="[[!_empty(notifications)]]">
|
||||
|
@@ -1,7 +1,9 @@
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
|
||||
const loadVoiceCommandDialog = () =>
|
||||
import(/* webpackChunkName: "ha-voice-command-dialog" */ "./ha-voice-command-dialog");
|
||||
import(
|
||||
/* webpackChunkName: "ha-voice-command-dialog" */ "./ha-voice-command-dialog"
|
||||
);
|
||||
|
||||
export const showVoiceCommandDialog = (element: HTMLElement): void => {
|
||||
fireEvent(element, "show-dialog", {
|
||||
|
@@ -54,9 +54,8 @@ class DialogZHADeviceInfo extends LitElement {
|
||||
class="card"
|
||||
.hass=${this.hass}
|
||||
.device=${this._device}
|
||||
showActions
|
||||
isJoinPage
|
||||
@zha-device-removed=${this._onDeviceRemoved}
|
||||
.showEntityDetail=${false}
|
||||
></zha-device-card>
|
||||
`}
|
||||
</ha-paper-dialog>
|
||||
|
@@ -5,7 +5,9 @@ export interface ZHADeviceInfoDialogParams {
|
||||
}
|
||||
|
||||
export const loadZHADeviceInfoDialog = () =>
|
||||
import(/* webpackChunkName: "dialog-zha-device-info" */ "./dialog-zha-device-info");
|
||||
import(
|
||||
/* webpackChunkName: "dialog-zha-device-info" */ "./dialog-zha-device-info"
|
||||
);
|
||||
|
||||
export const showZHADeviceInfoDialog = (
|
||||
element: HTMLElement,
|
||||
|
@@ -10,6 +10,8 @@ import "../auth/ha-authorize";
|
||||
/* polyfill for paper-dropdown */
|
||||
setTimeout(
|
||||
() =>
|
||||
import(/* webpackChunkName: "polyfill-web-animations-next" */ "web-animations-js/web-animations-next-lite.min"),
|
||||
import(
|
||||
/* webpackChunkName: "polyfill-web-animations-next" */ "web-animations-js/web-animations-next-lite.min"
|
||||
),
|
||||
2000
|
||||
);
|
||||
|
@@ -23,13 +23,16 @@ declare global {
|
||||
}
|
||||
}
|
||||
|
||||
const isExternal = location.search.includes("external_auth=1");
|
||||
const isExternal =
|
||||
window.externalApp ||
|
||||
window.webkit?.messageHandlers?.getExternalAuth ||
|
||||
location.search.includes("external_auth=1");
|
||||
|
||||
const authProm = isExternal
|
||||
? () =>
|
||||
import(/* webpackChunkName: "external_auth" */ "../external_app/external_auth").then(
|
||||
({ createExternalAuth }) => createExternalAuth(hassUrl)
|
||||
)
|
||||
import(
|
||||
/* webpackChunkName: "external_auth" */ "../external_app/external_auth"
|
||||
).then(({ createExternalAuth }) => createExternalAuth(hassUrl))
|
||||
: () =>
|
||||
getAuth({
|
||||
hassUrl,
|
||||
@@ -52,8 +55,12 @@ const connProm = async (auth) => {
|
||||
throw err;
|
||||
}
|
||||
// We can get invalid auth if auth tokens were stored that are no longer valid
|
||||
// Clear stored tokens.
|
||||
if (!isExternal) {
|
||||
if (isExternal) {
|
||||
// Tell the external app to force refresh the access tokens.
|
||||
// This should trigger their unauthorized handling.
|
||||
await auth.refreshAccessToken(true);
|
||||
} else {
|
||||
// Clear stored tokens.
|
||||
saveTokens(null);
|
||||
}
|
||||
auth = await authProm();
|
||||
@@ -63,6 +70,9 @@ const connProm = async (auth) => {
|
||||
};
|
||||
|
||||
if (__DEV__) {
|
||||
// Remove adoptedStyleSheets so style inspector works on shadow DOM.
|
||||
// @ts-ignore
|
||||
delete Document.prototype.adoptedStyleSheets;
|
||||
performance.mark("hass-start");
|
||||
}
|
||||
window.hassConnection = authProm().then(connProm);
|
||||
|
@@ -11,6 +11,10 @@ interface BasePayload {
|
||||
callback: string;
|
||||
}
|
||||
|
||||
interface GetExternalAuthPayload extends BasePayload {
|
||||
force?: boolean;
|
||||
}
|
||||
|
||||
interface RefreshTokenResponse {
|
||||
access_token: string;
|
||||
expires_in: number;
|
||||
@@ -26,7 +30,7 @@ declare global {
|
||||
webkit?: {
|
||||
messageHandlers: {
|
||||
getExternalAuth: {
|
||||
postMessage(payload: BasePayload);
|
||||
postMessage(payload: GetExternalAuthPayload);
|
||||
};
|
||||
revokeExternalAuth: {
|
||||
postMessage(payload: BasePayload);
|
||||
@@ -60,8 +64,13 @@ class ExternalAuth extends Auth {
|
||||
});
|
||||
}
|
||||
|
||||
public async refreshAccessToken() {
|
||||
const callbackPayload = { callback: CALLBACK_SET_TOKEN };
|
||||
public async refreshAccessToken(force?: boolean) {
|
||||
const payload: GetExternalAuthPayload = {
|
||||
callback: CALLBACK_SET_TOKEN,
|
||||
};
|
||||
if (force) {
|
||||
payload.force = true;
|
||||
}
|
||||
|
||||
const callbackPromise = new Promise<RefreshTokenResponse>(
|
||||
(resolve, reject) => {
|
||||
@@ -73,11 +82,9 @@ class ExternalAuth extends Auth {
|
||||
await 0;
|
||||
|
||||
if (window.externalApp) {
|
||||
window.externalApp.getExternalAuth(JSON.stringify(callbackPayload));
|
||||
window.externalApp.getExternalAuth(JSON.stringify(payload));
|
||||
} else {
|
||||
window.webkit!.messageHandlers.getExternalAuth.postMessage(
|
||||
callbackPayload
|
||||
);
|
||||
window.webkit!.messageHandlers.getExternalAuth.postMessage(payload);
|
||||
}
|
||||
|
||||
const tokens = await callbackPromise;
|
||||
@@ -87,7 +94,7 @@ class ExternalAuth extends Auth {
|
||||
}
|
||||
|
||||
public async revoke() {
|
||||
const callbackPayload = { callback: CALLBACK_REVOKE_TOKEN };
|
||||
const payload: BasePayload = { callback: CALLBACK_REVOKE_TOKEN };
|
||||
|
||||
const callbackPromise = new Promise((resolve, reject) => {
|
||||
window[CALLBACK_REVOKE_TOKEN] = (success, data) =>
|
||||
@@ -97,11 +104,9 @@ class ExternalAuth extends Auth {
|
||||
await 0;
|
||||
|
||||
if (window.externalApp) {
|
||||
window.externalApp.revokeExternalAuth(JSON.stringify(callbackPayload));
|
||||
window.externalApp.revokeExternalAuth(JSON.stringify(payload));
|
||||
} else {
|
||||
window.webkit!.messageHandlers.revokeExternalAuth.postMessage(
|
||||
callbackPayload
|
||||
);
|
||||
window.webkit!.messageHandlers.revokeExternalAuth.postMessage(payload);
|
||||
}
|
||||
|
||||
await callbackPromise;
|
||||
|
@@ -74,20 +74,23 @@ export const provideHass = (
|
||||
restResponses.push([path, callback]);
|
||||
}
|
||||
|
||||
mockAPI(new RegExp("states/.+"), (
|
||||
// @ts-ignore
|
||||
method,
|
||||
path,
|
||||
parameters
|
||||
) => {
|
||||
const [domain, objectId] = path.substr(7).split(".", 2);
|
||||
if (!domain || !objectId) {
|
||||
return;
|
||||
mockAPI(
|
||||
new RegExp("states/.+"),
|
||||
(
|
||||
// @ts-ignore
|
||||
method,
|
||||
path,
|
||||
parameters
|
||||
) => {
|
||||
const [domain, objectId] = path.substr(7).split(".", 2);
|
||||
if (!domain || !objectId) {
|
||||
return;
|
||||
}
|
||||
addEntities(
|
||||
getEntity(domain, objectId, parameters.state, parameters.attributes)
|
||||
);
|
||||
}
|
||||
addEntities(
|
||||
getEntity(domain, objectId, parameters.state, parameters.attributes)
|
||||
);
|
||||
});
|
||||
);
|
||||
|
||||
const localLanguage = getLocalLanguage();
|
||||
|
||||
@@ -117,9 +120,7 @@ export const provideHass = (
|
||||
? callback(msg)
|
||||
: Promise.reject({
|
||||
code: "command_not_mocked",
|
||||
message: `WS Command ${
|
||||
msg.type
|
||||
} is not implemented in provide_hass.`,
|
||||
message: `WS Command ${msg.type} is not implemented in provide_hass.`,
|
||||
});
|
||||
},
|
||||
subscribeMessage: async (onChange, msg) => {
|
||||
@@ -128,9 +129,7 @@ export const provideHass = (
|
||||
? callback(msg, onChange)
|
||||
: Promise.reject({
|
||||
code: "command_not_mocked",
|
||||
message: `WS Command ${
|
||||
msg.type
|
||||
} is not implemented in provide_hass.`,
|
||||
message: `WS Command ${msg.type} is not implemented in provide_hass.`,
|
||||
});
|
||||
},
|
||||
subscribeEvents: async (
|
||||
|
@@ -9,12 +9,14 @@ import {
|
||||
} from "lit-element";
|
||||
import "../components/ha-menu-button";
|
||||
import "../components/ha-paper-icon-button-arrow-prev";
|
||||
import { classMap } from "lit-html/directives/class-map";
|
||||
|
||||
@customElement("hass-subpage")
|
||||
class HassSubpage extends LitElement {
|
||||
@property()
|
||||
public header?: string;
|
||||
|
||||
@property({ type: Boolean })
|
||||
public showBackButton = true;
|
||||
@property({ type: Boolean })
|
||||
public hassio = false;
|
||||
|
||||
@@ -25,6 +27,7 @@ class HassSubpage extends LitElement {
|
||||
aria-label="Back"
|
||||
.hassio=${this.hassio}
|
||||
@click=${this._backTapped}
|
||||
class=${classMap({ hidden: !this.showBackButton })}
|
||||
></ha-paper-icon-button-arrow-prev>
|
||||
|
||||
<div main-title>${this.header}</div>
|
||||
@@ -53,9 +56,9 @@ class HassSubpage extends LitElement {
|
||||
height: 64px;
|
||||
padding: 0 16px;
|
||||
pointer-events: none;
|
||||
background-color: var(--primary-color);
|
||||
background-color: var(--app-header-background-color);
|
||||
font-weight: 400;
|
||||
color: var(--text-primary-color, white);
|
||||
color: var(--app-header-text-color, white);
|
||||
}
|
||||
|
||||
ha-menu-button,
|
||||
@@ -64,6 +67,10 @@ class HassSubpage extends LitElement {
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
ha-paper-icon-button-arrow-prev.hidden {
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
[main-title] {
|
||||
margin: 0 0 0 24px;
|
||||
line-height: 20px;
|
||||
|
@@ -45,7 +45,9 @@ export class HomeAssistantAppEl extends HassElement {
|
||||
this._initialize();
|
||||
setTimeout(registerServiceWorker, 1000);
|
||||
/* polyfill for paper-dropdown */
|
||||
import(/* webpackChunkName: "polyfill-web-animations-next" */ "web-animations-js/web-animations-next-lite.min");
|
||||
import(
|
||||
/* webpackChunkName: "polyfill-web-animations-next" */ "web-animations-js/web-animations-next-lite.min"
|
||||
);
|
||||
}
|
||||
|
||||
protected updated(changedProps: PropertyValues): void {
|
||||
@@ -55,9 +57,10 @@ export class HomeAssistantAppEl extends HassElement {
|
||||
this._updateHass({ panelUrl: this._panelUrl });
|
||||
}
|
||||
if (changedProps.has("hass")) {
|
||||
this.hassChanged(this.hass!, changedProps.get("hass") as
|
||||
| HomeAssistant
|
||||
| undefined);
|
||||
this.hassChanged(
|
||||
this.hass!,
|
||||
changedProps.get("hass") as HomeAssistant | undefined
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -12,33 +12,59 @@ import { removeInitSkeleton } from "../util/init-skeleton";
|
||||
const CACHE_COMPONENTS = ["lovelace", "states", "developer-tools"];
|
||||
const COMPONENTS = {
|
||||
calendar: () =>
|
||||
import(/* webpackChunkName: "panel-calendar" */ "../panels/calendar/ha-panel-calendar"),
|
||||
import(
|
||||
/* webpackChunkName: "panel-calendar" */ "../panels/calendar/ha-panel-calendar"
|
||||
),
|
||||
config: () =>
|
||||
import(/* webpackChunkName: "panel-config" */ "../panels/config/ha-panel-config"),
|
||||
import(
|
||||
/* webpackChunkName: "panel-config" */ "../panels/config/ha-panel-config"
|
||||
),
|
||||
custom: () =>
|
||||
import(/* webpackChunkName: "panel-custom" */ "../panels/custom/ha-panel-custom"),
|
||||
import(
|
||||
/* webpackChunkName: "panel-custom" */ "../panels/custom/ha-panel-custom"
|
||||
),
|
||||
"developer-tools": () =>
|
||||
import(/* webpackChunkName: "panel-developer-tools" */ "../panels/developer-tools/ha-panel-developer-tools"),
|
||||
import(
|
||||
/* webpackChunkName: "panel-developer-tools" */ "../panels/developer-tools/ha-panel-developer-tools"
|
||||
),
|
||||
lovelace: () =>
|
||||
import(/* webpackChunkName: "panel-lovelace" */ "../panels/lovelace/ha-panel-lovelace"),
|
||||
import(
|
||||
/* webpackChunkName: "panel-lovelace" */ "../panels/lovelace/ha-panel-lovelace"
|
||||
),
|
||||
states: () =>
|
||||
import(/* webpackChunkName: "panel-states" */ "../panels/states/ha-panel-states"),
|
||||
import(
|
||||
/* webpackChunkName: "panel-states" */ "../panels/states/ha-panel-states"
|
||||
),
|
||||
history: () =>
|
||||
import(/* webpackChunkName: "panel-history" */ "../panels/history/ha-panel-history"),
|
||||
import(
|
||||
/* webpackChunkName: "panel-history" */ "../panels/history/ha-panel-history"
|
||||
),
|
||||
iframe: () =>
|
||||
import(/* webpackChunkName: "panel-iframe" */ "../panels/iframe/ha-panel-iframe"),
|
||||
import(
|
||||
/* webpackChunkName: "panel-iframe" */ "../panels/iframe/ha-panel-iframe"
|
||||
),
|
||||
kiosk: () =>
|
||||
import(/* webpackChunkName: "panel-kiosk" */ "../panels/kiosk/ha-panel-kiosk"),
|
||||
import(
|
||||
/* webpackChunkName: "panel-kiosk" */ "../panels/kiosk/ha-panel-kiosk"
|
||||
),
|
||||
logbook: () =>
|
||||
import(/* webpackChunkName: "panel-logbook" */ "../panels/logbook/ha-panel-logbook"),
|
||||
import(
|
||||
/* webpackChunkName: "panel-logbook" */ "../panels/logbook/ha-panel-logbook"
|
||||
),
|
||||
mailbox: () =>
|
||||
import(/* webpackChunkName: "panel-mailbox" */ "../panels/mailbox/ha-panel-mailbox"),
|
||||
import(
|
||||
/* webpackChunkName: "panel-mailbox" */ "../panels/mailbox/ha-panel-mailbox"
|
||||
),
|
||||
map: () =>
|
||||
import(/* webpackChunkName: "panel-map" */ "../panels/map/ha-panel-map"),
|
||||
profile: () =>
|
||||
import(/* webpackChunkName: "panel-profile" */ "../panels/profile/ha-panel-profile"),
|
||||
import(
|
||||
/* webpackChunkName: "panel-profile" */ "../panels/profile/ha-panel-profile"
|
||||
),
|
||||
"shopping-list": () =>
|
||||
import(/* webpackChunkName: "panel-shopping-list" */ "../panels/shopping-list/ha-panel-shopping-list"),
|
||||
import(
|
||||
/* webpackChunkName: "panel-shopping-list" */ "../panels/shopping-list/ha-panel-shopping-list"
|
||||
),
|
||||
};
|
||||
|
||||
const getRoutes = (panels: Panels): RouterOptions => {
|
||||
|
@@ -84,8 +84,12 @@ class HaOnboarding extends litLocalizeLiteMixin(HassElement) {
|
||||
protected firstUpdated(changedProps: PropertyValues) {
|
||||
super.firstUpdated(changedProps);
|
||||
this._fetchOnboardingSteps();
|
||||
import(/* webpackChunkName: "onboarding-integrations" */ "./onboarding-integrations");
|
||||
import(/* webpackChunkName: "onboarding-core-config" */ "./onboarding-core-config");
|
||||
import(
|
||||
/* webpackChunkName: "onboarding-integrations" */ "./onboarding-integrations"
|
||||
);
|
||||
import(
|
||||
/* webpackChunkName: "onboarding-core-config" */ "./onboarding-core-config"
|
||||
);
|
||||
registerServiceWorker(false);
|
||||
this.addEventListener("onboarding-step", (ev) => this._handleStepDone(ev));
|
||||
}
|
||||
|
@@ -116,9 +116,13 @@ class OnboardingCoreConfig extends LitElement {
|
||||
@value-changed=${this._handleChange}
|
||||
>
|
||||
<span slot="suffix">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.core.section.core.core_config.elevation_meters"
|
||||
)}
|
||||
${this._unitSystem === "metric"
|
||||
? this.hass.localize(
|
||||
"ui.panel.config.core.section.core.core_config.elevation_meters"
|
||||
)
|
||||
: this.hass.localize(
|
||||
"ui.panel.config.core.section.core.core_config.elevation_feet"
|
||||
)}
|
||||
</span>
|
||||
</paper-input>
|
||||
</div>
|
||||
@@ -266,7 +270,7 @@ class OnboardingCoreConfig extends LitElement {
|
||||
});
|
||||
} catch (err) {
|
||||
this._working = false;
|
||||
alert("FAIL");
|
||||
alert(`Failed to save: ${err.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -124,7 +124,9 @@ class OnboardingIntegrations extends LitElement {
|
||||
loadConfigFlowDialog();
|
||||
this._loadConfigEntries();
|
||||
/* polyfill for paper-dropdown */
|
||||
import(/* webpackChunkName: "polyfill-web-animations-next" */ "web-animations-js/web-animations-next-lite.min");
|
||||
import(
|
||||
/* webpackChunkName: "polyfill-web-animations-next" */ "web-animations-js/web-animations-next-lite.min"
|
||||
);
|
||||
}
|
||||
|
||||
private _createFlow() {
|
||||
|
@@ -47,9 +47,7 @@ class DialogAreaDetail extends LitElement {
|
||||
<h2>
|
||||
${entry
|
||||
? entry.name
|
||||
: this.hass.localize(
|
||||
"ui.panel.config.area_registry.editor.default_name"
|
||||
)}
|
||||
: this.hass.localize("ui.panel.config.areas.editor.default_name")}
|
||||
</h2>
|
||||
<paper-dialog-scrollable>
|
||||
${this._error
|
||||
@@ -81,9 +79,7 @@ class DialogAreaDetail extends LitElement {
|
||||
@click="${this._deleteEntry}"
|
||||
.disabled=${this._submitting}
|
||||
>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.area_registry.editor.delete"
|
||||
)}
|
||||
${this.hass.localize("ui.panel.config.areas.editor.delete")}
|
||||
</mwc-button>
|
||||
`
|
||||
: html``}
|
||||
@@ -92,12 +88,8 @@ class DialogAreaDetail extends LitElement {
|
||||
.disabled=${nameInvalid || this._submitting}
|
||||
>
|
||||
${entry
|
||||
? this.hass.localize(
|
||||
"ui.panel.config.area_registry.editor.update"
|
||||
)
|
||||
: this.hass.localize(
|
||||
"ui.panel.config.area_registry.editor.create"
|
||||
)}
|
||||
? this.hass.localize("ui.panel.config.areas.editor.update")
|
||||
: this.hass.localize("ui.panel.config.areas.editor.create")}
|
||||
</mwc-button>
|
||||
</div>
|
||||
</ha-paper-dialog>
|
@@ -5,6 +5,7 @@ import {
|
||||
css,
|
||||
CSSResult,
|
||||
property,
|
||||
customElement,
|
||||
} from "lit-element";
|
||||
import "@polymer/paper-item/paper-item";
|
||||
import "@polymer/paper-item/paper-item-body";
|
||||
@@ -30,7 +31,8 @@ import { classMap } from "lit-html/directives/class-map";
|
||||
import { computeRTL } from "../../../common/util/compute_rtl";
|
||||
import { UnsubscribeFunc } from "home-assistant-js-websocket";
|
||||
|
||||
class HaConfigAreaRegistry extends LitElement {
|
||||
@customElement("ha-config-areas")
|
||||
export class HaConfigAreas extends LitElement {
|
||||
@property() public hass!: HomeAssistant;
|
||||
@property() public isWide?: boolean;
|
||||
@property() private _areas?: AreaRegistryEntry[];
|
||||
@@ -51,24 +53,23 @@ class HaConfigAreaRegistry extends LitElement {
|
||||
}
|
||||
return html`
|
||||
<hass-subpage
|
||||
header="${this.hass.localize("ui.panel.config.area_registry.caption")}"
|
||||
.header="${this.hass.localize("ui.panel.config.areas.caption")}"
|
||||
.showBackButton=${!this.isWide}
|
||||
>
|
||||
<ha-config-section .isWide=${this.isWide}>
|
||||
<span slot="header">
|
||||
${this.hass.localize("ui.panel.config.area_registry.picker.header")}
|
||||
${this.hass.localize("ui.panel.config.areas.picker.header")}
|
||||
</span>
|
||||
<span slot="introduction">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.area_registry.picker.introduction"
|
||||
)}
|
||||
${this.hass.localize("ui.panel.config.areas.picker.introduction")}
|
||||
<p>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.area_registry.picker.introduction2"
|
||||
"ui.panel.config.areas.picker.introduction2"
|
||||
)}
|
||||
</p>
|
||||
<a href="/config/integrations/dashboard">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.area_registry.picker.integrations_page"
|
||||
"ui.panel.config.areas.picker.integrations_page"
|
||||
)}
|
||||
</a>
|
||||
</span>
|
||||
@@ -85,13 +86,9 @@ class HaConfigAreaRegistry extends LitElement {
|
||||
${this._areas.length === 0
|
||||
? html`
|
||||
<div class="empty">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.area_registry.no_areas"
|
||||
)}
|
||||
${this.hass.localize("ui.panel.config.areas.no_areas")}
|
||||
<mwc-button @click=${this._createArea}>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.area_registry.create_area"
|
||||
)}
|
||||
${this.hass.localize("ui.panel.config.areas.create_area")}
|
||||
</mwc-button>
|
||||
</div>
|
||||
`
|
||||
@@ -103,9 +100,7 @@ class HaConfigAreaRegistry extends LitElement {
|
||||
<ha-fab
|
||||
?is-wide=${this.isWide}
|
||||
icon="hass:plus"
|
||||
title="${this.hass.localize(
|
||||
"ui.panel.config.area_registry.create_area"
|
||||
)}"
|
||||
title="${this.hass.localize("ui.panel.config.areas.create_area")}"
|
||||
@click=${this._createArea}
|
||||
class="${classMap({
|
||||
rtl: computeRTL(this.hass),
|
||||
@@ -208,5 +203,3 @@ All devices in this area will become unassigned.`)
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define("ha-config-area-registry", HaConfigAreaRegistry);
|
@@ -14,7 +14,9 @@ export interface AreaRegistryDetailDialogParams {
|
||||
}
|
||||
|
||||
export const loadAreaRegistryDetailDialog = () =>
|
||||
import(/* webpackChunkName: "area-registry-detail-dialog" */ "./dialog-area-registry-detail");
|
||||
import(
|
||||
/* webpackChunkName: "area-registry-detail-dialog" */ "./dialog-area-registry-detail"
|
||||
);
|
||||
|
||||
export const showAreaRegistryDetailDialog = (
|
||||
element: HTMLElement,
|
272
src/panels/config/automation/action/ha-automation-action-row.ts
Normal file
272
src/panels/config/automation/action/ha-automation-action-row.ts
Normal file
@@ -0,0 +1,272 @@
|
||||
import "@polymer/paper-icon-button/paper-icon-button";
|
||||
import "@polymer/paper-item/paper-item";
|
||||
import "@polymer/paper-listbox/paper-listbox";
|
||||
// tslint:disable-next-line
|
||||
import { PaperListboxElement } from "@polymer/paper-listbox/paper-listbox";
|
||||
import "@polymer/paper-menu-button/paper-menu-button";
|
||||
import {
|
||||
css,
|
||||
CSSResult,
|
||||
customElement,
|
||||
html,
|
||||
LitElement,
|
||||
property,
|
||||
} from "lit-element";
|
||||
import { dynamicElement } from "../../../../common/dom/dynamic-element-directive";
|
||||
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||
import "../../../../components/ha-card";
|
||||
import { HomeAssistant } from "../../../../types";
|
||||
|
||||
import { Action } from "../../../../data/script";
|
||||
|
||||
import "./types/ha-automation-action-service";
|
||||
import "./types/ha-automation-action-device_id";
|
||||
import "./types/ha-automation-action-delay";
|
||||
import "./types/ha-automation-action-event";
|
||||
import "./types/ha-automation-action-condition";
|
||||
import "./types/ha-automation-action-scene";
|
||||
import "./types/ha-automation-action-wait_template";
|
||||
|
||||
const OPTIONS = [
|
||||
"condition",
|
||||
"delay",
|
||||
"device_id",
|
||||
"event",
|
||||
"scene",
|
||||
"service",
|
||||
"wait_template",
|
||||
];
|
||||
|
||||
const getType = (action: Action) => {
|
||||
return OPTIONS.find((option) => option in action);
|
||||
};
|
||||
|
||||
declare global {
|
||||
// for fire event
|
||||
interface HASSDomEvents {
|
||||
"move-action": { direction: "up" | "down" };
|
||||
}
|
||||
}
|
||||
|
||||
export interface ActionElement extends LitElement {
|
||||
action: Action;
|
||||
}
|
||||
|
||||
export const handleChangeEvent = (element: ActionElement, ev: CustomEvent) => {
|
||||
ev.stopPropagation();
|
||||
const name = (ev.target as any)?.name;
|
||||
if (!name) {
|
||||
return;
|
||||
}
|
||||
const newVal = ev.detail.value;
|
||||
|
||||
if ((element.action[name] || "") === newVal) {
|
||||
return;
|
||||
}
|
||||
|
||||
let newAction: Action;
|
||||
if (!newVal) {
|
||||
newAction = { ...element.action };
|
||||
delete newAction[name];
|
||||
} else {
|
||||
newAction = { ...element.action, [name]: newVal };
|
||||
}
|
||||
fireEvent(element, "value-changed", { value: newAction });
|
||||
};
|
||||
|
||||
@customElement("ha-automation-action-row")
|
||||
export default class HaAutomationActionRow extends LitElement {
|
||||
@property() public hass!: HomeAssistant;
|
||||
@property() public action!: Action;
|
||||
@property() public index!: number;
|
||||
@property() public totalActions!: number;
|
||||
@property() private _yamlMode = false;
|
||||
|
||||
protected render() {
|
||||
const type = getType(this.action);
|
||||
const selected = type ? OPTIONS.indexOf(type) : -1;
|
||||
const yamlMode = this._yamlMode || selected === -1;
|
||||
|
||||
return html`
|
||||
<ha-card>
|
||||
<div class="card-content">
|
||||
<div class="card-menu">
|
||||
${this.index !== 0
|
||||
? html`
|
||||
<paper-icon-button
|
||||
icon="hass:arrow-up"
|
||||
@click=${this._moveUp}
|
||||
></paper-icon-button>
|
||||
`
|
||||
: ""}
|
||||
${this.index !== this.totalActions - 1
|
||||
? html`
|
||||
<paper-icon-button
|
||||
icon="hass:arrow-down"
|
||||
@click=${this._moveDown}
|
||||
></paper-icon-button>
|
||||
`
|
||||
: ""}
|
||||
<paper-menu-button
|
||||
no-animations
|
||||
horizontal-align="right"
|
||||
horizontal-offset="-5"
|
||||
vertical-offset="-5"
|
||||
close-on-activate
|
||||
>
|
||||
<paper-icon-button
|
||||
icon="hass:dots-vertical"
|
||||
slot="dropdown-trigger"
|
||||
></paper-icon-button>
|
||||
<paper-listbox slot="dropdown-content">
|
||||
<paper-item
|
||||
@click=${this._switchYamlMode}
|
||||
.disabled=${selected === -1}
|
||||
>
|
||||
${yamlMode
|
||||
? this.hass.localize(
|
||||
"ui.panel.config.automation.editor.edit_ui"
|
||||
)
|
||||
: this.hass.localize(
|
||||
"ui.panel.config.automation.editor.edit_yaml"
|
||||
)}
|
||||
</paper-item>
|
||||
<paper-item disabled>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.automation.editor.actions.duplicate"
|
||||
)}
|
||||
</paper-item>
|
||||
<paper-item @click=${this._onDelete}>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.automation.editor.actions.delete"
|
||||
)}
|
||||
</paper-item>
|
||||
</paper-listbox>
|
||||
</paper-menu-button>
|
||||
</div>
|
||||
${yamlMode
|
||||
? html`
|
||||
<div style="margin-right: 24px;">
|
||||
${selected === -1
|
||||
? html`
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.automation.editor.actions.unsupported_action",
|
||||
"action",
|
||||
type
|
||||
)}
|
||||
`
|
||||
: ""}
|
||||
<ha-yaml-editor
|
||||
.value=${this.action}
|
||||
@value-changed=${this._onYamlChange}
|
||||
></ha-yaml-editor>
|
||||
</div>
|
||||
`
|
||||
: html`
|
||||
<paper-dropdown-menu-light
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.automation.editor.actions.type_select"
|
||||
)}
|
||||
no-animations
|
||||
>
|
||||
<paper-listbox
|
||||
slot="dropdown-content"
|
||||
.selected=${selected}
|
||||
@iron-select=${this._typeChanged}
|
||||
>
|
||||
${OPTIONS.map(
|
||||
(opt) => html`
|
||||
<paper-item .action=${opt}>
|
||||
${this.hass.localize(
|
||||
`ui.panel.config.automation.editor.actions.type.${opt}.label`
|
||||
)}
|
||||
</paper-item>
|
||||
`
|
||||
)}
|
||||
</paper-listbox>
|
||||
</paper-dropdown-menu-light>
|
||||
<div>
|
||||
${dynamicElement(`ha-automation-action-${type}`, {
|
||||
hass: this.hass,
|
||||
action: this.action,
|
||||
})}
|
||||
</div>
|
||||
`}
|
||||
</div>
|
||||
</ha-card>
|
||||
`;
|
||||
}
|
||||
|
||||
private _moveUp() {
|
||||
fireEvent(this, "move-action", { direction: "up" });
|
||||
}
|
||||
|
||||
private _moveDown() {
|
||||
fireEvent(this, "move-action", { direction: "down" });
|
||||
}
|
||||
|
||||
private _onDelete() {
|
||||
if (
|
||||
confirm(
|
||||
this.hass.localize(
|
||||
"ui.panel.config.automation.editor.actions.delete_confirm"
|
||||
)
|
||||
)
|
||||
) {
|
||||
fireEvent(this, "value-changed", { value: null });
|
||||
}
|
||||
}
|
||||
|
||||
private _typeChanged(ev: CustomEvent) {
|
||||
const type = ((ev.target as PaperListboxElement)?.selectedItem as any)
|
||||
?.action;
|
||||
|
||||
if (!type) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (type !== getType(this.action)) {
|
||||
const elClass = customElements.get(`ha-automation-action-${type}`);
|
||||
|
||||
fireEvent(this, "value-changed", {
|
||||
value: {
|
||||
...elClass.defaultConfig,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private _onYamlChange(ev: CustomEvent) {
|
||||
ev.stopPropagation();
|
||||
fireEvent(this, "value-changed", { value: ev.detail.value });
|
||||
}
|
||||
|
||||
private _switchYamlMode() {
|
||||
this._yamlMode = !this._yamlMode;
|
||||
}
|
||||
|
||||
static get styles(): CSSResult {
|
||||
return css`
|
||||
.card-menu {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
z-index: 3;
|
||||
color: var(--primary-text-color);
|
||||
}
|
||||
.rtl .card-menu {
|
||||
right: auto;
|
||||
left: 0;
|
||||
}
|
||||
.card-menu paper-item {
|
||||
cursor: pointer;
|
||||
}
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-automation-action-row": HaAutomationActionRow;
|
||||
}
|
||||
}
|
98
src/panels/config/automation/action/ha-automation-action.ts
Normal file
98
src/panels/config/automation/action/ha-automation-action.ts
Normal file
@@ -0,0 +1,98 @@
|
||||
import "@material/mwc-button";
|
||||
import {
|
||||
css,
|
||||
CSSResult,
|
||||
customElement,
|
||||
html,
|
||||
LitElement,
|
||||
property,
|
||||
} from "lit-element";
|
||||
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||
import "../../../../components/ha-card";
|
||||
import { Action } from "../../../../data/script";
|
||||
import { HomeAssistant } from "../../../../types";
|
||||
import "./ha-automation-action-row";
|
||||
|
||||
@customElement("ha-automation-action")
|
||||
export default class HaAutomationAction extends LitElement {
|
||||
@property() public hass!: HomeAssistant;
|
||||
@property() public actions!: Action[];
|
||||
|
||||
protected render() {
|
||||
return html`
|
||||
${this.actions.map(
|
||||
(action, idx) => html`
|
||||
<ha-automation-action-row
|
||||
.index=${idx}
|
||||
.totalActions=${this.actions.length}
|
||||
.action=${action}
|
||||
@move-action=${this._move}
|
||||
@value-changed=${this._actionChanged}
|
||||
.hass=${this.hass}
|
||||
></ha-automation-action-row>
|
||||
`
|
||||
)}
|
||||
<ha-card>
|
||||
<div class="card-actions add-card">
|
||||
<mwc-button @click=${this._addAction}>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.automation.editor.actions.add"
|
||||
)}
|
||||
</mwc-button>
|
||||
</div>
|
||||
</ha-card>
|
||||
`;
|
||||
}
|
||||
|
||||
private _addAction() {
|
||||
const actions = this.actions.concat({
|
||||
service: "",
|
||||
});
|
||||
|
||||
fireEvent(this, "value-changed", { value: actions });
|
||||
}
|
||||
|
||||
private _move(ev: CustomEvent) {
|
||||
const index = (ev.target as any).index;
|
||||
const newIndex = ev.detail.direction === "up" ? index - 1 : index + 1;
|
||||
const actions = this.actions.concat();
|
||||
const action = actions.splice(index, 1)[0];
|
||||
actions.splice(newIndex, 0, action);
|
||||
fireEvent(this, "value-changed", { value: actions });
|
||||
}
|
||||
|
||||
private _actionChanged(ev: CustomEvent) {
|
||||
ev.stopPropagation();
|
||||
const actions = [...this.actions];
|
||||
const newValue = ev.detail.value;
|
||||
const index = (ev.target as any).index;
|
||||
|
||||
if (newValue === null) {
|
||||
actions.splice(index, 1);
|
||||
} else {
|
||||
actions[index] = newValue;
|
||||
}
|
||||
|
||||
fireEvent(this, "value-changed", { value: actions });
|
||||
}
|
||||
|
||||
static get styles(): CSSResult {
|
||||
return css`
|
||||
ha-automation-action-row,
|
||||
ha-card {
|
||||
display: block;
|
||||
margin-top: 16px;
|
||||
}
|
||||
.add-card mwc-button {
|
||||
display: block;
|
||||
text-align: center;
|
||||
}
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-automation-action": HaAutomationAction;
|
||||
}
|
||||
}
|
@@ -0,0 +1,41 @@
|
||||
import "../../condition/ha-automation-condition-editor";
|
||||
|
||||
import { LitElement, property, customElement, html } from "lit-element";
|
||||
import { ActionElement } from "../ha-automation-action-row";
|
||||
import { HomeAssistant } from "../../../../../types";
|
||||
import { fireEvent } from "../../../../../common/dom/fire_event";
|
||||
import { Condition } from "../../../../../data/automation";
|
||||
|
||||
@customElement("ha-automation-action-condition")
|
||||
export class HaConditionAction extends LitElement implements ActionElement {
|
||||
@property() public hass!: HomeAssistant;
|
||||
@property() public action!: Condition;
|
||||
|
||||
public static get defaultConfig() {
|
||||
return { condition: "state" };
|
||||
}
|
||||
|
||||
public render() {
|
||||
return html`
|
||||
<ha-automation-condition-editor
|
||||
.condition=${this.action}
|
||||
.hass=${this.hass}
|
||||
@value-changed=${this._conditionChanged}
|
||||
></ha-automation-condition-editor>
|
||||
`;
|
||||
}
|
||||
|
||||
private _conditionChanged(ev: CustomEvent) {
|
||||
ev.stopPropagation();
|
||||
|
||||
fireEvent(this, "value-changed", {
|
||||
value: ev.detail.value,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-automation-action-condition": HaConditionAction;
|
||||
}
|
||||
}
|
@@ -0,0 +1,43 @@
|
||||
import "@polymer/paper-input/paper-input";
|
||||
import "../../../../../components/ha-service-picker";
|
||||
import "../../../../../components/entity/ha-entity-picker";
|
||||
|
||||
import { LitElement, property, customElement, html } from "lit-element";
|
||||
import { ActionElement, handleChangeEvent } from "../ha-automation-action-row";
|
||||
import { HomeAssistant } from "../../../../../types";
|
||||
import { DelayAction } from "../../../../../data/script";
|
||||
|
||||
@customElement("ha-automation-action-delay")
|
||||
export class HaDelayAction extends LitElement implements ActionElement {
|
||||
@property() public hass!: HomeAssistant;
|
||||
@property() public action!: DelayAction;
|
||||
|
||||
public static get defaultConfig() {
|
||||
return { delay: "" };
|
||||
}
|
||||
|
||||
public render() {
|
||||
const { delay } = this.action;
|
||||
|
||||
return html`
|
||||
<paper-input
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.automation.editor.actions.type.delay.delay"
|
||||
)}
|
||||
name="delay"
|
||||
.value=${delay}
|
||||
@value-changed=${this._valueChanged}
|
||||
></paper-input>
|
||||
`;
|
||||
}
|
||||
|
||||
private _valueChanged(ev: CustomEvent): void {
|
||||
handleChangeEvent(this, ev);
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-automation-action-delay": HaDelayAction;
|
||||
}
|
||||
}
|
@@ -0,0 +1,129 @@
|
||||
import "../../../../../components/device/ha-device-picker";
|
||||
import "../../../../../components/device/ha-device-action-picker";
|
||||
import "../../../../../components/ha-form/ha-form";
|
||||
|
||||
import {
|
||||
fetchDeviceActionCapabilities,
|
||||
deviceAutomationsEqual,
|
||||
DeviceAction,
|
||||
} from "../../../../../data/device_automation";
|
||||
import { LitElement, customElement, property, html } from "lit-element";
|
||||
import { fireEvent } from "../../../../../common/dom/fire_event";
|
||||
import { HomeAssistant } from "../../../../../types";
|
||||
|
||||
@customElement("ha-automation-action-device_id")
|
||||
export class HaDeviceAction extends LitElement {
|
||||
@property() public hass!: HomeAssistant;
|
||||
@property() public action!: DeviceAction;
|
||||
@property() private _deviceId?: string;
|
||||
@property() private _capabilities?;
|
||||
private _origAction?: DeviceAction;
|
||||
|
||||
public static get defaultConfig() {
|
||||
return {
|
||||
device_id: "",
|
||||
domain: "",
|
||||
entity_id: "",
|
||||
};
|
||||
}
|
||||
|
||||
protected render() {
|
||||
const deviceId = this._deviceId || this.action.device_id;
|
||||
const extraFieldsData =
|
||||
this._capabilities && this._capabilities.extra_fields
|
||||
? this._capabilities.extra_fields.map((item) => {
|
||||
return { [item.name]: this.action[item.name] };
|
||||
})
|
||||
: undefined;
|
||||
|
||||
return html`
|
||||
<ha-device-picker
|
||||
.value=${deviceId}
|
||||
@value-changed=${this._devicePicked}
|
||||
.hass=${this.hass}
|
||||
label="Device"
|
||||
></ha-device-picker>
|
||||
<ha-device-action-picker
|
||||
.value=${this.action}
|
||||
.deviceId=${deviceId}
|
||||
@value-changed=${this._deviceActionPicked}
|
||||
.hass=${this.hass}
|
||||
label="Action"
|
||||
></ha-device-action-picker>
|
||||
${extraFieldsData
|
||||
? html`
|
||||
<ha-form
|
||||
.data=${Object.assign({}, ...extraFieldsData)}
|
||||
.schema=${this._capabilities.extra_fields}
|
||||
.computeLabel=${this._extraFieldsComputeLabelCallback(
|
||||
this.hass.localize
|
||||
)}
|
||||
@value-changed=${this._extraFieldsChanged}
|
||||
></ha-form>
|
||||
`
|
||||
: ""}
|
||||
`;
|
||||
}
|
||||
|
||||
protected firstUpdated() {
|
||||
if (!this._capabilities) {
|
||||
this._getCapabilities();
|
||||
}
|
||||
if (this.action) {
|
||||
this._origAction = this.action;
|
||||
}
|
||||
}
|
||||
|
||||
protected updated(changedPros) {
|
||||
const prevAction = changedPros.get("action");
|
||||
if (prevAction && !deviceAutomationsEqual(prevAction, this.action)) {
|
||||
this._getCapabilities();
|
||||
}
|
||||
}
|
||||
|
||||
private async _getCapabilities() {
|
||||
const action = this.action;
|
||||
|
||||
this._capabilities = action.domain
|
||||
? await fetchDeviceActionCapabilities(this.hass, action)
|
||||
: null;
|
||||
}
|
||||
|
||||
private _devicePicked(ev) {
|
||||
ev.stopPropagation();
|
||||
this._deviceId = ev.target.value;
|
||||
}
|
||||
|
||||
private _deviceActionPicked(ev) {
|
||||
ev.stopPropagation();
|
||||
let action = ev.detail.value;
|
||||
if (this._origAction && deviceAutomationsEqual(this._origAction, action)) {
|
||||
action = this._origAction;
|
||||
}
|
||||
fireEvent(this, "value-changed", { value: action });
|
||||
}
|
||||
|
||||
private _extraFieldsChanged(ev) {
|
||||
ev.stopPropagation();
|
||||
fireEvent(this, "value-changed", {
|
||||
value: {
|
||||
...this.action,
|
||||
...ev.detail.value,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
private _extraFieldsComputeLabelCallback(localize) {
|
||||
// Returns a callback for ha-form to calculate labels per schema object
|
||||
return (schema) =>
|
||||
localize(
|
||||
`ui.panel.config.automation.editor.actions.type.device.extra_fields.${schema.name}`
|
||||
) || schema.name;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-automation-action-device_id": HaDeviceAction;
|
||||
}
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user