mirror of
https://github.com/home-assistant/frontend.git
synced 2025-08-13 03:09:26 +00:00
Compare commits
110 Commits
20220301.2
...
sensor_dev
Author | SHA1 | Date | |
---|---|---|---|
![]() |
c7e9ee785d | ||
![]() |
079cc39a6e | ||
![]() |
d6a1d5af79 | ||
![]() |
c0dce08e19 | ||
![]() |
a7a347ed05 | ||
![]() |
2d9b50defc | ||
![]() |
840858b18c | ||
![]() |
afd2e71f6c | ||
![]() |
88af0aa788 | ||
![]() |
49124f6f09 | ||
![]() |
73f5580555 | ||
![]() |
bdde5268c6 | ||
![]() |
15e972c158 | ||
![]() |
0fc4c24f5a | ||
![]() |
9eba50df0c | ||
![]() |
0e0e07437f | ||
![]() |
6ac51ede52 | ||
![]() |
ccf1fb573a | ||
![]() |
fa537968c4 | ||
![]() |
6bf2111a3c | ||
![]() |
ddf1cc0733 | ||
![]() |
9c1d1cb6f6 | ||
![]() |
470225abde | ||
![]() |
ee230b86c1 | ||
![]() |
f927fc64a9 | ||
![]() |
03677c33f7 | ||
![]() |
bc36a206da | ||
![]() |
af06ab1e2d | ||
![]() |
3e2135a485 | ||
![]() |
2e7f8fb46f | ||
![]() |
102568c4bd | ||
![]() |
4fcdae842e | ||
![]() |
ea19740f5a | ||
![]() |
3e0942b631 | ||
![]() |
0261cea796 | ||
![]() |
5247b2813f | ||
![]() |
8a5090684e | ||
![]() |
1784ba5e68 | ||
![]() |
4fbe9a7b10 | ||
![]() |
1ca9c7838a | ||
![]() |
4fc2c3ef05 | ||
![]() |
73ff8e28a8 | ||
![]() |
dde1c5e03c | ||
![]() |
01eed22592 | ||
![]() |
94ebb63589 | ||
![]() |
29119db5ce | ||
![]() |
9908162ac2 | ||
![]() |
1e929ae78a | ||
![]() |
ab5df0fe6e | ||
![]() |
d5010dda9e | ||
![]() |
4ac097f32b | ||
![]() |
5d3d15072f | ||
![]() |
5c53bc4225 | ||
![]() |
d5a307f8f4 | ||
![]() |
a27dd1e7f1 | ||
![]() |
c86ed1fb3e | ||
![]() |
7fa7a48072 | ||
![]() |
4e0fc8ee08 | ||
![]() |
5f6490e54e | ||
![]() |
db78b046a2 | ||
![]() |
c37fe1e7ff | ||
![]() |
f1ec479d41 | ||
![]() |
e01cb3ca82 | ||
![]() |
b8d3c68a7a | ||
![]() |
641003bb2a | ||
![]() |
3358fc2b18 | ||
![]() |
dcf50e055b | ||
![]() |
1fa04baa16 | ||
![]() |
84ffa2369a | ||
![]() |
cc27ddb362 | ||
![]() |
c4dc6bfb0d | ||
![]() |
4fbcc30a37 | ||
![]() |
4916527e5f | ||
![]() |
fad8a27232 | ||
![]() |
a993d3a753 | ||
![]() |
5dfe17a43a | ||
![]() |
9b6c935ffb | ||
![]() |
f4e28da0a3 | ||
![]() |
294a69d7e4 | ||
![]() |
f89b8cffcf | ||
![]() |
99fd3a1b6f | ||
![]() |
246e426182 | ||
![]() |
9f1e9b43fe | ||
![]() |
8301ae262c | ||
![]() |
d968fe41ee | ||
![]() |
db830e9014 | ||
![]() |
fc6b594a27 | ||
![]() |
86dbf99ebe | ||
![]() |
68e7ce1883 | ||
![]() |
4cdff3faea | ||
![]() |
0dac10aa23 | ||
![]() |
4b8b14a69d | ||
![]() |
9d28df31bd | ||
![]() |
8258641443 | ||
![]() |
dfcb0f6ba0 | ||
![]() |
2e10eb04b6 | ||
![]() |
b4b52d3872 | ||
![]() |
3873203721 | ||
![]() |
ccb91e0b49 | ||
![]() |
bd20c15a55 | ||
![]() |
0936fd9ae4 | ||
![]() |
adefc7a4e2 | ||
![]() |
8f8017ecff | ||
![]() |
604b79696e | ||
![]() |
8c445f6409 | ||
![]() |
797c871137 | ||
![]() |
24829bd903 | ||
![]() |
add92a559d | ||
![]() |
35a41b3490 | ||
![]() |
f59cb661cd |
15
.github/workflows/release.yaml
vendored
15
.github/workflows/release.yaml
vendored
@@ -10,10 +10,18 @@ env:
|
||||
NODE_VERSION: 14
|
||||
NODE_OPTIONS: --max_old_space_size=6144
|
||||
|
||||
# Set default workflow permissions
|
||||
# All scopes not mentioned here are set to no access
|
||||
# https://docs.github.com/en/actions/security-guides/automatic-token-authentication#permissions-for-the-github_token
|
||||
permissions:
|
||||
actions: none
|
||||
|
||||
jobs:
|
||||
release:
|
||||
name: Release
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write # Required to upload release assets
|
||||
steps:
|
||||
- name: Checkout the repository
|
||||
uses: actions/checkout@v2
|
||||
@@ -47,6 +55,13 @@ jobs:
|
||||
|
||||
script/release
|
||||
|
||||
- name: Upload release assets
|
||||
uses: softprops/action-gh-release@v0.1.14
|
||||
with:
|
||||
files: |
|
||||
dist/*.whl
|
||||
dist/*.tar.gz
|
||||
|
||||
wheels-init:
|
||||
name: Init wheels build
|
||||
needs: release
|
||||
|
@@ -1,4 +1,3 @@
|
||||
import "web-animations-js/web-animations-next-lite.min";
|
||||
import "../../../src/resources/ha-style";
|
||||
import "../../../src/resources/roboto";
|
||||
import "./layout/hc-lovelace";
|
||||
|
BIN
gallery/public/images/clearspace.png
Normal file
BIN
gallery/public/images/clearspace.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 44 KiB |
BIN
gallery/public/images/logo-variants.png
Normal file
BIN
gallery/public/images/logo-variants.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 35 KiB |
BIN
gallery/public/images/logo-with-text.png
Normal file
BIN
gallery/public/images/logo-with-text.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 67 KiB |
BIN
gallery/public/images/logo.png
Normal file
BIN
gallery/public/images/logo.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 27 KiB |
BIN
gallery/public/images/using-our-logo.png
Normal file
BIN
gallery/public/images/using-our-logo.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 32 KiB |
@@ -23,7 +23,7 @@ if [[ "${PULL_REQUEST}" == "true" ]]; then
|
||||
createStatus "pending" "Building design preview" "https://app.netlify.com/sites/home-assistant-gallery/deploys/$BUILD_ID"
|
||||
gulp build-gallery
|
||||
if [ $? -eq 0 ]; then
|
||||
createStatus "success" "Build complete" "$DEPLOY_URL"
|
||||
createStatus "success" "Build complete" "$DEPLOY_PRIME_URL"
|
||||
else
|
||||
createStatus "error" "Build failed" "https://app.netlify.com/sites/home-assistant-gallery/deploys/$BUILD_ID"
|
||||
fi
|
||||
|
@@ -36,12 +36,17 @@ module.exports = [
|
||||
category: "misc",
|
||||
header: "Miscelaneous",
|
||||
},
|
||||
{
|
||||
category: "brand",
|
||||
header: "Brand",
|
||||
},
|
||||
{
|
||||
category: "user-test",
|
||||
header: "User Tests",
|
||||
header: "Users",
|
||||
pages: ["user-types", "configuration-menu"],
|
||||
},
|
||||
{
|
||||
category: "design.home-assistant.io",
|
||||
header: "Design Documentation",
|
||||
header: "About",
|
||||
},
|
||||
];
|
||||
|
@@ -12,7 +12,14 @@ class PageDescription extends HaMarkdown {
|
||||
if (!PAGES[this.page].description) {
|
||||
return html``;
|
||||
}
|
||||
|
||||
return html`
|
||||
<div class="heading">
|
||||
<div class="title">
|
||||
${PAGES[this.page].metadata.title || this.page.split("/")[1]}
|
||||
</div>
|
||||
<div class="subtitle">${PAGES[this.page].metadata.subtitle}</div>
|
||||
</div>
|
||||
${until(
|
||||
PAGES[this.page]
|
||||
.description()
|
||||
@@ -25,9 +32,22 @@ class PageDescription extends HaMarkdown {
|
||||
static styles = [
|
||||
HaMarkdown.styles,
|
||||
css`
|
||||
.heading {
|
||||
padding: 16px;
|
||||
border-bottom: 1px solid var(--secondary-background-color);
|
||||
}
|
||||
.title {
|
||||
font-size: 42px;
|
||||
line-height: 56px;
|
||||
padding-bottom: 8px;
|
||||
}
|
||||
.subtitle {
|
||||
font-size: 18px;
|
||||
line-height: 24px;
|
||||
}
|
||||
.root {
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
margin: 16px auto;
|
||||
}
|
||||
.root > *:first-child {
|
||||
margin-top: 0;
|
||||
|
@@ -5,6 +5,7 @@ import { html, css, LitElement, PropertyValues } from "lit";
|
||||
import { customElement, property, query } from "lit/decorators";
|
||||
import "../../src/components/ha-icon-button";
|
||||
import "../../src/managers/notification-manager";
|
||||
import "../../src/components/ha-expansion-panel";
|
||||
import { haStyle } from "../../src/resources/styles";
|
||||
import { PAGES, SIDEBAR } from "../build/import-pages";
|
||||
import { dynamicElement } from "../../src/common/dom/dynamic-element-directive";
|
||||
@@ -44,6 +45,10 @@ class HaGallery extends LitElement {
|
||||
for (const page of group.pages!) {
|
||||
const key = `${group.category}/${page}`;
|
||||
const active = this._page === key;
|
||||
if (!(key in PAGES)) {
|
||||
console.error("Undefined page referenced in sidebar.js:", key);
|
||||
continue;
|
||||
}
|
||||
const title = PAGES[key].metadata.title || page;
|
||||
links.push(html`
|
||||
<a ?active=${active} href=${`#${group.category}/${page}`}>${title}</a>
|
||||
@@ -53,10 +58,9 @@ class HaGallery extends LitElement {
|
||||
sidebar.push(
|
||||
group.header
|
||||
? html`
|
||||
<details>
|
||||
<summary class="section">${group.header}</summary>
|
||||
<ha-expansion-panel .header=${group.header}>
|
||||
${links}
|
||||
</details>
|
||||
</ha-expansion-panel>
|
||||
`
|
||||
: links
|
||||
);
|
||||
@@ -92,27 +96,34 @@ class HaGallery extends LitElement {
|
||||
${dynamicElement(`demo-${this._page.replace("/", "-")}`)}
|
||||
</div>
|
||||
<div class="page-footer">
|
||||
${PAGES[this._page].description ||
|
||||
Object.keys(PAGES[this._page].metadata).length > 0
|
||||
? html`
|
||||
<a
|
||||
href=${`${GITHUB_DEMO_URL}${this._page}.markdown`}
|
||||
target="_blank"
|
||||
>
|
||||
Edit text
|
||||
</a>
|
||||
`
|
||||
: ""}
|
||||
${PAGES[this._page].demo
|
||||
? html`
|
||||
<a
|
||||
href=${`${GITHUB_DEMO_URL}${this._page}.ts`}
|
||||
target="_blank"
|
||||
>
|
||||
Edit demo
|
||||
</a>
|
||||
`
|
||||
: ""}
|
||||
<div class="header">Help us to improve our documentation</div>
|
||||
<div class="secondary">
|
||||
Suggest an edit to this page, or provide/view feedback for this
|
||||
page.
|
||||
</div>
|
||||
<div>
|
||||
${PAGES[this._page].description ||
|
||||
Object.keys(PAGES[this._page].metadata).length > 0
|
||||
? html`
|
||||
<a
|
||||
href=${`${GITHUB_DEMO_URL}${this._page}.markdown`}
|
||||
target="_blank"
|
||||
>
|
||||
Edit text
|
||||
</a>
|
||||
`
|
||||
: ""}
|
||||
${PAGES[this._page].demo
|
||||
? html`
|
||||
<a
|
||||
href=${`${GITHUB_DEMO_URL}${this._page}.ts`}
|
||||
target="_blank"
|
||||
>
|
||||
Edit demo
|
||||
</a>
|
||||
`
|
||||
: ""}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</mwc-drawer>
|
||||
@@ -186,27 +197,16 @@ class HaGallery extends LitElement {
|
||||
padding: 4px;
|
||||
}
|
||||
|
||||
.sidebar details {
|
||||
margin-top: 1em;
|
||||
margin-left: 1em;
|
||||
}
|
||||
|
||||
.sidebar summary {
|
||||
cursor: pointer;
|
||||
font-weight: bold;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.sidebar a {
|
||||
color: var(--primary-text-color);
|
||||
display: block;
|
||||
padding: 4px 12px;
|
||||
padding: 12px;
|
||||
text-decoration: none;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.sidebar a[active]::before {
|
||||
border-radius: 4px;
|
||||
border-radius: 12px;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 2px;
|
||||
@@ -237,14 +237,32 @@ class HaGallery extends LitElement {
|
||||
|
||||
.page-footer {
|
||||
text-align: center;
|
||||
margin: 16px 0;
|
||||
padding-top: 16px;
|
||||
border-top: 1px solid rgba(0, 0, 0, 0.12);
|
||||
margin: 16px;
|
||||
padding: 16px;
|
||||
border-radius: 12px;
|
||||
background-color: var(--primary-background-color);
|
||||
}
|
||||
|
||||
.page-footer div {
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.page-footer .header {
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
line-height: 28px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.page-footer .secondary {
|
||||
line-height: 23px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.page-footer a {
|
||||
display: inline-block;
|
||||
margin: 0 8px;
|
||||
text-decoration: none;
|
||||
}
|
||||
`,
|
||||
];
|
||||
|
34
gallery/src/pages/brand/logo.markdown
Normal file
34
gallery/src/pages/brand/logo.markdown
Normal file
@@ -0,0 +1,34 @@
|
||||
---
|
||||
title: "Logo"
|
||||
---
|
||||
|
||||

|
||||
|
||||
# Using our logo
|
||||
|
||||
As a community, we are proud of our logo. Follow these guidelines to ensure it always looks its best. Our logo follows Google's material design spec and uses the blue interface color.
|
||||
|
||||
[Download Logo](https://github.com/home-assistant/assets/tree/master/logo)
|
||||
|
||||

|
||||
|
||||
|
||||
## Using the icon
|
||||
|
||||
Our icon is a shorter and most used version of our logo. The icon can exist without the wordmark, the wordmark should never exist without the icon.
|
||||
|
||||

|
||||
|
||||
## Using the right variant
|
||||
|
||||
The pretty blue logo with a background shadow, pictured top left, is our primary logo. It should only be used with black, white, and non-duotone photography.
|
||||
|
||||
When needed you can use our logo without a shadow, as seen as the second variant.
|
||||
|
||||
The outlined logo should only be used on packaging.
|
||||
|
||||
## Exclusion zone
|
||||
|
||||
The logo needs some personal space. It's exclusion zone is equal to a quarter the height of the icon.
|
||||
|
||||

|
41
gallery/src/pages/brand/our-story.markdown
Normal file
41
gallery/src/pages/brand/our-story.markdown
Normal file
@@ -0,0 +1,41 @@
|
||||
---
|
||||
title: "Our story"
|
||||
---
|
||||
|
||||
## Open source home automation that puts local control and privacy first
|
||||
|
||||
Home Assistant is a free and open-source software for home automation that is designed to be the central control system for smart home devices with a focus on local control and privacy. It can be accessed via a web-based user interface, via apps for Android and iOS, or using voice commands via a supported virtual assistant like Google Assistant and Amazon Alexa.
|
||||
|
||||
IoT devices and services are supported by modular support for controlling proprietary ecosystems if they provide public access via an Open API for third-party integrations and protocols like Bluetooth, MQTT, Zigbee, and Z-Wave, After the Home Assistant software application is installed as a computer appliance it will act as a central control system for home automation. Information from all entities it sees can be used and controlled from within scripts trigger automations using scheduling and "blueprint" subroutines, e.g. for controlling lighting, climate, entertainment systems, and appliances.
|
||||
|
||||
# Open Home
|
||||
|
||||
The Open Home is our vision for the smart home. It defines the values that we put at the heart of every decision we make at Home Assistant. It’s woven into our architecture, licensing, community, and everything else.
|
||||
|
||||
The Open Home is about privacy, choice, and durability.
|
||||
|
||||
## Privacy
|
||||
|
||||
Your home should be your safe space. A place where you can be your true self without having to bother about what the world thinks of you. A place where you don’t need to act differently to avoid an algorithm categorizing your behavior. Privacy for the Open Home means that devices need to work locally. No one else needs to know if you turn on a light bulb or change the thermostat.
|
||||
|
||||
It is okay for a product to offer a cloud connection, but it should be extra and opt-in.
|
||||
|
||||
## Choice
|
||||
|
||||
Devices in your home gather data about themselves and their surroundings. Your data. Vendors shouldn’t be able to limit your access to your data or limit the interoperability of your devices with the rest of your smart home.
|
||||
|
||||
Choice for the Open Home means that devices need to make the gathered data available through local APIs. This avoids vendor lock-in and allows users to create their own smart home with devices from different manufacturers.
|
||||
|
||||
## Durability
|
||||
|
||||
If there is one thing that technology firms are very good at, it is launching new products. However, maintaining the products and making sure they keep working is an afterthought for most. The result is that vendors can decide to no longer support your device, crippling its features or even preventing it from working at all. As we install more and more devices in our home, durability is becoming more and more important. We shouldn’t have to buy everything new every couple of years because the manufacturer decided to move on.
|
||||
|
||||
Durability for the Open Home means that devices are designed and built to keep working. Not just this year, but for the next decade.
|
||||
|
||||
# Our history
|
||||
|
||||
The project was started as a Python application by Paulus Schoutsen in September 2013 and first published publicly on GitHub in November 2013. In July 2017, a managed operating system called Hass.io was initially introduced to make it easier use to use Home Assistant on single-board computers like the Raspberry Pi series. Its bundled "supervisor" management system allowed users to manage, backup, and update the local installation and introduced the option to extend the functionality of the software with add-ons.
|
||||
|
||||
An optional subscription service was introduced in December 2017 for $5/month to solve the complexities associated with secured remote access, as well as linking to Amazon Alexa and Google Assistant. Nabu Casa, Inc. was formed in September 2018 to take over the subscription service. The company's funding is based solely on revenue from the subscription service. It is used to finance the project's infrastructure and to pay for full-time employees contributing to the project.
|
||||
|
||||
In January 2020, branding was adjusted to make it easier to refer to different parts of the project. The main piece of software was renamed to Home Assistant Core, while the full suite of software with the embedded operating system and bundled "supervisor" management system was renamed to Home Assistant.
|
@@ -1,5 +1,6 @@
|
||||
---
|
||||
title: Alerts
|
||||
subtitle: An alert displays a short, important message in a way that attracts the user's attention without interrupting the user's task.
|
||||
---
|
||||
|
||||
# Alert `<ha-alert>`
|
||||
|
@@ -12,6 +12,98 @@ import { mockEntityRegistry } from "../../../../demo/src/stubs/entity_registry";
|
||||
import { mockHassioSupervisor } from "../../../../demo/src/stubs/hassio_supervisor";
|
||||
import { provideHass } from "../../../../src/fake_data/provide_hass";
|
||||
import { HomeAssistant } from "../../../../src/types";
|
||||
import { getEntity } from "../../../../src/fake_data/entity";
|
||||
|
||||
const ENTITIES = [
|
||||
getEntity("alarm_control_panel", "alarm", "disarmed", {
|
||||
friendly_name: "Alarm",
|
||||
}),
|
||||
getEntity("media_player", "livingroom", "playing", {
|
||||
friendly_name: "Livingroom",
|
||||
}),
|
||||
getEntity("media_player", "lounge", "idle", {
|
||||
friendly_name: "Lounge",
|
||||
supported_features: 444983,
|
||||
}),
|
||||
getEntity("light", "bedroom", "on", {
|
||||
friendly_name: "Bedroom",
|
||||
}),
|
||||
getEntity("switch", "coffee", "off", {
|
||||
friendly_name: "Coffee",
|
||||
}),
|
||||
];
|
||||
|
||||
const DEVICES = [
|
||||
{
|
||||
area_id: "bedroom",
|
||||
configuration_url: null,
|
||||
config_entries: ["config_entry_1"],
|
||||
connections: [],
|
||||
disabled_by: null,
|
||||
entry_type: null,
|
||||
id: "device_1",
|
||||
identifiers: [["demo", "volume1"] as [string, string]],
|
||||
manufacturer: null,
|
||||
model: null,
|
||||
name_by_user: null,
|
||||
name: "Dishwasher",
|
||||
sw_version: null,
|
||||
hw_version: null,
|
||||
via_device_id: null,
|
||||
},
|
||||
{
|
||||
area_id: "backyard",
|
||||
configuration_url: null,
|
||||
config_entries: ["config_entry_2"],
|
||||
connections: [],
|
||||
disabled_by: null,
|
||||
entry_type: null,
|
||||
id: "device_2",
|
||||
identifiers: [["demo", "pwm1"] as [string, string]],
|
||||
manufacturer: null,
|
||||
model: null,
|
||||
name_by_user: null,
|
||||
name: "Lamp",
|
||||
sw_version: null,
|
||||
hw_version: null,
|
||||
via_device_id: null,
|
||||
},
|
||||
{
|
||||
area_id: null,
|
||||
configuration_url: null,
|
||||
config_entries: ["config_entry_3"],
|
||||
connections: [],
|
||||
disabled_by: null,
|
||||
entry_type: null,
|
||||
id: "device_3",
|
||||
identifiers: [["demo", "pwm1"] as [string, string]],
|
||||
manufacturer: null,
|
||||
model: null,
|
||||
name_by_user: "User name",
|
||||
name: "Technical name",
|
||||
sw_version: null,
|
||||
hw_version: null,
|
||||
via_device_id: null,
|
||||
},
|
||||
];
|
||||
|
||||
const AREAS = [
|
||||
{
|
||||
area_id: "backyard",
|
||||
name: "Backyard",
|
||||
picture: null,
|
||||
},
|
||||
{
|
||||
area_id: "bedroom",
|
||||
name: "Bedroom",
|
||||
picture: null,
|
||||
},
|
||||
{
|
||||
area_id: "livingroom",
|
||||
name: "Livingroom",
|
||||
picture: null,
|
||||
},
|
||||
];
|
||||
|
||||
const SCHEMAS: {
|
||||
title: string;
|
||||
@@ -39,6 +131,7 @@ const SCHEMAS: {
|
||||
icon: "Icon",
|
||||
media: "Media",
|
||||
location: "Location",
|
||||
entities: "Entities",
|
||||
},
|
||||
schema: [
|
||||
{ name: "addon", selector: { addon: {} } },
|
||||
@@ -46,6 +139,7 @@ const SCHEMAS: {
|
||||
{
|
||||
name: "Attribute",
|
||||
selector: { attribute: { entity_id: "" } },
|
||||
context: { filter_entity: "entity" },
|
||||
},
|
||||
{ name: "Device", selector: { device: {} } },
|
||||
{ name: "Duration", selector: { duration: {} } },
|
||||
@@ -80,6 +174,10 @@ const SCHEMAS: {
|
||||
name: "location",
|
||||
selector: { location: { radius: true, icon: "mdi:home" } },
|
||||
},
|
||||
{
|
||||
name: "entities",
|
||||
selector: { entity: { multiple: true } },
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
@@ -320,9 +418,10 @@ class DemoHaForm extends LitElement {
|
||||
const hass = provideHass(this);
|
||||
hass.updateTranslations(null, "en");
|
||||
hass.updateTranslations("config", "en");
|
||||
hass.addEntities(ENTITIES);
|
||||
mockEntityRegistry(hass);
|
||||
mockDeviceRegistry(hass);
|
||||
mockAreaRegistry(hass);
|
||||
mockDeviceRegistry(hass, DEVICES);
|
||||
mockAreaRegistry(hass, AREAS);
|
||||
mockHassioSupervisor(hass);
|
||||
}
|
||||
|
||||
|
@@ -146,6 +146,8 @@ const SCHEMAS: {
|
||||
},
|
||||
boolean: { name: "Boolean", selector: { boolean: {} } },
|
||||
time: { name: "Time", selector: { time: {} } },
|
||||
date: { name: "Date", selector: { date: {} } },
|
||||
datetime: { name: "Date Time", selector: { datetime: {} } },
|
||||
action: { name: "Action", selector: { action: {} } },
|
||||
text: {
|
||||
name: "Text",
|
||||
@@ -162,9 +164,24 @@ const SCHEMAS: {
|
||||
},
|
||||
},
|
||||
object: { name: "Object", selector: { object: {} } },
|
||||
select_radio: {
|
||||
name: "Select (Radio)",
|
||||
selector: { select: { options: ["Option 1", "Option 2"] } },
|
||||
},
|
||||
select: {
|
||||
name: "Select",
|
||||
selector: { select: { options: ["Option 1", "Option 2"] } },
|
||||
selector: {
|
||||
select: {
|
||||
options: [
|
||||
"Option 1",
|
||||
"Option 2",
|
||||
"Option 3",
|
||||
"Option 4",
|
||||
"Option 5",
|
||||
"Option 6",
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
icon: { name: "Icon", selector: { icon: {} } },
|
||||
media: { name: "Media", selector: { media: {} } },
|
||||
@@ -173,6 +190,18 @@ const SCHEMAS: {
|
||||
name: "Location with radius",
|
||||
selector: { location: { radius: true, icon: "mdi:home" } },
|
||||
},
|
||||
color_temp: {
|
||||
name: "Color Temperature",
|
||||
selector: { color_temp: {} },
|
||||
},
|
||||
color_rgb: { name: "Color", selector: { color_rgb: {} } },
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Multiples",
|
||||
input: {
|
||||
entity: { name: "Entity", selector: { entity: { multiple: true } } },
|
||||
device: { name: "Device", selector: { device: { multiple: true } } },
|
||||
},
|
||||
},
|
||||
];
|
||||
|
@@ -2,6 +2,8 @@
|
||||
title: Editing design.home-assistant.io
|
||||
---
|
||||
|
||||

|
||||
|
||||
# How to edit design.home-assistant.io
|
||||
|
||||
All pages are stored in [the pages folder][pages-folder] on GitHub. Pages are grouped in a folder per sidebar section. Each page can contain a `<page name>.markdown` description file, a `<page name>.ts` demo file or both. If both are defined the description is rendered first. The description can contain metadata to specify the title of the page.
|
||||
@@ -41,15 +43,12 @@ import { html, css, LitElement } from "lit";
|
||||
import { customElement } from "lit/decorators";
|
||||
import "../../../../src/components/ha-card";
|
||||
|
||||
|
||||
@customElement("demo-user-experience-usability")
|
||||
export class DemoUserExperienceUsability extends LitElement {
|
||||
protected render() {
|
||||
return html`
|
||||
<ha-card>
|
||||
<div class="card-content">
|
||||
Hello world!
|
||||
</div>
|
||||
<div class="card-content">Hello world!</div>
|
||||
</ha-card>
|
||||
`;
|
||||
}
|
||||
|
@@ -188,6 +188,7 @@ const createEntityRegistryEntries = (
|
||||
device_id: "mock-device-id",
|
||||
area_id: null,
|
||||
disabled_by: null,
|
||||
hidden_by: null,
|
||||
entity_category: null,
|
||||
entity_id: "binary_sensor.updater",
|
||||
name: null,
|
||||
|
3
gallery/src/pages/more-info/update.markdown
Normal file
3
gallery/src/pages/more-info/update.markdown
Normal file
@@ -0,0 +1,3 @@
|
||||
---
|
||||
title: Update
|
||||
---
|
140
gallery/src/pages/more-info/update.ts
Normal file
140
gallery/src/pages/more-info/update.ts
Normal file
@@ -0,0 +1,140 @@
|
||||
import { html, LitElement, PropertyValues, TemplateResult } from "lit";
|
||||
import { customElement, property, query } from "lit/decorators";
|
||||
import "../../../../src/components/ha-card";
|
||||
import {
|
||||
UPDATE_SUPPORT_BACKUP,
|
||||
UPDATE_SUPPORT_PROGRESS,
|
||||
UPDATE_SUPPORT_INSTALL,
|
||||
} from "../../../../src/data/update";
|
||||
import "../../../../src/dialogs/more-info/more-info-content";
|
||||
import { getEntity } from "../../../../src/fake_data/entity";
|
||||
import {
|
||||
MockHomeAssistant,
|
||||
provideHass,
|
||||
} from "../../../../src/fake_data/provide_hass";
|
||||
import "../../components/demo-more-infos";
|
||||
|
||||
const base_attributes = {
|
||||
title: "Awesome",
|
||||
current_version: "1.2.2",
|
||||
latest_version: "1.2.3",
|
||||
release_url: "https://home-assistant.io",
|
||||
supported_features: UPDATE_SUPPORT_INSTALL,
|
||||
skipped_version: null,
|
||||
in_progress: false,
|
||||
release_summary:
|
||||
"Lorem ipsum dolor sit amet, consectetur adipiscing elit. In nec metus aliquet, porta mi ut, ultrices odio. Etiam egestas orci tellus, non semper metus blandit tincidunt. Praesent elementum turpis vel tempor pharetra. Sed quis cursus diam. Proin sem justo.",
|
||||
};
|
||||
|
||||
const ENTITIES = [
|
||||
getEntity("update", "update1", "on", {
|
||||
...base_attributes,
|
||||
friendly_name: "Update",
|
||||
}),
|
||||
getEntity("update", "update2", "on", {
|
||||
...base_attributes,
|
||||
title: null,
|
||||
friendly_name: "Update without title",
|
||||
}),
|
||||
getEntity("update", "update3", "on", {
|
||||
...base_attributes,
|
||||
release_url: null,
|
||||
friendly_name: "Update without release_url",
|
||||
}),
|
||||
getEntity("update", "update4", "on", {
|
||||
...base_attributes,
|
||||
release_summary: null,
|
||||
friendly_name: "Update without release_summary",
|
||||
}),
|
||||
getEntity("update", "update5", "off", {
|
||||
...base_attributes,
|
||||
current_version: "1.2.3",
|
||||
friendly_name: "No update",
|
||||
}),
|
||||
getEntity("update", "update6", "off", {
|
||||
...base_attributes,
|
||||
skipped_version: "1.2.3",
|
||||
friendly_name: "Skipped version",
|
||||
}),
|
||||
getEntity("update", "update7", "on", {
|
||||
...base_attributes,
|
||||
supported_features:
|
||||
base_attributes.supported_features + UPDATE_SUPPORT_BACKUP,
|
||||
friendly_name: "With backup support",
|
||||
}),
|
||||
getEntity("update", "update8", "on", {
|
||||
...base_attributes,
|
||||
in_progress: true,
|
||||
friendly_name: "With true in_progress",
|
||||
}),
|
||||
getEntity("update", "update9", "on", {
|
||||
...base_attributes,
|
||||
in_progress: 25,
|
||||
supported_features:
|
||||
base_attributes.supported_features + UPDATE_SUPPORT_PROGRESS,
|
||||
friendly_name: "With 25 in_progress",
|
||||
}),
|
||||
getEntity("update", "update10", "on", {
|
||||
...base_attributes,
|
||||
in_progress: 50,
|
||||
supported_features:
|
||||
base_attributes.supported_features + UPDATE_SUPPORT_PROGRESS,
|
||||
friendly_name: "With 50 in_progress",
|
||||
}),
|
||||
getEntity("update", "update11", "on", {
|
||||
...base_attributes,
|
||||
in_progress: 75,
|
||||
supported_features:
|
||||
base_attributes.supported_features + UPDATE_SUPPORT_PROGRESS,
|
||||
friendly_name: "With 75 in_progress",
|
||||
}),
|
||||
getEntity("update", "update12", "unavailable", {
|
||||
...base_attributes,
|
||||
in_progress: 50,
|
||||
friendly_name: "Unavailable",
|
||||
}),
|
||||
getEntity("update", "update13", "on", {
|
||||
...base_attributes,
|
||||
supported_features: 0,
|
||||
friendly_name: "No install support",
|
||||
}),
|
||||
getEntity("update", "update14", "off", {
|
||||
...base_attributes,
|
||||
current_version: null,
|
||||
friendly_name: "Update without current_version",
|
||||
}),
|
||||
getEntity("update", "update15", "off", {
|
||||
...base_attributes,
|
||||
latest_version: null,
|
||||
friendly_name: "Update without latest_version",
|
||||
}),
|
||||
];
|
||||
|
||||
@customElement("demo-more-info-update")
|
||||
class DemoMoreInfoUpdate extends LitElement {
|
||||
@property() public hass!: MockHomeAssistant;
|
||||
|
||||
@query("demo-more-infos") private _demoRoot!: HTMLElement;
|
||||
|
||||
protected render(): TemplateResult {
|
||||
return html`
|
||||
<demo-more-infos
|
||||
.hass=${this.hass}
|
||||
.entities=${ENTITIES.map((ent) => ent.entityId)}
|
||||
></demo-more-infos>
|
||||
`;
|
||||
}
|
||||
|
||||
protected firstUpdated(changedProperties: PropertyValues) {
|
||||
super.firstUpdated(changedProperties);
|
||||
const hass = provideHass(this._demoRoot);
|
||||
hass.updateTranslations(null, "en");
|
||||
hass.addEntities(ENTITIES);
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"demo-more-info-update": DemoMoreInfoUpdate;
|
||||
}
|
||||
}
|
17
gallery/src/pages/user-test/user-types.markdown
Normal file
17
gallery/src/pages/user-test/user-types.markdown
Normal file
@@ -0,0 +1,17 @@
|
||||
---
|
||||
title: "User types"
|
||||
---
|
||||
|
||||
We have defined three user types for Home Assistant. They are a lean segmentation of users that helps us make decisions throughout the product. User types differ from traditional personas in that the segmentation criteria aren’t demographic and don’t personify a group into a single character with a fictitious background story.
|
||||
|
||||
# Outgrowers
|
||||
|
||||
Users that outgrow big tech smart home solutions. It just needs to work with easy setup via an app.
|
||||
|
||||
# Tinkerers
|
||||
|
||||
Technoid users in home networking and development that know how to code.
|
||||
|
||||
# Questioner
|
||||
|
||||
Users who want more advanced home automation, but need support to make it work.
|
@@ -1,9 +1,12 @@
|
||||
// Compat needs to be first import
|
||||
import "../../src/resources/compatibility";
|
||||
import { setCancelSyntheticClickEvents } from "@polymer/polymer/lib/utils/settings";
|
||||
import "../../src/resources/roboto";
|
||||
import "../../src/resources/safari-14-attachshadow-patch";
|
||||
import "./hassio-main";
|
||||
|
||||
setCancelSyntheticClickEvents(false);
|
||||
|
||||
const styleEl = document.createElement("style");
|
||||
styleEl.innerHTML = `
|
||||
body {
|
||||
|
@@ -79,7 +79,6 @@
|
||||
"@polymer/iron-icon": "^3.0.1",
|
||||
"@polymer/iron-input": "^3.0.1",
|
||||
"@polymer/iron-resizable-behavior": "^3.0.1",
|
||||
"@polymer/paper-dropdown-menu": "^3.2.0",
|
||||
"@polymer/paper-input": "^3.2.1",
|
||||
"@polymer/paper-item": "^3.0.1",
|
||||
"@polymer/paper-listbox": "^3.0.1",
|
||||
@@ -109,7 +108,7 @@
|
||||
"fuse.js": "^6.0.0",
|
||||
"google-timezones-json": "^1.0.2",
|
||||
"hls.js": "^1.1.5",
|
||||
"home-assistant-js-websocket": "^6.0.1",
|
||||
"home-assistant-js-websocket": "^7.0.1",
|
||||
"idb-keyval": "^5.1.3",
|
||||
"intl-messageformat": "^9.9.1",
|
||||
"js-yaml": "^4.1.0",
|
||||
@@ -136,7 +135,6 @@
|
||||
"vis-network": "^8.5.4",
|
||||
"vue": "^2.6.12",
|
||||
"vue2-daterange-picker": "^0.5.1",
|
||||
"web-animations-js": "^2.3.2",
|
||||
"workbox-cacheable-response": "^6.4.2",
|
||||
"workbox-core": "^6.4.2",
|
||||
"workbox-expiration": "^6.4.2",
|
||||
|
@@ -1,6 +1,6 @@
|
||||
[metadata]
|
||||
name = home-assistant-frontend
|
||||
version = 20220301.2
|
||||
version = 20220322.0
|
||||
author = The Home Assistant Authors
|
||||
author_email = hello@home-assistant.io
|
||||
license = Apache-2.0
|
||||
|
7
setup.py
7
setup.py
@@ -1,7 +0,0 @@
|
||||
"""
|
||||
Entry point for setuptools. Required for editable installs.
|
||||
TODO: Remove file after updating to pip 21.3
|
||||
"""
|
||||
from setuptools import setup
|
||||
|
||||
setup()
|
@@ -187,6 +187,7 @@ export const DOMAINS_WITH_MORE_INFO = [
|
||||
"scene",
|
||||
"sun",
|
||||
"timer",
|
||||
"update",
|
||||
"vacuum",
|
||||
"water_heater",
|
||||
"weather",
|
||||
@@ -200,6 +201,7 @@ export const DOMAINS_HIDE_DEFAULT_MORE_INFO = [
|
||||
"input_text",
|
||||
"number",
|
||||
"scene",
|
||||
"update",
|
||||
"select",
|
||||
];
|
||||
|
||||
|
@@ -1,12 +1,18 @@
|
||||
import { HassEntity } from "home-assistant-js-websocket";
|
||||
import { UNAVAILABLE, UNKNOWN } from "../../data/entity";
|
||||
import { FrontendLocaleData } from "../../data/translation";
|
||||
import {
|
||||
updateIsInstalling,
|
||||
UpdateEntity,
|
||||
UPDATE_SUPPORT_PROGRESS,
|
||||
} from "../../data/update";
|
||||
import { formatDate } from "../datetime/format_date";
|
||||
import { formatDateTime } from "../datetime/format_date_time";
|
||||
import { formatTime } from "../datetime/format_time";
|
||||
import { formatNumber, isNumericState } from "../number/format_number";
|
||||
import { LocalizeFunc } from "../translations/localize";
|
||||
import { computeStateDomain } from "./compute_state_domain";
|
||||
import { supportsFeature } from "./supports-feature";
|
||||
|
||||
export const computeStateDisplay = (
|
||||
localize: LocalizeFunc,
|
||||
@@ -130,6 +136,28 @@ export const computeStateDisplay = (
|
||||
}
|
||||
}
|
||||
|
||||
if (domain === "update") {
|
||||
// When updating, and entity does not support % show "Installing"
|
||||
// When updating, and entity does support % show "Installing (xx%)"
|
||||
// When update available, show the version
|
||||
// When the latest version is skipped, show the latest version
|
||||
// When update is not available, show "Up-to-date"
|
||||
// When update is not available and there is no latest_version show "Unavailable"
|
||||
return compareState === "on"
|
||||
? updateIsInstalling(stateObj as UpdateEntity)
|
||||
? supportsFeature(stateObj, UPDATE_SUPPORT_PROGRESS)
|
||||
? localize("ui.card.update.installing_with_progress", {
|
||||
progress: stateObj.attributes.in_progress,
|
||||
})
|
||||
: localize("ui.card.update.installing")
|
||||
: stateObj.attributes.latest_version
|
||||
: stateObj.attributes.skipped_version ===
|
||||
stateObj.attributes.latest_version
|
||||
? stateObj.attributes.latest_version ??
|
||||
localize("state.default.unavailable")
|
||||
: localize("ui.card.update.up_to_date");
|
||||
}
|
||||
|
||||
return (
|
||||
// Return device class translation
|
||||
(stateObj.attributes.device_class &&
|
||||
|
@@ -1,4 +1,4 @@
|
||||
import { HassEntity } from "home-assistant-js-websocket";
|
||||
import type { HassEntity } from "home-assistant-js-websocket";
|
||||
import { computeDomain } from "./compute_domain";
|
||||
|
||||
export const computeStateDomain = (stateObj: HassEntity) =>
|
||||
|
@@ -9,11 +9,10 @@ import {
|
||||
mdiCast,
|
||||
mdiCastConnected,
|
||||
mdiClock,
|
||||
mdiEmoticonDead,
|
||||
mdiFlash,
|
||||
mdiGestureTapButton,
|
||||
mdiLanConnect,
|
||||
mdiLanDisconnect,
|
||||
mdiLightSwitch,
|
||||
mdiLock,
|
||||
mdiLockAlert,
|
||||
mdiLockClock,
|
||||
@@ -22,16 +21,16 @@ import {
|
||||
mdiPowerPlug,
|
||||
mdiPowerPlugOff,
|
||||
mdiRestart,
|
||||
mdiSleep,
|
||||
mdiTimerSand,
|
||||
mdiToggleSwitch,
|
||||
mdiToggleSwitchOff,
|
||||
mdiCheckCircleOutline,
|
||||
mdiCloseCircleOutline,
|
||||
mdiWeatherNight,
|
||||
mdiZWave,
|
||||
mdiPackage,
|
||||
mdiPackageDown,
|
||||
} from "@mdi/js";
|
||||
import { HassEntity } from "home-assistant-js-websocket";
|
||||
import { updateIsInstalling, UpdateEntity } from "../../data/update";
|
||||
/**
|
||||
* Return the icon to be used for a domain.
|
||||
*
|
||||
@@ -112,19 +111,7 @@ export const domainIcon = (
|
||||
case "switch":
|
||||
return compareState === "on" ? mdiToggleSwitch : mdiToggleSwitchOff;
|
||||
default:
|
||||
return mdiFlash;
|
||||
}
|
||||
|
||||
case "zwave":
|
||||
switch (compareState) {
|
||||
case "dead":
|
||||
return mdiEmoticonDead;
|
||||
case "sleeping":
|
||||
return mdiSleep;
|
||||
case "initializing":
|
||||
return mdiTimerSand;
|
||||
default:
|
||||
return mdiZWave;
|
||||
return mdiLightSwitch;
|
||||
}
|
||||
|
||||
case "sensor": {
|
||||
@@ -149,6 +136,13 @@ export const domainIcon = (
|
||||
return stateObj?.state === "above_horizon"
|
||||
? FIXED_DOMAIN_ICONS[domain]
|
||||
: mdiWeatherNight;
|
||||
|
||||
case "update":
|
||||
return compareState === "on"
|
||||
? updateIsInstalling(stateObj as UpdateEntity)
|
||||
? mdiPackageDown
|
||||
: mdiPackageUp
|
||||
: mdiPackage;
|
||||
}
|
||||
|
||||
if (domain in FIXED_DOMAIN_ICONS) {
|
||||
|
4
src/common/string/is_ip_address.ts
Normal file
4
src/common/string/is_ip_address.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
const regexp =
|
||||
/^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/;
|
||||
|
||||
export const isIPAddress = (input: string): boolean => regexp.test(input);
|
@@ -70,9 +70,6 @@ export const iconColorCSS = css`
|
||||
}
|
||||
|
||||
ha-state-icon[data-domain="plant"][data-state="problem"],
|
||||
ha-state-icon[data-domain="zwave"][data-state="dead"] {
|
||||
color: var(--state-icon-error-color);
|
||||
}
|
||||
|
||||
/* Color the icon if unavailable */
|
||||
ha-state-icon[data-state="unavailable"] {
|
||||
|
@@ -1,8 +1,9 @@
|
||||
import "@material/mwc-button";
|
||||
import type { Button } from "@material/mwc-button";
|
||||
import { mdiAlertOctagram, mdiCheckBold } from "@mdi/js";
|
||||
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
|
||||
import { customElement, property, query } from "lit/decorators";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import "../ha-circular-progress";
|
||||
import "../ha-svg-icon";
|
||||
|
||||
@customElement("ha-progress-button")
|
||||
export class HaProgressButton extends LitElement {
|
||||
@@ -12,38 +13,53 @@ export class HaProgressButton extends LitElement {
|
||||
|
||||
@property({ type: Boolean }) public raised = false;
|
||||
|
||||
@query("mwc-button", true) private _button?: Button;
|
||||
@state() private _result?: "success" | "error";
|
||||
|
||||
public render(): TemplateResult {
|
||||
const overlay = this._result || this.progress;
|
||||
return html`
|
||||
<mwc-button
|
||||
?raised=${this.raised}
|
||||
.disabled=${this.disabled || this.progress}
|
||||
@click=${this._buttonTapped}
|
||||
class=${this._result || ""}
|
||||
>
|
||||
<slot></slot>
|
||||
</mwc-button>
|
||||
${this.progress
|
||||
? html`<div class="progress">
|
||||
<ha-circular-progress size="small" active></ha-circular-progress>
|
||||
</div>`
|
||||
: ""}
|
||||
${!overlay
|
||||
? ""
|
||||
: html`
|
||||
<div class="progress">
|
||||
${this._result === "success"
|
||||
? html`<ha-svg-icon .path=${mdiCheckBold}></ha-svg-icon>`
|
||||
: this._result === "error"
|
||||
? html`<ha-svg-icon .path=${mdiAlertOctagram}></ha-svg-icon>`
|
||||
: this.progress
|
||||
? html`
|
||||
<ha-circular-progress
|
||||
size="small"
|
||||
active
|
||||
></ha-circular-progress>
|
||||
`
|
||||
: ""}
|
||||
</div>
|
||||
`}
|
||||
`;
|
||||
}
|
||||
|
||||
public actionSuccess(): void {
|
||||
this._tempClass("success");
|
||||
this._setResult("success");
|
||||
}
|
||||
|
||||
public actionError(): void {
|
||||
this._tempClass("error");
|
||||
this._setResult("error");
|
||||
}
|
||||
|
||||
private _tempClass(className: string): void {
|
||||
this._button!.classList.add(className);
|
||||
private _setResult(result: "success" | "error"): void {
|
||||
this._result = result;
|
||||
setTimeout(() => {
|
||||
this._button!.classList.remove(className);
|
||||
}, 1000);
|
||||
this._result = undefined;
|
||||
}, 2000);
|
||||
}
|
||||
|
||||
private _buttonTapped(ev: Event): void {
|
||||
@@ -69,6 +85,7 @@ export class HaProgressButton extends LitElement {
|
||||
background-color: var(--success-color);
|
||||
transition: none;
|
||||
border-radius: 4px;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
mwc-button[raised].success {
|
||||
@@ -81,6 +98,7 @@ export class HaProgressButton extends LitElement {
|
||||
background-color: var(--error-color);
|
||||
transition: none;
|
||||
border-radius: 4px;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
mwc-button[raised].error {
|
||||
@@ -89,13 +107,21 @@ export class HaProgressButton extends LitElement {
|
||||
}
|
||||
|
||||
.progress {
|
||||
bottom: 0;
|
||||
margin-top: 4px;
|
||||
bottom: 4px;
|
||||
position: absolute;
|
||||
text-align: center;
|
||||
top: 0;
|
||||
top: 4px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
ha-svg-icon {
|
||||
color: white;
|
||||
}
|
||||
|
||||
mwc-button.success slot,
|
||||
mwc-button.error slot {
|
||||
visibility: hidden;
|
||||
}
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
@@ -115,7 +115,7 @@ class DateRangePickerElement extends WrappedElement {
|
||||
color: var(--primary-text-color);
|
||||
min-width: initial !important;
|
||||
}
|
||||
.daterangepicker:after {
|
||||
.daterangepicker:before {
|
||||
display: none;
|
||||
}
|
||||
.daterangepicker:after {
|
||||
|
@@ -1,4 +1,4 @@
|
||||
import { html, LitElement, TemplateResult } from "lit";
|
||||
import { css, html, LitElement, TemplateResult } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import { PolymerChangedEvent } from "../../polymer-types";
|
||||
@@ -116,6 +116,12 @@ class HaDevicesPicker extends LitElement {
|
||||
|
||||
this._updateDevices([...currentDevices, toAdd]);
|
||||
}
|
||||
|
||||
static override styles = css`
|
||||
div {
|
||||
margin-top: 8px;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
declare global {
|
||||
|
@@ -46,11 +46,29 @@ class HaEntitiesPickerLight extends LitElement {
|
||||
@property({ type: Array, attribute: "include-unit-of-measurement" })
|
||||
public includeUnitOfMeasurement?: string[];
|
||||
|
||||
/**
|
||||
* List of allowed entities to show. Will ignore all other filters.
|
||||
* @type {Array}
|
||||
* @attr include-entities
|
||||
*/
|
||||
@property({ type: Array, attribute: "include-entities" })
|
||||
public includeEntities?: string[];
|
||||
|
||||
/**
|
||||
* List of entities to be excluded.
|
||||
* @type {Array}
|
||||
* @attr exclude-entities
|
||||
*/
|
||||
@property({ type: Array, attribute: "exclude-entities" })
|
||||
public excludeEntities?: string[];
|
||||
|
||||
@property({ attribute: "picked-entity-label" })
|
||||
public pickedEntityLabel?: string;
|
||||
|
||||
@property({ attribute: "pick-entity-label" }) public pickEntityLabel?: string;
|
||||
|
||||
@property() public entityFilter?: HaEntityPickerEntityFilterFunc;
|
||||
|
||||
protected render(): TemplateResult {
|
||||
if (!this.hass) {
|
||||
return html``;
|
||||
@@ -67,6 +85,8 @@ class HaEntitiesPickerLight extends LitElement {
|
||||
.hass=${this.hass}
|
||||
.includeDomains=${this.includeDomains}
|
||||
.excludeDomains=${this.excludeDomains}
|
||||
.includeEntities=${this.includeEntities}
|
||||
.excludeEntities=${this.excludeEntities}
|
||||
.includeDeviceClasses=${this.includeDeviceClasses}
|
||||
.includeUnitOfMeasurement=${this.includeUnitOfMeasurement}
|
||||
.entityFilter=${this._entityFilter}
|
||||
@@ -82,6 +102,8 @@ class HaEntitiesPickerLight extends LitElement {
|
||||
.hass=${this.hass}
|
||||
.includeDomains=${this.includeDomains}
|
||||
.excludeDomains=${this.excludeDomains}
|
||||
.includeEntities=${this.includeEntities}
|
||||
.excludeEntities=${this.excludeEntities}
|
||||
.includeDeviceClasses=${this.includeDeviceClasses}
|
||||
.includeUnitOfMeasurement=${this.includeUnitOfMeasurement}
|
||||
.entityFilter=${this._entityFilter}
|
||||
@@ -94,7 +116,9 @@ class HaEntitiesPickerLight extends LitElement {
|
||||
|
||||
private _entityFilter: HaEntityPickerEntityFilterFunc = (
|
||||
stateObj: HassEntity
|
||||
) => !this.value || !this.value.includes(stateObj.entity_id);
|
||||
) =>
|
||||
(!this.value || !this.value.includes(stateObj.entity_id)) &&
|
||||
(!this.entityFilter || this.entityFilter(stateObj));
|
||||
|
||||
private get _currentEntities() {
|
||||
return this.value || [];
|
||||
|
@@ -7,6 +7,7 @@ import memoizeOne from "memoize-one";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import { computeDomain } from "../../common/entity/compute_domain";
|
||||
import { computeStateName } from "../../common/entity/compute_state_name";
|
||||
import { caseInsensitiveStringCompare } from "../../common/string/compare";
|
||||
import { PolymerChangedEvent } from "../../polymer-types";
|
||||
import { HomeAssistant } from "../../types";
|
||||
import "../ha-combo-box";
|
||||
@@ -15,18 +16,21 @@ import "../ha-icon-button";
|
||||
import "../ha-svg-icon";
|
||||
import "./state-badge";
|
||||
|
||||
interface HassEntityWithCachedName extends HassEntity {
|
||||
friendly_name: string;
|
||||
}
|
||||
|
||||
export type HaEntityPickerEntityFilterFunc = (entityId: HassEntity) => boolean;
|
||||
|
||||
// eslint-disable-next-line lit/prefer-static-styles
|
||||
const rowRenderer: ComboBoxLitRenderer<HassEntity & { friendly_name: string }> =
|
||||
(item) =>
|
||||
html`<mwc-list-item graphic="avatar" .twoline=${!!item.entity_id}>
|
||||
${item.state
|
||||
? html`<state-badge slot="graphic" .stateObj=${item}></state-badge>`
|
||||
: ""}
|
||||
<span>${item.friendly_name}</span>
|
||||
<span slot="secondary">${item.entity_id}</span>
|
||||
</mwc-list-item>`;
|
||||
const rowRenderer: ComboBoxLitRenderer<HassEntityWithCachedName> = (item) =>
|
||||
html`<mwc-list-item graphic="avatar" .twoline=${!!item.entity_id}>
|
||||
${item.state
|
||||
? html`<state-badge slot="graphic" .stateObj=${item}></state-badge>`
|
||||
: ""}
|
||||
<span>${item.friendly_name}</span>
|
||||
<span slot="secondary">${item.entity_id}</span>
|
||||
</mwc-list-item>`;
|
||||
@customElement("ha-entity-picker")
|
||||
export class HaEntityPicker extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
@@ -74,6 +78,22 @@ export class HaEntityPicker extends LitElement {
|
||||
@property({ type: Array, attribute: "include-unit-of-measurement" })
|
||||
public includeUnitOfMeasurement?: string[];
|
||||
|
||||
/**
|
||||
* List of allowed entities to show. Will ignore all other filters.
|
||||
* @type {Array}
|
||||
* @attr include-entities
|
||||
*/
|
||||
@property({ type: Array, attribute: "include-entities" })
|
||||
public includeEntities?: string[];
|
||||
|
||||
/**
|
||||
* List of entities to be excluded.
|
||||
* @type {Array}
|
||||
* @attr exclude-entities
|
||||
*/
|
||||
@property({ type: Array, attribute: "exclude-entities" })
|
||||
public excludeEntities?: string[];
|
||||
|
||||
@property() public entityFilter?: HaEntityPickerEntityFilterFunc;
|
||||
|
||||
@property({ type: Boolean }) public hideClearIcon = false;
|
||||
@@ -96,7 +116,7 @@ export class HaEntityPicker extends LitElement {
|
||||
|
||||
private _initedStates = false;
|
||||
|
||||
private _states: HassEntity[] = [];
|
||||
private _states: HassEntityWithCachedName[] = [];
|
||||
|
||||
private _getStates = memoizeOne(
|
||||
(
|
||||
@@ -106,9 +126,11 @@ export class HaEntityPicker extends LitElement {
|
||||
excludeDomains: this["excludeDomains"],
|
||||
entityFilter: this["entityFilter"],
|
||||
includeDeviceClasses: this["includeDeviceClasses"],
|
||||
includeUnitOfMeasurement: this["includeUnitOfMeasurement"]
|
||||
) => {
|
||||
let states: HassEntity[] = [];
|
||||
includeUnitOfMeasurement: this["includeUnitOfMeasurement"],
|
||||
includeEntities: this["includeEntities"],
|
||||
excludeEntities: this["excludeEntities"]
|
||||
): HassEntityWithCachedName[] => {
|
||||
let states: HassEntityWithCachedName[] = [];
|
||||
|
||||
if (!hass) {
|
||||
return [];
|
||||
@@ -122,7 +144,7 @@ export class HaEntityPicker extends LitElement {
|
||||
state: "",
|
||||
last_changed: "",
|
||||
last_updated: "",
|
||||
context: { id: "", user_id: null },
|
||||
context: { id: "", user_id: null, parent_id: null },
|
||||
friendly_name: this.hass!.localize(
|
||||
"ui.components.entity.entity-picker.no_entities"
|
||||
),
|
||||
@@ -136,6 +158,30 @@ export class HaEntityPicker extends LitElement {
|
||||
];
|
||||
}
|
||||
|
||||
if (includeEntities) {
|
||||
entityIds = entityIds.filter((entityId) =>
|
||||
this.includeEntities!.includes(entityId)
|
||||
);
|
||||
|
||||
return entityIds
|
||||
.map((key) => ({
|
||||
...hass!.states[key],
|
||||
friendly_name: computeStateName(hass!.states[key]) || key,
|
||||
}))
|
||||
.sort((entityA, entityB) =>
|
||||
caseInsensitiveStringCompare(
|
||||
entityA.friendly_name,
|
||||
entityB.friendly_name
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
if (excludeEntities) {
|
||||
entityIds = entityIds.filter(
|
||||
(entityId) => !excludeEntities!.includes(entityId)
|
||||
);
|
||||
}
|
||||
|
||||
if (includeDomains) {
|
||||
entityIds = entityIds.filter((eid) =>
|
||||
includeDomains.includes(computeDomain(eid))
|
||||
@@ -148,10 +194,17 @@ export class HaEntityPicker extends LitElement {
|
||||
);
|
||||
}
|
||||
|
||||
states = entityIds.sort().map((key) => ({
|
||||
...hass!.states[key],
|
||||
friendly_name: computeStateName(hass!.states[key]) || key,
|
||||
}));
|
||||
states = entityIds
|
||||
.map((key) => ({
|
||||
...hass!.states[key],
|
||||
friendly_name: computeStateName(hass!.states[key]) || key,
|
||||
}))
|
||||
.sort((entityA, entityB) =>
|
||||
caseInsensitiveStringCompare(
|
||||
entityA.friendly_name,
|
||||
entityB.friendly_name
|
||||
)
|
||||
);
|
||||
|
||||
if (includeDeviceClasses) {
|
||||
states = states.filter(
|
||||
@@ -190,7 +243,7 @@ export class HaEntityPicker extends LitElement {
|
||||
state: "",
|
||||
last_changed: "",
|
||||
last_updated: "",
|
||||
context: { id: "", user_id: null },
|
||||
context: { id: "", user_id: null, parent_id: null },
|
||||
friendly_name: this.hass!.localize(
|
||||
"ui.components.entity.entity-picker.no_match"
|
||||
),
|
||||
@@ -228,7 +281,9 @@ export class HaEntityPicker extends LitElement {
|
||||
this.excludeDomains,
|
||||
this.entityFilter,
|
||||
this.includeDeviceClasses,
|
||||
this.includeUnitOfMeasurement
|
||||
this.includeUnitOfMeasurement,
|
||||
this.includeEntities,
|
||||
this.excludeEntities
|
||||
);
|
||||
if (this._initedStates) {
|
||||
(this.comboBox as any).filteredItems = this._states;
|
||||
|
@@ -1,4 +1,5 @@
|
||||
import { HaFormSchema } from "./types";
|
||||
import type { Selector } from "../../data/selector";
|
||||
import type { HaFormSchema } from "./types";
|
||||
|
||||
export const computeInitialHaFormData = (
|
||||
schema: HaFormSchema[]
|
||||
@@ -31,6 +32,25 @@ export const computeInitialHaFormData = (
|
||||
minutes: 0,
|
||||
seconds: 0,
|
||||
};
|
||||
} else if ("selector" in field) {
|
||||
const selector: Selector = field.selector;
|
||||
if ("boolean" in selector) {
|
||||
data[field.name] = false;
|
||||
} else if ("text" in selector) {
|
||||
data[field.name] = "";
|
||||
} else if ("number" in selector) {
|
||||
data[field.name] = "min" in selector.number ? selector.number.min : 0;
|
||||
} else if ("select" in selector) {
|
||||
if (selector.select.options.length) {
|
||||
data[field.name] = selector.select.options[0][0];
|
||||
}
|
||||
} else if ("duration" in selector) {
|
||||
data[field.name] = {
|
||||
hours: 0,
|
||||
minutes: 0,
|
||||
seconds: 0,
|
||||
};
|
||||
}
|
||||
}
|
||||
});
|
||||
return data;
|
||||
|
@@ -1,16 +1,20 @@
|
||||
import "@material/mwc-list/mwc-list-item";
|
||||
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
|
||||
import { customElement, property, query } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { html, LitElement, TemplateResult } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import { stopPropagation } from "../../common/dom/stop_propagation";
|
||||
import "../ha-radio";
|
||||
import type { HaRadio } from "../ha-radio";
|
||||
import "../ha-select";
|
||||
import type { HaSelect } from "../ha-select";
|
||||
import { HaFormElement, HaFormSelectData, HaFormSelectSchema } from "./types";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import type {
|
||||
HaFormElement,
|
||||
HaFormSelectData,
|
||||
HaFormSelectSchema,
|
||||
} from "./types";
|
||||
import type { SelectSelector } from "../../data/selector";
|
||||
import "../ha-selector/ha-selector-select";
|
||||
|
||||
@customElement("ha-form-select")
|
||||
export class HaFormSelect extends LitElement implements HaFormElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public schema!: HaFormSelectSchema;
|
||||
|
||||
@property() public data!: HaFormSelectData;
|
||||
@@ -19,60 +23,35 @@ export class HaFormSelect extends LitElement implements HaFormElement {
|
||||
|
||||
@property({ type: Boolean }) public disabled = false;
|
||||
|
||||
@query("ha-select", true) private _input?: HTMLElement;
|
||||
|
||||
public focus() {
|
||||
if (this._input) {
|
||||
this._input.focus();
|
||||
}
|
||||
}
|
||||
private _selectSchema = memoizeOne(
|
||||
(options): SelectSelector => ({
|
||||
select: {
|
||||
options: options.map((option) => ({
|
||||
value: option[0],
|
||||
label: option[1],
|
||||
})),
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
protected render(): TemplateResult {
|
||||
if (this.schema.required && this.schema.options!.length < 6) {
|
||||
return html`
|
||||
<div>
|
||||
${this.label}
|
||||
${this.schema.options.map(
|
||||
([value, label]) => html`
|
||||
<mwc-formfield .label=${label}>
|
||||
<ha-radio
|
||||
.checked=${value === this.data}
|
||||
.value=${value}
|
||||
.disabled=${this.disabled}
|
||||
@change=${this._valueChanged}
|
||||
></ha-radio>
|
||||
</mwc-formfield>
|
||||
`
|
||||
)}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
return html`
|
||||
<ha-select
|
||||
fixedMenuPosition
|
||||
naturalMenuWidth
|
||||
.label=${this.label}
|
||||
<ha-selector-select
|
||||
.hass=${this.hass}
|
||||
.schema=${this.schema}
|
||||
.value=${this.data}
|
||||
.label=${this.label}
|
||||
.disabled=${this.disabled}
|
||||
@closed=${stopPropagation}
|
||||
@selected=${this._valueChanged}
|
||||
>
|
||||
${!this.schema.required
|
||||
? html`<mwc-list-item value=""></mwc-list-item>`
|
||||
: ""}
|
||||
${this.schema.options!.map(
|
||||
([value, label]) => html`
|
||||
<mwc-list-item .value=${value}>${label}</mwc-list-item>
|
||||
`
|
||||
)}
|
||||
</ha-select>
|
||||
.required=${this.schema.required}
|
||||
.selector=${this._selectSchema(this.schema.options)}
|
||||
@value-changed=${this._valueChanged}
|
||||
></ha-selector-select>
|
||||
`;
|
||||
}
|
||||
|
||||
private _valueChanged(ev: CustomEvent) {
|
||||
ev.stopPropagation();
|
||||
let value: string | undefined = (ev.target as HaSelect | HaRadio).value;
|
||||
let value: string | undefined = ev.detail.value;
|
||||
|
||||
if (value === this.data) {
|
||||
return;
|
||||
@@ -86,15 +65,6 @@ export class HaFormSelect extends LitElement implements HaFormElement {
|
||||
value,
|
||||
});
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return css`
|
||||
ha-select,
|
||||
mwc-formfield {
|
||||
display: block;
|
||||
}
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
|
@@ -106,6 +106,7 @@ export class HaForm extends LitElement implements HaFormElement {
|
||||
.disabled=${this.disabled}
|
||||
.helper=${this._computeHelper(item)}
|
||||
.required=${item.required || false}
|
||||
.context=${this._generateContext(item)}
|
||||
></ha-selector>`
|
||||
: dynamicElement(`ha-form-${item.type}`, {
|
||||
schema: item,
|
||||
@@ -115,6 +116,7 @@ export class HaForm extends LitElement implements HaFormElement {
|
||||
hass: this.hass,
|
||||
computeLabel: this.computeLabel,
|
||||
computeHelper: this.computeHelper,
|
||||
context: this._generateContext(item),
|
||||
})}
|
||||
`;
|
||||
})}
|
||||
@@ -122,6 +124,20 @@ export class HaForm extends LitElement implements HaFormElement {
|
||||
`;
|
||||
}
|
||||
|
||||
private _generateContext(
|
||||
schema: HaFormSchema
|
||||
): Record<string, any> | undefined {
|
||||
if (!schema.context) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const context = {};
|
||||
for (const [context_key, data_key] of Object.entries(schema.context)) {
|
||||
context[context_key] = this.data[data_key];
|
||||
}
|
||||
return context;
|
||||
}
|
||||
|
||||
protected createRenderRoot() {
|
||||
const root = super.createRenderRoot();
|
||||
// attach it as soon as possible to make sure we fetch all events.
|
||||
|
@@ -24,6 +24,7 @@ export interface HaFormBaseSchema {
|
||||
// This value will be set initially when form is loaded
|
||||
suggested_value?: HaFormData;
|
||||
};
|
||||
context?: Record<string, string>;
|
||||
}
|
||||
|
||||
export interface HaFormGridSchema extends HaFormBaseSchema {
|
||||
|
@@ -28,7 +28,11 @@ export class HaAreaSelector extends LitElement {
|
||||
oldSelector !== this.selector &&
|
||||
this.selector.area.device?.integration
|
||||
) {
|
||||
this._loadConfigEntries();
|
||||
getConfigEntries(this.hass, {
|
||||
domain: this.selector.area.device.integration,
|
||||
}).then((entries) => {
|
||||
this._configEntries = entries;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -85,12 +89,6 @@ export class HaAreaSelector extends LitElement {
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
private async _loadConfigEntries() {
|
||||
this._configEntries = (await getConfigEntries(this.hass)).filter(
|
||||
(entry) => entry.domain === this.selector.area.device?.integration
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
|
@@ -1,9 +1,10 @@
|
||||
import "../entity/ha-entity-attribute-picker";
|
||||
import { html, LitElement } from "lit";
|
||||
import { html, LitElement, PropertyValues } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { AttributeSelector } from "../../data/selector";
|
||||
import { SubscribeMixin } from "../../mixins/subscribe-mixin";
|
||||
import { HomeAssistant } from "../../types";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
|
||||
@customElement("ha-selector-attribute")
|
||||
export class HaSelectorAttribute extends SubscribeMixin(LitElement) {
|
||||
@@ -17,11 +18,16 @@ export class HaSelectorAttribute extends SubscribeMixin(LitElement) {
|
||||
|
||||
@property({ type: Boolean }) public disabled = false;
|
||||
|
||||
@property() public context?: {
|
||||
filter_entity?: string;
|
||||
};
|
||||
|
||||
protected render() {
|
||||
return html`
|
||||
<ha-entity-attribute-picker
|
||||
.hass=${this.hass}
|
||||
.entityId=${this.selector.attribute.entity_id}
|
||||
.entityId=${this.selector.attribute.entity_id ||
|
||||
this.context?.filter_entity}
|
||||
.value=${this.value}
|
||||
.label=${this.label}
|
||||
.disabled=${this.disabled}
|
||||
@@ -29,6 +35,47 @@ export class HaSelectorAttribute extends SubscribeMixin(LitElement) {
|
||||
></ha-entity-attribute-picker>
|
||||
`;
|
||||
}
|
||||
|
||||
protected updated(changedProps: PropertyValues): void {
|
||||
super.updated(changedProps);
|
||||
if (
|
||||
// No need to filter value if no value
|
||||
!this.value ||
|
||||
// Only adjust value if we used the context
|
||||
this.selector.attribute.entity_id ||
|
||||
// Only check if context has changed
|
||||
!changedProps.has("context")
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const oldContext = changedProps.get("context") as this["context"];
|
||||
|
||||
if (
|
||||
!this.context ||
|
||||
oldContext?.filter_entity === this.context.filter_entity
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Validate that that the attribute is still valid for this entity, else unselect.
|
||||
let invalid = false;
|
||||
if (this.context.filter_entity) {
|
||||
const stateObj = this.hass.states[this.context.filter_entity];
|
||||
|
||||
if (!(stateObj && this.value in stateObj.attributes)) {
|
||||
invalid = true;
|
||||
}
|
||||
} else {
|
||||
invalid = this.value !== undefined;
|
||||
}
|
||||
|
||||
if (invalid) {
|
||||
fireEvent(this, "value-changed", {
|
||||
value: undefined,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
|
58
src/components/ha-selector/ha-selector-color-rgb.ts
Normal file
58
src/components/ha-selector/ha-selector-color-rgb.ts
Normal file
@@ -0,0 +1,58 @@
|
||||
import { css, html, LitElement } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import type { ColorRGBSelector } from "../../data/selector";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import { hex2rgb, rgb2hex } from "../../common/color/convert-color";
|
||||
import "../ha-textfield";
|
||||
|
||||
@customElement("ha-selector-color_rgb")
|
||||
export class HaColorRGBSelector extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public selector!: ColorRGBSelector;
|
||||
|
||||
@property() public value?: string;
|
||||
|
||||
@property() public label?: string;
|
||||
|
||||
@property({ type: Boolean, reflect: true }) public disabled = false;
|
||||
|
||||
protected render() {
|
||||
return html`
|
||||
<ha-textfield
|
||||
type="color"
|
||||
.value=${this.value ? rgb2hex(this.value as any) : ""}
|
||||
.label=${this.label || ""}
|
||||
@change=${this._valueChanged}
|
||||
></ha-textfield>
|
||||
`;
|
||||
}
|
||||
|
||||
private _valueChanged(ev: CustomEvent) {
|
||||
const value = (ev.target as any).value;
|
||||
fireEvent(this, "value-changed", {
|
||||
value: hex2rgb(value),
|
||||
});
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
:host {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
align-items: center;
|
||||
}
|
||||
ha-textfield {
|
||||
--text-field-padding: 8px;
|
||||
min-width: 75px;
|
||||
flex-grow: 1;
|
||||
margin: 0 4px;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-selector-color_rgb": HaColorRGBSelector;
|
||||
}
|
||||
}
|
58
src/components/ha-selector/ha-selector-color-temp.ts
Normal file
58
src/components/ha-selector/ha-selector-color-temp.ts
Normal file
@@ -0,0 +1,58 @@
|
||||
import { css, html, LitElement } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import type { ColorTempSelector } from "../../data/selector";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import "../ha-labeled-slider";
|
||||
|
||||
@customElement("ha-selector-color_temp")
|
||||
export class HaColorTempSelector extends LitElement {
|
||||
@property() public hass!: HomeAssistant;
|
||||
|
||||
@property() public selector!: ColorTempSelector;
|
||||
|
||||
@property() public value?: string;
|
||||
|
||||
@property() public label?: string;
|
||||
|
||||
@property({ type: Boolean, reflect: true }) public disabled = false;
|
||||
|
||||
protected render() {
|
||||
return html`
|
||||
<ha-labeled-slider
|
||||
pin
|
||||
icon="hass:thermometer"
|
||||
.caption=${this.label}
|
||||
.min=${this.selector.color_temp.min_mireds ?? 153}
|
||||
.max=${this.selector.color_temp.max_mireds ?? 500}
|
||||
.value=${this.value}
|
||||
@change=${this._valueChanged}
|
||||
></ha-labeled-slider>
|
||||
`;
|
||||
}
|
||||
|
||||
private _valueChanged(ev: CustomEvent) {
|
||||
fireEvent(this, "value-changed", {
|
||||
value: Number((ev.target as any).value),
|
||||
});
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
ha-labeled-slider {
|
||||
--ha-slider-background: -webkit-linear-gradient(
|
||||
right,
|
||||
rgb(255, 160, 0) 0%,
|
||||
white 50%,
|
||||
rgb(166, 209, 255) 100%
|
||||
);
|
||||
/* The color temp minimum value shouldn't be rendered differently. It's not "off". */
|
||||
--paper-slider-knob-start-border-color: var(--primary-color);
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-selector-color_temp": HaColorTempSelector;
|
||||
}
|
||||
}
|
36
src/components/ha-selector/ha-selector-date.ts
Normal file
36
src/components/ha-selector/ha-selector-date.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
import { html, LitElement } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import type { DateSelector } from "../../data/selector";
|
||||
import "../ha-date-input";
|
||||
|
||||
@customElement("ha-selector-date")
|
||||
export class HaDateSelector extends LitElement {
|
||||
@property() public hass!: HomeAssistant;
|
||||
|
||||
@property() public selector!: DateSelector;
|
||||
|
||||
@property() public value?: string;
|
||||
|
||||
@property() public label?: string;
|
||||
|
||||
@property({ type: Boolean, reflect: true }) public disabled = false;
|
||||
|
||||
protected render() {
|
||||
return html`
|
||||
<ha-date-input
|
||||
.label=${this.label}
|
||||
.locale=${this.hass.locale}
|
||||
.disabled=${this.disabled}
|
||||
.value=${this.value}
|
||||
>
|
||||
</ha-date-input>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-selector-date": HaDateSelector;
|
||||
}
|
||||
}
|
74
src/components/ha-selector/ha-selector-datetime.ts
Normal file
74
src/components/ha-selector/ha-selector-datetime.ts
Normal file
@@ -0,0 +1,74 @@
|
||||
import { css, html, LitElement } from "lit";
|
||||
import { customElement, property, query } from "lit/decorators";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import type { DateTimeSelector } from "../../data/selector";
|
||||
import type { HaDateInput } from "../ha-date-input";
|
||||
import type { HaTimeInput } from "../ha-time-input";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import "../ha-date-input";
|
||||
import "../ha-time-input";
|
||||
|
||||
@customElement("ha-selector-datetime")
|
||||
export class HaDateTimeSelector extends LitElement {
|
||||
@property() public hass!: HomeAssistant;
|
||||
|
||||
@property() public selector!: DateTimeSelector;
|
||||
|
||||
@property() public value?: string;
|
||||
|
||||
@property() public label?: string;
|
||||
|
||||
@property({ type: Boolean, reflect: true }) public disabled = false;
|
||||
|
||||
@query("ha-date-input") private _dateInput!: HaDateInput;
|
||||
|
||||
@query("ha-time-input") private _timeInput!: HaTimeInput;
|
||||
|
||||
protected render() {
|
||||
const values = this.value?.split(" ");
|
||||
|
||||
return html`
|
||||
<ha-date-input
|
||||
.label=${this.label}
|
||||
.locale=${this.hass.locale}
|
||||
.disabled=${this.disabled}
|
||||
.value=${values?.[0]}
|
||||
@value-changed=${this._valueChanged}
|
||||
>
|
||||
</ha-date-input>
|
||||
<ha-time-input
|
||||
enable-second
|
||||
.value=${values?.[1] || "0:00:00"}
|
||||
.locale=${this.hass.locale}
|
||||
.disabled=${this.disabled}
|
||||
@value-changed=${this._valueChanged}
|
||||
></ha-time-input>
|
||||
`;
|
||||
}
|
||||
|
||||
private _valueChanged(ev: CustomEvent): void {
|
||||
ev.stopPropagation();
|
||||
fireEvent(this, "value-changed", {
|
||||
value: `${this._dateInput.value} ${this._timeInput.value}`,
|
||||
});
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
:host {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
ha-date-input {
|
||||
min-width: 150px;
|
||||
margin-right: 4px;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-selector-datetime": HaDateTimeSelector;
|
||||
}
|
||||
}
|
@@ -1,10 +1,11 @@
|
||||
import { html, LitElement } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { ConfigEntry, getConfigEntries } from "../../data/config_entries";
|
||||
import { DeviceRegistryEntry } from "../../data/device_registry";
|
||||
import { DeviceSelector } from "../../data/selector";
|
||||
import { HomeAssistant } from "../../types";
|
||||
import type { DeviceRegistryEntry } from "../../data/device_registry";
|
||||
import type { DeviceSelector } from "../../data/selector";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import "../device/ha-device-picker";
|
||||
import "../device/ha-devices-picker";
|
||||
|
||||
@customElement("ha-selector-device")
|
||||
export class HaDeviceSelector extends LitElement {
|
||||
@@ -24,26 +25,46 @@ export class HaDeviceSelector extends LitElement {
|
||||
if (changedProperties.has("selector")) {
|
||||
const oldSelector = changedProperties.get("selector");
|
||||
if (oldSelector !== this.selector && this.selector.device?.integration) {
|
||||
this._loadConfigEntries();
|
||||
getConfigEntries(this.hass, {
|
||||
domain: this.selector.device.integration,
|
||||
}).then((entries) => {
|
||||
this._configEntries = entries;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected render() {
|
||||
return html`<ha-device-picker
|
||||
.hass=${this.hass}
|
||||
.value=${this.value}
|
||||
.label=${this.label}
|
||||
.deviceFilter=${this._filterDevices}
|
||||
.includeDeviceClasses=${this.selector.device.entity?.device_class
|
||||
? [this.selector.device.entity.device_class]
|
||||
: undefined}
|
||||
.includeDomains=${this.selector.device.entity?.domain
|
||||
? [this.selector.device.entity.domain]
|
||||
: undefined}
|
||||
.disabled=${this.disabled}
|
||||
allow-custom-entity
|
||||
></ha-device-picker>`;
|
||||
if (!this.selector.device.multiple) {
|
||||
return html`<ha-device-picker
|
||||
.hass=${this.hass}
|
||||
.value=${this.value}
|
||||
.label=${this.label}
|
||||
.deviceFilter=${this._filterDevices}
|
||||
.includeDeviceClasses=${this.selector.device.entity?.device_class
|
||||
? [this.selector.device.entity.device_class]
|
||||
: undefined}
|
||||
.includeDomains=${this.selector.device.entity?.domain
|
||||
? [this.selector.device.entity.domain]
|
||||
: undefined}
|
||||
.disabled=${this.disabled}
|
||||
allow-custom-entity
|
||||
></ha-device-picker> `;
|
||||
}
|
||||
|
||||
return html`
|
||||
${this.label ? html`<label>${this.label}</label>` : ""}
|
||||
<ha-devices-picker
|
||||
.hass=${this.hass}
|
||||
.value=${this.value}
|
||||
.includeDeviceClasses=${this.selector.device.entity?.device_class
|
||||
? [this.selector.device.entity.device_class]
|
||||
: undefined}
|
||||
.includeDomains=${this.selector.device.entity?.domain
|
||||
? [this.selector.device.entity.domain]
|
||||
: undefined}
|
||||
></ha-devices-picker>
|
||||
`;
|
||||
}
|
||||
|
||||
private _filterDevices = (device: DeviceRegistryEntry): boolean => {
|
||||
@@ -71,12 +92,6 @@ export class HaDeviceSelector extends LitElement {
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
private async _loadConfigEntries() {
|
||||
this._configEntries = (await getConfigEntries(this.hass)).filter(
|
||||
(entry) => entry.domain === this.selector.device.integration
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
|
@@ -1,8 +1,8 @@
|
||||
import "../ha-duration-input";
|
||||
import { html, LitElement } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { DurationSelector } from "../../data/selector";
|
||||
import { HomeAssistant } from "../../types";
|
||||
import type { DurationSelector } from "../../data/selector";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import "../ha-duration-input";
|
||||
|
||||
@customElement("ha-selector-duration")
|
||||
export class HaTimeDuration extends LitElement {
|
||||
|
@@ -6,6 +6,7 @@ import { subscribeEntityRegistry } from "../../data/entity_registry";
|
||||
import { EntitySelector } from "../../data/selector";
|
||||
import { SubscribeMixin } from "../../mixins/subscribe-mixin";
|
||||
import { HomeAssistant } from "../../types";
|
||||
import "../entity/ha-entities-picker";
|
||||
import "../entity/ha-entity-picker";
|
||||
|
||||
@customElement("ha-selector-entity")
|
||||
@@ -23,14 +24,29 @@ export class HaEntitySelector extends SubscribeMixin(LitElement) {
|
||||
@property({ type: Boolean }) public disabled = false;
|
||||
|
||||
protected render() {
|
||||
return html`<ha-entity-picker
|
||||
.hass=${this.hass}
|
||||
.value=${this.value}
|
||||
.label=${this.label}
|
||||
.entityFilter=${this._filterEntities}
|
||||
.disabled=${this.disabled}
|
||||
allow-custom-entity
|
||||
></ha-entity-picker>`;
|
||||
if (!this.selector.entity.multiple) {
|
||||
return html`<ha-entity-picker
|
||||
.hass=${this.hass}
|
||||
.value=${this.value}
|
||||
.label=${this.label}
|
||||
.includeEntities=${this.selector.entity.includeEntities}
|
||||
.excludeEntities=${this.selector.entity.excludeEntities}
|
||||
.entityFilter=${this._filterEntities}
|
||||
.disabled=${this.disabled}
|
||||
allow-custom-entity
|
||||
></ha-entity-picker>`;
|
||||
}
|
||||
|
||||
return html`
|
||||
${this.label ? html`<label>${this.label}</label>` : ""}
|
||||
<ha-entities-picker
|
||||
.hass=${this.hass}
|
||||
.value=${this.value}
|
||||
.entityFilter=${this._filterEntities}
|
||||
.includeEntities=${this.selector.entity.includeEntities}
|
||||
.excludeEntities=${this.selector.entity.excludeEntities}
|
||||
></ha-entities-picker>
|
||||
`;
|
||||
}
|
||||
|
||||
public hassSubscribe(): UnsubscribeFunc[] {
|
||||
|
@@ -46,7 +46,7 @@ export class HaNumberSelector extends LitElement {
|
||||
class=${classMap({ single: this.selector.number.mode === "box" })}
|
||||
.min=${this.selector.number.min}
|
||||
.max=${this.selector.number.max}
|
||||
.value=${this.value || ""}
|
||||
.value=${this.value ?? ""}
|
||||
.step=${this.selector.number.step ?? 1}
|
||||
.disabled=${this.disabled}
|
||||
.required=${this.required}
|
||||
|
@@ -1,17 +1,19 @@
|
||||
import "@material/mwc-formfield/mwc-formfield";
|
||||
import "@material/mwc-list/mwc-list-item";
|
||||
import { css, CSSResultGroup, html, LitElement } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import { stopPropagation } from "../../common/dom/stop_propagation";
|
||||
import { SelectOption, SelectSelector } from "../../data/selector";
|
||||
import { HomeAssistant } from "../../types";
|
||||
import type { SelectOption, SelectSelector } from "../../data/selector";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import "../ha-select";
|
||||
import "../ha-radio";
|
||||
|
||||
@customElement("ha-selector-select")
|
||||
export class HaSelectSelector extends LitElement {
|
||||
@property() public hass!: HomeAssistant;
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property() public selector!: SelectSelector;
|
||||
@property({ attribute: false }) public selector!: SelectSelector;
|
||||
|
||||
@property() public value?: string;
|
||||
|
||||
@@ -21,24 +23,51 @@ export class HaSelectSelector extends LitElement {
|
||||
|
||||
@property({ type: Boolean }) public disabled = false;
|
||||
|
||||
protected render() {
|
||||
return html`<ha-select
|
||||
fixedMenuPosition
|
||||
naturalMenuWidth
|
||||
.label=${this.label}
|
||||
.value=${this.value}
|
||||
.helper=${this.helper}
|
||||
.disabled=${this.disabled}
|
||||
@closed=${stopPropagation}
|
||||
@selected=${this._valueChanged}
|
||||
>
|
||||
${this.selector.select.options.map((item: string | SelectOption) => {
|
||||
const value = typeof item === "object" ? item.value : item;
|
||||
const label = typeof item === "object" ? item.label : item;
|
||||
@property({ type: Boolean }) public required = true;
|
||||
|
||||
return html`<mwc-list-item .value=${value}>${label}</mwc-list-item>`;
|
||||
})}
|
||||
</ha-select>`;
|
||||
protected render() {
|
||||
if (this.required && this.selector.select.options!.length < 6) {
|
||||
return html`
|
||||
<div>
|
||||
${this.label}
|
||||
${this.selector.select.options.map((item: string | SelectOption) => {
|
||||
const value = typeof item === "object" ? item.value : item;
|
||||
const label = typeof item === "object" ? item.label : item;
|
||||
|
||||
return html`
|
||||
<mwc-formfield .label=${label}>
|
||||
<ha-radio
|
||||
.checked=${value === this.value}
|
||||
.value=${value}
|
||||
.disabled=${this.disabled}
|
||||
@change=${this._valueChanged}
|
||||
></ha-radio>
|
||||
</mwc-formfield>
|
||||
`;
|
||||
})}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
return html`
|
||||
<ha-select
|
||||
fixedMenuPosition
|
||||
naturalMenuWidth
|
||||
.label=${this.label}
|
||||
.value=${this.value}
|
||||
.helper=${this.helper}
|
||||
.disabled=${this.disabled}
|
||||
@closed=${stopPropagation}
|
||||
@selected=${this._valueChanged}
|
||||
>
|
||||
${this.selector.select.options.map((item: string | SelectOption) => {
|
||||
const value = typeof item === "object" ? item.value : item;
|
||||
const label = typeof item === "object" ? item.label : item;
|
||||
|
||||
return html`<mwc-list-item .value=${value}>${label}</mwc-list-item>`;
|
||||
})}
|
||||
</ha-select>
|
||||
`;
|
||||
}
|
||||
|
||||
private _valueChanged(ev) {
|
||||
@@ -56,6 +85,9 @@ export class HaSelectSelector extends LitElement {
|
||||
ha-select {
|
||||
width: 100%;
|
||||
}
|
||||
mwc-formfield {
|
||||
display: block;
|
||||
}
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
@@ -134,9 +134,8 @@ export class HaTargetSelector extends SubscribeMixin(LitElement) {
|
||||
private async _loadConfigEntries() {
|
||||
this._configEntries = (await getConfigEntries(this.hass)).filter(
|
||||
(entry) =>
|
||||
entry.domain ===
|
||||
(this.selector.target.device?.integration ||
|
||||
this.selector.target.entity?.integration)
|
||||
entry.domain === this.selector.target.device?.integration ||
|
||||
entry.domain === this.selector.target.entity?.integration
|
||||
);
|
||||
}
|
||||
|
||||
|
@@ -8,6 +8,9 @@ import "./ha-selector-addon";
|
||||
import "./ha-selector-area";
|
||||
import "./ha-selector-attribute";
|
||||
import "./ha-selector-boolean";
|
||||
import "./ha-selector-color-rgb";
|
||||
import "./ha-selector-date";
|
||||
import "./ha-selector-datetime";
|
||||
import "./ha-selector-device";
|
||||
import "./ha-selector-duration";
|
||||
import "./ha-selector-entity";
|
||||
@@ -21,6 +24,7 @@ import "./ha-selector-icon";
|
||||
import "./ha-selector-media";
|
||||
import "./ha-selector-theme";
|
||||
import "./ha-selector-location";
|
||||
import "./ha-selector-color-temp";
|
||||
|
||||
@customElement("ha-selector")
|
||||
export class HaSelector extends LitElement {
|
||||
@@ -40,6 +44,8 @@ export class HaSelector extends LitElement {
|
||||
|
||||
@property({ type: Boolean }) public required = true;
|
||||
|
||||
@property() public context?: Record<string, any>;
|
||||
|
||||
public focus() {
|
||||
this.shadowRoot?.getElementById("selector")?.focus();
|
||||
}
|
||||
@@ -59,6 +65,7 @@ export class HaSelector extends LitElement {
|
||||
disabled: this.disabled,
|
||||
required: this.required,
|
||||
helper: this.helper,
|
||||
context: this.context,
|
||||
id: "selector",
|
||||
})}
|
||||
`;
|
||||
|
@@ -42,9 +42,7 @@ export class HaTab extends LitElement {
|
||||
@keydown=${this._handleKeyDown}
|
||||
>
|
||||
${this.narrow ? html`<slot name="icon"></slot>` : ""}
|
||||
${!this.narrow || this.active
|
||||
? html`<span class="name">${this.name}</span>`
|
||||
: ""}
|
||||
<span class="name">${this.name}</span>
|
||||
${this._shouldRenderRipple ? html`<mwc-ripple></mwc-ripple>` : ""}
|
||||
</div>
|
||||
`;
|
||||
|
@@ -31,6 +31,8 @@ export class HaYamlEditor extends LitElement {
|
||||
|
||||
@property() public label?: string;
|
||||
|
||||
@property({ type: Boolean }) public readOnly = false;
|
||||
|
||||
@state() private _yaml = "";
|
||||
|
||||
public setValue(value): void {
|
||||
@@ -61,6 +63,7 @@ export class HaYamlEditor extends LitElement {
|
||||
<ha-code-editor
|
||||
.hass=${this.hass}
|
||||
.value=${this._yaml}
|
||||
.readOnly=${this.readOnly}
|
||||
mode="yaml"
|
||||
autocomplete-entities
|
||||
.error=${this.isValid === false}
|
||||
|
@@ -7,6 +7,7 @@ import {
|
||||
HistoryResult,
|
||||
LineChartUnit,
|
||||
TimelineEntity,
|
||||
entityIdHistoryNeedsAttributes,
|
||||
} from "./history";
|
||||
|
||||
export interface CacheConfig {
|
||||
@@ -53,7 +54,17 @@ export const getRecent = (
|
||||
return cache.data;
|
||||
}
|
||||
|
||||
const prom = fetchRecent(hass, entityId, startTime, endTime).then(
|
||||
const noAttributes = !entityIdHistoryNeedsAttributes(hass, entityId);
|
||||
const prom = fetchRecent(
|
||||
hass,
|
||||
entityId,
|
||||
startTime,
|
||||
endTime,
|
||||
false,
|
||||
undefined,
|
||||
true,
|
||||
noAttributes
|
||||
).then(
|
||||
(stateHistory) => computeHistory(hass, stateHistory, localize),
|
||||
(err) => {
|
||||
delete RECENT_CACHE[entityId];
|
||||
@@ -120,6 +131,7 @@ export const getRecentWithCache = (
|
||||
}
|
||||
|
||||
const curCacheProm = cache.prom;
|
||||
const noAttributes = !entityIdHistoryNeedsAttributes(hass, entityId);
|
||||
|
||||
const genProm = async () => {
|
||||
let fetchedHistory: HassEntity[][];
|
||||
@@ -132,7 +144,10 @@ export const getRecentWithCache = (
|
||||
entityId,
|
||||
toFetchStartTime,
|
||||
endTime,
|
||||
appendingToCache
|
||||
appendingToCache,
|
||||
undefined,
|
||||
true,
|
||||
noAttributes
|
||||
),
|
||||
]);
|
||||
fetchedHistory = results[1];
|
||||
|
@@ -6,6 +6,7 @@ import { AutomationConfig } from "./automation";
|
||||
interface CloudStatusNotLoggedIn {
|
||||
logged_in: false;
|
||||
cloud: "disconnected" | "connecting" | "connected";
|
||||
http_use_ssl: boolean;
|
||||
}
|
||||
|
||||
export interface GoogleEntityConfig {
|
||||
@@ -59,6 +60,7 @@ export interface CloudStatusLoggedIn {
|
||||
remote_connected: boolean;
|
||||
remote_certificate: undefined | CertificateInformation;
|
||||
http_use_ssl: boolean;
|
||||
active_subscription: boolean;
|
||||
}
|
||||
|
||||
export type CloudStatus = CloudStatusNotLoggedIn | CloudStatusLoggedIn;
|
||||
|
@@ -34,8 +34,24 @@ export const ERROR_STATES: ConfigEntry["state"][] = [
|
||||
"setup_retry",
|
||||
];
|
||||
|
||||
export const getConfigEntries = (hass: HomeAssistant) =>
|
||||
hass.callApi<ConfigEntry[]>("GET", "config/config_entries/entry");
|
||||
export const getConfigEntries = (
|
||||
hass: HomeAssistant,
|
||||
filters?: { type?: "helper" | "integration"; domain?: string }
|
||||
): Promise<ConfigEntry[]> => {
|
||||
const params = new URLSearchParams();
|
||||
if (filters) {
|
||||
if (filters.type) {
|
||||
params.append("type", filters.type);
|
||||
}
|
||||
if (filters.domain) {
|
||||
params.append("domain", filters.domain);
|
||||
}
|
||||
}
|
||||
return hass.callApi<ConfigEntry[]>(
|
||||
"GET",
|
||||
`config/config_entries/entry?${params.toString()}`
|
||||
);
|
||||
};
|
||||
|
||||
export const updateConfigEntry = (
|
||||
hass: HomeAssistant,
|
||||
|
@@ -65,8 +65,14 @@ export const ignoreConfigFlow = (
|
||||
export const deleteConfigFlow = (hass: HomeAssistant, flowId: string) =>
|
||||
hass.callApi("DELETE", `config/config_entries/flow/${flowId}`);
|
||||
|
||||
export const getConfigFlowHandlers = (hass: HomeAssistant) =>
|
||||
hass.callApi<string[]>("GET", "config/config_entries/flow_handlers");
|
||||
export const getConfigFlowHandlers = (
|
||||
hass: HomeAssistant,
|
||||
type?: "helper" | "integration"
|
||||
) =>
|
||||
hass.callApi<string[]>(
|
||||
"GET",
|
||||
`config/config_entries/flow_handlers${type ? `?type=${type}` : ""}`
|
||||
);
|
||||
|
||||
export const fetchConfigFlowInProgress = (
|
||||
conn: Connection
|
||||
|
@@ -28,7 +28,7 @@ export interface DataEntryFlowStepForm {
|
||||
step_id: string;
|
||||
data_schema: HaFormSchema[];
|
||||
errors: Record<string, string>;
|
||||
description_placeholders: Record<string, string>;
|
||||
description_placeholders?: Record<string, string>;
|
||||
last_step: boolean | null;
|
||||
}
|
||||
|
||||
@@ -49,7 +49,7 @@ export interface DataEntryFlowStepCreateEntry {
|
||||
title: string;
|
||||
result?: ConfigEntry;
|
||||
description: string;
|
||||
description_placeholders: Record<string, string>;
|
||||
description_placeholders?: Record<string, string>;
|
||||
}
|
||||
|
||||
export interface DataEntryFlowStepAbort {
|
||||
@@ -57,7 +57,7 @@ export interface DataEntryFlowStepAbort {
|
||||
flow_id: string;
|
||||
handler: string;
|
||||
reason: string;
|
||||
description_placeholders: Record<string, string>;
|
||||
description_placeholders?: Record<string, string>;
|
||||
}
|
||||
|
||||
export interface DataEntryFlowStepProgress {
|
||||
@@ -66,7 +66,17 @@ export interface DataEntryFlowStepProgress {
|
||||
handler: string;
|
||||
step_id: string;
|
||||
progress_action: string;
|
||||
description_placeholders: Record<string, string>;
|
||||
description_placeholders?: Record<string, string>;
|
||||
}
|
||||
|
||||
export interface DataEntryFlowStepMenu {
|
||||
type: "menu";
|
||||
flow_id: string;
|
||||
handler: string;
|
||||
step_id: string;
|
||||
/** If array, use value to lookup translations in strings.json */
|
||||
menu_options: string[] | Record<string, string>;
|
||||
description_placeholders?: Record<string, string>;
|
||||
}
|
||||
|
||||
export type DataEntryFlowStep =
|
||||
@@ -74,7 +84,8 @@ export type DataEntryFlowStep =
|
||||
| DataEntryFlowStepExternal
|
||||
| DataEntryFlowStepCreateEntry
|
||||
| DataEntryFlowStepAbort
|
||||
| DataEntryFlowStepProgress;
|
||||
| DataEntryFlowStepProgress
|
||||
| DataEntryFlowStepMenu;
|
||||
|
||||
export const subscribeDataEntryFlowProgressed = (
|
||||
conn: Connection,
|
||||
|
@@ -12,7 +12,12 @@ import { subscribeOne } from "../common/util/subscribe-one";
|
||||
import { HomeAssistant } from "../types";
|
||||
import { ConfigEntry, getConfigEntries } from "./config_entries";
|
||||
import { subscribeEntityRegistry } from "./entity_registry";
|
||||
import { fetchStatistics, Statistics } from "./history";
|
||||
import {
|
||||
fetchStatistics,
|
||||
Statistics,
|
||||
StatisticsMetaData,
|
||||
getStatisticMetadata,
|
||||
} from "./history";
|
||||
|
||||
const energyCollectionKeys: (string | undefined)[] = [];
|
||||
|
||||
@@ -136,6 +141,7 @@ export interface GasSourceTypeEnergyPreference {
|
||||
entity_energy_from: string | null;
|
||||
entity_energy_price: string | null;
|
||||
number_energy_price: number | null;
|
||||
unit_of_measurement?: string | null;
|
||||
}
|
||||
|
||||
type EnergySource =
|
||||
@@ -241,14 +247,14 @@ const getEnergyData = async (
|
||||
end?: Date
|
||||
): Promise<EnergyData> => {
|
||||
const [configEntries, entityRegistryEntries, info] = await Promise.all([
|
||||
getConfigEntries(hass),
|
||||
getConfigEntries(hass, { domain: "co2signal" }),
|
||||
subscribeOne(hass.connection, subscribeEntityRegistry),
|
||||
getEnergyInfo(hass),
|
||||
]);
|
||||
|
||||
const co2SignalConfigEntry = configEntries.find(
|
||||
(entry) => entry.domain === "co2signal"
|
||||
);
|
||||
const co2SignalConfigEntry = configEntries.length
|
||||
? configEntries[0]
|
||||
: undefined;
|
||||
|
||||
let co2SignalEntity: string | undefined;
|
||||
|
||||
@@ -271,6 +277,15 @@ const getEnergyData = async (
|
||||
|
||||
const consumptionStatIDs: string[] = [];
|
||||
const statIDs: string[] = [];
|
||||
const gasSources: GasSourceTypeEnergyPreference[] =
|
||||
prefs.energy_sources.filter(
|
||||
(source) => source.type === "gas"
|
||||
) as GasSourceTypeEnergyPreference[];
|
||||
const gasStatisticIdsWithMeta: StatisticsMetaData[] =
|
||||
await getStatisticMetadata(
|
||||
hass,
|
||||
gasSources.map((source) => source.stat_energy_from)
|
||||
);
|
||||
|
||||
for (const source of prefs.energy_sources) {
|
||||
if (source.type === "solar") {
|
||||
@@ -280,6 +295,20 @@ const getEnergyData = async (
|
||||
|
||||
if (source.type === "gas") {
|
||||
statIDs.push(source.stat_energy_from);
|
||||
const entity = hass.states[source.stat_energy_from];
|
||||
if (!entity) {
|
||||
for (const statisticIdWithMeta of gasStatisticIdsWithMeta) {
|
||||
if (
|
||||
statisticIdWithMeta?.statistic_id === source.stat_energy_from &&
|
||||
statisticIdWithMeta?.unit_of_measurement
|
||||
) {
|
||||
source.unit_of_measurement =
|
||||
statisticIdWithMeta?.unit_of_measurement === "Wh"
|
||||
? "kWh"
|
||||
: statisticIdWithMeta?.unit_of_measurement;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (source.stat_cost) {
|
||||
statIDs.push(source.stat_cost);
|
||||
}
|
||||
@@ -559,6 +588,9 @@ export const getEnergyGasUnit = (
|
||||
? "kWh"
|
||||
: entity.attributes.unit_of_measurement;
|
||||
}
|
||||
if (source.unit_of_measurement) {
|
||||
return source.unit_of_measurement;
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
|
@@ -14,6 +14,7 @@ export interface EntityRegistryEntry {
|
||||
device_id: string | null;
|
||||
area_id: string | null;
|
||||
disabled_by: string | null;
|
||||
hidden_by: string | null;
|
||||
entity_category: "config" | "diagnostic" | null;
|
||||
}
|
||||
|
||||
@@ -38,6 +39,7 @@ export interface EntityRegistryEntryUpdateParams {
|
||||
device_class?: string | null;
|
||||
area_id?: string | null;
|
||||
disabled_by?: string | null;
|
||||
hidden_by: string | null;
|
||||
new_entity_id?: string;
|
||||
}
|
||||
|
||||
|
71
src/data/helpers_crud.ts
Normal file
71
src/data/helpers_crud.ts
Normal file
@@ -0,0 +1,71 @@
|
||||
import { fetchCounter, updateCounter, deleteCounter } from "./counter";
|
||||
import {
|
||||
fetchInputBoolean,
|
||||
updateInputBoolean,
|
||||
deleteInputBoolean,
|
||||
} from "./input_boolean";
|
||||
import {
|
||||
fetchInputButton,
|
||||
updateInputButton,
|
||||
deleteInputButton,
|
||||
} from "./input_button";
|
||||
import {
|
||||
fetchInputDateTime,
|
||||
updateInputDateTime,
|
||||
deleteInputDateTime,
|
||||
} from "./input_datetime";
|
||||
import {
|
||||
fetchInputNumber,
|
||||
updateInputNumber,
|
||||
deleteInputNumber,
|
||||
} from "./input_number";
|
||||
import {
|
||||
fetchInputSelect,
|
||||
updateInputSelect,
|
||||
deleteInputSelect,
|
||||
} from "./input_select";
|
||||
import { fetchInputText, updateInputText, deleteInputText } from "./input_text";
|
||||
import { fetchTimer, updateTimer, deleteTimer } from "./timer";
|
||||
|
||||
export const HELPERS_CRUD = {
|
||||
input_boolean: {
|
||||
fetch: fetchInputBoolean,
|
||||
update: updateInputBoolean,
|
||||
delete: deleteInputBoolean,
|
||||
},
|
||||
input_button: {
|
||||
fetch: fetchInputButton,
|
||||
update: updateInputButton,
|
||||
delete: deleteInputButton,
|
||||
},
|
||||
input_text: {
|
||||
fetch: fetchInputText,
|
||||
update: updateInputText,
|
||||
delete: deleteInputText,
|
||||
},
|
||||
input_number: {
|
||||
fetch: fetchInputNumber,
|
||||
update: updateInputNumber,
|
||||
delete: deleteInputNumber,
|
||||
},
|
||||
input_datetime: {
|
||||
fetch: fetchInputDateTime,
|
||||
update: updateInputDateTime,
|
||||
delete: deleteInputDateTime,
|
||||
},
|
||||
input_select: {
|
||||
fetch: fetchInputSelect,
|
||||
update: updateInputSelect,
|
||||
delete: deleteInputSelect,
|
||||
},
|
||||
counter: {
|
||||
fetch: fetchCounter,
|
||||
update: updateCounter,
|
||||
delete: deleteCounter,
|
||||
},
|
||||
timer: {
|
||||
fetch: fetchTimer,
|
||||
update: updateTimer,
|
||||
delete: deleteTimer,
|
||||
},
|
||||
};
|
@@ -1,4 +1,5 @@
|
||||
import { HassEntity } from "home-assistant-js-websocket";
|
||||
import { computeDomain } from "../common/entity/compute_domain";
|
||||
import { computeStateDisplay } from "../common/entity/compute_state_display";
|
||||
import { computeStateDomain } from "../common/entity/compute_state_domain";
|
||||
import { computeStateName } from "../common/entity/compute_state_name";
|
||||
@@ -7,6 +8,13 @@ import { HomeAssistant } from "../types";
|
||||
import { FrontendLocaleData } from "./translation";
|
||||
|
||||
const DOMAINS_USE_LAST_UPDATED = ["climate", "humidifier", "water_heater"];
|
||||
const NEED_ATTRIBUTE_DOMAINS = [
|
||||
"climate",
|
||||
"humidifier",
|
||||
"input_datetime",
|
||||
"thermostat",
|
||||
"water_heater",
|
||||
];
|
||||
const LINE_ATTRIBUTES_TO_KEEP = [
|
||||
"temperature",
|
||||
"current_temperature",
|
||||
@@ -131,6 +139,13 @@ export interface StatisticsValidationResults {
|
||||
[statisticId: string]: StatisticsValidationResult[];
|
||||
}
|
||||
|
||||
export const entityIdHistoryNeedsAttributes = (
|
||||
hass: HomeAssistant,
|
||||
entityId: string
|
||||
) =>
|
||||
!hass.states[entityId] ||
|
||||
NEED_ATTRIBUTE_DOMAINS.includes(computeDomain(entityId));
|
||||
|
||||
export const fetchRecent = (
|
||||
hass: HomeAssistant,
|
||||
entityId: string,
|
||||
@@ -138,7 +153,8 @@ export const fetchRecent = (
|
||||
endTime: Date,
|
||||
skipInitialState = false,
|
||||
significantChangesOnly?: boolean,
|
||||
minimalResponse = true
|
||||
minimalResponse = true,
|
||||
noAttributes?: boolean
|
||||
): Promise<HassEntity[][]> => {
|
||||
let url = "history/period";
|
||||
if (startTime) {
|
||||
@@ -157,7 +173,9 @@ export const fetchRecent = (
|
||||
if (minimalResponse) {
|
||||
url += "&minimal_response";
|
||||
}
|
||||
|
||||
if (noAttributes) {
|
||||
url += "&no_attributes";
|
||||
}
|
||||
return hass.callApi("GET", url);
|
||||
};
|
||||
|
||||
@@ -171,6 +189,10 @@ export const fetchDate = (
|
||||
"GET",
|
||||
`history/period/${startTime.toISOString()}?end_time=${endTime.toISOString()}&minimal_response${
|
||||
entityId ? `&filter_entity_id=${entityId}` : ``
|
||||
}${
|
||||
entityId && !entityIdHistoryNeedsAttributes(hass, entityId)
|
||||
? `&no_attributes`
|
||||
: ``
|
||||
}`
|
||||
);
|
||||
|
||||
@@ -278,6 +300,10 @@ const processLineChartEntities = (
|
||||
};
|
||||
};
|
||||
|
||||
const stateUsesUnits = (state: HassEntity) =>
|
||||
"unit_of_measurement" in state.attributes ||
|
||||
"state_class" in state.attributes;
|
||||
|
||||
export const computeHistory = (
|
||||
hass: HomeAssistant,
|
||||
stateHistory: HassEntity[][],
|
||||
@@ -294,16 +320,18 @@ export const computeHistory = (
|
||||
return;
|
||||
}
|
||||
|
||||
const stateWithUnitorStateClass = stateInfo.find(
|
||||
(state) =>
|
||||
state.attributes &&
|
||||
("unit_of_measurement" in state.attributes ||
|
||||
"state_class" in state.attributes)
|
||||
);
|
||||
const entityId = stateInfo[0].entity_id;
|
||||
const currentState =
|
||||
entityId in hass.states ? hass.states[entityId] : undefined;
|
||||
const stateWithUnitorStateClass =
|
||||
!currentState &&
|
||||
stateInfo.find((state) => state.attributes && stateUsesUnits(state));
|
||||
|
||||
let unit: string | undefined;
|
||||
|
||||
if (stateWithUnitorStateClass) {
|
||||
if (currentState && stateUsesUnits(currentState)) {
|
||||
unit = currentState.attributes.unit_of_measurement || " ";
|
||||
} else if (stateWithUnitorStateClass) {
|
||||
unit = stateWithUnitorStateClass.attributes.unit_of_measurement || " ";
|
||||
} else {
|
||||
unit = {
|
||||
@@ -313,7 +341,7 @@ export const computeHistory = (
|
||||
input_number: "#",
|
||||
number: "#",
|
||||
water_heater: hass.config.unit_system.temperature,
|
||||
}[computeStateDomain(stateInfo[0])];
|
||||
}[computeDomain(entityId)];
|
||||
}
|
||||
|
||||
if (!unit) {
|
||||
@@ -345,6 +373,15 @@ export const getStatisticIds = (
|
||||
statistic_type,
|
||||
});
|
||||
|
||||
export const getStatisticMetadata = (
|
||||
hass: HomeAssistant,
|
||||
statistic_ids?: string[]
|
||||
) =>
|
||||
hass.callWS<StatisticsMetaData[]>({
|
||||
type: "recorder/get_statistics_metadata",
|
||||
statistic_ids,
|
||||
});
|
||||
|
||||
export const fetchStatistics = (
|
||||
hass: HomeAssistant,
|
||||
startTime: Date,
|
||||
@@ -428,3 +465,16 @@ export const statisticsHaveType = (
|
||||
stats: StatisticValue[],
|
||||
type: StatisticType
|
||||
) => stats.some((stat) => stat[type] !== null);
|
||||
|
||||
export const adjustStatisticsSum = (
|
||||
hass: HomeAssistant,
|
||||
statistic_id: string,
|
||||
start_time: string,
|
||||
adjustment: number
|
||||
): Promise<void> =>
|
||||
hass.callWS({
|
||||
type: "recorder/adjust_sum_statistics",
|
||||
statistic_id,
|
||||
start_time,
|
||||
adjustment,
|
||||
});
|
||||
|
213
src/data/ozw.ts
213
src/data/ozw.ts
@@ -1,213 +0,0 @@
|
||||
import { HomeAssistant } from "../types";
|
||||
import { DeviceRegistryEntry } from "./device_registry";
|
||||
|
||||
export interface OZWNodeIdentifiers {
|
||||
ozw_instance: number;
|
||||
node_id: number;
|
||||
}
|
||||
|
||||
export interface OZWDevice {
|
||||
node_id: number;
|
||||
node_query_stage: string;
|
||||
is_awake: boolean;
|
||||
is_failed: boolean;
|
||||
is_zwave_plus: boolean;
|
||||
ozw_instance: number;
|
||||
event: string;
|
||||
node_manufacturer_name: string;
|
||||
node_product_name: string;
|
||||
}
|
||||
|
||||
export interface OZWDeviceMetaDataResponse {
|
||||
node_id: number;
|
||||
ozw_instance: number;
|
||||
metadata: OZWDeviceMetaData;
|
||||
}
|
||||
|
||||
export interface OZWDeviceMetaData {
|
||||
OZWInfoURL: string;
|
||||
ZWAProductURL: string;
|
||||
ProductPic: string;
|
||||
Description: string;
|
||||
ProductManualURL: string;
|
||||
ProductPageURL: string;
|
||||
InclusionHelp: string;
|
||||
ExclusionHelp: string;
|
||||
ResetHelp: string;
|
||||
WakeupHelp: string;
|
||||
ProductSupportURL: string;
|
||||
Frequency: string;
|
||||
Name: string;
|
||||
ProductPicBase64: string;
|
||||
}
|
||||
|
||||
export interface OZWInstance {
|
||||
ozw_instance: number;
|
||||
OZWDaemon_Version: string;
|
||||
OpenZWave_Version: string;
|
||||
QTOpenZWave_Version: string;
|
||||
Status: string;
|
||||
getControllerPath: string;
|
||||
homeID: string;
|
||||
}
|
||||
|
||||
export interface OZWNetworkStatistics {
|
||||
ozw_instance: number;
|
||||
node_count: number;
|
||||
readCnt: number;
|
||||
writeCnt: number;
|
||||
ACKCnt: number;
|
||||
CANCnt: number;
|
||||
NAKCnt: number;
|
||||
dropped: number;
|
||||
retries: number;
|
||||
}
|
||||
|
||||
export interface OZWDeviceConfig {
|
||||
label: string;
|
||||
type: string;
|
||||
value: string | number;
|
||||
parameter: number;
|
||||
min: number;
|
||||
max: number;
|
||||
help: string;
|
||||
}
|
||||
|
||||
export const nodeQueryStages = [
|
||||
"ProtocolInfo",
|
||||
"Probe",
|
||||
"WakeUp",
|
||||
"ManufacturerSpecific1",
|
||||
"NodeInfo",
|
||||
"NodePlusInfo",
|
||||
"ManufacturerSpecific2",
|
||||
"Versions",
|
||||
"Instances",
|
||||
"Static",
|
||||
"CacheLoad",
|
||||
"Associations",
|
||||
"Neighbors",
|
||||
"Session",
|
||||
"Dynamic",
|
||||
"Configuration",
|
||||
"Complete",
|
||||
];
|
||||
|
||||
export const networkOnlineStatuses = [
|
||||
"driverAllNodesQueried",
|
||||
"driverAllNodesQueriedSomeDead",
|
||||
"driverAwakeNodesQueried",
|
||||
];
|
||||
export const networkStartingStatuses = [
|
||||
"starting",
|
||||
"started",
|
||||
"Ready",
|
||||
"driverReady",
|
||||
];
|
||||
export const networkOfflineStatuses = [
|
||||
"Offline",
|
||||
"stopped",
|
||||
"driverFailed",
|
||||
"driverReset",
|
||||
"driverRemoved",
|
||||
"driverAllNodesOnFire",
|
||||
];
|
||||
|
||||
export const getIdentifiersFromDevice = function (
|
||||
device: DeviceRegistryEntry
|
||||
): OZWNodeIdentifiers | undefined {
|
||||
if (!device) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const ozwIdentifier = device.identifiers.find(
|
||||
(identifier) => identifier[0] === "ozw"
|
||||
);
|
||||
if (!ozwIdentifier) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const identifiers = ozwIdentifier[1].split(".");
|
||||
return {
|
||||
node_id: parseInt(identifiers[1]),
|
||||
ozw_instance: parseInt(identifiers[0]),
|
||||
};
|
||||
};
|
||||
|
||||
export const fetchOZWInstances = (
|
||||
hass: HomeAssistant
|
||||
): Promise<OZWInstance[]> =>
|
||||
hass.callWS({
|
||||
type: "ozw/get_instances",
|
||||
});
|
||||
|
||||
export const fetchOZWNetworkStatus = (
|
||||
hass: HomeAssistant,
|
||||
ozw_instance: number
|
||||
): Promise<OZWInstance> =>
|
||||
hass.callWS({
|
||||
type: "ozw/network_status",
|
||||
ozw_instance,
|
||||
});
|
||||
|
||||
export const fetchOZWNetworkStatistics = (
|
||||
hass: HomeAssistant,
|
||||
ozw_instance: number
|
||||
): Promise<OZWNetworkStatistics> =>
|
||||
hass.callWS({
|
||||
type: "ozw/network_statistics",
|
||||
ozw_instance,
|
||||
});
|
||||
|
||||
export const fetchOZWNodes = (
|
||||
hass: HomeAssistant,
|
||||
ozw_instance: number
|
||||
): Promise<OZWDevice[]> =>
|
||||
hass.callWS({
|
||||
type: "ozw/get_nodes",
|
||||
ozw_instance,
|
||||
});
|
||||
|
||||
export const fetchOZWNodeStatus = (
|
||||
hass: HomeAssistant,
|
||||
ozw_instance: number,
|
||||
node_id: number
|
||||
): Promise<OZWDevice> =>
|
||||
hass.callWS({
|
||||
type: "ozw/node_status",
|
||||
ozw_instance,
|
||||
node_id,
|
||||
});
|
||||
|
||||
export const fetchOZWNodeMetadata = (
|
||||
hass: HomeAssistant,
|
||||
ozw_instance: number,
|
||||
node_id: number
|
||||
): Promise<OZWDeviceMetaDataResponse> =>
|
||||
hass.callWS({
|
||||
type: "ozw/node_metadata",
|
||||
ozw_instance,
|
||||
node_id,
|
||||
});
|
||||
|
||||
export const fetchOZWNodeConfig = (
|
||||
hass: HomeAssistant,
|
||||
ozw_instance: number,
|
||||
node_id: number
|
||||
): Promise<OZWDeviceConfig[]> =>
|
||||
hass.callWS({
|
||||
type: "ozw/get_config_parameters",
|
||||
ozw_instance,
|
||||
node_id,
|
||||
});
|
||||
|
||||
export const refreshNodeInfo = (
|
||||
hass: HomeAssistant,
|
||||
ozw_instance: number,
|
||||
node_id: number
|
||||
): Promise<OZWDevice> =>
|
||||
hass.callWS({
|
||||
type: "ozw/refresh_node_info",
|
||||
ozw_instance,
|
||||
node_id,
|
||||
});
|
@@ -2,6 +2,8 @@ export type Selector =
|
||||
| AddonSelector
|
||||
| AttributeSelector
|
||||
| EntitySelector
|
||||
| DateSelector
|
||||
| DateTimeSelector
|
||||
| DeviceSelector
|
||||
| DurationSelector
|
||||
| AreaSelector
|
||||
@@ -16,22 +18,42 @@ export type Selector =
|
||||
| IconSelector
|
||||
| MediaSelector
|
||||
| ThemeSelector
|
||||
| LocationSelector;
|
||||
| LocationSelector
|
||||
| ColorTempSelector
|
||||
| ColorRGBSelector;
|
||||
|
||||
export interface EntitySelector {
|
||||
entity: {
|
||||
integration?: string;
|
||||
domain?: string | string[];
|
||||
device_class?: string;
|
||||
multiple?: boolean;
|
||||
includeEntities?: string[];
|
||||
excludeEntities?: string[];
|
||||
};
|
||||
}
|
||||
|
||||
export interface AttributeSelector {
|
||||
attribute: {
|
||||
entity_id: string;
|
||||
entity_id?: string;
|
||||
};
|
||||
}
|
||||
|
||||
export interface ColorRGBSelector {
|
||||
// eslint-disable-next-line @typescript-eslint/ban-types
|
||||
color_rgb: {};
|
||||
}
|
||||
|
||||
export interface DateSelector {
|
||||
// eslint-disable-next-line @typescript-eslint/ban-types
|
||||
date: {};
|
||||
}
|
||||
|
||||
export interface DateTimeSelector {
|
||||
// eslint-disable-next-line @typescript-eslint/ban-types
|
||||
datetime: {};
|
||||
}
|
||||
|
||||
export interface DeviceSelector {
|
||||
device: {
|
||||
integration?: string;
|
||||
@@ -41,6 +63,7 @@ export interface DeviceSelector {
|
||||
domain?: EntitySelector["entity"]["domain"];
|
||||
device_class?: EntitySelector["entity"]["device_class"];
|
||||
};
|
||||
multiple?: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -96,6 +119,13 @@ export interface NumberSelector {
|
||||
};
|
||||
}
|
||||
|
||||
export interface ColorTempSelector {
|
||||
color_temp: {
|
||||
min_mireds?: number;
|
||||
max_mireds?: number;
|
||||
};
|
||||
}
|
||||
|
||||
export interface BooleanSelector {
|
||||
// eslint-disable-next-line @typescript-eslint/ban-types
|
||||
boolean: {};
|
||||
|
36
src/data/update.ts
Normal file
36
src/data/update.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
import type {
|
||||
HassEntityAttributeBase,
|
||||
HassEntityBase,
|
||||
} from "home-assistant-js-websocket";
|
||||
import { supportsFeature } from "../common/entity/supports-feature";
|
||||
|
||||
export const UPDATE_SUPPORT_INSTALL = 1;
|
||||
export const UPDATE_SUPPORT_SPECIFIC_VERSION = 2;
|
||||
export const UPDATE_SUPPORT_PROGRESS = 4;
|
||||
export const UPDATE_SUPPORT_BACKUP = 8;
|
||||
|
||||
interface UpdateEntityAttributes extends HassEntityAttributeBase {
|
||||
current_version: string | null;
|
||||
in_progress: boolean | number;
|
||||
latest_version: string | null;
|
||||
release_summary: string | null;
|
||||
release_url: string | null;
|
||||
skipped_version: string | null;
|
||||
title: string | null;
|
||||
}
|
||||
|
||||
export interface UpdateEntity extends HassEntityBase {
|
||||
attributes: UpdateEntityAttributes;
|
||||
}
|
||||
|
||||
export const updateUsesProgress = (entity: UpdateEntity): boolean =>
|
||||
supportsFeature(entity, UPDATE_SUPPORT_PROGRESS) &&
|
||||
typeof entity.attributes.in_progress === "number";
|
||||
|
||||
export const updateCanInstall = (entity: UpdateEntity): boolean =>
|
||||
supportsFeature(entity, UPDATE_SUPPORT_INSTALL) &&
|
||||
entity.attributes.latest_version !== entity.attributes.current_version &&
|
||||
entity.attributes.latest_version !== entity.attributes.skipped_version;
|
||||
|
||||
export const updateIsInstalling = (entity: UpdateEntity): boolean =>
|
||||
updateUsesProgress(entity) || !!entity.attributes.in_progress;
|
@@ -1,81 +0,0 @@
|
||||
import { HomeAssistant } from "../types";
|
||||
|
||||
export interface ZWaveNetworkStatus {
|
||||
state: number;
|
||||
}
|
||||
|
||||
export interface ZWaveValue {
|
||||
key: number;
|
||||
value: {
|
||||
index: number;
|
||||
instance: number;
|
||||
label: string;
|
||||
poll_intensity: number;
|
||||
};
|
||||
}
|
||||
|
||||
export interface ZWaveConfigItem {
|
||||
key: number;
|
||||
value: {
|
||||
data: any;
|
||||
data_items: any[];
|
||||
help: string;
|
||||
label: string;
|
||||
max: number;
|
||||
min: number;
|
||||
type: string;
|
||||
};
|
||||
}
|
||||
|
||||
export interface ZWaveConfigServiceData {
|
||||
node_id: number;
|
||||
parameter: number;
|
||||
value: number | string;
|
||||
}
|
||||
|
||||
export interface ZWaveNode {
|
||||
attributes: ZWaveAttributes;
|
||||
}
|
||||
|
||||
export interface ZWaveAttributes {
|
||||
node_id: number;
|
||||
wake_up_interval?: number;
|
||||
}
|
||||
|
||||
export interface ZWaveMigrationConfig {
|
||||
usb_path: string;
|
||||
network_key: string;
|
||||
}
|
||||
|
||||
export const ZWAVE_NETWORK_STATE_STOPPED = 0;
|
||||
export const ZWAVE_NETWORK_STATE_FAILED = 1;
|
||||
export const ZWAVE_NETWORK_STATE_STARTED = 5;
|
||||
export const ZWAVE_NETWORK_STATE_AWAKED = 7;
|
||||
export const ZWAVE_NETWORK_STATE_READY = 10;
|
||||
|
||||
export const fetchNetworkStatus = (
|
||||
hass: HomeAssistant
|
||||
): Promise<ZWaveNetworkStatus> =>
|
||||
hass.callWS({
|
||||
type: "zwave/network_status",
|
||||
});
|
||||
|
||||
export const startZwaveJsConfigFlow = (
|
||||
hass: HomeAssistant
|
||||
): Promise<{ flow_id: string }> =>
|
||||
hass.callWS({
|
||||
type: "zwave/start_zwave_js_config_flow",
|
||||
});
|
||||
|
||||
export const fetchMigrationConfig = (
|
||||
hass: HomeAssistant
|
||||
): Promise<ZWaveMigrationConfig> =>
|
||||
hass.callWS({
|
||||
type: "zwave/get_migration_config",
|
||||
});
|
||||
|
||||
export const fetchValues = (hass: HomeAssistant, nodeId: number) =>
|
||||
hass.callApi<ZWaveValue[]>("GET", `zwave/values/${nodeId}`);
|
||||
|
||||
export const fetchNodeConfig = (hass: HomeAssistant, nodeId: number) =>
|
||||
hass.callApi<ZWaveConfigItem[]>("GET", `zwave/config/${nodeId}`);
|
@@ -1,5 +1,5 @@
|
||||
import "@material/mwc-button";
|
||||
import { mdiClose } from "@mdi/js";
|
||||
import { mdiClose, mdiHelpCircle } from "@mdi/js";
|
||||
import type { UnsubscribeFunc } from "home-assistant-js-websocket";
|
||||
import {
|
||||
css,
|
||||
@@ -33,6 +33,7 @@ import {
|
||||
} from "../../data/device_registry";
|
||||
import { haStyleDialog } from "../../resources/styles";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import { documentationUrl } from "../../util/documentation-url";
|
||||
import { showAlertDialog } from "../generic/show-dialog-box";
|
||||
import {
|
||||
DataEntryFlowDialogParams,
|
||||
@@ -46,6 +47,7 @@ import "./step-flow-loading";
|
||||
import "./step-flow-pick-flow";
|
||||
import "./step-flow-pick-handler";
|
||||
import "./step-flow-progress";
|
||||
import "./step-flow-menu";
|
||||
|
||||
let instance = 0;
|
||||
|
||||
@@ -235,14 +237,33 @@ class DataEntryFlowDialog extends LitElement {
|
||||
// to reset the element.
|
||||
""
|
||||
: html`
|
||||
<ha-icon-button
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.integrations.config_flow.dismiss"
|
||||
)}
|
||||
.path=${mdiClose}
|
||||
dialogAction="close"
|
||||
?rtl=${computeRTL(this.hass)}
|
||||
></ha-icon-button>
|
||||
<div class="dialog-actions">
|
||||
${this._step
|
||||
? html`
|
||||
<a
|
||||
href=${documentationUrl(
|
||||
this.hass,
|
||||
`/integrations/${this._step.handler}`
|
||||
)}
|
||||
target="_blank"
|
||||
rel="noreferrer noopener"
|
||||
><ha-icon-button
|
||||
.label=${this.hass.localize("ui.common.help")}
|
||||
.path=${mdiHelpCircle}
|
||||
?rtl=${computeRTL(this.hass)}
|
||||
></ha-icon-button
|
||||
></a>
|
||||
`
|
||||
: ""}
|
||||
<ha-icon-button
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.integrations.config_flow.dismiss"
|
||||
)}
|
||||
.path=${mdiClose}
|
||||
dialogAction="close"
|
||||
?rtl=${computeRTL(this.hass)}
|
||||
></ha-icon-button>
|
||||
</div>
|
||||
${this._step === null
|
||||
? this._handler
|
||||
? html`<step-flow-pick-flow
|
||||
@@ -292,6 +313,14 @@ class DataEntryFlowDialog extends LitElement {
|
||||
.hass=${this.hass}
|
||||
></step-flow-progress>
|
||||
`
|
||||
: this._step.type === "menu"
|
||||
? html`
|
||||
<step-flow-menu
|
||||
.flowConfig=${this._params.flowConfig}
|
||||
.step=${this._step}
|
||||
.hass=${this.hass}
|
||||
></step-flow-menu>
|
||||
`
|
||||
: this._devices === undefined || this._areas === undefined
|
||||
? // When it's a create entry result, we will fetch device & area registry
|
||||
html`
|
||||
@@ -421,7 +450,7 @@ class DataEntryFlowDialog extends LitElement {
|
||||
title: this.hass.localize(
|
||||
"ui.panel.config.integrations.config_flow.error"
|
||||
),
|
||||
text: err.message || err.body,
|
||||
text: err?.body?.message,
|
||||
});
|
||||
return;
|
||||
} finally {
|
||||
@@ -463,16 +492,19 @@ class DataEntryFlowDialog extends LitElement {
|
||||
ha-dialog {
|
||||
--dialog-content-padding: 0;
|
||||
}
|
||||
ha-icon-button {
|
||||
.dialog-actions {
|
||||
padding: 16px;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
}
|
||||
ha-icon-button[rtl] {
|
||||
.dialog-actions[rtl] {
|
||||
right: auto;
|
||||
left: 0;
|
||||
}
|
||||
.dialog-actions > * {
|
||||
color: var(--secondary-text-color);
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
@@ -24,7 +24,7 @@ export const showConfigFlowDialog = (
|
||||
loadDevicesAndAreas: true,
|
||||
getFlowHandlers: async (hass) => {
|
||||
const [handlers] = await Promise.all([
|
||||
getConfigFlowHandlers(hass),
|
||||
getConfigFlowHandlers(hass, "integration"),
|
||||
hass.loadBackendTranslation("title", undefined, true),
|
||||
]);
|
||||
|
||||
@@ -181,6 +181,21 @@ export const showConfigFlowDialog = (
|
||||
: "";
|
||||
},
|
||||
|
||||
renderMenuHeader(hass, step) {
|
||||
return (
|
||||
hass.localize(
|
||||
`component.${step.handler}.config.step.${step.step_id}.title`
|
||||
) || hass.localize(`component.${step.handler}.title`)
|
||||
);
|
||||
},
|
||||
|
||||
renderMenuOption(hass, step, option) {
|
||||
return hass.localize(
|
||||
`component.${step.handler}.config.step.${step.step_id}.menu_options.${option}`,
|
||||
step.description_placeholders
|
||||
);
|
||||
},
|
||||
|
||||
renderLoadingDescription(hass, reason, handler, step) {
|
||||
if (!["loading_flow", "loading_step"].includes(reason)) {
|
||||
return "";
|
||||
|
@@ -7,6 +7,7 @@ import {
|
||||
DataEntryFlowStepCreateEntry,
|
||||
DataEntryFlowStepExternal,
|
||||
DataEntryFlowStepForm,
|
||||
DataEntryFlowStepMenu,
|
||||
DataEntryFlowStepProgress,
|
||||
} from "../../data/data_entry_flow";
|
||||
import { HomeAssistant } from "../../types";
|
||||
@@ -80,6 +81,14 @@ export interface FlowConfig {
|
||||
step: DataEntryFlowStepProgress
|
||||
): TemplateResult | "";
|
||||
|
||||
renderMenuHeader(hass: HomeAssistant, step: DataEntryFlowStepMenu): string;
|
||||
|
||||
renderMenuOption(
|
||||
hass: HomeAssistant,
|
||||
step: DataEntryFlowStepMenu,
|
||||
option: string
|
||||
): string;
|
||||
|
||||
renderLoadingDescription(
|
||||
hass: HomeAssistant,
|
||||
loadingReason: LoadingReason,
|
||||
|
@@ -134,6 +134,21 @@ export const showOptionsFlowDialog = (
|
||||
: "";
|
||||
},
|
||||
|
||||
renderMenuHeader(hass, step) {
|
||||
return (
|
||||
hass.localize(
|
||||
`component.${step.handler}.option.step.${step.step_id}.title`
|
||||
) || hass.localize(`component.${step.handler}.title`)
|
||||
);
|
||||
},
|
||||
|
||||
renderMenuOption(hass, step, option) {
|
||||
return hass.localize(
|
||||
`component.${step.handler}.options.step.${step.step_id}.menu_options.${option}`,
|
||||
step.description_placeholders
|
||||
);
|
||||
},
|
||||
|
||||
renderLoadingDescription(hass, reason) {
|
||||
return (
|
||||
hass.localize(`component.${configEntry.domain}.options.loading`) ||
|
||||
|
80
src/dialogs/config-flow/step-flow-menu.ts
Normal file
80
src/dialogs/config-flow/step-flow-menu.ts
Normal file
@@ -0,0 +1,80 @@
|
||||
import "@material/mwc-list/mwc-list-item";
|
||||
import { css, html, LitElement, TemplateResult } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import type { DataEntryFlowStepMenu } from "../../data/data_entry_flow";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import type { FlowConfig } from "./show-dialog-data-entry-flow";
|
||||
import "../../components/ha-icon-next";
|
||||
import { configFlowContentStyles } from "./styles";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
|
||||
@customElement("step-flow-menu")
|
||||
class StepFlowMenu extends LitElement {
|
||||
@property({ attribute: false }) public flowConfig!: FlowConfig;
|
||||
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public step!: DataEntryFlowStepMenu;
|
||||
|
||||
protected render(): TemplateResult {
|
||||
let options: string[];
|
||||
let translations: Record<string, string>;
|
||||
|
||||
if (Array.isArray(this.step.menu_options)) {
|
||||
options = this.step.menu_options;
|
||||
translations = {};
|
||||
for (const option of options) {
|
||||
translations[option] = this.flowConfig.renderMenuOption(
|
||||
this.hass,
|
||||
this.step,
|
||||
option
|
||||
);
|
||||
}
|
||||
} else {
|
||||
options = Object.keys(this.step.menu_options);
|
||||
translations = this.step.menu_options;
|
||||
}
|
||||
|
||||
return html`
|
||||
<h2>${this.flowConfig.renderMenuHeader(this.hass, this.step)}</h2>
|
||||
<div class="options">
|
||||
${options.map(
|
||||
(option) => html`
|
||||
<mwc-list-item hasMeta .step=${option} @click=${this._handleStep}>
|
||||
<span>${translations[option]}</span>
|
||||
<ha-icon-next slot="meta"></ha-icon-next>
|
||||
</mwc-list-item>
|
||||
`
|
||||
)}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
private _handleStep(ev) {
|
||||
fireEvent(this, "flow-update", {
|
||||
stepPromise: this.flowConfig.handleFlowStep(
|
||||
this.hass,
|
||||
this.step.flow_id,
|
||||
{
|
||||
next_step_id: ev.currentTarget.step,
|
||||
}
|
||||
),
|
||||
});
|
||||
}
|
||||
|
||||
static styles = [
|
||||
configFlowContentStyles,
|
||||
css`
|
||||
.options {
|
||||
margin-top: 20px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"step-flow-menu": StepFlowMenu;
|
||||
}
|
||||
}
|
@@ -216,15 +216,16 @@ class StepFlowPickHandler extends LitElement {
|
||||
|
||||
if (handler.is_add) {
|
||||
if (handler.slug === "zwave_js") {
|
||||
const entries = await getConfigEntries(this.hass);
|
||||
const entry = entries.find((ent) => ent.domain === "zwave_js");
|
||||
const entries = await getConfigEntries(this.hass, {
|
||||
domain: "zwave_js",
|
||||
});
|
||||
|
||||
if (!entry) {
|
||||
if (!entries.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
showZWaveJSAddNodeDialog(this, {
|
||||
entry_id: entry.entry_id,
|
||||
entry_id: entries[0].entry_id,
|
||||
});
|
||||
} else if (handler.slug === "zha") {
|
||||
navigate("/config/zha/add");
|
||||
|
@@ -12,7 +12,7 @@ import {
|
||||
import type { HomeAssistant } from "../../../types";
|
||||
|
||||
const BUTTONS = ["1", "2", "3", "4", "5", "6", "7", "8", "9", "", "0", "clear"];
|
||||
const ARM_ACTIONS = ["arm_away", "arm_home"];
|
||||
const ARM_ACTIONS = ["arm_home", "arm_away"];
|
||||
const DISARM_ACTIONS = ["disarm"];
|
||||
|
||||
@customElement("more-info-alarm_control_panel")
|
||||
|
212
src/dialogs/more-info/controls/more-info-update.ts
Normal file
212
src/dialogs/more-info/controls/more-info-update.ts
Normal file
@@ -0,0 +1,212 @@
|
||||
import "@material/mwc-button/mwc-button";
|
||||
import "@material/mwc-linear-progress/mwc-linear-progress";
|
||||
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { supportsFeature } from "../../../common/entity/supports-feature";
|
||||
import "../../../components/ha-checkbox";
|
||||
import "../../../components/ha-formfield";
|
||||
import "../../../components/ha-markdown";
|
||||
import { UNAVAILABLE_STATES } from "../../../data/entity";
|
||||
import {
|
||||
updateIsInstalling,
|
||||
UpdateEntity,
|
||||
UPDATE_SUPPORT_BACKUP,
|
||||
UPDATE_SUPPORT_INSTALL,
|
||||
UPDATE_SUPPORT_PROGRESS,
|
||||
UPDATE_SUPPORT_SPECIFIC_VERSION,
|
||||
} from "../../../data/update";
|
||||
import type { HomeAssistant } from "../../../types";
|
||||
|
||||
@customElement("more-info-update")
|
||||
class MoreInfoUpdate extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public stateObj?: UpdateEntity;
|
||||
|
||||
protected render(): TemplateResult {
|
||||
if (
|
||||
!this.hass ||
|
||||
!this.stateObj ||
|
||||
UNAVAILABLE_STATES.includes(this.stateObj.state)
|
||||
) {
|
||||
return html``;
|
||||
}
|
||||
|
||||
const skippedVersion =
|
||||
this.stateObj.attributes.latest_version &&
|
||||
this.stateObj.attributes.skipped_version ===
|
||||
this.stateObj.attributes.latest_version;
|
||||
|
||||
return html`
|
||||
${this.stateObj.attributes.in_progress
|
||||
? supportsFeature(this.stateObj, UPDATE_SUPPORT_PROGRESS) &&
|
||||
typeof this.stateObj.attributes.in_progress === "number"
|
||||
? html`<mwc-linear-progress
|
||||
.progress=${this.stateObj.attributes.in_progress / 100}
|
||||
buffer=""
|
||||
></mwc-linear-progress>`
|
||||
: html`<mwc-linear-progress indeterminate></mwc-linear-progress>`
|
||||
: ""}
|
||||
${this.stateObj.attributes.title
|
||||
? html`<h3>${this.stateObj.attributes.title}</h3>`
|
||||
: ""}
|
||||
|
||||
<div class="row">
|
||||
<div class="key">
|
||||
${this.hass.localize(
|
||||
"ui.dialogs.more_info_control.update.current_version"
|
||||
)}
|
||||
</div>
|
||||
<div class="value">
|
||||
${this.stateObj.attributes.current_version ??
|
||||
this.hass.localize("state.default.unavailable")}
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="key">
|
||||
${this.hass.localize(
|
||||
"ui.dialogs.more_info_control.update.latest_version"
|
||||
)}
|
||||
</div>
|
||||
<div class="value">
|
||||
${this.stateObj.attributes.latest_version ??
|
||||
this.hass.localize("state.default.unavailable")}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
${this.stateObj.attributes.release_url
|
||||
? html`<div class="row">
|
||||
<div class="key">
|
||||
<a
|
||||
href=${this.stateObj.attributes.release_url}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
${this.hass.localize(
|
||||
"ui.dialogs.more_info_control.update.release_announcement"
|
||||
)}
|
||||
</a>
|
||||
</div>
|
||||
</div>`
|
||||
: ""}
|
||||
${this.stateObj.attributes.release_summary
|
||||
? html`<hr />
|
||||
<ha-markdown
|
||||
.content=${this.stateObj.attributes.release_summary}
|
||||
></ha-markdown> `
|
||||
: ""}
|
||||
${supportsFeature(this.stateObj, UPDATE_SUPPORT_BACKUP)
|
||||
? html`<hr />
|
||||
<ha-formfield
|
||||
.label=${this.hass.localize(
|
||||
"ui.dialogs.more_info_control.update.create_backup"
|
||||
)}
|
||||
>
|
||||
<ha-checkbox
|
||||
checked
|
||||
.disabled=${updateIsInstalling(this.stateObj)}
|
||||
></ha-checkbox>
|
||||
</ha-formfield> `
|
||||
: ""}
|
||||
<hr />
|
||||
<div class="actions">
|
||||
<mwc-button
|
||||
@click=${this._handleSkip}
|
||||
.disabled=${skippedVersion ||
|
||||
this.stateObj.state === "off" ||
|
||||
updateIsInstalling(this.stateObj)}
|
||||
>
|
||||
${this.hass.localize("ui.dialogs.more_info_control.update.skip")}
|
||||
</mwc-button>
|
||||
${supportsFeature(this.stateObj, UPDATE_SUPPORT_INSTALL)
|
||||
? html`
|
||||
<mwc-button
|
||||
@click=${this._handleInstall}
|
||||
.disabled=${(this.stateObj.state === "off" &&
|
||||
!skippedVersion) ||
|
||||
updateIsInstalling(this.stateObj)}
|
||||
>
|
||||
${this.hass.localize(
|
||||
"ui.dialogs.more_info_control.update.install"
|
||||
)}
|
||||
</mwc-button>
|
||||
`
|
||||
: ""}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
get _shouldCreateBackup(): boolean | null {
|
||||
if (!supportsFeature(this.stateObj!, UPDATE_SUPPORT_BACKUP)) {
|
||||
return null;
|
||||
}
|
||||
const checkbox = this.shadowRoot?.querySelector("ha-checkbox");
|
||||
if (checkbox) {
|
||||
return checkbox.checked;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private _handleInstall(): void {
|
||||
const installData: Record<string, any> = {
|
||||
entity_id: this.stateObj!.entity_id,
|
||||
};
|
||||
|
||||
if (this._shouldCreateBackup) {
|
||||
installData.backup = true;
|
||||
}
|
||||
|
||||
if (
|
||||
supportsFeature(this.stateObj!, UPDATE_SUPPORT_SPECIFIC_VERSION) &&
|
||||
this.stateObj!.attributes.latest_version
|
||||
) {
|
||||
installData.version = this.stateObj!.attributes.latest_version;
|
||||
}
|
||||
|
||||
this.hass.callService("update", "install", installData);
|
||||
}
|
||||
|
||||
private _handleSkip(): void {
|
||||
this.hass.callService("update", "skip", {
|
||||
entity_id: this.stateObj!.entity_id,
|
||||
});
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return css`
|
||||
hr {
|
||||
border-color: var(--divider-color);
|
||||
border-bottom: none;
|
||||
margin: 16px 0;
|
||||
}
|
||||
ha-expansion-panel {
|
||||
margin: 16px 0;
|
||||
}
|
||||
.row {
|
||||
margin: 0;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
}
|
||||
.actions {
|
||||
margin: 8px 0 0;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.actions mwc-button {
|
||||
margin: 0 4px 4px;
|
||||
}
|
||||
a {
|
||||
color: var(--primary-color);
|
||||
}
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"more-info-update": MoreInfoUpdate;
|
||||
}
|
||||
}
|
@@ -25,6 +25,7 @@ const LAZY_LOADED_MORE_INFO_CONTROL = {
|
||||
script: () => import("./controls/more-info-script"),
|
||||
sun: () => import("./controls/more-info-sun"),
|
||||
timer: () => import("./controls/more-info-timer"),
|
||||
update: () => import("./controls/more-info-update"),
|
||||
vacuum: () => import("./controls/more-info-vacuum"),
|
||||
water_heater: () => import("./controls/more-info-water_heater"),
|
||||
weather: () => import("./controls/more-info-weather"),
|
||||
|
@@ -169,8 +169,8 @@ class OnboardingIntegrations extends LitElement {
|
||||
}
|
||||
|
||||
private async _loadConfigEntries() {
|
||||
const entries = await getConfigEntries(this.hass!);
|
||||
// We filter out the config entry for the local weather and rpi_power.
|
||||
const entries = await getConfigEntries(this.hass!, { type: "integration" });
|
||||
// We filter out the config entries that are automatically created during onboarding.
|
||||
// It is one that we create automatically and it will confuse the user
|
||||
// if it starts showing up during onboarding.
|
||||
this._entries = entries.filter(
|
||||
|
@@ -7,12 +7,18 @@ import { fireEvent } from "../../../../common/dom/fire_event";
|
||||
import { handleStructError } from "../../../../common/structs/handle-errors";
|
||||
import "../../../../components/ha-button-menu";
|
||||
import "../../../../components/ha-card";
|
||||
import "../../../../components/buttons/ha-progress-button";
|
||||
import type { HaProgressButton } from "../../../../components/buttons/ha-progress-button";
|
||||
import "../../../../components/ha-icon-button";
|
||||
import { Condition } from "../../../../data/automation";
|
||||
import { showConfirmationDialog } from "../../../../dialogs/generic/show-dialog-box";
|
||||
import { Condition, testCondition } from "../../../../data/automation";
|
||||
import {
|
||||
showAlertDialog,
|
||||
showConfirmationDialog,
|
||||
} from "../../../../dialogs/generic/show-dialog-box";
|
||||
import { haStyle } from "../../../../resources/styles";
|
||||
import { HomeAssistant } from "../../../../types";
|
||||
import "./ha-automation-condition-editor";
|
||||
import { validateConfig } from "../../../../data/config";
|
||||
|
||||
export interface ConditionElement extends LitElement {
|
||||
condition: Condition;
|
||||
@@ -61,6 +67,11 @@ export default class HaAutomationConditionRow extends LitElement {
|
||||
<ha-card>
|
||||
<div class="card-content">
|
||||
<div class="card-menu">
|
||||
<ha-progress-button @click=${this._testCondition}>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.automation.editor.conditions.test"
|
||||
)}
|
||||
</ha-progress-button>
|
||||
<ha-button-menu corner="BOTTOM_START" @action=${this._handleAction}>
|
||||
<ha-icon-button
|
||||
slot="trigger"
|
||||
@@ -165,6 +176,64 @@ export default class HaAutomationConditionRow extends LitElement {
|
||||
this._yamlMode = !this._yamlMode;
|
||||
}
|
||||
|
||||
private async _testCondition(ev) {
|
||||
const condition = this.condition;
|
||||
const button = ev.target as HaProgressButton;
|
||||
if (button.progress) {
|
||||
return;
|
||||
}
|
||||
button.progress = true;
|
||||
|
||||
try {
|
||||
const validateResult = await validateConfig(this.hass, {
|
||||
condition,
|
||||
});
|
||||
|
||||
// Abort if condition changed.
|
||||
if (this.condition !== condition) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!validateResult.condition.valid) {
|
||||
showAlertDialog(this, {
|
||||
title: this.hass.localize(
|
||||
"ui.panel.config.automation.editor.conditions.invalid_condition"
|
||||
),
|
||||
text: validateResult.condition.error,
|
||||
});
|
||||
return;
|
||||
}
|
||||
let result: { result: boolean };
|
||||
try {
|
||||
result = await testCondition(this.hass, condition);
|
||||
} catch (err: any) {
|
||||
if (this.condition !== condition) {
|
||||
return;
|
||||
}
|
||||
|
||||
showAlertDialog(this, {
|
||||
title: this.hass.localize(
|
||||
"ui.panel.config.automation.editor.conditions.test_failed"
|
||||
),
|
||||
text: err.message,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.condition !== condition) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (result.result) {
|
||||
button.actionSuccess();
|
||||
} else {
|
||||
button.actionError();
|
||||
}
|
||||
} finally {
|
||||
button.progress = false;
|
||||
}
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return [
|
||||
haStyle,
|
||||
@@ -173,6 +242,8 @@ export default class HaAutomationConditionRow extends LitElement {
|
||||
float: right;
|
||||
z-index: 3;
|
||||
--mdc-theme-text-primary-on-background: var(--primary-text-color);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
.rtl .card-menu {
|
||||
float: left;
|
||||
|
@@ -16,12 +16,16 @@ import "../../../../components/ha-alert";
|
||||
import "../../../../components/ha-button-menu";
|
||||
import "../../../../components/ha-card";
|
||||
import "../../../../components/ha-icon-button";
|
||||
import "../../../../components/ha-yaml-editor";
|
||||
import "../../../../components/ha-select";
|
||||
import type { HaSelect } from "../../../../components/ha-select";
|
||||
import "../../../../components/ha-textfield";
|
||||
import { subscribeTrigger, Trigger } from "../../../../data/automation";
|
||||
import { validateConfig } from "../../../../data/config";
|
||||
import { showConfirmationDialog } from "../../../../dialogs/generic/show-dialog-box";
|
||||
import {
|
||||
showAlertDialog,
|
||||
showConfirmationDialog,
|
||||
} from "../../../../dialogs/generic/show-dialog-box";
|
||||
import { haStyle } from "../../../../resources/styles";
|
||||
import type { HomeAssistant } from "../../../../types";
|
||||
import "./types/ha-automation-trigger-device";
|
||||
@@ -94,7 +98,7 @@ export default class HaAutomationTriggerRow extends LitElement {
|
||||
|
||||
@state() private _requestShowId = false;
|
||||
|
||||
@state() private _triggered = false;
|
||||
@state() private _triggered?: Record<string, unknown>;
|
||||
|
||||
@state() private _triggerColor = false;
|
||||
|
||||
@@ -231,9 +235,10 @@ export default class HaAutomationTriggerRow extends LitElement {
|
||||
</div>
|
||||
<div
|
||||
class="triggered ${classMap({
|
||||
active: this._triggered,
|
||||
active: this._triggered !== undefined,
|
||||
accent: this._triggerColor,
|
||||
})}"
|
||||
@click=${this._showTriggeredInfo}
|
||||
>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.automation.editor.triggers.triggered"
|
||||
@@ -288,7 +293,7 @@ export default class HaAutomationTriggerRow extends LitElement {
|
||||
}
|
||||
|
||||
const validateResult = await validateConfig(this.hass, {
|
||||
trigger: this.trigger,
|
||||
trigger,
|
||||
});
|
||||
|
||||
// Don't do anything if trigger not valid or if trigger changed.
|
||||
@@ -298,16 +303,16 @@ export default class HaAutomationTriggerRow extends LitElement {
|
||||
|
||||
const triggerUnsub = subscribeTrigger(
|
||||
this.hass,
|
||||
() => {
|
||||
(result) => {
|
||||
if (untriggerTimeout !== undefined) {
|
||||
clearTimeout(untriggerTimeout);
|
||||
this._triggerColor = !this._triggerColor;
|
||||
} else {
|
||||
this._triggerColor = false;
|
||||
}
|
||||
this._triggered = true;
|
||||
this._triggered = result;
|
||||
untriggerTimeout = window.setTimeout(() => {
|
||||
this._triggered = false;
|
||||
this._triggered = undefined;
|
||||
untriggerTimeout = undefined;
|
||||
}, showTriggeredTime);
|
||||
},
|
||||
@@ -416,6 +421,18 @@ export default class HaAutomationTriggerRow extends LitElement {
|
||||
this._yamlMode = !this._yamlMode;
|
||||
}
|
||||
|
||||
private _showTriggeredInfo() {
|
||||
showAlertDialog(this, {
|
||||
text: html`
|
||||
<ha-yaml-editor
|
||||
readOnly
|
||||
.hass=${this.hass}
|
||||
.defaultValue=${this._triggered}
|
||||
></ha-yaml-editor>
|
||||
`,
|
||||
});
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return [
|
||||
haStyle,
|
||||
@@ -426,12 +443,12 @@ export default class HaAutomationTriggerRow extends LitElement {
|
||||
--mdc-theme-text-primary-on-background: var(--primary-text-color);
|
||||
}
|
||||
.triggered {
|
||||
cursor: pointer;
|
||||
position: absolute;
|
||||
top: 0px;
|
||||
right: 0px;
|
||||
left: 0px;
|
||||
text-transform: uppercase;
|
||||
pointer-events: none;
|
||||
font-weight: bold;
|
||||
font-size: 14px;
|
||||
background-color: var(--primary-color);
|
||||
@@ -446,6 +463,9 @@ export default class HaAutomationTriggerRow extends LitElement {
|
||||
.triggered.active {
|
||||
max-height: 100px;
|
||||
}
|
||||
.triggered:hover {
|
||||
opacity: 0.8;
|
||||
}
|
||||
.triggered.accent {
|
||||
background-color: var(--accent-color);
|
||||
color: var(--text-accent-color, var(--text-primary-color));
|
||||
|
@@ -1,12 +1,25 @@
|
||||
import "@material/mwc-button/mwc-button";
|
||||
import "@polymer/paper-input/paper-input";
|
||||
import type { PaperInputElement } from "@polymer/paper-input/paper-input";
|
||||
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
|
||||
import {
|
||||
css,
|
||||
CSSResultGroup,
|
||||
html,
|
||||
LitElement,
|
||||
PropertyValues,
|
||||
TemplateResult,
|
||||
} from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { isComponentLoaded } from "../../../common/config/is_component_loaded";
|
||||
import "../../../components/ha-card";
|
||||
import "../../../components/ha-switch";
|
||||
import "../../../components/ha-alert";
|
||||
import "../../../components/ha-formfield";
|
||||
import "../../../components/ha-textfield";
|
||||
import type { HaTextField } from "../../../components/ha-textfield";
|
||||
import { CloudStatus, fetchCloudStatus } from "../../../data/cloud";
|
||||
import { saveCoreConfig } from "../../../data/core";
|
||||
import type { PolymerChangedEvent } from "../../../polymer-types";
|
||||
import type { HomeAssistant } from "../../../types";
|
||||
import { isIPAddress } from "../../../common/string/is_ip_address";
|
||||
|
||||
@customElement("ha-config-url-form")
|
||||
class ConfigUrlForm extends LitElement {
|
||||
@@ -20,18 +33,48 @@ class ConfigUrlForm extends LitElement {
|
||||
|
||||
@state() private _internal_url?: string;
|
||||
|
||||
@state() private _cloudStatus?: CloudStatus | null;
|
||||
|
||||
@state() private _showCustomExternalUrl = false;
|
||||
|
||||
@state() private _showCustomInternalUrl = false;
|
||||
|
||||
protected render(): TemplateResult {
|
||||
const canEdit = ["storage", "default"].includes(
|
||||
this.hass.config.config_source
|
||||
);
|
||||
const disabled = this._working || !canEdit;
|
||||
|
||||
if (!this.hass.userData?.showAdvanced) {
|
||||
if (!this.hass.userData?.showAdvanced || this._cloudStatus === undefined) {
|
||||
return html``;
|
||||
}
|
||||
|
||||
const internalUrl = this._internalUrlValue;
|
||||
const externalUrl = this._externalUrlValue;
|
||||
let hasCloud: boolean;
|
||||
let remoteEnabled: boolean;
|
||||
let httpUseHttps: boolean;
|
||||
|
||||
if (this._cloudStatus === null) {
|
||||
hasCloud = false;
|
||||
remoteEnabled = false;
|
||||
httpUseHttps = false;
|
||||
} else {
|
||||
httpUseHttps = this._cloudStatus.http_use_ssl;
|
||||
|
||||
if (this._cloudStatus.logged_in) {
|
||||
hasCloud = true;
|
||||
remoteEnabled =
|
||||
this._cloudStatus.active_subscription &&
|
||||
this._cloudStatus.prefs.remote_enabled;
|
||||
} else {
|
||||
hasCloud = false;
|
||||
remoteEnabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
return html`
|
||||
<ha-card>
|
||||
<ha-card .header=${this.hass.localize("ui.panel.config.url.caption")}>
|
||||
<div class="card-content">
|
||||
${!canEdit
|
||||
? html`
|
||||
@@ -43,46 +86,147 @@ class ConfigUrlForm extends LitElement {
|
||||
`
|
||||
: ""}
|
||||
${this._error ? html`<div class="error">${this._error}</div>` : ""}
|
||||
<div class="row">
|
||||
<div class="flex">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.core.section.core.core_config.external_url"
|
||||
)}
|
||||
</div>
|
||||
|
||||
<paper-input
|
||||
class="flex"
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.core.section.core.core_config.external_url"
|
||||
)}
|
||||
name="external_url"
|
||||
type="url"
|
||||
.disabled=${disabled}
|
||||
.value=${this._externalUrlValue}
|
||||
@value-changed=${this._handleChange}
|
||||
>
|
||||
</paper-input>
|
||||
<div class="description">
|
||||
${this.hass.localize("ui.panel.config.url.description")}
|
||||
</div>
|
||||
|
||||
${hasCloud
|
||||
? html`
|
||||
<div class="row">
|
||||
<div class="flex">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.url.external_url_label"
|
||||
)}
|
||||
</div>
|
||||
<ha-formfield
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.url.external_use_ha_cloud"
|
||||
)}
|
||||
>
|
||||
<ha-switch
|
||||
.disabled=${disabled}
|
||||
.checked=${externalUrl === null}
|
||||
@change=${this._toggleCloud}
|
||||
></ha-switch>
|
||||
</ha-formfield>
|
||||
</div>
|
||||
`
|
||||
: ""}
|
||||
${!this._showCustomExternalUrl
|
||||
? ""
|
||||
: html`
|
||||
<div class="row">
|
||||
<div class="flex">
|
||||
${hasCloud
|
||||
? ""
|
||||
: this.hass.localize(
|
||||
"ui.panel.config.url.external_url_label"
|
||||
)}
|
||||
</div>
|
||||
<ha-textfield
|
||||
class="flex"
|
||||
name="external_url"
|
||||
type="url"
|
||||
.disabled=${disabled}
|
||||
.value=${externalUrl || ""}
|
||||
@change=${this._handleChange}
|
||||
placeholder="https://example.duckdns.org:8123"
|
||||
>
|
||||
</ha-textfield>
|
||||
</div>
|
||||
`}
|
||||
${hasCloud || !isComponentLoaded(this.hass, "cloud")
|
||||
? ""
|
||||
: html`
|
||||
<div class="row">
|
||||
<div class="flex"></div>
|
||||
<a href="/config/cloud"
|
||||
>${this.hass.localize(
|
||||
"ui.panel.config.url.external_get_ha_cloud"
|
||||
)}</a
|
||||
>
|
||||
</div>
|
||||
`}
|
||||
${!this._showCustomExternalUrl && hasCloud
|
||||
? html`
|
||||
${remoteEnabled
|
||||
? ""
|
||||
: html`
|
||||
<ha-alert alert-type="error">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.url.ha_cloud_remote_not_enabled"
|
||||
)}
|
||||
<a href="/config/cloud" slot="action"
|
||||
><mwc-button
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.url.enable_remote"
|
||||
)}
|
||||
></mwc-button
|
||||
></a>
|
||||
</ha-alert>
|
||||
`}
|
||||
`
|
||||
: ""}
|
||||
|
||||
<div class="row">
|
||||
<div class="flex">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.core.section.core.core_config.internal_url"
|
||||
)}
|
||||
${this.hass.localize("ui.panel.config.url.internal_url_label")}
|
||||
</div>
|
||||
<paper-input
|
||||
class="flex"
|
||||
|
||||
<ha-formfield
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.core.section.core.core_config.internal_url"
|
||||
"ui.panel.config.url.internal_url_automatic"
|
||||
)}
|
||||
name="internal_url"
|
||||
type="url"
|
||||
.disabled=${disabled}
|
||||
.value=${this._internalUrlValue}
|
||||
@value-changed=${this._handleChange}
|
||||
>
|
||||
</paper-input>
|
||||
<ha-switch
|
||||
.checked=${internalUrl === null}
|
||||
@change=${this._toggleInternalAutomatic}
|
||||
></ha-switch>
|
||||
</ha-formfield>
|
||||
</div>
|
||||
|
||||
${!this._showCustomInternalUrl
|
||||
? ""
|
||||
: html`
|
||||
<div class="row">
|
||||
<div class="flex"></div>
|
||||
<ha-textfield
|
||||
class="flex"
|
||||
name="internal_url"
|
||||
type="url"
|
||||
placeholder="http://<some IP address>:8123"
|
||||
.disabled=${disabled}
|
||||
.value=${internalUrl || ""}
|
||||
@change=${this._handleChange}
|
||||
>
|
||||
</ha-textfield>
|
||||
</div>
|
||||
`}
|
||||
${
|
||||
// If the user has configured a cert, show an error if
|
||||
httpUseHttps && // there is no internal url configured
|
||||
(!internalUrl ||
|
||||
// the internal url does not start with https
|
||||
!internalUrl.startsWith("https://") ||
|
||||
// the internal url points at an IP address
|
||||
isIPAddress(new URL(internalUrl).hostname))
|
||||
? html`
|
||||
<ha-alert
|
||||
.alertType=${this._showCustomInternalUrl
|
||||
? "info"
|
||||
: "warning"}
|
||||
.title=${this.hass.localize(
|
||||
"ui.panel.config.url.intenral_url_https_error_title"
|
||||
)}
|
||||
>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.url.internal_url_https_error_description"
|
||||
)}
|
||||
</ha-alert>
|
||||
`
|
||||
: ""
|
||||
}
|
||||
</div>
|
||||
<div class="card-actions">
|
||||
<mwc-button @click=${this._save} .disabled=${disabled}>
|
||||
@@ -95,6 +239,24 @@ class ConfigUrlForm extends LitElement {
|
||||
`;
|
||||
}
|
||||
|
||||
protected override firstUpdated(changedProps: PropertyValues) {
|
||||
super.firstUpdated(changedProps);
|
||||
|
||||
this._showCustomInternalUrl = this._internalUrlValue !== null;
|
||||
|
||||
if (isComponentLoaded(this.hass, "cloud")) {
|
||||
fetchCloudStatus(this.hass).then((cloudStatus) => {
|
||||
if (cloudStatus.logged_in) {
|
||||
this._cloudStatus = cloudStatus;
|
||||
this._showCustomExternalUrl = this._externalUrlValue !== null;
|
||||
}
|
||||
});
|
||||
} else {
|
||||
this._cloudStatus = null;
|
||||
this._showCustomExternalUrl = true;
|
||||
}
|
||||
}
|
||||
|
||||
private get _internalUrlValue() {
|
||||
return this._internal_url !== undefined
|
||||
? this._internal_url
|
||||
@@ -107,9 +269,17 @@ class ConfigUrlForm extends LitElement {
|
||||
: this.hass.config.external_url;
|
||||
}
|
||||
|
||||
private _toggleCloud(ev) {
|
||||
this._showCustomExternalUrl = !ev.currentTarget.checked;
|
||||
}
|
||||
|
||||
private _toggleInternalAutomatic(ev) {
|
||||
this._showCustomInternalUrl = !ev.currentTarget.checked;
|
||||
}
|
||||
|
||||
private _handleChange(ev: PolymerChangedEvent<string>) {
|
||||
const target = ev.currentTarget as PaperInputElement;
|
||||
this[`_${target.name}`] = target.value;
|
||||
const target = ev.currentTarget as HaTextField;
|
||||
this[`_${target.name}`] = target.value || null;
|
||||
}
|
||||
|
||||
private async _save() {
|
||||
@@ -117,8 +287,12 @@ class ConfigUrlForm extends LitElement {
|
||||
this._error = undefined;
|
||||
try {
|
||||
await saveCoreConfig(this.hass, {
|
||||
external_url: this._external_url || null,
|
||||
internal_url: this._internal_url || null,
|
||||
external_url: this._showCustomExternalUrl
|
||||
? this._external_url || null
|
||||
: null,
|
||||
internal_url: this._showCustomInternalUrl
|
||||
? this._internal_url || null
|
||||
: null,
|
||||
});
|
||||
} catch (err: any) {
|
||||
this._error = err.message || err;
|
||||
@@ -129,11 +303,15 @@ class ConfigUrlForm extends LitElement {
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return css`
|
||||
.description {
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
.row {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
margin: 0 -8px;
|
||||
align-items: center;
|
||||
padding: 8px 0;
|
||||
}
|
||||
|
||||
.secondary {
|
||||
@@ -154,6 +332,10 @@ class ConfigUrlForm extends LitElement {
|
||||
.card-actions {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
a {
|
||||
color: var(--primary-color);
|
||||
}
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
@@ -40,7 +40,7 @@ export class HaDeviceEntitiesCard extends LitElement {
|
||||
|
||||
@property() public entities!: EntityRegistryStateEntry[];
|
||||
|
||||
@property() public showDisabled = false;
|
||||
@property() public showHidden = false;
|
||||
|
||||
@state() private _extDisabledEntityEntries?: Record<
|
||||
string,
|
||||
@@ -60,77 +60,77 @@ export class HaDeviceEntitiesCard extends LitElement {
|
||||
}
|
||||
|
||||
protected render(): TemplateResult {
|
||||
const disabledEntities: EntityRegistryStateEntry[] = [];
|
||||
if (!this.entities.length) {
|
||||
return html`
|
||||
<ha-card .header=${this.header}>
|
||||
<div class="empty card-content">
|
||||
${this.hass.localize("ui.panel.config.devices.entities.none")}
|
||||
</div>
|
||||
</ha-card>
|
||||
`;
|
||||
}
|
||||
|
||||
const shownEntities: EntityRegistryStateEntry[] = [];
|
||||
const hiddenEntities: EntityRegistryStateEntry[] = [];
|
||||
this._entityRows = [];
|
||||
|
||||
this.entities.forEach((entry) => {
|
||||
if (entry.disabled_by || entry.hidden_by) {
|
||||
if (this._extDisabledEntityEntries) {
|
||||
hiddenEntities.push(
|
||||
this._extDisabledEntityEntries[entry.entity_id] || entry
|
||||
);
|
||||
} else {
|
||||
hiddenEntities.push(entry);
|
||||
}
|
||||
} else {
|
||||
shownEntities.push(entry);
|
||||
}
|
||||
});
|
||||
|
||||
return html`
|
||||
<ha-card .header=${this.header}>
|
||||
${this.entities.length
|
||||
? html`
|
||||
<div id="entities" @hass-more-info=${this._overrideMoreInfo}>
|
||||
${this.entities.map((entry: EntityRegistryStateEntry) => {
|
||||
if (entry.disabled_by) {
|
||||
if (this._extDisabledEntityEntries) {
|
||||
disabledEntities.push(
|
||||
this._extDisabledEntityEntries[entry.entity_id] || entry
|
||||
);
|
||||
} else {
|
||||
disabledEntities.push(entry);
|
||||
}
|
||||
return "";
|
||||
}
|
||||
return this.hass.states[entry.entity_id]
|
||||
? this._renderEntity(entry)
|
||||
: this._renderEntry(entry);
|
||||
})}
|
||||
</div>
|
||||
${disabledEntities.length
|
||||
? !this.showDisabled
|
||||
? html`
|
||||
<button
|
||||
class="show-more"
|
||||
@click=${this._toggleShowDisabled}
|
||||
>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.devices.entities.disabled_entities",
|
||||
"count",
|
||||
disabledEntities.length
|
||||
)}
|
||||
</button>
|
||||
`
|
||||
: html`
|
||||
${disabledEntities.map((entry) =>
|
||||
this._renderEntry(entry)
|
||||
)}
|
||||
<button
|
||||
class="show-more"
|
||||
@click=${this._toggleShowDisabled}
|
||||
>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.devices.entities.hide_disabled"
|
||||
)}
|
||||
</button>
|
||||
`
|
||||
: ""}
|
||||
<div class="card-actions">
|
||||
<mwc-button @click=${this._addToLovelaceView}>
|
||||
<div id="entities" @hass-more-info=${this._overrideMoreInfo}>
|
||||
${shownEntities.map((entry) =>
|
||||
this.hass.states[entry.entity_id]
|
||||
? this._renderEntity(entry)
|
||||
: this._renderEntry(entry)
|
||||
)}
|
||||
</div>
|
||||
${hiddenEntities.length
|
||||
? !this.showHidden
|
||||
? html`
|
||||
<button class="show-more" @click=${this._toggleShowHidden}>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.devices.entities.add_entities_lovelace"
|
||||
"ui.panel.config.devices.entities.hidden_entities",
|
||||
"count",
|
||||
hiddenEntities.length
|
||||
)}
|
||||
</mwc-button>
|
||||
</div>
|
||||
`
|
||||
: html`
|
||||
<div class="empty card-content">
|
||||
${this.hass.localize("ui.panel.config.devices.entities.none")}
|
||||
</div>
|
||||
`}
|
||||
</button>
|
||||
`
|
||||
: html`
|
||||
${hiddenEntities.map((entry) => this._renderEntry(entry))}
|
||||
<button class="show-more" @click=${this._toggleShowHidden}>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.devices.entities.hide_disabled"
|
||||
)}
|
||||
</button>
|
||||
`
|
||||
: ""}
|
||||
<div class="card-actions">
|
||||
<mwc-button @click=${this._addToLovelaceView}>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.devices.entities.add_entities_lovelace"
|
||||
)}
|
||||
</mwc-button>
|
||||
</div>
|
||||
</ha-card>
|
||||
`;
|
||||
}
|
||||
|
||||
private _toggleShowDisabled() {
|
||||
this.showDisabled = !this.showDisabled;
|
||||
if (!this.showDisabled || this._extDisabledEntityEntries !== undefined) {
|
||||
private _toggleShowHidden() {
|
||||
this.showHidden = !this.showHidden;
|
||||
if (!this.showHidden || this._extDisabledEntityEntries !== undefined) {
|
||||
return;
|
||||
}
|
||||
this._extDisabledEntityEntries = {};
|
||||
|
@@ -1,84 +0,0 @@
|
||||
import "@material/mwc-button/mwc-button";
|
||||
import {
|
||||
css,
|
||||
CSSResultGroup,
|
||||
html,
|
||||
LitElement,
|
||||
PropertyValues,
|
||||
TemplateResult,
|
||||
} from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { navigate } from "../../../../../../common/navigate";
|
||||
import { DeviceRegistryEntry } from "../../../../../../data/device_registry";
|
||||
import {
|
||||
getIdentifiersFromDevice,
|
||||
OZWNodeIdentifiers,
|
||||
} from "../../../../../../data/ozw";
|
||||
import { haStyle } from "../../../../../../resources/styles";
|
||||
import { HomeAssistant } from "../../../../../../types";
|
||||
import { showOZWRefreshNodeDialog } from "../../../../integrations/integration-panels/ozw/show-dialog-ozw-refresh-node";
|
||||
|
||||
@customElement("ha-device-actions-ozw")
|
||||
export class HaDeviceActionsOzw extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property() public device!: DeviceRegistryEntry;
|
||||
|
||||
@property()
|
||||
private node_id = 0;
|
||||
|
||||
@property()
|
||||
private ozw_instance = 1;
|
||||
|
||||
protected updated(changedProperties: PropertyValues) {
|
||||
if (changedProperties.has("device")) {
|
||||
const identifiers: OZWNodeIdentifiers | undefined =
|
||||
getIdentifiersFromDevice(this.device);
|
||||
if (!identifiers) {
|
||||
return;
|
||||
}
|
||||
this.ozw_instance = identifiers.ozw_instance;
|
||||
this.node_id = identifiers.node_id;
|
||||
}
|
||||
}
|
||||
|
||||
protected render(): TemplateResult {
|
||||
if (!this.ozw_instance || !this.node_id) {
|
||||
return html``;
|
||||
}
|
||||
return html`
|
||||
<mwc-button @click=${this._nodeDetailsClicked}>
|
||||
${this.hass.localize("ui.panel.config.ozw.node.button")}
|
||||
</mwc-button>
|
||||
<mwc-button @click=${this._refreshNodeClicked}>
|
||||
${this.hass.localize("ui.panel.config.ozw.refresh_node.button")}
|
||||
</mwc-button>
|
||||
`;
|
||||
}
|
||||
|
||||
private async _refreshNodeClicked() {
|
||||
showOZWRefreshNodeDialog(this, {
|
||||
node_id: this.node_id,
|
||||
ozw_instance: this.ozw_instance,
|
||||
});
|
||||
}
|
||||
|
||||
private async _nodeDetailsClicked() {
|
||||
navigate(
|
||||
`/config/ozw/network/${this.ozw_instance}/node/${this.node_id}/dashboard`
|
||||
);
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return [
|
||||
haStyle,
|
||||
css`
|
||||
:host {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
}
|
@@ -1,99 +0,0 @@
|
||||
import {
|
||||
css,
|
||||
CSSResultGroup,
|
||||
html,
|
||||
LitElement,
|
||||
PropertyValues,
|
||||
TemplateResult,
|
||||
} from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { DeviceRegistryEntry } from "../../../../../../data/device_registry";
|
||||
import {
|
||||
fetchOZWNodeStatus,
|
||||
getIdentifiersFromDevice,
|
||||
OZWDevice,
|
||||
OZWNodeIdentifiers,
|
||||
} from "../../../../../../data/ozw";
|
||||
import { haStyle } from "../../../../../../resources/styles";
|
||||
import { HomeAssistant } from "../../../../../../types";
|
||||
|
||||
@customElement("ha-device-info-ozw")
|
||||
export class HaDeviceInfoOzw extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property() public device!: DeviceRegistryEntry;
|
||||
|
||||
@property()
|
||||
private node_id = 0;
|
||||
|
||||
@property()
|
||||
private ozw_instance = 1;
|
||||
|
||||
@state() private _ozwDevice?: OZWDevice;
|
||||
|
||||
protected updated(changedProperties: PropertyValues) {
|
||||
if (changedProperties.has("device")) {
|
||||
const identifiers: OZWNodeIdentifiers | undefined =
|
||||
getIdentifiersFromDevice(this.device);
|
||||
if (!identifiers) {
|
||||
return;
|
||||
}
|
||||
this.ozw_instance = identifiers.ozw_instance;
|
||||
this.node_id = identifiers.node_id;
|
||||
|
||||
this._fetchNodeDetails();
|
||||
}
|
||||
}
|
||||
|
||||
protected async _fetchNodeDetails() {
|
||||
this._ozwDevice = await fetchOZWNodeStatus(
|
||||
this.hass,
|
||||
this.ozw_instance,
|
||||
this.node_id
|
||||
);
|
||||
}
|
||||
|
||||
protected render(): TemplateResult {
|
||||
if (!this._ozwDevice) {
|
||||
return html``;
|
||||
}
|
||||
return html`
|
||||
<h4>
|
||||
${this.hass.localize("ui.panel.config.ozw.device_info.zwave_info")}
|
||||
</h4>
|
||||
<div>
|
||||
${this.hass.localize("ui.panel.config.ozw.common.node_id")}:
|
||||
${this._ozwDevice.node_id}
|
||||
</div>
|
||||
<div>
|
||||
${this.hass.localize("ui.panel.config.ozw.device_info.stage")}:
|
||||
${this._ozwDevice.node_query_stage}
|
||||
</div>
|
||||
<div>
|
||||
${this.hass.localize("ui.panel.config.ozw.common.ozw_instance")}:
|
||||
${this._ozwDevice.ozw_instance}
|
||||
</div>
|
||||
<div>
|
||||
${this.hass.localize("ui.panel.config.ozw.device_info.node_failed")}:
|
||||
${this._ozwDevice.is_failed
|
||||
? this.hass.localize("ui.common.yes")
|
||||
: this.hass.localize("ui.common.no")}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return [
|
||||
haStyle,
|
||||
css`
|
||||
h4 {
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
div {
|
||||
word-break: break-all;
|
||||
margin-top: 2px;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
}
|
@@ -27,7 +27,7 @@ import { HomeAssistant } from "../../../../../../types";
|
||||
export class HaDeviceInfoZWaveJS extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property() public device!: DeviceRegistryEntry;
|
||||
@property({ attribute: false }) public device!: DeviceRegistryEntry;
|
||||
|
||||
@state() private _entryId?: string;
|
||||
|
||||
@@ -58,12 +58,11 @@ export class HaDeviceInfoZWaveJS extends LitElement {
|
||||
return;
|
||||
}
|
||||
|
||||
const configEntries = await getConfigEntries(this.hass);
|
||||
const configEntries = await getConfigEntries(this.hass, {
|
||||
domain: "zwave_js",
|
||||
});
|
||||
let zwaveJsConfEntries = 0;
|
||||
for (const entry of configEntries) {
|
||||
if (entry.domain !== "zwave_js") {
|
||||
continue;
|
||||
}
|
||||
if (zwaveJsConfEntries) {
|
||||
this._multipleConfigEntries = true;
|
||||
}
|
||||
@@ -173,3 +172,9 @@ export class HaDeviceInfoZWaveJS extends LitElement {
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-device-info-zwave_js": HaDeviceInfoZWaveJS;
|
||||
}
|
||||
}
|
||||
|
@@ -33,6 +33,7 @@ import {
|
||||
fetchDiagnosticHandler,
|
||||
getDeviceDiagnosticsDownloadUrl,
|
||||
getConfigEntryDiagnosticsDownloadUrl,
|
||||
DiagnosticInfo,
|
||||
} from "../../../data/diagnostics";
|
||||
import {
|
||||
EntityRegistryEntry,
|
||||
@@ -219,22 +220,32 @@ export class HaConfigDevicePage extends LitElement {
|
||||
}
|
||||
|
||||
let links = await Promise.all(
|
||||
this._integrations(device, this.entries).map(async (entry) => {
|
||||
if (entry.state !== "loaded") {
|
||||
return false;
|
||||
}
|
||||
const info = await fetchDiagnosticHandler(this.hass, entry.domain);
|
||||
this._integrations(device, this.entries).map(
|
||||
async (entry): Promise<boolean | { link: string; domain: string }> => {
|
||||
if (entry.state !== "loaded") {
|
||||
return false;
|
||||
}
|
||||
let info: DiagnosticInfo;
|
||||
try {
|
||||
info = await fetchDiagnosticHandler(this.hass, entry.domain);
|
||||
} catch (err: any) {
|
||||
if (err.code === "not_found") {
|
||||
return false;
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
|
||||
if (!info.handlers.device && !info.handlers.config_entry) {
|
||||
return false;
|
||||
if (!info.handlers.device && !info.handlers.config_entry) {
|
||||
return false;
|
||||
}
|
||||
return {
|
||||
link: info.handlers.device
|
||||
? getDeviceDiagnosticsDownloadUrl(entry.entry_id, this.deviceId)
|
||||
: getConfigEntryDiagnosticsDownloadUrl(entry.entry_id),
|
||||
domain: entry.domain,
|
||||
};
|
||||
}
|
||||
return {
|
||||
link: info.handlers.device
|
||||
? getDeviceDiagnosticsDownloadUrl(entry.entry_id, this.deviceId)
|
||||
: getConfigEntryDiagnosticsDownloadUrl(entry.entry_id),
|
||||
domain: entry.domain,
|
||||
};
|
||||
})
|
||||
)
|
||||
);
|
||||
|
||||
links = links.filter(Boolean);
|
||||
@@ -557,7 +568,7 @@ export class HaConfigDevicePage extends LitElement {
|
||||
)}
|
||||
.deviceName=${deviceName}
|
||||
.entities=${entitiesByCategory[category]}
|
||||
.showDisabled=${device.disabled_by !== null}
|
||||
.showHidden=${device.disabled_by !== null}
|
||||
>
|
||||
</ha-device-entities-card>
|
||||
`
|
||||
@@ -902,22 +913,6 @@ export class HaConfigDevicePage extends LitElement {
|
||||
></ha-device-actions-mqtt>
|
||||
`);
|
||||
}
|
||||
if (domains.includes("ozw")) {
|
||||
import("./device-detail/integration-elements/ozw/ha-device-actions-ozw");
|
||||
import("./device-detail/integration-elements/ozw/ha-device-info-ozw");
|
||||
deviceInfo.push(html`
|
||||
<ha-device-info-ozw
|
||||
.hass=${this.hass}
|
||||
.device=${device}
|
||||
></ha-device-info-ozw>
|
||||
`);
|
||||
deviceActions.push(html`
|
||||
<ha-device-actions-ozw
|
||||
.hass=${this.hass}
|
||||
.device=${device}
|
||||
></ha-device-actions-ozw>
|
||||
`);
|
||||
}
|
||||
if (domains.includes("zha")) {
|
||||
import("./device-detail/integration-elements/zha/ha-device-actions-zha");
|
||||
import("./device-detail/integration-elements/zha/ha-device-info-zha");
|
||||
|
@@ -121,6 +121,7 @@ export class EnergyGasSettings extends LitElement {
|
||||
showEnergySettingsGasDialog(this, {
|
||||
unit: getEnergyGasUnitCategory(this.hass, this.preferences),
|
||||
saveCallback: async (source) => {
|
||||
delete source.unit_of_measurement;
|
||||
await this._savePreferences({
|
||||
...this.preferences,
|
||||
energy_sources: this.preferences.energy_sources.concat(source),
|
||||
|
@@ -54,7 +54,7 @@ export class EnergyGridSettings extends LitElement {
|
||||
@property({ attribute: false })
|
||||
public validationResult?: EnergyPreferencesValidation;
|
||||
|
||||
@state() private _configEntries?: ConfigEntry[];
|
||||
@state() private _co2ConfigEntry?: ConfigEntry;
|
||||
|
||||
protected firstUpdated() {
|
||||
this._fetchCO2SignalConfigEntries();
|
||||
@@ -195,28 +195,28 @@ export class EnergyGridSettings extends LitElement {
|
||||
"ui.panel.config.energy.grid.grid_carbon_footprint"
|
||||
)}
|
||||
</h3>
|
||||
${this._configEntries?.map(
|
||||
(entry) => html`<div class="row" .entry=${entry}>
|
||||
<img
|
||||
referrerpolicy="no-referrer"
|
||||
src=${brandsUrl({
|
||||
domain: "co2signal",
|
||||
type: "icon",
|
||||
darkOptimized: this.hass.themes?.darkMode,
|
||||
})}
|
||||
/>
|
||||
<span class="content">${entry.title}</span>
|
||||
<a href=${`/config/integrations#config_entry=${entry.entry_id}`}>
|
||||
<ha-icon-button .path=${mdiPencil}></ha-icon-button>
|
||||
</a>
|
||||
<ha-icon-button
|
||||
@click=${this._removeCO2Sensor}
|
||||
.path=${mdiDelete}
|
||||
></ha-icon-button>
|
||||
</div>`
|
||||
)}
|
||||
${this._configEntries?.length === 0
|
||||
? html`
|
||||
${this._co2ConfigEntry
|
||||
? html`<div class="row" .entry=${this._co2ConfigEntry}>
|
||||
<img
|
||||
referrerpolicy="no-referrer"
|
||||
src=${brandsUrl({
|
||||
domain: "co2signal",
|
||||
type: "icon",
|
||||
darkOptimized: this.hass.themes?.darkMode,
|
||||
})}
|
||||
/>
|
||||
<span class="content">${this._co2ConfigEntry.title}</span>
|
||||
<a
|
||||
href=${`/config/integrations#config_entry=${this._co2ConfigEntry.entry_id}`}
|
||||
>
|
||||
<ha-icon-button .path=${mdiPencil}></ha-icon-button>
|
||||
</a>
|
||||
<ha-icon-button
|
||||
@click=${this._removeCO2Sensor}
|
||||
.path=${mdiDelete}
|
||||
></ha-icon-button>
|
||||
</div>`
|
||||
: html`
|
||||
<div class="row border-bottom">
|
||||
<img
|
||||
referrerpolicy="no-referrer"
|
||||
@@ -232,17 +232,15 @@ export class EnergyGridSettings extends LitElement {
|
||||
)}
|
||||
</mwc-button>
|
||||
</div>
|
||||
`
|
||||
: ""}
|
||||
`}
|
||||
</div>
|
||||
</ha-card>
|
||||
`;
|
||||
}
|
||||
|
||||
private async _fetchCO2SignalConfigEntries() {
|
||||
this._configEntries = (await getConfigEntries(this.hass)).filter(
|
||||
(entry) => entry.domain === "co2signal"
|
||||
);
|
||||
const entries = await getConfigEntries(this.hass, { domain: "co2signal" });
|
||||
this._co2ConfigEntry = entries.length ? entries[0] : undefined;
|
||||
}
|
||||
|
||||
private _addCO2Sensor() {
|
||||
|
@@ -176,9 +176,17 @@ export class DialogEnergySolarSettings
|
||||
|
||||
private async _fetchSolarForecastConfigEntries() {
|
||||
const domains = this._params!.info.solar_forecast_domains;
|
||||
this._configEntries = (await getConfigEntries(this.hass)).filter((entry) =>
|
||||
domains.includes(entry.domain)
|
||||
);
|
||||
this._configEntries =
|
||||
domains.length === 0
|
||||
? []
|
||||
: domains.length === 1
|
||||
? await getConfigEntries(this.hass, {
|
||||
type: "integration",
|
||||
domain: domains[0],
|
||||
})
|
||||
: (await getConfigEntries(this.hass, { type: "integration" })).filter(
|
||||
(entry) => domains.includes(entry.domain)
|
||||
);
|
||||
}
|
||||
|
||||
private _handleForecastChanged(ev: CustomEvent) {
|
||||
|
@@ -10,50 +10,11 @@ import { customElement, property, state, query } from "lit/decorators";
|
||||
import { isComponentLoaded } from "../../../../../common/config/is_component_loaded";
|
||||
import { dynamicElement } from "../../../../../common/dom/dynamic-element-directive";
|
||||
import { fireEvent } from "../../../../../common/dom/fire_event";
|
||||
import {
|
||||
deleteCounter,
|
||||
fetchCounter,
|
||||
updateCounter,
|
||||
} from "../../../../../data/counter";
|
||||
import {
|
||||
ExtEntityRegistryEntry,
|
||||
removeEntityRegistryEntry,
|
||||
} from "../../../../../data/entity_registry";
|
||||
import {
|
||||
deleteInputBoolean,
|
||||
fetchInputBoolean,
|
||||
updateInputBoolean,
|
||||
} from "../../../../../data/input_boolean";
|
||||
import {
|
||||
deleteInputButton,
|
||||
fetchInputButton,
|
||||
updateInputButton,
|
||||
} from "../../../../../data/input_button";
|
||||
import {
|
||||
deleteInputDateTime,
|
||||
fetchInputDateTime,
|
||||
updateInputDateTime,
|
||||
} from "../../../../../data/input_datetime";
|
||||
import {
|
||||
deleteInputNumber,
|
||||
fetchInputNumber,
|
||||
updateInputNumber,
|
||||
} from "../../../../../data/input_number";
|
||||
import {
|
||||
deleteInputSelect,
|
||||
fetchInputSelect,
|
||||
updateInputSelect,
|
||||
} from "../../../../../data/input_select";
|
||||
import {
|
||||
deleteInputText,
|
||||
fetchInputText,
|
||||
updateInputText,
|
||||
} from "../../../../../data/input_text";
|
||||
import {
|
||||
deleteTimer,
|
||||
fetchTimer,
|
||||
updateTimer,
|
||||
} from "../../../../../data/timer";
|
||||
import { HELPERS_CRUD } from "../../../../../data/helpers_crud";
|
||||
import { showConfirmationDialog } from "../../../../../dialogs/generic/show-dialog-box";
|
||||
import { haStyle } from "../../../../../resources/styles";
|
||||
import type { HomeAssistant } from "../../../../../types";
|
||||
@@ -69,49 +30,6 @@ import "../../../helpers/forms/ha-timer-form";
|
||||
import "../../entity-registry-basic-editor";
|
||||
import type { HaEntityRegistryBasicEditor } from "../../entity-registry-basic-editor";
|
||||
|
||||
const HELPERS = {
|
||||
input_boolean: {
|
||||
fetch: fetchInputBoolean,
|
||||
update: updateInputBoolean,
|
||||
delete: deleteInputBoolean,
|
||||
},
|
||||
input_button: {
|
||||
fetch: fetchInputButton,
|
||||
update: updateInputButton,
|
||||
delete: deleteInputButton,
|
||||
},
|
||||
input_text: {
|
||||
fetch: fetchInputText,
|
||||
update: updateInputText,
|
||||
delete: deleteInputText,
|
||||
},
|
||||
input_number: {
|
||||
fetch: fetchInputNumber,
|
||||
update: updateInputNumber,
|
||||
delete: deleteInputNumber,
|
||||
},
|
||||
input_datetime: {
|
||||
fetch: fetchInputDateTime,
|
||||
update: updateInputDateTime,
|
||||
delete: deleteInputDateTime,
|
||||
},
|
||||
input_select: {
|
||||
fetch: fetchInputSelect,
|
||||
update: updateInputSelect,
|
||||
delete: deleteInputSelect,
|
||||
},
|
||||
counter: {
|
||||
fetch: fetchCounter,
|
||||
update: updateCounter,
|
||||
delete: deleteCounter,
|
||||
},
|
||||
timer: {
|
||||
fetch: fetchTimer,
|
||||
update: updateTimer,
|
||||
delete: deleteTimer,
|
||||
},
|
||||
};
|
||||
|
||||
@customElement("entity-settings-helper-tab")
|
||||
export class EntityRegistrySettingsHelper extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
@@ -198,7 +116,7 @@ export class EntityRegistrySettingsHelper extends LitElement {
|
||||
}
|
||||
|
||||
private async _getItem() {
|
||||
const items = await HELPERS[this.entry.platform].fetch(this.hass!);
|
||||
const items = await HELPERS_CRUD[this.entry.platform].fetch(this.hass!);
|
||||
this._item = items.find((item) => item.id === this.entry.unique_id) || null;
|
||||
}
|
||||
|
||||
@@ -206,7 +124,7 @@ export class EntityRegistrySettingsHelper extends LitElement {
|
||||
this._submitting = true;
|
||||
try {
|
||||
if (this._componentLoaded && this._item) {
|
||||
await HELPERS[this.entry.platform].update(
|
||||
await HELPERS_CRUD[this.entry.platform].update(
|
||||
this.hass!,
|
||||
this._item.id,
|
||||
this._item
|
||||
@@ -236,7 +154,10 @@ export class EntityRegistrySettingsHelper extends LitElement {
|
||||
|
||||
try {
|
||||
if (this._componentLoaded && this._item) {
|
||||
await HELPERS[this.entry.platform].delete(this.hass!, this._item.id);
|
||||
await HELPERS_CRUD[this.entry.platform].delete(
|
||||
this.hass!,
|
||||
this._item.id
|
||||
);
|
||||
} else {
|
||||
const stateObj = this.hass.states[this.entry.entity_id];
|
||||
if (!stateObj?.attributes.restored) {
|
||||
|
@@ -1,11 +1,13 @@
|
||||
import "@material/mwc-formfield/mwc-formfield";
|
||||
import { UnsubscribeFunc } from "home-assistant-js-websocket";
|
||||
import { css, html, LitElement, PropertyValues, TemplateResult } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { computeDomain } from "../../../common/entity/compute_domain";
|
||||
import "../../../components/ha-area-picker";
|
||||
import "../../../components/ha-expansion-panel";
|
||||
import "../../../components/ha-radio";
|
||||
import "../../../components/ha-switch";
|
||||
import "../../../components/ha-textfield";
|
||||
import type { HaSwitch } from "../../../components/ha-switch";
|
||||
import {
|
||||
DeviceRegistryEntry,
|
||||
subscribeDeviceRegistry,
|
||||
@@ -33,6 +35,8 @@ export class HaEntityRegistryBasicEditor extends SubscribeMixin(LitElement) {
|
||||
|
||||
@state() private _disabledBy!: string | null;
|
||||
|
||||
@state() private _hiddenBy!: string | null;
|
||||
|
||||
private _deviceLookup?: Record<string, DeviceRegistryEntry>;
|
||||
|
||||
@state() private _device?: DeviceRegistryEntry;
|
||||
@@ -51,6 +55,12 @@ export class HaEntityRegistryBasicEditor extends SubscribeMixin(LitElement) {
|
||||
) {
|
||||
params.disabled_by = this._disabledBy;
|
||||
}
|
||||
if (
|
||||
this.entry.hidden_by !== this._hiddenBy &&
|
||||
(this._hiddenBy === null || this._hiddenBy === "user")
|
||||
) {
|
||||
params.hidden_by = this._hiddenBy;
|
||||
}
|
||||
try {
|
||||
const result = await updateEntityRegistryEntry(
|
||||
this.hass!,
|
||||
@@ -101,6 +111,7 @@ export class HaEntityRegistryBasicEditor extends SubscribeMixin(LitElement) {
|
||||
this._origEntityId = this.entry.entity_id;
|
||||
this._entityId = this.entry.entity_id;
|
||||
this._disabledBy = this.entry.disabled_by;
|
||||
this._hiddenBy = this.entry.hidden_by;
|
||||
this._areaId = this.entry.area_id;
|
||||
this._device =
|
||||
this.entry.device_id && this._deviceLookup
|
||||
@@ -138,37 +149,104 @@ export class HaEntityRegistryBasicEditor extends SubscribeMixin(LitElement) {
|
||||
.placeholder=${this._device?.area_id}
|
||||
@value-changed=${this._areaPicked}
|
||||
></ha-area-picker>
|
||||
<div class="row">
|
||||
<ha-switch
|
||||
.checked=${!this._disabledBy}
|
||||
@change=${this._disabledByChanged}
|
||||
>
|
||||
</ha-switch>
|
||||
<div>
|
||||
<div>
|
||||
${this.hass.localize(
|
||||
|
||||
<ha-expansion-panel
|
||||
.header=${this.hass.localize(
|
||||
"ui.dialogs.entity_registry.editor.advanced"
|
||||
)}
|
||||
outlined
|
||||
>
|
||||
<div class="label">
|
||||
${this.hass.localize(
|
||||
"ui.dialogs.entity_registry.editor.entity_status"
|
||||
)}:
|
||||
</div>
|
||||
<div class="secondary">
|
||||
${this._disabledBy && this._disabledBy !== "user"
|
||||
? this.hass.localize(
|
||||
"ui.dialogs.entity_registry.editor.enabled_cause",
|
||||
"cause",
|
||||
this.hass.localize(
|
||||
`config_entry.disabled_by.${this._disabledBy}`
|
||||
)
|
||||
)
|
||||
: ""}
|
||||
</div>
|
||||
<div class="row">
|
||||
<mwc-formfield
|
||||
.label=${this.hass.localize(
|
||||
"ui.dialogs.entity_registry.editor.enabled_label"
|
||||
)}
|
||||
</div>
|
||||
<div class="secondary">
|
||||
${this._disabledBy && this._disabledBy !== "user"
|
||||
? this.hass.localize(
|
||||
"ui.dialogs.entity_registry.editor.enabled_cause",
|
||||
"cause",
|
||||
this.hass.localize(
|
||||
`config_entry.disabled_by.${this._disabledBy}`
|
||||
)
|
||||
)
|
||||
: ""}
|
||||
${this.hass.localize(
|
||||
"ui.dialogs.entity_registry.editor.enabled_description"
|
||||
>
|
||||
<ha-radio
|
||||
name="hiddendisabled"
|
||||
value="enabled"
|
||||
.checked=${!this._hiddenBy && !this._disabledBy}
|
||||
.disabled=${this._device?.disabled_by ||
|
||||
(this._disabledBy &&
|
||||
!(
|
||||
this._disabledBy === "user" ||
|
||||
this._disabledBy === "integration"
|
||||
))}
|
||||
@change=${this._viewStatusChanged}
|
||||
></ha-radio>
|
||||
</mwc-formfield>
|
||||
<mwc-formfield
|
||||
.label=${this.hass.localize(
|
||||
"ui.dialogs.entity_registry.editor.hidden_label"
|
||||
)}
|
||||
<br />${this.hass.localize(
|
||||
"ui.dialogs.entity_registry.editor.note"
|
||||
>
|
||||
<ha-radio
|
||||
name="hiddendisabled"
|
||||
value="hidden"
|
||||
.checked=${this._hiddenBy !== null}
|
||||
.disabled=${this._device?.disabled_by ||
|
||||
(this._disabledBy &&
|
||||
!(
|
||||
this._disabledBy === "user" ||
|
||||
this._disabledBy === "integration"
|
||||
))}
|
||||
@change=${this._viewStatusChanged}
|
||||
></ha-radio>
|
||||
</mwc-formfield>
|
||||
<mwc-formfield
|
||||
.label=${this.hass.localize(
|
||||
"ui.dialogs.entity_registry.editor.disabled_label"
|
||||
)}
|
||||
</div>
|
||||
>
|
||||
<ha-radio
|
||||
name="hiddendisabled"
|
||||
value="disabled"
|
||||
.checked=${this._disabledBy !== null}
|
||||
.disabled=${this._device?.disabled_by ||
|
||||
(this._disabledBy &&
|
||||
!(
|
||||
this._disabledBy === "user" ||
|
||||
this._disabledBy === "integration"
|
||||
))}
|
||||
@change=${this._viewStatusChanged}
|
||||
></ha-radio>
|
||||
</mwc-formfield>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
${this._disabledBy !== null
|
||||
? html`
|
||||
<div class="secondary">
|
||||
${this.hass.localize(
|
||||
"ui.dialogs.entity_registry.editor.enabled_description"
|
||||
)}
|
||||
</div>
|
||||
`
|
||||
: this._hiddenBy !== null
|
||||
? html`
|
||||
<div class="secondary">
|
||||
${this.hass.localize(
|
||||
"ui.dialogs.entity_registry.editor.hidden_description"
|
||||
)}
|
||||
</div>
|
||||
`
|
||||
: ""}
|
||||
</ha-expansion-panel>
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -180,8 +258,21 @@ export class HaEntityRegistryBasicEditor extends SubscribeMixin(LitElement) {
|
||||
this._entityId = ev.target.value;
|
||||
}
|
||||
|
||||
private _disabledByChanged(ev: Event): void {
|
||||
this._disabledBy = (ev.target as HaSwitch).checked ? null : "user";
|
||||
private _viewStatusChanged(ev: CustomEvent): void {
|
||||
switch ((ev.target as any).value) {
|
||||
case "enabled":
|
||||
this._disabledBy = null;
|
||||
this._hiddenBy = null;
|
||||
break;
|
||||
case "disabled":
|
||||
this._disabledBy = "user";
|
||||
this._hiddenBy = null;
|
||||
break;
|
||||
case "hidden":
|
||||
this._hiddenBy = "user";
|
||||
this._disabledBy = null;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
static get styles() {
|
||||
@@ -202,6 +293,12 @@ export class HaEntityRegistryBasicEditor extends SubscribeMixin(LitElement) {
|
||||
display: block;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
ha-expansion-panel {
|
||||
margin-top: 8px;
|
||||
}
|
||||
.label {
|
||||
margin-top: 16px;
|
||||
}
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
@@ -1,3 +1,5 @@
|
||||
import "@material/mwc-formfield/mwc-formfield";
|
||||
import "../../../components/ha-radio";
|
||||
import "@material/mwc-button/mwc-button";
|
||||
import "@material/mwc-list/mwc-list-item";
|
||||
import { HassEntity, UnsubscribeFunc } from "home-assistant-js-websocket";
|
||||
@@ -20,7 +22,6 @@ import "../../../components/ha-expansion-panel";
|
||||
import "../../../components/ha-icon-picker";
|
||||
import "../../../components/ha-select";
|
||||
import "../../../components/ha-switch";
|
||||
import type { HaSwitch } from "../../../components/ha-switch";
|
||||
import "../../../components/ha-textfield";
|
||||
import {
|
||||
DeviceRegistryEntry,
|
||||
@@ -41,9 +42,26 @@ import { SubscribeMixin } from "../../../mixins/subscribe-mixin";
|
||||
import { haStyle } from "../../../resources/styles";
|
||||
import type { HomeAssistant } from "../../../types";
|
||||
import { showDeviceRegistryDetailDialog } from "../devices/device-registry-detail/show-dialog-device-registry-detail";
|
||||
import {
|
||||
ConfigEntry,
|
||||
deleteConfigEntry,
|
||||
getConfigEntries,
|
||||
} from "../../../data/config_entries";
|
||||
import { showOptionsFlowDialog } from "../../../dialogs/config-flow/show-dialog-options-flow";
|
||||
|
||||
const OVERRIDE_DEVICE_CLASSES = {
|
||||
cover: ["window", "door", "garage", "gate"],
|
||||
cover: [
|
||||
"awning",
|
||||
"blind",
|
||||
"curtain",
|
||||
"damper",
|
||||
"door",
|
||||
"garage",
|
||||
"gate",
|
||||
"shade",
|
||||
"shutter",
|
||||
"window",
|
||||
],
|
||||
binary_sensor: ["window", "door", "garage_door", "opening"],
|
||||
};
|
||||
|
||||
@@ -65,10 +83,14 @@ export class EntityRegistrySettings extends SubscribeMixin(LitElement) {
|
||||
|
||||
@state() private _disabledBy!: string | null;
|
||||
|
||||
@state() private _hiddenBy!: string | null;
|
||||
|
||||
private _deviceLookup?: Record<string, DeviceRegistryEntry>;
|
||||
|
||||
@state() private _device?: DeviceRegistryEntry;
|
||||
|
||||
@state() private _helperConfigEntry?: ConfigEntry;
|
||||
|
||||
@state() private _error?: string;
|
||||
|
||||
@state() private _submitting?: boolean;
|
||||
@@ -89,6 +111,20 @@ export class EntityRegistrySettings extends SubscribeMixin(LitElement) {
|
||||
];
|
||||
}
|
||||
|
||||
protected firstUpdated(changedProps: PropertyValues): void {
|
||||
super.firstUpdated(changedProps);
|
||||
if (this.entry.config_entry_id) {
|
||||
getConfigEntries(this.hass, {
|
||||
type: "helper",
|
||||
domain: this.entry.platform,
|
||||
}).then((entries) => {
|
||||
this._helperConfigEntry = entries.find(
|
||||
(ent) => ent.entry_id === this.entry.config_entry_id
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
protected updated(changedProperties: PropertyValues) {
|
||||
super.updated(changedProperties);
|
||||
if (changedProperties.has("entry")) {
|
||||
@@ -101,6 +137,7 @@ export class EntityRegistrySettings extends SubscribeMixin(LitElement) {
|
||||
this._areaId = this.entry.area_id;
|
||||
this._entityId = this.entry.entity_id;
|
||||
this._disabledBy = this.entry.disabled_by;
|
||||
this._hiddenBy = this.entry.hidden_by;
|
||||
this._device =
|
||||
this.entry.device_id && this._deviceLookup
|
||||
? this._deviceLookup[this.entry.device_id]
|
||||
@@ -200,82 +237,148 @@ export class EntityRegistrySettings extends SubscribeMixin(LitElement) {
|
||||
@value-changed=${this._areaPicked}
|
||||
></ha-area-picker>`
|
||||
: ""}
|
||||
<div class="row">
|
||||
<ha-switch
|
||||
.checked=${!this._disabledBy}
|
||||
.disabled=${this._device?.disabled_by}
|
||||
@change=${this._disabledByChanged}
|
||||
>
|
||||
</ha-switch>
|
||||
<div>
|
||||
<div>
|
||||
${this.hass.localize(
|
||||
${this._helperConfigEntry
|
||||
? html`
|
||||
<div class="row">
|
||||
<mwc-button
|
||||
@click=${this._showOptionsFlow}
|
||||
.disabled=${this._submitting}
|
||||
>
|
||||
${this.hass.localize(
|
||||
"ui.dialogs.entity_registry.editor.configure_state"
|
||||
)}
|
||||
</mwc-button>
|
||||
</div>
|
||||
`
|
||||
: ""}
|
||||
|
||||
<ha-expansion-panel
|
||||
.header=${this.hass.localize(
|
||||
"ui.dialogs.entity_registry.editor.advanced"
|
||||
)}
|
||||
outlined
|
||||
>
|
||||
<div class="label">
|
||||
${this.hass.localize(
|
||||
"ui.dialogs.entity_registry.editor.entity_status"
|
||||
)}:
|
||||
</div>
|
||||
<div class="secondary">
|
||||
${this._disabledBy && this._disabledBy !== "user"
|
||||
? this.hass.localize(
|
||||
"ui.dialogs.entity_registry.editor.enabled_cause",
|
||||
"cause",
|
||||
this.hass.localize(
|
||||
`config_entry.disabled_by.${this._disabledBy}`
|
||||
)
|
||||
)
|
||||
: ""}
|
||||
</div>
|
||||
<div class="row">
|
||||
<mwc-formfield
|
||||
.label=${this.hass.localize(
|
||||
"ui.dialogs.entity_registry.editor.enabled_label"
|
||||
)}
|
||||
</div>
|
||||
<div class="secondary">
|
||||
${this._disabledBy && this._disabledBy !== "user"
|
||||
? this.hass.localize(
|
||||
"ui.dialogs.entity_registry.editor.enabled_cause",
|
||||
"cause",
|
||||
this.hass.localize(
|
||||
`config_entry.disabled_by.${this._disabledBy}`
|
||||
)
|
||||
)
|
||||
: ""}
|
||||
${this.hass.localize(
|
||||
"ui.dialogs.entity_registry.editor.enabled_description"
|
||||
)}
|
||||
<br />${this.hass.localize(
|
||||
"ui.dialogs.entity_registry.editor.note"
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
${this.entry.device_id
|
||||
? html`<ha-expansion-panel
|
||||
.header=${this.hass.localize(
|
||||
"ui.dialogs.entity_registry.editor.advanced"
|
||||
)}
|
||||
outlined
|
||||
>
|
||||
<p>
|
||||
${this.hass.localize(
|
||||
"ui.dialogs.entity_registry.editor.area_note"
|
||||
)}
|
||||
</p>
|
||||
${this._areaId
|
||||
? html`<mwc-button @click=${this._clearArea}
|
||||
>${this.hass.localize(
|
||||
"ui.dialogs.entity_registry.editor.follow_device_area"
|
||||
)}</mwc-button
|
||||
>`
|
||||
: this._device
|
||||
? html`<mwc-button @click=${this._openDeviceSettings}
|
||||
>${this.hass.localize(
|
||||
"ui.dialogs.entity_registry.editor.change_device_area"
|
||||
)}</mwc-button
|
||||
>`
|
||||
: ""}
|
||||
<ha-area-picker
|
||||
.hass=${this.hass}
|
||||
.value=${this._areaId}
|
||||
.placeholder=${this._device?.area_id}
|
||||
.label=${this.hass.localize(
|
||||
"ui.dialogs.entity_registry.editor.area"
|
||||
)}
|
||||
@value-changed=${this._areaPicked}
|
||||
></ha-area-picker
|
||||
></ha-expansion-panel>`
|
||||
: ""}
|
||||
<ha-radio
|
||||
name="hiddendisabled"
|
||||
value="enabled"
|
||||
.checked=${!this._hiddenBy && !this._disabledBy}
|
||||
.disabled=${(this._hiddenBy && this._hiddenBy !== "user") ||
|
||||
this._device?.disabled_by ||
|
||||
(this._disabledBy && this._disabledBy !== "user")}
|
||||
@change=${this._viewStatusChanged}
|
||||
></ha-radio>
|
||||
</mwc-formfield>
|
||||
<mwc-formfield
|
||||
.label=${this.hass.localize(
|
||||
"ui.dialogs.entity_registry.editor.hidden_label"
|
||||
)}
|
||||
>
|
||||
<ha-radio
|
||||
name="hiddendisabled"
|
||||
value="hidden"
|
||||
.checked=${this._hiddenBy !== null}
|
||||
.disabled=${(this._hiddenBy && this._hiddenBy !== "user") ||
|
||||
Boolean(this._device?.disabled_by) ||
|
||||
(this._disabledBy && this._disabledBy !== "user")}
|
||||
@change=${this._viewStatusChanged}
|
||||
></ha-radio>
|
||||
</mwc-formfield>
|
||||
<mwc-formfield
|
||||
.label=${this.hass.localize(
|
||||
"ui.dialogs.entity_registry.editor.disabled_label"
|
||||
)}
|
||||
>
|
||||
<ha-radio
|
||||
name="hiddendisabled"
|
||||
value="disabled"
|
||||
.checked=${this._disabledBy !== null}
|
||||
.disabled=${(this._hiddenBy && this._hiddenBy !== "user") ||
|
||||
Boolean(this._device?.disabled_by) ||
|
||||
(this._disabledBy && this._disabledBy !== "user")}
|
||||
@change=${this._viewStatusChanged}
|
||||
></ha-radio>
|
||||
</mwc-formfield>
|
||||
</div>
|
||||
|
||||
${this._disabledBy !== null
|
||||
? html`
|
||||
<div class="secondary">
|
||||
${this.hass.localize(
|
||||
"ui.dialogs.entity_registry.editor.enabled_description"
|
||||
)}
|
||||
</div>
|
||||
`
|
||||
: this._hiddenBy !== null
|
||||
? html`
|
||||
<div class="secondary">
|
||||
${this.hass.localize(
|
||||
"ui.dialogs.entity_registry.editor.hidden_description"
|
||||
)}
|
||||
</div>
|
||||
`
|
||||
: ""}
|
||||
${this.entry.device_id
|
||||
? html`
|
||||
<div class="label">
|
||||
${this.hass.localize(
|
||||
"ui.dialogs.entity_registry.editor.change_area"
|
||||
)}:
|
||||
</div>
|
||||
<ha-area-picker
|
||||
.hass=${this.hass}
|
||||
.value=${this._areaId}
|
||||
.placeholder=${this._device?.area_id}
|
||||
.label=${this.hass.localize(
|
||||
"ui.dialogs.entity_registry.editor.area"
|
||||
)}
|
||||
@value-changed=${this._areaPicked}
|
||||
></ha-area-picker>
|
||||
<div class="secondary">
|
||||
${this.hass.localize(
|
||||
"ui.dialogs.entity_registry.editor.area_note"
|
||||
)}
|
||||
${this._device
|
||||
? html`
|
||||
<button class="link" @click=${this._openDeviceSettings}>
|
||||
${this.hass.localize(
|
||||
"ui.dialogs.entity_registry.editor.change_device_area"
|
||||
)}
|
||||
</button>
|
||||
`
|
||||
: ""}
|
||||
</div>
|
||||
`
|
||||
: ""}
|
||||
</ha-expansion-panel>
|
||||
</div>
|
||||
<div class="buttons">
|
||||
<mwc-button
|
||||
class="warning"
|
||||
@click=${this._confirmDeleteEntry}
|
||||
.disabled=${this._submitting ||
|
||||
!(stateObj && stateObj.attributes.restored)}
|
||||
(!this._helperConfigEntry && !stateObj.attributes.restored)}
|
||||
>
|
||||
${this.hass.localize("ui.dialogs.entity_registry.editor.delete")}
|
||||
</mwc-button>
|
||||
@@ -314,9 +417,21 @@ export class EntityRegistrySettings extends SubscribeMixin(LitElement) {
|
||||
this._areaId = ev.detail.value;
|
||||
}
|
||||
|
||||
private _clearArea() {
|
||||
this._error = undefined;
|
||||
this._areaId = null;
|
||||
private _viewStatusChanged(ev: CustomEvent): void {
|
||||
switch ((ev.target as any).value) {
|
||||
case "enabled":
|
||||
this._disabledBy = null;
|
||||
this._hiddenBy = null;
|
||||
break;
|
||||
case "disabled":
|
||||
this._disabledBy = "user";
|
||||
this._hiddenBy = null;
|
||||
break;
|
||||
case "hidden":
|
||||
this._hiddenBy = "user";
|
||||
this._disabledBy = null;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private _openDeviceSettings() {
|
||||
@@ -343,6 +458,12 @@ export class EntityRegistrySettings extends SubscribeMixin(LitElement) {
|
||||
) {
|
||||
params.disabled_by = this._disabledBy;
|
||||
}
|
||||
if (
|
||||
this.entry.hidden_by !== this._hiddenBy &&
|
||||
(this._hiddenBy === null || this._hiddenBy === "user")
|
||||
) {
|
||||
params.hidden_by = this._hiddenBy;
|
||||
}
|
||||
try {
|
||||
const result = await updateEntityRegistryEntry(
|
||||
this.hass!,
|
||||
@@ -387,15 +508,19 @@ export class EntityRegistrySettings extends SubscribeMixin(LitElement) {
|
||||
this._submitting = true;
|
||||
|
||||
try {
|
||||
await removeEntityRegistryEntry(this.hass!, this._origEntityId);
|
||||
if (this._helperConfigEntry) {
|
||||
await deleteConfigEntry(this.hass, this._helperConfigEntry.entry_id);
|
||||
} else {
|
||||
await removeEntityRegistryEntry(this.hass!, this._origEntityId);
|
||||
}
|
||||
fireEvent(this, "close-dialog");
|
||||
} finally {
|
||||
this._submitting = false;
|
||||
}
|
||||
}
|
||||
|
||||
private _disabledByChanged(ev: Event): void {
|
||||
this._disabledBy = (ev.target as HaSwitch).checked ? null : "user";
|
||||
private async _showOptionsFlow() {
|
||||
showOptionsFlowDialog(this, this._helperConfigEntry!);
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
@@ -435,14 +560,22 @@ export class EntityRegistrySettings extends SubscribeMixin(LitElement) {
|
||||
display: block;
|
||||
margin: 8px 0;
|
||||
}
|
||||
ha-area-picker {
|
||||
margin: 8px 0;
|
||||
display: block;
|
||||
}
|
||||
.row {
|
||||
margin: 8px 0;
|
||||
color: var(--primary-text-color);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
p {
|
||||
.label {
|
||||
margin-top: 16px;
|
||||
}
|
||||
.secondary {
|
||||
margin: 8px 0;
|
||||
width: 340px;
|
||||
}
|
||||
`,
|
||||
];
|
||||
|
@@ -3,6 +3,7 @@ import {
|
||||
mdiAlertCircle,
|
||||
mdiCancel,
|
||||
mdiDelete,
|
||||
mdiEyeOff,
|
||||
mdiFilterVariant,
|
||||
mdiPencilOff,
|
||||
mdiPlus,
|
||||
@@ -101,6 +102,8 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) {
|
||||
|
||||
@state() private _showDisabled = false;
|
||||
|
||||
@state() private _showHidden = false;
|
||||
|
||||
@state() private _showUnavailable = true;
|
||||
|
||||
@state() private _showReadOnly = true;
|
||||
@@ -249,7 +252,10 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) {
|
||||
filterable: true,
|
||||
width: "68px",
|
||||
template: (_status, entity: EntityRow) =>
|
||||
entity.unavailable || entity.disabled_by || entity.readonly
|
||||
entity.unavailable ||
|
||||
entity.disabled_by ||
|
||||
entity.hidden_by ||
|
||||
entity.readonly
|
||||
? html`
|
||||
<div
|
||||
tabindex="0"
|
||||
@@ -265,6 +271,8 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) {
|
||||
? mdiAlertCircle
|
||||
: entity.disabled_by
|
||||
? mdiCancel
|
||||
: entity.hidden_by
|
||||
? mdiEyeOff
|
||||
: mdiPencilOff}
|
||||
></ha-svg-icon>
|
||||
<paper-tooltip animation-delay="0" position="left">
|
||||
@@ -280,6 +288,10 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) {
|
||||
? this.hass.localize(
|
||||
"ui.panel.config.entities.picker.status.disabled"
|
||||
)
|
||||
: entity.hidden_by
|
||||
? this.hass.localize(
|
||||
"ui.panel.config.entities.picker.status.hidden"
|
||||
)
|
||||
: this.hass.localize(
|
||||
"ui.panel.config.entities.picker.status.readonly"
|
||||
)}
|
||||
@@ -301,6 +313,7 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) {
|
||||
showDisabled: boolean,
|
||||
showUnavailable: boolean,
|
||||
showReadOnly: boolean,
|
||||
showHidden: boolean,
|
||||
entries?: ConfigEntry[]
|
||||
) => {
|
||||
const result: EntityRow[] = [];
|
||||
@@ -362,6 +375,12 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) {
|
||||
);
|
||||
}
|
||||
|
||||
if (!showHidden) {
|
||||
filteredEntities = filteredEntities.filter(
|
||||
(entity) => !entity.hidden_by
|
||||
);
|
||||
}
|
||||
|
||||
for (const entry of filteredEntities) {
|
||||
const entity = this.hass.states[entry.entity_id];
|
||||
const unavailable = entity?.state === UNAVAILABLE;
|
||||
@@ -465,6 +484,7 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) {
|
||||
this._showDisabled,
|
||||
this._showUnavailable,
|
||||
this._showReadOnly,
|
||||
this._showHidden,
|
||||
this._entries
|
||||
);
|
||||
|
||||
@@ -533,6 +553,11 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) {
|
||||
"ui.panel.config.entities.picker.disable_selected.button"
|
||||
)}</mwc-button
|
||||
>
|
||||
<mwc-button @click=${this._hideSelected}
|
||||
>${this.hass.localize(
|
||||
"ui.panel.config.entities.picker.hide_selected.button"
|
||||
)}</mwc-button
|
||||
>
|
||||
<mwc-button @click=${this._removeSelected} class="warning"
|
||||
>${this.hass.localize(
|
||||
"ui.panel.config.entities.picker.remove_selected.button"
|
||||
@@ -562,6 +587,17 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) {
|
||||
"ui.panel.config.entities.picker.disable_selected.button"
|
||||
)}
|
||||
</paper-tooltip>
|
||||
<ha-icon-button
|
||||
id="hide-btn"
|
||||
@click=${this._hideSelected}
|
||||
.path=${mdiCancel}
|
||||
.label=${this.hass.localize("ui.common.hide")}
|
||||
></ha-icon-button>
|
||||
<paper-tooltip animation-delay="0" for="hide-btn">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.entities.picker.hide_selected.button"
|
||||
)}
|
||||
</paper-tooltip>
|
||||
<ha-icon-button
|
||||
class="warning"
|
||||
id="remove-btn"
|
||||
@@ -603,6 +639,15 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) {
|
||||
"ui.panel.config.entities.picker.filter.show_disabled"
|
||||
)}
|
||||
</ha-check-list-item>
|
||||
<ha-check-list-item
|
||||
@request-selected=${this._showHiddenChanged}
|
||||
.selected=${this._showHidden}
|
||||
left
|
||||
>
|
||||
${this.hass!.localize(
|
||||
"ui.panel.config.entities.picker.filter.show_hidden"
|
||||
)}
|
||||
</ha-check-list-item>
|
||||
<ha-check-list-item
|
||||
@request-selected=${this._showRestoredChanged}
|
||||
graphic="control"
|
||||
@@ -671,6 +716,7 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) {
|
||||
entity_id: entityId,
|
||||
platform: computeDomain(entityId),
|
||||
disabled_by: null,
|
||||
hidden_by: null,
|
||||
area_id: null,
|
||||
config_entry_id: null,
|
||||
device_id: null,
|
||||
@@ -693,6 +739,13 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) {
|
||||
this._showDisabled = ev.detail.selected;
|
||||
}
|
||||
|
||||
private _showHiddenChanged(ev: CustomEvent<RequestSelectedDetail>) {
|
||||
if (ev.detail.source !== "property") {
|
||||
return;
|
||||
}
|
||||
this._showHidden = ev.detail.selected;
|
||||
}
|
||||
|
||||
private _showRestoredChanged(ev: CustomEvent<RequestSelectedDetail>) {
|
||||
if (ev.detail.source !== "property") {
|
||||
return;
|
||||
@@ -791,6 +844,29 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) {
|
||||
});
|
||||
}
|
||||
|
||||
private _hideSelected() {
|
||||
showConfirmationDialog(this, {
|
||||
title: this.hass.localize(
|
||||
"ui.panel.config.entities.picker.hide_selected.confirm_title",
|
||||
"number",
|
||||
this._selectedEntities.length
|
||||
),
|
||||
text: this.hass.localize(
|
||||
"ui.panel.config.entities.picker.hide_selected.confirm_text"
|
||||
),
|
||||
confirmText: this.hass.localize("ui.common.hide"),
|
||||
dismissText: this.hass.localize("ui.common.cancel"),
|
||||
confirm: () => {
|
||||
this._selectedEntities.forEach((entity) =>
|
||||
updateEntityRegistryEntry(this.hass, entity, {
|
||||
hidden_by: "user",
|
||||
})
|
||||
);
|
||||
this._clearSelection();
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
private _removeSelected() {
|
||||
const removeableEntities = this._selectedEntities.filter((entity) => {
|
||||
const stateObj = this.hass.states[entity];
|
||||
|
@@ -376,21 +376,11 @@ class HaPanelConfig extends HassRouterPage {
|
||||
"./integrations/integration-panels/zha/zha-config-dashboard-router"
|
||||
),
|
||||
},
|
||||
zwave: {
|
||||
tag: "zwave-config-router",
|
||||
load: () =>
|
||||
import("./integrations/integration-panels/zwave/zwave-config-router"),
|
||||
},
|
||||
mqtt: {
|
||||
tag: "mqtt-config-panel",
|
||||
load: () =>
|
||||
import("./integrations/integration-panels/mqtt/mqtt-config-panel"),
|
||||
},
|
||||
ozw: {
|
||||
tag: "ozw-config-router",
|
||||
load: () =>
|
||||
import("./integrations/integration-panels/ozw/ozw-config-router"),
|
||||
},
|
||||
zwave_js: {
|
||||
tag: "zwave_js-config-router",
|
||||
load: () =>
|
||||
|
@@ -1,11 +1,11 @@
|
||||
import { Counter } from "../../../data/counter";
|
||||
import { InputBoolean } from "../../../data/input_boolean";
|
||||
import { InputButton } from "../../../data/input_button";
|
||||
import { InputDateTime } from "../../../data/input_datetime";
|
||||
import { InputNumber } from "../../../data/input_number";
|
||||
import { InputSelect } from "../../../data/input_select";
|
||||
import { InputText } from "../../../data/input_text";
|
||||
import { Timer } from "../../../data/timer";
|
||||
import type { Counter } from "../../../data/counter";
|
||||
import type { InputBoolean } from "../../../data/input_boolean";
|
||||
import type { InputButton } from "../../../data/input_button";
|
||||
import type { InputDateTime } from "../../../data/input_datetime";
|
||||
import type { InputNumber } from "../../../data/input_number";
|
||||
import type { InputSelect } from "../../../data/input_select";
|
||||
import type { InputText } from "../../../data/input_text";
|
||||
import type { Timer } from "../../../data/timer";
|
||||
|
||||
export const HELPER_DOMAINS = [
|
||||
"input_boolean",
|
||||
|
@@ -8,6 +8,8 @@ import { isComponentLoaded } from "../../../common/config/is_component_loaded";
|
||||
import { dynamicElement } from "../../../common/dom/dynamic-element-directive";
|
||||
import { domainIcon } from "../../../common/entity/domain_icon";
|
||||
import "../../../components/ha-dialog";
|
||||
import "../../../components/ha-circular-progress";
|
||||
import { getConfigFlowHandlers } from "../../../data/config_flow";
|
||||
import { createCounter } from "../../../data/counter";
|
||||
import { createInputBoolean } from "../../../data/input_boolean";
|
||||
import { createInputButton } from "../../../data/input_button";
|
||||
@@ -16,6 +18,7 @@ import { createInputNumber } from "../../../data/input_number";
|
||||
import { createInputSelect } from "../../../data/input_select";
|
||||
import { createInputText } from "../../../data/input_text";
|
||||
import { createTimer } from "../../../data/timer";
|
||||
import { showConfigFlowDialog } from "../../../dialogs/config-flow/show-dialog-config-flow";
|
||||
import { haStyleDialog } from "../../../resources/styles";
|
||||
import { HomeAssistant } from "../../../types";
|
||||
import { Helper } from "./const";
|
||||
@@ -27,6 +30,8 @@ import "./forms/ha-input_number-form";
|
||||
import "./forms/ha-input_select-form";
|
||||
import "./forms/ha-input_text-form";
|
||||
import "./forms/ha-timer-form";
|
||||
import { domainToName } from "../../../data/integration";
|
||||
import type { ShowDialogHelperDetailParams } from "./show-dialog-helper-detail";
|
||||
|
||||
const HELPERS = {
|
||||
input_boolean: createInputBoolean,
|
||||
@@ -47,7 +52,7 @@ export class DialogHelperDetail extends LitElement {
|
||||
|
||||
@state() private _opened = false;
|
||||
|
||||
@state() private _platform?: string;
|
||||
@state() private _domain?: string;
|
||||
|
||||
@state() private _error?: string;
|
||||
|
||||
@@ -55,102 +60,135 @@ export class DialogHelperDetail extends LitElement {
|
||||
|
||||
@query(".form") private _form?: HTMLDivElement;
|
||||
|
||||
public async showDialog(): Promise<void> {
|
||||
this._platform = undefined;
|
||||
@state() private _helperFlows?: string[];
|
||||
|
||||
private _params?: ShowDialogHelperDetailParams;
|
||||
|
||||
public async showDialog(params: ShowDialogHelperDetailParams): Promise<void> {
|
||||
this._params = params;
|
||||
this._domain = undefined;
|
||||
this._item = undefined;
|
||||
this._opened = true;
|
||||
await this.updateComplete;
|
||||
Promise.all([
|
||||
getConfigFlowHandlers(this.hass, "helper"),
|
||||
// Ensure the titles are loaded before we render the flows.
|
||||
this.hass.loadBackendTranslation("title", undefined, true),
|
||||
]).then(([flows]) => {
|
||||
this._helperFlows = flows;
|
||||
});
|
||||
}
|
||||
|
||||
public closeDialog(): void {
|
||||
this._opened = false;
|
||||
this._error = "";
|
||||
this._params = undefined;
|
||||
}
|
||||
|
||||
protected render(): TemplateResult {
|
||||
let content: TemplateResult;
|
||||
|
||||
if (this._domain) {
|
||||
content = html`
|
||||
<div class="form" @value-changed=${this._valueChanged}>
|
||||
${this._error ? html` <div class="error">${this._error}</div> ` : ""}
|
||||
${dynamicElement(`ha-${this._domain}-form`, {
|
||||
hass: this.hass,
|
||||
item: this._item,
|
||||
new: true,
|
||||
})}
|
||||
</div>
|
||||
<mwc-button
|
||||
slot="primaryAction"
|
||||
@click=${this._createItem}
|
||||
.disabled=${this._submitting}
|
||||
>
|
||||
${this.hass!.localize("ui.panel.config.helpers.dialog.create")}
|
||||
</mwc-button>
|
||||
<mwc-button
|
||||
slot="secondaryAction"
|
||||
@click=${this._goBack}
|
||||
.disabled=${this._submitting}
|
||||
>
|
||||
${this.hass!.localize("ui.common.back")}
|
||||
</mwc-button>
|
||||
`;
|
||||
} else if (this._helperFlows === undefined) {
|
||||
content = html`<ha-circular-progress active></ha-circular-progress>`;
|
||||
} else {
|
||||
const items: [string, string][] = [];
|
||||
|
||||
for (const helper of Object.keys(HELPERS)) {
|
||||
items.push([
|
||||
helper,
|
||||
this.hass.localize(`ui.panel.config.helpers.types.${helper}`) ||
|
||||
helper,
|
||||
]);
|
||||
}
|
||||
|
||||
for (const domain of this._helperFlows) {
|
||||
items.push([domain, domainToName(this.hass.localize, domain)]);
|
||||
}
|
||||
|
||||
items.sort((a, b) => a[1].localeCompare(b[1]));
|
||||
|
||||
content = html`
|
||||
${items.map(([domain, label]) => {
|
||||
// Only OG helpers need to be loaded prior adding one
|
||||
const isLoaded =
|
||||
!(domain in HELPERS) || isComponentLoaded(this.hass, domain);
|
||||
return html`
|
||||
<mwc-list-item
|
||||
.disabled=${!isLoaded}
|
||||
.domain=${domain}
|
||||
@click=${this._domainPicked}
|
||||
@keydown=${this._handleEnter}
|
||||
dialogInitialFocus
|
||||
graphic="icon"
|
||||
>
|
||||
<ha-svg-icon
|
||||
slot="graphic"
|
||||
.path=${domainIcon(domain)}
|
||||
></ha-svg-icon>
|
||||
<span class="item-text"> ${label} </span>
|
||||
</mwc-list-item>
|
||||
${!isLoaded
|
||||
? html`
|
||||
<paper-tooltip animation-delay="0"
|
||||
>${this.hass.localize(
|
||||
"ui.dialogs.helper_settings.platform_not_loaded",
|
||||
"platform",
|
||||
domain
|
||||
)}</paper-tooltip
|
||||
>
|
||||
`
|
||||
: ""}
|
||||
`;
|
||||
})}
|
||||
<mwc-button slot="primaryAction" @click=${this.closeDialog}>
|
||||
${this.hass!.localize("ui.common.cancel")}
|
||||
</mwc-button>
|
||||
`;
|
||||
}
|
||||
|
||||
return html`
|
||||
<ha-dialog
|
||||
.open=${this._opened}
|
||||
@closed=${this.closeDialog}
|
||||
class=${classMap({ "button-left": !this._platform })}
|
||||
class=${classMap({ "button-left": !this._domain })}
|
||||
scrimClickAction
|
||||
escapeKeyAction
|
||||
.heading=${this._platform
|
||||
.heading=${this._domain
|
||||
? this.hass.localize(
|
||||
"ui.panel.config.helpers.dialog.add_platform",
|
||||
"platform",
|
||||
this.hass.localize(
|
||||
`ui.panel.config.helpers.types.${this._platform}`
|
||||
) || this._platform
|
||||
`ui.panel.config.helpers.types.${this._domain}`
|
||||
) || this._domain
|
||||
)
|
||||
: this.hass.localize("ui.panel.config.helpers.dialog.add_helper")}
|
||||
>
|
||||
${this._platform
|
||||
? html`
|
||||
<div class="form" @value-changed=${this._valueChanged}>
|
||||
${this._error
|
||||
? html` <div class="error">${this._error}</div> `
|
||||
: ""}
|
||||
${dynamicElement(`ha-${this._platform}-form`, {
|
||||
hass: this.hass,
|
||||
item: this._item,
|
||||
new: true,
|
||||
})}
|
||||
</div>
|
||||
<mwc-button
|
||||
slot="primaryAction"
|
||||
@click=${this._createItem}
|
||||
.disabled=${this._submitting}
|
||||
>
|
||||
${this.hass!.localize("ui.panel.config.helpers.dialog.create")}
|
||||
</mwc-button>
|
||||
<mwc-button
|
||||
slot="secondaryAction"
|
||||
@click=${this._goBack}
|
||||
.disabled=${this._submitting}
|
||||
>
|
||||
${this.hass!.localize("ui.common.back")}
|
||||
</mwc-button>
|
||||
`
|
||||
: html`
|
||||
${Object.keys(HELPERS).map((platform: string) => {
|
||||
const isLoaded = isComponentLoaded(this.hass, platform);
|
||||
return html`
|
||||
<mwc-list-item
|
||||
.disabled=${!isLoaded}
|
||||
.platform=${platform}
|
||||
@click=${this._platformPicked}
|
||||
@keydown=${this._handleEnter}
|
||||
dialogInitialFocus
|
||||
graphic="icon"
|
||||
>
|
||||
<ha-svg-icon
|
||||
slot="graphic"
|
||||
.path=${domainIcon(platform)}
|
||||
></ha-svg-icon>
|
||||
<span class="item-text">
|
||||
${this.hass.localize(
|
||||
`ui.panel.config.helpers.types.${platform}`
|
||||
) || platform}
|
||||
</span>
|
||||
</mwc-list-item>
|
||||
${!isLoaded
|
||||
? html`
|
||||
<paper-tooltip animation-delay="0"
|
||||
>${this.hass.localize(
|
||||
"ui.dialogs.helper_settings.platform_not_loaded",
|
||||
"platform",
|
||||
platform
|
||||
)}</paper-tooltip
|
||||
>
|
||||
`
|
||||
: ""}
|
||||
`;
|
||||
})}
|
||||
<mwc-button slot="primaryAction" @click=${this.closeDialog}>
|
||||
${this.hass!.localize("ui.common.cancel")}
|
||||
</mwc-button>
|
||||
`}
|
||||
${content}
|
||||
</ha-dialog>
|
||||
`;
|
||||
}
|
||||
@@ -160,13 +198,13 @@ export class DialogHelperDetail extends LitElement {
|
||||
}
|
||||
|
||||
private async _createItem(): Promise<void> {
|
||||
if (!this._platform || !this._item) {
|
||||
if (!this._domain || !this._item) {
|
||||
return;
|
||||
}
|
||||
this._submitting = true;
|
||||
this._error = "";
|
||||
try {
|
||||
await HELPERS[this._platform](this.hass, this._item);
|
||||
await HELPERS[this._domain](this.hass, this._item);
|
||||
this.closeDialog();
|
||||
} catch (err: any) {
|
||||
this._error = err.message || "Unknown error";
|
||||
@@ -181,12 +219,22 @@ export class DialogHelperDetail extends LitElement {
|
||||
}
|
||||
ev.stopPropagation();
|
||||
ev.preventDefault();
|
||||
this._platformPicked(ev);
|
||||
this._domainPicked(ev);
|
||||
}
|
||||
|
||||
private _platformPicked(ev: Event): void {
|
||||
this._platform = (ev.currentTarget! as any).platform;
|
||||
this._focusForm();
|
||||
private _domainPicked(ev: Event): void {
|
||||
const domain = (ev.currentTarget! as any).domain;
|
||||
|
||||
if (domain in HELPERS) {
|
||||
this._domain = domain;
|
||||
this._focusForm();
|
||||
} else {
|
||||
showConfigFlowDialog(this, {
|
||||
startFlowHandler: domain,
|
||||
dialogClosedCallback: this._params!.dialogClosedCallback,
|
||||
});
|
||||
this.closeDialog();
|
||||
}
|
||||
}
|
||||
|
||||
private async _focusForm(): Promise<void> {
|
||||
@@ -195,7 +243,7 @@ export class DialogHelperDetail extends LitElement {
|
||||
}
|
||||
|
||||
private _goBack() {
|
||||
this._platform = undefined;
|
||||
this._domain = undefined;
|
||||
this._item = undefined;
|
||||
this._error = undefined;
|
||||
}
|
||||
|
@@ -1,28 +1,58 @@
|
||||
import { mdiPencilOff, mdiPlus } from "@mdi/js";
|
||||
import "@polymer/paper-tooltip/paper-tooltip";
|
||||
import { HassEntity } from "home-assistant-js-websocket";
|
||||
import { HassEntity, UnsubscribeFunc } from "home-assistant-js-websocket";
|
||||
import { html, LitElement, PropertyValues, TemplateResult } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import memoize from "memoize-one";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { computeStateDomain } from "../../../common/entity/compute_state_domain";
|
||||
import { domainIcon } from "../../../common/entity/domain_icon";
|
||||
import { LocalizeFunc } from "../../../common/translations/localize";
|
||||
import {
|
||||
DataTableColumnContainer,
|
||||
RowClickedEvent,
|
||||
} from "../../../components/data-table/ha-data-table";
|
||||
import "../../../components/ha-fab";
|
||||
import "../../../components/ha-icon-overflow-menu";
|
||||
import "../../../components/ha-icon";
|
||||
import "../../../components/ha-svg-icon";
|
||||
import { ConfigEntry, getConfigEntries } from "../../../data/config_entries";
|
||||
import {
|
||||
EntityRegistryEntry,
|
||||
subscribeEntityRegistry,
|
||||
} from "../../../data/entity_registry";
|
||||
import { domainToName } from "../../../data/integration";
|
||||
import "../../../layouts/hass-loading-screen";
|
||||
import "../../../layouts/hass-tabs-subpage-data-table";
|
||||
import { SubscribeMixin } from "../../../mixins/subscribe-mixin";
|
||||
import { HomeAssistant, Route } from "../../../types";
|
||||
import { showEntityEditorDialog } from "../entities/show-dialog-entity-editor";
|
||||
import { configSections } from "../ha-panel-config";
|
||||
import { HELPER_DOMAINS } from "./const";
|
||||
import { showHelperDetailDialog } from "./show-dialog-helper-detail";
|
||||
|
||||
// This groups items by a key but only returns last entry per key.
|
||||
const groupByOne = <T>(
|
||||
items: T[],
|
||||
keySelector: (item: T) => string
|
||||
): Record<string, T> => {
|
||||
const result: Record<string, T> = {};
|
||||
for (const item of items) {
|
||||
result[keySelector(item)] = item;
|
||||
}
|
||||
return result;
|
||||
};
|
||||
|
||||
const getConfigEntry = (
|
||||
entityEntries: Record<string, EntityRegistryEntry>,
|
||||
configEntries: Record<string, ConfigEntry>,
|
||||
entityId: string
|
||||
) => {
|
||||
const configEntryId = entityEntries![entityId]?.config_entry_id;
|
||||
return configEntryId ? configEntries![configEntryId] : undefined;
|
||||
};
|
||||
|
||||
@customElement("ha-config-helpers")
|
||||
export class HaConfigHelpers extends LitElement {
|
||||
export class HaConfigHelpers extends SubscribeMixin(LitElement) {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property() public isWide!: boolean;
|
||||
@@ -33,98 +63,122 @@ export class HaConfigHelpers extends LitElement {
|
||||
|
||||
@state() private _stateItems: HassEntity[] = [];
|
||||
|
||||
private _columns = memoize((narrow, _language): DataTableColumnContainer => {
|
||||
const columns: DataTableColumnContainer = {
|
||||
icon: {
|
||||
@state() private _entityEntries?: Record<string, EntityRegistryEntry>;
|
||||
|
||||
@state() private _configEntries?: Record<string, ConfigEntry>;
|
||||
|
||||
private _columns = memoizeOne(
|
||||
(narrow: boolean, localize: LocalizeFunc): DataTableColumnContainer => {
|
||||
const columns: DataTableColumnContainer = {
|
||||
icon: {
|
||||
title: "",
|
||||
label: localize("ui.panel.config.helpers.picker.headers.icon"),
|
||||
type: "icon",
|
||||
template: (icon, helper: any) =>
|
||||
icon
|
||||
? html` <ha-icon .icon=${icon}></ha-icon> `
|
||||
: html`<ha-svg-icon
|
||||
.path=${domainIcon(helper.type)}
|
||||
></ha-svg-icon>`,
|
||||
},
|
||||
name: {
|
||||
title: localize("ui.panel.config.helpers.picker.headers.name"),
|
||||
sortable: true,
|
||||
filterable: true,
|
||||
grows: true,
|
||||
direction: "asc",
|
||||
template: (name, item: any) =>
|
||||
html`
|
||||
${name}
|
||||
${narrow
|
||||
? html` <div class="secondary">${item.entity_id}</div> `
|
||||
: ""}
|
||||
`,
|
||||
},
|
||||
};
|
||||
if (!narrow) {
|
||||
columns.entity_id = {
|
||||
title: localize("ui.panel.config.helpers.picker.headers.entity_id"),
|
||||
sortable: true,
|
||||
filterable: true,
|
||||
width: "25%",
|
||||
};
|
||||
}
|
||||
columns.type = {
|
||||
title: localize("ui.panel.config.helpers.picker.headers.type"),
|
||||
sortable: true,
|
||||
width: "25%",
|
||||
filterable: true,
|
||||
template: (type, row) =>
|
||||
row.configEntry
|
||||
? domainToName(localize, type)
|
||||
: html`
|
||||
${localize(`ui.panel.config.helpers.types.${type}`) || type}
|
||||
`,
|
||||
};
|
||||
columns.editable = {
|
||||
title: "",
|
||||
label: this.hass.localize(
|
||||
"ui.panel.config.helpers.picker.headers.icon"
|
||||
"ui.panel.config.helpers.picker.headers.editable"
|
||||
),
|
||||
type: "icon",
|
||||
template: (icon, helper: any) =>
|
||||
icon
|
||||
? html` <ha-icon .icon=${icon}></ha-icon> `
|
||||
: html`<ha-svg-icon
|
||||
.path=${domainIcon(helper.type)}
|
||||
></ha-svg-icon>`,
|
||||
},
|
||||
name: {
|
||||
title: this.hass.localize(
|
||||
"ui.panel.config.helpers.picker.headers.name"
|
||||
),
|
||||
sortable: true,
|
||||
filterable: true,
|
||||
grows: true,
|
||||
direction: "asc",
|
||||
template: (name, item: any) =>
|
||||
html`
|
||||
${name}
|
||||
${narrow
|
||||
? html` <div class="secondary">${item.entity_id}</div> `
|
||||
: ""}
|
||||
`,
|
||||
},
|
||||
};
|
||||
if (!narrow) {
|
||||
columns.entity_id = {
|
||||
title: this.hass.localize(
|
||||
"ui.panel.config.helpers.picker.headers.entity_id"
|
||||
),
|
||||
sortable: true,
|
||||
filterable: true,
|
||||
width: "25%",
|
||||
};
|
||||
}
|
||||
columns.type = {
|
||||
title: this.hass.localize("ui.panel.config.helpers.picker.headers.type"),
|
||||
sortable: true,
|
||||
width: "25%",
|
||||
filterable: true,
|
||||
template: (type) =>
|
||||
html`
|
||||
${this.hass.localize(`ui.panel.config.helpers.types.${type}`) || type}
|
||||
template: (editable) => html`
|
||||
${!editable
|
||||
? html`
|
||||
<div
|
||||
tabindex="0"
|
||||
style="display:inline-block; position: relative;"
|
||||
>
|
||||
<ha-svg-icon .path=${mdiPencilOff}></ha-svg-icon>
|
||||
<paper-tooltip animation-delay="0" position="left">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.entities.picker.status.readonly"
|
||||
)}
|
||||
</paper-tooltip>
|
||||
</div>
|
||||
`
|
||||
: ""}
|
||||
`,
|
||||
};
|
||||
columns.editable = {
|
||||
title: "",
|
||||
label: this.hass.localize(
|
||||
"ui.panel.config.helpers.picker.headers.editable"
|
||||
),
|
||||
type: "icon",
|
||||
template: (editable) => html`
|
||||
${!editable
|
||||
? html`
|
||||
<div
|
||||
tabindex="0"
|
||||
style="display:inline-block; position: relative;"
|
||||
>
|
||||
<ha-svg-icon .path=${mdiPencilOff}></ha-svg-icon>
|
||||
<paper-tooltip animation-delay="0" position="left">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.entities.picker.status.readonly"
|
||||
)}
|
||||
</paper-tooltip>
|
||||
</div>
|
||||
`
|
||||
: ""}
|
||||
`,
|
||||
};
|
||||
return columns;
|
||||
});
|
||||
};
|
||||
return columns;
|
||||
}
|
||||
);
|
||||
|
||||
private _getItems = memoize((stateItems: HassEntity[]) =>
|
||||
stateItems.map((entityState) => ({
|
||||
id: entityState.entity_id,
|
||||
icon: entityState.attributes.icon,
|
||||
name: entityState.attributes.friendly_name || "",
|
||||
entity_id: entityState.entity_id,
|
||||
editable: entityState.attributes.editable,
|
||||
type: computeStateDomain(entityState),
|
||||
}))
|
||||
private _getItems = memoizeOne(
|
||||
(
|
||||
stateItems: HassEntity[],
|
||||
entityEntries: Record<string, EntityRegistryEntry>,
|
||||
configEntries: Record<string, ConfigEntry>
|
||||
) =>
|
||||
stateItems.map((entityState) => {
|
||||
const configEntry = getConfigEntry(
|
||||
entityEntries,
|
||||
configEntries,
|
||||
entityState.entity_id
|
||||
);
|
||||
|
||||
return {
|
||||
id: entityState.entity_id,
|
||||
icon: entityState.attributes.icon,
|
||||
name: entityState.attributes.friendly_name || "",
|
||||
entity_id: entityState.entity_id,
|
||||
editable:
|
||||
configEntry !== undefined || entityState.attributes.editable,
|
||||
type: configEntry
|
||||
? configEntry.domain
|
||||
: computeStateDomain(entityState),
|
||||
configEntry,
|
||||
};
|
||||
})
|
||||
);
|
||||
|
||||
protected render(): TemplateResult {
|
||||
if (!this.hass || this._stateItems === undefined) {
|
||||
if (
|
||||
!this.hass ||
|
||||
this._stateItems === undefined ||
|
||||
this._entityEntries === undefined ||
|
||||
this._configEntries === undefined
|
||||
) {
|
||||
return html` <hass-loading-screen></hass-loading-screen> `;
|
||||
}
|
||||
|
||||
@@ -135,8 +189,12 @@ export class HaConfigHelpers extends LitElement {
|
||||
back-path="/config"
|
||||
.route=${this.route}
|
||||
.tabs=${configSections.automations}
|
||||
.columns=${this._columns(this.narrow, this.hass.language)}
|
||||
.data=${this._getItems(this._stateItems)}
|
||||
.columns=${this._columns(this.narrow, this.hass.localize)}
|
||||
.data=${this._getItems(
|
||||
this._stateItems,
|
||||
this._entityEntries,
|
||||
this._configEntries
|
||||
)}
|
||||
@row-click=${this._openEditDialog}
|
||||
hasFab
|
||||
clickable
|
||||
@@ -160,32 +218,67 @@ export class HaConfigHelpers extends LitElement {
|
||||
|
||||
protected firstUpdated(changedProps: PropertyValues) {
|
||||
super.firstUpdated(changedProps);
|
||||
this._getStates();
|
||||
this._getConfigEntries();
|
||||
}
|
||||
|
||||
protected updated(changedProps: PropertyValues) {
|
||||
super.updated(changedProps);
|
||||
const oldHass = changedProps.get("hass") as HomeAssistant | undefined;
|
||||
if (oldHass && this._stateItems) {
|
||||
this._getStates(oldHass);
|
||||
protected willUpdate(changedProps: PropertyValues) {
|
||||
super.willUpdate(changedProps);
|
||||
|
||||
if (!this._entityEntries || !this._configEntries) {
|
||||
return;
|
||||
}
|
||||
|
||||
let changed =
|
||||
!this._stateItems ||
|
||||
changedProps.has("_entityEntries") ||
|
||||
changedProps.has("_configEntries");
|
||||
|
||||
if (!changed && changedProps.has("hass")) {
|
||||
const oldHass = changedProps.get("hass") as HomeAssistant | undefined;
|
||||
changed = !oldHass || oldHass.states !== this.hass.states;
|
||||
}
|
||||
if (!changed) {
|
||||
return;
|
||||
}
|
||||
|
||||
const extraEntities = new Set<string>();
|
||||
|
||||
for (const entityEntry of Object.values(this._entityEntries)) {
|
||||
if (
|
||||
entityEntry.config_entry_id &&
|
||||
entityEntry.config_entry_id in this._configEntries
|
||||
) {
|
||||
extraEntities.add(entityEntry.entity_id);
|
||||
}
|
||||
}
|
||||
|
||||
const newStates = Object.values(this.hass!.states).filter(
|
||||
(entity) =>
|
||||
extraEntities.has(entity.entity_id) ||
|
||||
HELPER_DOMAINS.includes(computeStateDomain(entity))
|
||||
);
|
||||
|
||||
if (
|
||||
this._stateItems.length !== newStates.length ||
|
||||
!this._stateItems.every((val, idx) => newStates[idx] === val)
|
||||
) {
|
||||
this._stateItems = newStates;
|
||||
}
|
||||
}
|
||||
|
||||
private _getStates(oldHass?: HomeAssistant) {
|
||||
let changed = false;
|
||||
const tempStates = Object.values(this.hass!.states).filter((entity) => {
|
||||
if (!HELPER_DOMAINS.includes(computeStateDomain(entity))) {
|
||||
return false;
|
||||
}
|
||||
if (oldHass?.states[entity.entity_id] !== entity) {
|
||||
changed = true;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
public hassSubscribe(): UnsubscribeFunc[] {
|
||||
return [
|
||||
subscribeEntityRegistry(this.hass.connection!, (entries) => {
|
||||
this._entityEntries = groupByOne(entries, (entry) => entry.entity_id);
|
||||
}),
|
||||
];
|
||||
}
|
||||
|
||||
if (changed || this._stateItems.length !== tempStates.length) {
|
||||
this._stateItems = tempStates;
|
||||
}
|
||||
private async _getConfigEntries() {
|
||||
this._configEntries = groupByOne(
|
||||
await getConfigEntries(this.hass, { type: "helper" }),
|
||||
(entry) => entry.entry_id
|
||||
);
|
||||
}
|
||||
|
||||
private async _openEditDialog(ev: CustomEvent): Promise<void> {
|
||||
@@ -196,6 +289,12 @@ export class HaConfigHelpers extends LitElement {
|
||||
}
|
||||
|
||||
private _createHelpler() {
|
||||
showHelperDetailDialog(this);
|
||||
showHelperDetailDialog(this, {
|
||||
dialogClosedCallback: (params) => {
|
||||
if (params.flowFinished) {
|
||||
this._getConfigEntries();
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@@ -1,11 +1,20 @@
|
||||
import { fireEvent } from "../../../common/dom/fire_event";
|
||||
import { DataEntryFlowDialogParams } from "../../../dialogs/config-flow/show-dialog-data-entry-flow";
|
||||
|
||||
export const loadHelperDetailDialog = () => import("./dialog-helper-detail");
|
||||
|
||||
export const showHelperDetailDialog = (element: HTMLElement) => {
|
||||
export interface ShowDialogHelperDetailParams {
|
||||
// Only used for config entries
|
||||
dialogClosedCallback: DataEntryFlowDialogParams["dialogClosedCallback"];
|
||||
}
|
||||
|
||||
export const showHelperDetailDialog = (
|
||||
element: HTMLElement,
|
||||
params: ShowDialogHelperDetailParams
|
||||
) => {
|
||||
fireEvent(element, "show-dialog", {
|
||||
dialogTag: "dialog-helper-detail",
|
||||
dialogImport: loadHelperDetailDialog,
|
||||
dialogParams: {},
|
||||
dialogParams: params,
|
||||
});
|
||||
};
|
||||
|
@@ -521,24 +521,26 @@ class HaConfigIntegrations extends SubscribeMixin(LitElement) {
|
||||
}
|
||||
|
||||
private _loadConfigEntries() {
|
||||
getConfigEntries(this.hass).then((configEntries) => {
|
||||
this._configEntries = configEntries
|
||||
.map(
|
||||
(entry: ConfigEntry): ConfigEntryExtended => ({
|
||||
...entry,
|
||||
localized_domain_name: domainToName(
|
||||
this.hass.localize,
|
||||
entry.domain
|
||||
),
|
||||
})
|
||||
)
|
||||
.sort((conf1, conf2) =>
|
||||
caseInsensitiveStringCompare(
|
||||
conf1.localized_domain_name + conf1.title,
|
||||
conf2.localized_domain_name + conf2.title
|
||||
getConfigEntries(this.hass, { type: "integration" }).then(
|
||||
(configEntries) => {
|
||||
this._configEntries = configEntries
|
||||
.map(
|
||||
(entry: ConfigEntry): ConfigEntryExtended => ({
|
||||
...entry,
|
||||
localized_domain_name: domainToName(
|
||||
this.hass.localize,
|
||||
entry.domain
|
||||
),
|
||||
})
|
||||
)
|
||||
);
|
||||
});
|
||||
.sort((conf1, conf2) =>
|
||||
caseInsensitiveStringCompare(
|
||||
conf1.localized_domain_name + conf1.title,
|
||||
conf2.localized_domain_name + conf2.title
|
||||
)
|
||||
);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
private async _scanUSBDevices() {
|
||||
@@ -656,7 +658,7 @@ class HaConfigIntegrations extends SubscribeMixin(LitElement) {
|
||||
if (!domain) {
|
||||
return;
|
||||
}
|
||||
const handlers = await getConfigFlowHandlers(this.hass);
|
||||
const handlers = await getConfigFlowHandlers(this.hass, "integration");
|
||||
|
||||
if (!handlers.includes(domain)) {
|
||||
showAlertDialog(this, {
|
||||
|
@@ -55,8 +55,6 @@ const integrationsWithPanel = {
|
||||
hassio: "/hassio/dashboard",
|
||||
mqtt: "/config/mqtt",
|
||||
zha: "/config/zha/dashboard",
|
||||
ozw: "/config/ozw/dashboard",
|
||||
zwave: "/config/zwave",
|
||||
zwave_js: "/config/zwave_js/dashboard",
|
||||
};
|
||||
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user