Compare commits

...

90 Commits

Author SHA1 Message Date
Zack
14de223ffc Update for other editors 2022-03-22 10:21:26 -05:00
Zack
1290336cc5 update style 2022-03-18 13:18:46 -05:00
Zack
abaf5dd0f2 Stack Action Inputs in the Button Editor 2022-03-18 13:15:55 -05:00
Zack Barett
9c1d1cb6f6 Merge pull request #12069 from matthiasdebaat/patch-2 2022-03-18 07:32:14 -05:00
Matthias de Baat
470225abde Update logo.markdown 2022-03-18 11:26:19 +01:00
Matthias de Baat
ee230b86c1 Update gallery/src/pages/brand/logo.markdown
Co-authored-by: Zack Barett <zackbarett@hey.com>
2022-03-18 11:25:41 +01:00
Matthias de Baat
f927fc64a9 Update gallery/src/pages/brand/logo.markdown
Co-authored-by: Zack Barett <zackbarett@hey.com>
2022-03-18 11:25:34 +01:00
Zack Barett
03677c33f7 Bumped version to 20220317.0 (#12074) 2022-03-17 15:27:27 -07:00
Zack Barett
bc36a206da Merge pull request #11984 from Mariusthvdb/patch-4 2022-03-17 17:15:41 -05:00
Zack Barett
af06ab1e2d Merge pull request #12073 from home-assistant/bump-haws-701 2022-03-17 17:11:53 -05:00
Zack Barett
3e2135a485 Add radio Form Logic to Select Selector (#12063) 2022-03-17 15:04:59 -07:00
Zack Barett
2e7f8fb46f Add Date Time Selector (#12070) 2022-03-17 15:01:08 -07:00
Paulus Schoutsen
102568c4bd Update lock 2022-03-17 14:58:45 -07:00
Paulus Schoutsen
4fcdae842e Bump HAWS to 7.0.1 2022-03-17 14:50:16 -07:00
Paulus Schoutsen
ea19740f5a Ignore diagnostics not found exceptions (#12066) 2022-03-17 13:44:06 -07:00
Matthias de Baat
3e0942b631 Add files via upload 2022-03-17 14:19:28 +01:00
Matthias de Baat
0261cea796 Create new Logo page 2022-03-17 14:14:13 +01:00
Paulus Schoutsen
5247b2813f Bump HAWS to 7.0.0 (#12067) 2022-03-16 19:54:25 -10:00
Zack Barett
8a5090684e Merge pull request #12064 from home-assistant/20220316.0 2022-03-16 17:59:26 -05:00
Zack Barett
1784ba5e68 Merge pull request #12061 from home-assistant/Add-Date-Selector 2022-03-16 17:52:40 -05:00
Zack Barett
4fbe9a7b10 Merge pull request #12049 from home-assistant/hide-hidden-entities 2022-03-16 17:49:20 -05:00
Zack
1ca9c7838a Bumped version to 20220316.0 2022-03-16 17:46:32 -05:00
Zack
4fc2c3ef05 Remvoe redunency 2022-03-16 17:42:11 -05:00
Zack Barett
73ff8e28a8 Add Devices Picker (#12056) 2022-03-16 15:40:34 -07:00
Zack
dde1c5e03c Entity Status 2022-03-16 17:38:38 -05:00
Zack
01eed22592 clean up 2022-03-16 17:34:09 -05:00
Zack
94ebb63589 add to basic editor and update advanced style 2022-03-16 17:25:08 -05:00
Zack
29119db5ce Add translation 2022-03-16 17:05:52 -05:00
Paulus Schoutsen
9908162ac2 Add support for menu data entry flow option (#12055) 2022-03-16 14:14:38 -07:00
Paulus Schoutsen
1e929ae78a Revamp URL form (#12060) 2022-03-16 14:14:25 -07:00
Paulus Schoutsen
ab5df0fe6e test condition (#11925) 2022-03-16 14:13:13 -07:00
Paulus Schoutsen
d5010dda9e Add ha-form context (#12062) 2022-03-16 14:12:10 -07:00
Zack
4ac097f32b Add Date Selector 2022-03-16 14:20:45 -05:00
Zack Barett
5d3d15072f Merge pull request #12054 from home-assistant/Add-image-to-design-docs 2022-03-16 13:24:03 -05:00
Zack Barett
5c53bc4225 Add Color RGB Selector (#12039) 2022-03-15 15:34:02 -07:00
Zack
d5a307f8f4 Add icons and buttons 2022-03-15 15:00:35 -05:00
Zack
a27dd1e7f1 Add Description of chosen 2022-03-15 14:47:15 -05:00
Zack
c86ed1fb3e remove 1 2022-03-15 14:33:11 -05:00
Zack
7fa7a48072 Disabled by 2022-03-15 14:32:49 -05:00
Zack
4e0fc8ee08 Update Translations 2022-03-15 14:26:21 -05:00
Zack
5f6490e54e Add HA to public folder and show in markdown 2022-03-15 14:17:24 -05:00
Matthias de Baat
db78b046a2 Add Brand folder and Our story page (#11978)
Co-authored-by: Zack Barett <zackbarett@hey.com>
2022-03-15 09:01:42 -05:00
Zack
c37fe1e7ff add to demo 2022-03-14 20:39:03 -05:00
Zack
f1ec479d41 Reviews 2022-03-14 20:37:37 -05:00
Zack
e01cb3ca82 Utilize Hide Hidden Entities 2022-03-14 14:22:45 -05:00
Zack Barett
b8d3c68a7a Add Color Temp Selector (#12041) 2022-03-14 11:07:15 -07:00
Matthias de Baat
641003bb2a Rename Lovelace Dashboard to just Dashboard (#12044)
Co-authored-by: Zack Barett <zackbarett@hey.com>
2022-03-14 11:05:44 -07:00
Zack Barett
3358fc2b18 Add all cover device classes (#12042) 2022-03-14 10:45:12 -07:00
Zack Barett
dcf50e055b Fix @changed where using ev.detail (#12043) 2022-03-14 16:11:46 +00:00
Zack Barett
1fa04baa16 Fix: Changing Blueprint Automation Name (#12036) 2022-03-14 08:33:49 -07:00
jpearl
84ffa2369a Add shade to device class overrides (#11874) 2022-03-14 10:19:43 -05:00
Paulus Schoutsen
cc27ddb362 Bumped version to 20220312.0 2022-03-12 13:47:05 -08:00
Paulus Schoutsen
c4dc6bfb0d Bumped version to 20220301.2 2022-03-12 13:45:34 -08:00
Paulus Schoutsen
4fbcc30a37 Merge remote-tracking branch 'origin/master' into dev 2022-03-12 13:43:33 -08:00
Paulus Schoutsen
4916527e5f Bumped version to 20220301.1 2022-03-12 13:42:42 -08:00
Paulus Schoutsen
fad8a27232 HAWS 6.1 (#12016) 2022-03-12 09:56:25 -10:00
Zack Barett
a993d3a753 Script ID update with Alias (#12008) 2022-03-11 21:25:09 -08:00
Zack Barett
5dfe17a43a Fix: Allow for deleting Input_select options (#12007) 2022-03-11 17:07:56 -08:00
Zack Barett
9b6c935ffb Fix For Selecting Device Class (#12010) 2022-03-11 09:39:04 -08:00
Zack Barett
f4e28da0a3 Fix Dashboard Editing (#12011) 2022-03-11 09:38:18 -08:00
Zack Barett
294a69d7e4 Fix changing cost number in energy settings (#12009) 2022-03-11 09:37:22 -08:00
Charles Garwood
f89b8cffcf Fix zwave_js set config dropdown default value (#11974)
Co-authored-by: Paulus Schoutsen <paulus@home-assistant.io>
2022-03-10 09:21:04 -06:00
Raman Gupta
99fd3a1b6f Fix zwave_js 'add/remove device' disabled bug (#12000)
* Fix zwave_js 'add/remove device' disabled bug

* revert extra change
2022-03-10 08:45:12 -05:00
Emil Stjerneman
246e426182 #11971 Change order of alarm panel buttons (#11998) 2022-03-09 19:54:40 -06:00
Paulus Schoutsen
9f1e9b43fe Use entities-picker in entity selector (#11990) 2022-03-08 21:33:23 -06:00
Marius
8301ae262c change icon to mimic physical device
and follow comments
2022-03-08 21:40:42 +01:00
Zack Barett
d968fe41ee Update Style of Design Page (#11982) 2022-03-08 10:19:18 -08:00
Bram Kragten
db830e9014 Fix theme setting (#11977) 2022-03-08 10:13:08 -08:00
Paulus Schoutsen
fc6b594a27 Allow selecting multiple entities (#11986) 2022-03-08 10:09:45 -08:00
Marius
86dbf99ebe replace default switch icon
to  make it stand out against a power entity which uses the same mdiFlash https://github.com/home-assistant/core/issues/67620#issuecomment-1061949527

suggest the Outline version, so create a subtle difference with the on/off icons.
2022-03-08 17:41:32 +01:00
Joakim Sørensen
68e7ce1883 Add systemd_resolved unsupported reason (#11971) 2022-03-07 17:42:49 +01:00
Bram Kragten
4cdff3faea Add location selector, convert zone editor (#11902) 2022-03-07 08:47:20 -06:00
Bram Kragten
0dac10aa23 Convert file upload to mdc (#11906) 2022-03-07 08:42:40 -06:00
Bram Kragten
4b8b14a69d A11y expansion panel (#11967) 2022-03-07 08:40:19 -06:00
Zack Barett
9d28df31bd Fix for Statistics Editor (#11942)
Co-authored-by: Bram Kragten <mail@bramkragten.nl>
2022-03-07 15:29:00 +01:00
Bram Kragten
8258641443 Make min width of select configurable (#11965) 2022-03-07 14:55:44 +01:00
Bram Kragten
dfcb0f6ba0 Fix humidifier more info mode dropdown (#11964) 2022-03-07 07:25:38 -06:00
Philip Allgaier
2e10eb04b6 Correct media upload error + add file name (#11949)
Co-authored-by: Bram Kragten <mail@bramkragten.nl>
2022-03-07 12:08:54 +00:00
Raman Gupta
b4b52d3872 Remove some additional old zwave code (#11941) 2022-03-07 12:49:51 +01:00
Bram Kragten
3873203721 Convert inputs (#11907)
* Convert inputs

* Update dialog-thingtalk.ts

* imports
2022-03-07 12:45:39 +01:00
Paulus Schoutsen
ccb91e0b49 Allow marking YAML editor as read only (#11960) 2022-03-07 12:39:16 +01:00
Paulus Schoutsen
bd20c15a55 Show triggered vars on click (#11924) 2022-03-04 23:24:31 -08:00
Paulus Schoutsen
0936fd9ae4 Guard setting up config flow for an unsupported domain (#11937) 2022-03-04 14:31:11 -08:00
Bram Kragten
adefc7a4e2 Convert lovelace config dialogs to ha-form (#11910) 2022-03-04 23:15:10 +01:00
Bram Kragten
8f8017ecff Remove zwave and ozw panels (#11911)
Remove zwave and ozw panels
2022-03-04 14:10:44 -08:00
Robin Wittebol
604b79696e Always show tab labels (#11919) 2022-03-03 19:46:14 +01:00
Robin Wittebol
8c445f6409 Fix datepicker triangle (#11920) 2022-03-03 19:45:03 +01:00
Bram Kragten
797c871137 Convert objects to string in config flow error (#11908) 2022-03-03 13:55:40 +01:00
Steve Repsher
24829bd903 Supervisor mobile click accessibility (#11915) 2022-03-03 10:15:22 +01:00
Bram Kragten
add92a559d Fix quickbar overlaying, fix click handling (#11900) 2022-03-02 17:50:01 +01:00
112 changed files with 2062 additions and 6335 deletions

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 67 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

View File

@@ -36,6 +36,10 @@ module.exports = [
category: "misc",
header: "Miscelaneous",
},
{
category: "brand",
header: "Brand",
},
{
category: "user-test",
header: "User Tests",

View File

@@ -78,6 +78,9 @@ class DemoCards extends LitElement {
ha-formfield {
margin-right: 16px;
}
#container {
background-color: var(--primary-background-color);
}
`;
}

View File

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

View File

@@ -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";
@@ -53,10 +54,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 +92,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 +193,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 +233,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;
}
`,
];

View File

@@ -0,0 +1,34 @@
---
title: "Logo"
---
![Using our logo](/images/using-our-logo.png)
# 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)
![Logo](/images/logo.png)
## 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.
![Logo variants](/images/logo-variants.png)
## 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.
![Clearspace](/images/clearspace.png)

View 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. Its 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 dont 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 shouldnt 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 shouldnt 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.

View File

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

View File

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

View File

@@ -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 } } },
},
},
];

View File

@@ -2,6 +2,8 @@
title: Editing design.home-assistant.io
---
![Home Assistant Logo](/images/logo-with-text.png)
# 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>
`;
}

View File

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

View File

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

View File

@@ -121,7 +121,8 @@ export class HassioMain extends SupervisorBaseElement {
this.parentElement,
this.hass.themes,
themeName,
themeSettings
themeSettings,
true
);
}
}

View File

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

View File

@@ -1,6 +1,6 @@
[metadata]
name = home-assistant-frontend
version = 20220301.1
version = 20220317.0
author = The Home Assistant Authors
author_email = hello@home-assistant.io
license = Apache-2.0

View File

@@ -101,13 +101,19 @@ class HaAuthorize extends litLocalizeLiteMixin(LitElement) {
this._fetchAuthProviders();
if (matchMedia("(prefers-color-scheme: dark)").matches) {
applyThemesOnElement(document.documentElement, {
default_theme: "default",
default_dark_theme: null,
themes: {},
darkMode: true,
theme: "default",
});
applyThemesOnElement(
document.documentElement,
{
default_theme: "default",
default_dark_theme: null,
themes: {},
darkMode: true,
theme: "default",
},
undefined,
undefined,
true
);
}
if (!this.redirectUri) {

View File

@@ -31,11 +31,12 @@ export const applyThemesOnElement = (
element,
themes: HomeAssistant["themes"],
selectedTheme?: string,
themeSettings?: Partial<HomeAssistant["selectedTheme"]>
themeSettings?: Partial<HomeAssistant["selectedTheme"]>,
main?: boolean
) => {
// If there is no explicitly desired theme provided, we automatically
// If there is no explicitly desired theme provided, and the element is the main element we automatically
// use the active one from `themes`.
const themeToApply = selectedTheme || themes.theme;
const themeToApply = selectedTheme || (main ? themes.theme : undefined);
// If there is no explicitly desired dark mode provided, we automatically
// use the active one from `themes`.
@@ -47,7 +48,7 @@ export const applyThemesOnElement = (
let cacheKey = themeToApply;
let themeRules: Partial<ThemeVars> = {};
if (darkMode) {
if (themeToApply && darkMode) {
cacheKey = `${cacheKey}__dark`;
themeRules = { ...darkStyles };
}

View File

@@ -9,11 +9,10 @@ import {
mdiCast,
mdiCastConnected,
mdiClock,
mdiEmoticonDead,
mdiFlash,
mdiGestureTapButton,
mdiLanConnect,
mdiLanDisconnect,
mdiLightSwitch,
mdiLock,
mdiLockAlert,
mdiLockClock,
@@ -22,14 +21,11 @@ import {
mdiPowerPlug,
mdiPowerPlugOff,
mdiRestart,
mdiSleep,
mdiTimerSand,
mdiToggleSwitch,
mdiToggleSwitchOff,
mdiCheckCircleOutline,
mdiCloseCircleOutline,
mdiWeatherNight,
mdiZWave,
} from "@mdi/js";
import { HassEntity } from "home-assistant-js-websocket";
/**
@@ -112,19 +108,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": {

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

View File

@@ -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"] {

View File

@@ -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;
}
`;
}
}

View File

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

View File

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

View File

@@ -51,6 +51,8 @@ class HaEntitiesPickerLight extends LitElement {
@property({ attribute: "pick-entity-label" }) public pickEntityLabel?: string;
@property() public entityFilter?: HaEntityPickerEntityFilterFunc;
protected render(): TemplateResult {
if (!this.hass) {
return html``;
@@ -94,7 +96,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 || [];

View File

@@ -15,18 +15,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;
@@ -96,7 +99,7 @@ export class HaEntityPicker extends LitElement {
private _initedStates = false;
private _states: HassEntity[] = [];
private _states: HassEntityWithCachedName[] = [];
private _getStates = memoizeOne(
(
@@ -107,8 +110,8 @@ export class HaEntityPicker extends LitElement {
entityFilter: this["entityFilter"],
includeDeviceClasses: this["includeDeviceClasses"],
includeUnitOfMeasurement: this["includeUnitOfMeasurement"]
) => {
let states: HassEntity[] = [];
): HassEntityWithCachedName[] => {
let states: HassEntityWithCachedName[] = [];
if (!hass) {
return [];
@@ -122,7 +125,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"
),
@@ -190,7 +193,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"
),

View File

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

View File

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

View File

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

View File

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

View 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;
}
}

View 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;
}
}

View 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;
}
}

View File

@@ -0,0 +1,73 @@
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] || "00: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;
}
}

View File

@@ -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 {
@@ -30,20 +31,36 @@ export class HaDeviceSelector extends LitElement {
}
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 => {

View File

@@ -7,6 +7,7 @@ import { EntitySelector } from "../../data/selector";
import { SubscribeMixin } from "../../mixins/subscribe-mixin";
import { HomeAssistant } from "../../types";
import "../entity/ha-entity-picker";
import "../entity/ha-entities-picker";
@customElement("ha-selector-entity")
export class HaEntitySelector extends SubscribeMixin(LitElement) {
@@ -23,14 +24,25 @@ 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}
.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}
></ha-entities-picker>
`;
}
public hassSubscribe(): UnsubscribeFunc[] {

View File

@@ -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;
}
`;
}
}

View File

@@ -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",
})}
`;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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,
});

View File

@@ -2,6 +2,8 @@ export type Selector =
| AddonSelector
| AttributeSelector
| EntitySelector
| DateSelector
| DateTimeSelector
| DeviceSelector
| DurationSelector
| AreaSelector
@@ -16,22 +18,40 @@ export type Selector =
| IconSelector
| MediaSelector
| ThemeSelector
| LocationSelector;
| LocationSelector
| ColorTempSelector
| ColorRGBSelector;
export interface EntitySelector {
entity: {
integration?: string;
domain?: string | string[];
device_class?: string;
multiple?: boolean;
};
}
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 +61,7 @@ export interface DeviceSelector {
domain?: EntitySelector["entity"]["domain"];
device_class?: EntitySelector["entity"]["device_class"];
};
multiple?: boolean;
};
}
@@ -96,6 +117,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: {};

View File

@@ -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}`);

View File

@@ -46,6 +46,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;
@@ -292,6 +293,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 +430,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 {

View File

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

View File

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

View File

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

View 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;
}
}

View File

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

View File

@@ -300,7 +300,9 @@ export const provideHass = (
applyThemesOnElement(
document.documentElement,
themes,
selectedTheme!.theme
selectedTheme!.theme,
undefined,
true
);
},

View File

@@ -101,7 +101,8 @@ class SupervisorErrorScreen extends LitElement {
this.parentElement,
this.hass.themes,
themeName,
themeSettings
themeSettings,
true
);
}

View File

@@ -133,13 +133,19 @@ class HaOnboarding extends litLocalizeLiteMixin(HassElement) {
import("./particles");
}
if (matchMedia("(prefers-color-scheme: dark)").matches) {
applyThemesOnElement(document.documentElement, {
default_theme: "default",
default_dark_theme: null,
themes: {},
darkMode: true,
theme: "default",
});
applyThemesOnElement(
document.documentElement,
{
default_theme: "default",
default_dark_theme: null,
themes: {},
darkMode: true,
theme: "default",
},
undefined,
undefined,
true
);
}
}

View File

@@ -278,7 +278,7 @@ export class HaBlueprintAutomationEditor extends LitElement {
if (!name) {
return;
}
const newVal = ev.detail.value;
const newVal = target.value;
if ((this.config![name] || "") === newVal) {
return;
}

View File

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

View File

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

View File

@@ -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);
}
`;
}
}

View File

@@ -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 = {};

View File

@@ -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;
}
`,
];
}
}

View File

@@ -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;
}
`,
];
}
}

View File

@@ -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;
@@ -173,3 +173,9 @@ export class HaDeviceInfoZWaveJS extends LitElement {
];
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-device-info-zwave_js": HaDeviceInfoZWaveJS;
}
}

View File

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

View File

@@ -240,7 +240,7 @@ export class DialogEnergyGridFlowSettings
this._costStat = null;
this._source = {
...this._source!,
number_energy_price: Number(ev.detail.value),
number_energy_price: Number((ev.target as any).value),
entity_energy_price: null,
};
}

View File

@@ -1,3 +1,5 @@
import "../../../components/ha-expansion-panel";
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";
@@ -5,7 +7,7 @@ import { computeDomain } from "../../../common/entity/compute_domain";
import "../../../components/ha-area-picker";
import "../../../components/ha-switch";
import "../../../components/ha-textfield";
import type { HaSwitch } from "../../../components/ha-switch";
import "../../../components/ha-radio";
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,95 @@ 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._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"
)}
<br />${this.hass.localize(
"ui.dialogs.entity_registry.editor.note"
>
<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"
)}
</div>
>
<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>
</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 +249,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 +284,12 @@ export class HaEntityRegistryBasicEditor extends SubscribeMixin(LitElement) {
display: block;
margin-bottom: 8px;
}
ha-expansion-panel {
margin-top: 8px;
}
.label {
margin-top: 16px;
}
`;
}
}

View File

@@ -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";
@@ -11,6 +13,7 @@ import {
} from "lit";
import { customElement, property, state } from "lit/decorators";
import { fireEvent } from "../../../common/dom/fire_event";
import { stopPropagation } from "../../../common/dom/stop_propagation";
import { computeDomain } from "../../../common/entity/compute_domain";
import { domainIcon } from "../../../common/entity/domain_icon";
import "../../../components/ha-alert";
@@ -19,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,
@@ -42,7 +44,18 @@ import type { HomeAssistant } from "../../../types";
import { showDeviceRegistryDetailDialog } from "../devices/device-registry-detail/show-dialog-device-registry-detail";
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"],
};
@@ -64,6 +77,8 @@ 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;
@@ -100,6 +115,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]
@@ -166,7 +182,10 @@ export class EntityRegistrySettings extends SubscribeMixin(LitElement) {
"ui.dialogs.entity_registry.editor.device_class"
)}
.value=${this._deviceClass}
naturalMenuWidth
fixedMenuPosition
@selected=${this._deviceClassChanged}
@closed=${stopPropagation}
>
${OVERRIDE_DEVICE_CLASSES[domain].map(
(deviceClass: string) => html`
@@ -196,75 +215,126 @@ 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(
<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
@@ -310,9 +380,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() {
@@ -339,6 +421,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!,
@@ -390,10 +478,6 @@ export class EntityRegistrySettings extends SubscribeMixin(LitElement) {
}
}
private _disabledByChanged(ev: Event): void {
this._disabledBy = (ev.target as HaSwitch).checked ? null : "user";
}
static get styles(): CSSResultGroup {
return [
haStyle,
@@ -422,6 +506,7 @@ export class EntityRegistrySettings extends SubscribeMixin(LitElement) {
}
ha-select {
width: 100%;
margin: 8px 0;
}
ha-switch {
margin-right: 16px;
@@ -430,14 +515,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;
}
`,
];

View File

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

View File

@@ -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: () =>

View File

@@ -85,7 +85,7 @@ class HaInputSelectForm extends LitElement {
${this._options.length
? this._options.map(
(option, index) => html`
<mwc-list-item class="option" hasMeta noninteractive>
<mwc-list-item class="option" hasMeta>
${option}
<ha-icon-button
slot="meta"

View File

@@ -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",
};

View File

@@ -1,269 +0,0 @@
import {
css,
CSSResultGroup,
html,
LitElement,
PropertyValues,
TemplateResult,
} from "lit";
import { customElement, property, state } from "lit/decorators";
import "../../../../../components/ha-circular-progress";
import "../../../../../components/ha-code-editor";
import { createCloseHeading } from "../../../../../components/ha-dialog";
import {
fetchOZWNodeMetadata,
nodeQueryStages,
OZWDevice,
OZWDeviceMetaData,
} from "../../../../../data/ozw";
import { haStyleDialog } from "../../../../../resources/styles";
import { HomeAssistant } from "../../../../../types";
import { OZWRefreshNodeDialogParams } from "./show-dialog-ozw-refresh-node";
@customElement("dialog-ozw-refresh-node")
class DialogOZWRefreshNode extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@state() private _node_id?: number;
@state() private _ozw_instance = 1;
@state() private _nodeMetaData?: OZWDeviceMetaData;
@state() private _node?: OZWDevice;
@state() private _active = false;
@state() private _complete = false;
private _refreshDevicesTimeoutHandle?: number;
private _subscribed?: Promise<() => Promise<void>>;
public disconnectedCallback(): void {
super.disconnectedCallback();
this._unsubscribe();
}
protected updated(changedProperties: PropertyValues): void {
super.update(changedProperties);
if (changedProperties.has("node_id")) {
this._fetchData();
}
}
private async _fetchData() {
if (!this._node_id) {
return;
}
const metaDataResponse = await fetchOZWNodeMetadata(
this.hass,
this._ozw_instance,
this._node_id
);
this._nodeMetaData = metaDataResponse.metadata;
}
public async showDialog(params: OZWRefreshNodeDialogParams): Promise<void> {
this._node_id = params.node_id;
this._ozw_instance = params.ozw_instance;
this._fetchData();
}
protected render(): TemplateResult {
if (!this._node_id) {
return html``;
}
return html`
<ha-dialog
open
@closed=${this._close}
.heading=${createCloseHeading(
this.hass,
this.hass.localize("ui.panel.config.ozw.refresh_node.title")
)}
>
${this._complete
? html`
<p>
${this.hass.localize(
"ui.panel.config.ozw.refresh_node.complete"
)}
</p>
<mwc-button slot="primaryAction" @click=${this._close}>
${this.hass.localize("ui.common.close")}
</mwc-button>
`
: html`
${this._active
? html`
<div class="flex-container">
<ha-circular-progress active></ha-circular-progress>
<div>
<p>
<b>
${this.hass.localize(
"ui.panel.config.ozw.refresh_node.refreshing_description"
)}
</b>
</p>
${this._node
? html`
<p>
${this.hass.localize(
"ui.panel.config.ozw.refresh_node.node_status"
)}:
${this._node.node_query_stage}
(${this.hass.localize(
"ui.panel.config.ozw.refresh_node.step"
)}
${nodeQueryStages.indexOf(
this._node.node_query_stage
) + 1}/17)
</p>
<p>
<em>
${this.hass.localize(
"ui.panel.config.ozw.node_query_stages." +
this._node.node_query_stage.toLowerCase()
)}</em
>
</p>
`
: ``}
</div>
</div>
`
: html`
${this.hass.localize(
"ui.panel.config.ozw.refresh_node.description"
)}
<p>
${this.hass.localize(
"ui.panel.config.ozw.refresh_node.battery_note"
)}
</p>
`}
${this._nodeMetaData?.WakeupHelp !== ""
? html`
<b>
${this.hass.localize(
"ui.panel.config.ozw.refresh_node.wakeup_header"
)}
${this._nodeMetaData!.Name}
</b>
<blockquote>
${this._nodeMetaData!.WakeupHelp}
<br />
<em>
${this.hass.localize(
"ui.panel.config.ozw.refresh_node.wakeup_instructions_source"
)}
</em>
</blockquote>
`
: ""}
${!this._active
? html`
<mwc-button
slot="primaryAction"
@click=${this._startRefresh}
>
${this.hass.localize(
"ui.panel.config.ozw.refresh_node.start_refresh_button"
)}
</mwc-button>
`
: html``}
`}
</ha-dialog>
`;
}
private _startRefresh(): void {
this._subscribe();
}
private _handleMessage(message: any): void {
if (message.type === "node_updated") {
this._node = message;
if (message.node_query_stage === "Complete") {
this._unsubscribe();
this._complete = true;
}
}
}
private _unsubscribe(): void {
this._active = false;
if (this._refreshDevicesTimeoutHandle) {
clearTimeout(this._refreshDevicesTimeoutHandle);
}
if (this._subscribed) {
this._subscribed.then((unsub) => unsub());
this._subscribed = undefined;
}
}
private _subscribe(): void {
if (!this.hass) {
return;
}
this._active = true;
this._subscribed = this.hass.connection.subscribeMessage(
(message) => this._handleMessage(message),
{
type: "ozw/refresh_node_info",
node_id: this._node_id,
ozw_instance: this._ozw_instance,
}
);
this._refreshDevicesTimeoutHandle = window.setTimeout(
() => this._unsubscribe(),
120000
);
}
private _close(): void {
this._complete = false;
this._node_id = undefined;
this._node = undefined;
}
static get styles(): CSSResultGroup {
return [
haStyleDialog,
css`
blockquote {
display: block;
background-color: #ddd;
padding: 8px;
margin: 8px 0;
font-size: 0.9em;
}
blockquote em {
font-size: 0.9em;
margin-top: 6px;
}
.flex-container {
display: flex;
align-items: center;
}
.flex-container ha-circular-progress {
margin-right: 20px;
}
`,
];
}
}
declare global {
interface HTMLElementTagNameMap {
"dialog-ozw-refresh-node": DialogOZWRefreshNode;
}
}

View File

@@ -1,260 +0,0 @@
import "@material/mwc-button/mwc-button";
import { mdiCheckCircle, mdiCircle, mdiCloseCircle, mdiZWave } from "@mdi/js";
import "@polymer/paper-item/paper-icon-item";
import "@polymer/paper-item/paper-item-body";
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { customElement, property, state } from "lit/decorators";
import { navigate } from "../../../../../common/navigate";
import "../../../../../components/ha-card";
import "../../../../../components/ha-icon-next";
import {
fetchOZWInstances,
networkOfflineStatuses,
networkOnlineStatuses,
networkStartingStatuses,
OZWInstance,
} from "../../../../../data/ozw";
import "../../../../../layouts/hass-error-screen";
import "../../../../../layouts/hass-loading-screen";
import "../../../../../layouts/hass-tabs-subpage";
import type { PageNavigation } from "../../../../../layouts/hass-tabs-subpage";
import { haStyle } from "../../../../../resources/styles";
import type { HomeAssistant, Route } from "../../../../../types";
import "../../../ha-config-section";
import "../../../../../components/ha-alert";
export const ozwTabs: PageNavigation[] = [];
@customElement("ozw-config-dashboard")
class OZWConfigDashboard extends LitElement {
@property({ type: Object }) public hass!: HomeAssistant;
@property({ type: Object }) public route!: Route;
@property({ type: Boolean }) public narrow!: boolean;
@property({ type: Boolean }) public isWide!: boolean;
@property() public configEntryId?: string;
@state() private _instances?: OZWInstance[];
protected firstUpdated() {
this._fetchData();
}
protected render(): TemplateResult {
if (!this._instances) {
return html`<hass-loading-screen></hass-loading-screen>`;
}
if (this._instances.length === 0) {
return html`<hass-error-screen
.hass=${this.hass}
.error=${this.hass.localize(
"ui.panel.config.ozw.select_instance.none_found"
)}
></hass-error-screen>`;
}
return html`
<hass-tabs-subpage
.hass=${this.hass}
.narrow=${this.narrow}
.route=${this.route}
.tabs=${ozwTabs}
back-path="/config/integrations"
>
<ha-alert
alert-type="warning"
title="This integration will stop working soon"
>
The OpenZWave integration is deprecated and will no longer receive any
updates. The technical dependencies will render this integration
unusable in the near future. We strongly advise you to migrate to the
new
<a
href="https://www.home-assistant.io/integrations/zwave_js"
target="_blank"
rel="noreferrer"
>Z-Wave JS integration</a
>.
<a
slot="action"
href="https://alerts.home-assistant.io/#ozw.markdown"
target="_blank"
rel="noreferrer"
>
<mwc-button>learn more</mwc-button>
</a>
</ha-alert>
<ha-config-section .narrow=${this.narrow} .isWide=${this.isWide}>
<div slot="header">
${this.hass.localize("ui.panel.config.ozw.select_instance.header")}
</div>
<div slot="introduction">
${this.hass.localize(
"ui.panel.config.ozw.select_instance.introduction"
)}
</div>
${this._instances.length > 0
? html`
${this._instances.map((instance) => {
let status = "unknown";
let icon = mdiCircle;
if (networkOnlineStatuses.includes(instance.Status)) {
status = "online";
icon = mdiCheckCircle;
}
if (networkStartingStatuses.includes(instance.Status)) {
status = "starting";
}
if (networkOfflineStatuses.includes(instance.Status)) {
status = "offline";
icon = mdiCloseCircle;
}
return html`
<ha-card>
<a
href="/config/ozw/network/${instance.ozw_instance}"
role="option"
tabindex="-1"
>
<paper-icon-item>
<ha-svg-icon .path=${mdiZWave} slot="item-icon">
</ha-svg-icon>
<paper-item-body>
${this.hass.localize(
"ui.panel.config.ozw.common.instance"
)}
${instance.ozw_instance}
<div secondary>
<ha-svg-icon
.path=${icon}
class="network-status-icon ${status}"
></ha-svg-icon>
${this.hass.localize(
"ui.panel.config.ozw.network_status." + status
)}
-
${this.hass.localize(
"ui.panel.config.ozw.network_status.details." +
instance.Status.toLowerCase()
)}<br />
${this.hass.localize(
"ui.panel.config.ozw.common.controller"
)}
: ${instance.getControllerPath}<br />
OZWDaemon ${instance.OZWDaemon_Version} (OpenZWave
${instance.OpenZWave_Version})
</div>
</paper-item-body>
<ha-icon-next></ha-icon-next>
</paper-icon-item>
</a>
</ha-card>
`;
})}
`
: ""}
</ha-config-section>
</hass-tabs-subpage>
`;
}
private async _fetchData() {
this._instances = await fetchOZWInstances(this.hass!);
if (this._instances.length === 1) {
navigate(`/config/ozw/network/${this._instances[0].ozw_instance}`, {
replace: true,
});
}
}
static get styles(): CSSResultGroup {
return [
haStyle,
css`
ha-card:last-child {
margin-bottom: 24px;
}
ha-config-section {
margin-top: -12px;
}
:host([narrow]) ha-config-section {
margin-top: -20px;
}
ha-alert {
display: block;
margin: 16px;
}
ha-alert a {
text-decoration: none;
}
ha-card {
overflow: hidden;
}
ha-card a {
text-decoration: none;
color: var(--primary-text-color);
}
paper-item-body {
margin: 16px 0;
}
a {
text-decoration: none;
color: var(--primary-text-color);
position: relative;
display: block;
outline: 0;
}
ha-svg-icon.network-status-icon {
height: 14px;
width: 14px;
}
.online {
color: green;
}
.starting {
color: orange;
}
.offline {
color: red;
}
ha-svg-icon,
ha-icon-next {
color: var(--secondary-text-color);
}
.iron-selected paper-item::before,
a:not(.iron-selected):focus::before {
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
pointer-events: none;
content: "";
transition: opacity 15ms linear;
will-change: opacity;
}
a:not(.iron-selected):focus::before {
background-color: currentColor;
opacity: var(--dark-divider-opacity);
}
.iron-selected paper-item:focus::before,
.iron-selected:focus paper-item::before {
opacity: 0.2;
}
`,
];
}
}
declare global {
interface HTMLElementTagNameMap {
"ozw-config-dashboard": OZWConfigDashboard;
}
}

View File

@@ -1,67 +0,0 @@
import { customElement, property } from "lit/decorators";
import memoizeOne from "memoize-one";
import {
HassRouterPage,
RouterOptions,
} from "../../../../../layouts/hass-router-page";
import { HomeAssistant, Route } from "../../../../../types";
export const computeTail = memoizeOne((route: Route) => {
const dividerPos = route.path.indexOf("/", 1);
return dividerPos === -1
? {
prefix: route.prefix + route.path,
path: "",
}
: {
prefix: route.prefix + route.path.substr(0, dividerPos),
path: route.path.substr(dividerPos),
};
});
@customElement("ozw-config-router")
class OZWConfigRouter extends HassRouterPage {
@property({ attribute: false }) public hass!: HomeAssistant;
@property() public isWide!: boolean;
@property() public narrow!: boolean;
private _configEntry = new URLSearchParams(window.location.search).get(
"config_entry"
);
protected routerOptions: RouterOptions = {
defaultPage: "dashboard",
showLoading: true,
routes: {
dashboard: {
tag: "ozw-config-dashboard",
load: () => import("./ozw-config-dashboard"),
},
network: {
tag: "ozw-network-router",
load: () => import("./ozw-network-router"),
},
},
};
protected updatePageEl(el): void {
el.route = this.routeTail;
el.hass = this.hass;
el.isWide = this.isWide;
el.narrow = this.narrow;
el.configEntryId = this._configEntry;
if (this._currentPage === "network") {
const path = this.routeTail.path.split("/");
el.ozwInstance = path[1];
el.route = computeTail(this.routeTail);
}
}
}
declare global {
interface HTMLElementTagNameMap {
"ozw-config-router": OZWConfigRouter;
}
}

View File

@@ -1,245 +0,0 @@
import "@material/mwc-button/mwc-button";
import { mdiCheckCircle, mdiCircle, mdiCloseCircle } from "@mdi/js";
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { customElement, property, state } from "lit/decorators";
import { classMap } from "lit/directives/class-map";
import { navigate } from "../../../../../common/navigate";
import "../../../../../components/buttons/ha-call-service-button";
import "../../../../../components/ha-card";
import {
fetchOZWNetworkStatistics,
fetchOZWNetworkStatus,
networkOfflineStatuses,
networkOnlineStatuses,
networkStartingStatuses,
OZWInstance,
OZWNetworkStatistics,
} from "../../../../../data/ozw";
import "../../../../../layouts/hass-tabs-subpage";
import { haStyle } from "../../../../../resources/styles";
import type { HomeAssistant, Route } from "../../../../../types";
import "../../../ha-config-section";
import { ozwNetworkTabs } from "./ozw-network-router";
@customElement("ozw-network-dashboard")
class OZWNetworkDashboard extends LitElement {
@property({ type: Object }) public hass!: HomeAssistant;
@property({ type: Object }) public route!: Route;
@property({ type: Boolean }) public narrow!: boolean;
@property({ type: Boolean }) public isWide!: boolean;
@property() public configEntryId?: string;
@property() public ozwInstance?: number;
@state() private _network?: OZWInstance;
@state() private _statistics?: OZWNetworkStatistics;
@state() private _status = "unknown";
@state() private _icon = mdiCircle;
protected firstUpdated() {
if (!this.ozwInstance) {
navigate("/config/ozw/dashboard", { replace: true });
} else if (this.hass) {
this._fetchData();
}
}
protected render(): TemplateResult {
return html`
<hass-tabs-subpage
.hass=${this.hass}
.narrow=${this.narrow}
.route=${this.route}
.tabs=${ozwNetworkTabs(this.ozwInstance!)}
>
<ha-config-section .narrow=${this.narrow} .isWide=${this.isWide}>
<div slot="header">
${this.hass.localize("ui.panel.config.ozw.network.header")}
</div>
<div slot="introduction">
${this.hass.localize("ui.panel.config.ozw.network.introduction")}
</div>
${this._network
? html`
<ha-card class="content network-status">
<div class="card-content">
<div class="details">
<ha-svg-icon
.path=${this._icon}
class="network-status-icon ${classMap({
[this._status]: true,
})}"
slot="item-icon"
></ha-svg-icon>
${this.hass.localize(
"ui.panel.config.ozw.common.network"
)}
${this.hass.localize(
`ui.panel.config.ozw.network_status.${this._status}`
)}
<br />
<small>
${this.hass.localize(
`ui.panel.config.ozw.network_status.details.${this._network.Status.toLowerCase()}`
)}
</small>
</div>
<div class="secondary">
${this.hass.localize(
"ui.panel.config.ozw.common.ozw_instance"
)}
${this._network.ozw_instance}
${this._statistics
? html`
&bull;
${this.hass.localize(
"ui.panel.config.ozw.network.node_count",
"count",
this._statistics.node_count
)}
`
: ``}
<br />
${this.hass.localize(
"ui.panel.config.ozw.common.controller"
)}:
${this._network.getControllerPath}<br />
OZWDaemon ${this._network.OZWDaemon_Version} (OpenZWave
${this._network.OpenZWave_Version})
</div>
</div>
<div class="card-actions">
${this._generateServiceButton("add_node")}
${this._generateServiceButton("remove_node")}
${this._generateServiceButton("cancel_command")}
</div>
</ha-card>
`
: ``}
</ha-config-section>
</hass-tabs-subpage>
`;
}
private async _fetchData() {
if (!this.ozwInstance) return;
this._network = await fetchOZWNetworkStatus(this.hass!, this.ozwInstance);
this._statistics = await fetchOZWNetworkStatistics(
this.hass!,
this.ozwInstance
);
if (networkOnlineStatuses.includes(this._network!.Status)) {
this._status = "online";
this._icon = mdiCheckCircle;
}
if (networkStartingStatuses.includes(this._network!.Status)) {
this._status = "starting";
}
if (networkOfflineStatuses.includes(this._network!.Status)) {
this._status = "offline";
this._icon = mdiCloseCircle;
}
}
private _generateServiceButton(service: string) {
const serviceData = { instance_id: this.ozwInstance };
return html`
<ha-call-service-button
.hass=${this.hass}
domain="ozw"
.service=${service}
.serviceData=${serviceData}
>
${this.hass!.localize(`ui.panel.config.ozw.services.${service}`)}
</ha-call-service-button>
`;
}
static get styles(): CSSResultGroup {
return [
haStyle,
css`
.secondary {
color: var(--secondary-text-color);
}
.online {
color: green;
}
.starting {
color: orange;
}
.offline {
color: red;
}
.content {
margin-top: 24px;
}
.sectionHeader {
position: relative;
padding-right: 40px;
}
.network-status {
text-align: center;
}
.network-status div.details {
font-size: 1.5rem;
margin-bottom: 16px;
}
.network-status ha-svg-icon {
display: block;
margin: 0px auto 16px;
width: 48px;
height: 48px;
}
.network-status small {
font-size: 1rem;
}
ha-card {
margin: 0 auto;
max-width: 600px;
}
.card-actions.warning ha-call-service-button {
color: var(--error-color);
}
.toggle-help-icon {
position: absolute;
top: -6px;
right: 0;
color: var(--primary-color);
}
ha-service-description {
display: block;
color: grey;
padding: 0 8px 12px;
}
[hidden] {
display: none;
}
`,
];
}
}
declare global {
interface HTMLElementTagNameMap {
"ozw-network-dashboard": OZWNetworkDashboard;
}
}

View File

@@ -1,131 +0,0 @@
import "@material/mwc-button/mwc-button";
import { mdiAlert, mdiCheck } from "@mdi/js";
import { CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { customElement, property, state } from "lit/decorators";
import memoizeOne from "memoize-one";
import { HASSDomEvent } from "../../../../../common/dom/fire_event";
import { navigate } from "../../../../../common/navigate";
import "../../../../../components/buttons/ha-call-service-button";
import {
DataTableColumnContainer,
RowClickedEvent,
} from "../../../../../components/data-table/ha-data-table";
import "../../../../../components/ha-card";
import { fetchOZWNodes, OZWDevice } from "../../../../../data/ozw";
import "../../../../../layouts/hass-tabs-subpage";
import "../../../../../layouts/hass-tabs-subpage-data-table";
import { haStyle } from "../../../../../resources/styles";
import type { HomeAssistant, Route } from "../../../../../types";
import "../../../ha-config-section";
import { ozwNetworkTabs } from "./ozw-network-router";
export interface NodeRowData extends OZWDevice {
node?: NodeRowData;
id?: number;
}
@customElement("ozw-network-nodes")
class OZWNetworkNodes extends LitElement {
@property({ type: Object }) public hass!: HomeAssistant;
@property({ type: Object }) public route!: Route;
@property({ type: Boolean }) public narrow!: boolean;
@property({ type: Boolean }) public isWide!: boolean;
@property() public configEntryId?: string;
@property() public ozwInstance = 0;
@state() private _nodes: OZWDevice[] = [];
private _columns = memoizeOne(
(narrow: boolean): DataTableColumnContainer => ({
node_id: {
title: this.hass.localize("ui.panel.config.ozw.nodes_table.id"),
sortable: true,
type: "numeric",
width: "72px",
filterable: true,
direction: "asc",
},
node_product_name: {
title: this.hass.localize("ui.panel.config.ozw.nodes_table.model"),
sortable: true,
width: narrow ? "75%" : "25%",
},
node_manufacturer_name: {
title: this.hass.localize(
"ui.panel.config.ozw.nodes_table.manufacturer"
),
sortable: true,
hidden: narrow,
width: "25%",
},
node_query_stage: {
title: this.hass.localize(
"ui.panel.config.ozw.nodes_table.query_stage"
),
sortable: true,
width: narrow ? "25%" : "15%",
},
is_zwave_plus: {
title: this.hass.localize("ui.panel.config.ozw.nodes_table.zwave_plus"),
hidden: narrow,
template: (value: boolean) =>
value ? html` <ha-svg-icon .path=${mdiCheck}></ha-svg-icon>` : "",
},
is_failed: {
title: this.hass.localize("ui.panel.config.ozw.nodes_table.failed"),
hidden: narrow,
template: (value: boolean) =>
value ? html` <ha-svg-icon .path=${mdiAlert}></ha-svg-icon>` : "",
},
})
);
protected firstUpdated() {
if (!this.ozwInstance) {
navigate("/config/ozw/dashboard", { replace: true });
} else if (this.hass) {
this._fetchData();
}
}
protected render(): TemplateResult {
return html`
<hass-tabs-subpage-data-table
.hass=${this.hass}
.narrow=${this.narrow}
.route=${this.route}
.tabs=${ozwNetworkTabs(this.ozwInstance)}
.columns=${this._columns(this.narrow)}
.data=${this._nodes}
id="node_id"
@row-click=${this._handleRowClicked}
clickable
>
</hass-tabs-subpage-data-table>
`;
}
private async _fetchData() {
this._nodes = await fetchOZWNodes(this.hass!, this.ozwInstance!);
}
private _handleRowClicked(ev: HASSDomEvent<RowClickedEvent>) {
const nodeId = ev.detail.id;
navigate(`/config/ozw/network/${this.ozwInstance}/node/${nodeId}`);
}
static get styles(): CSSResultGroup {
return haStyle;
}
}
declare global {
interface HTMLElementTagNameMap {
"ozw-network-nodes": OZWNetworkNodes;
}
}

View File

@@ -1,74 +0,0 @@
import { mdiNetwork, mdiServerNetwork } from "@mdi/js";
import { customElement, property } from "lit/decorators";
import {
HassRouterPage,
RouterOptions,
} from "../../../../../layouts/hass-router-page";
import { PageNavigation } from "../../../../../layouts/hass-tabs-subpage";
import { HomeAssistant } from "../../../../../types";
import { computeTail } from "./ozw-config-router";
export const ozwNetworkTabs = (instance: number): PageNavigation[] => [
{
translationKey: "ui.panel.config.ozw.navigation.network",
path: `/config/ozw/network/${instance}/dashboard`,
iconPath: mdiServerNetwork,
},
{
translationKey: "ui.panel.config.ozw.navigation.nodes",
path: `/config/ozw/network/${instance}/nodes`,
iconPath: mdiNetwork,
},
];
@customElement("ozw-network-router")
class OZWNetworkRouter extends HassRouterPage {
@property({ attribute: false }) public hass!: HomeAssistant;
@property() public isWide!: boolean;
@property() public narrow!: boolean;
@property() public ozwInstance!: number;
private _configEntry = new URLSearchParams(window.location.search).get(
"config_entry"
);
protected routerOptions: RouterOptions = {
defaultPage: "dashboard",
showLoading: true,
routes: {
dashboard: {
tag: "ozw-network-dashboard",
load: () => import("./ozw-network-dashboard"),
},
nodes: {
tag: "ozw-network-nodes",
load: () => import("./ozw-network-nodes"),
},
node: {
tag: "ozw-node-router",
load: () => import("./ozw-node-router"),
},
},
};
protected updatePageEl(el): void {
el.route = computeTail(this.routeTail);
el.hass = this.hass;
el.isWide = this.isWide;
el.narrow = this.narrow;
el.configEntryId = this._configEntry;
el.ozwInstance = this.ozwInstance;
if (this._currentPage === "node") {
el.nodeId = this.routeTail.path.split("/")[1];
}
}
}
declare global {
interface HTMLElementTagNameMap {
"ozw-network-router": OZWNetworkRouter;
}
}

View File

@@ -1,265 +0,0 @@
import "@material/mwc-button/mwc-button";
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { customElement, property, state } from "lit/decorators";
import { navigate } from "../../../../../common/navigate";
import "../../../../../components/buttons/ha-call-service-button";
import "../../../../../components/ha-card";
import {
fetchOZWNodeConfig,
fetchOZWNodeMetadata,
fetchOZWNodeStatus,
OZWDevice,
OZWDeviceConfig,
OZWDeviceMetaDataResponse,
} from "../../../../../data/ozw";
import { ERR_NOT_FOUND } from "../../../../../data/websocket_api";
import "../../../../../layouts/hass-tabs-subpage";
import { haStyle } from "../../../../../resources/styles";
import type { HomeAssistant, Route } from "../../../../../types";
import "../../../ha-config-section";
import { ozwNodeTabs } from "./ozw-node-router";
import { showOZWRefreshNodeDialog } from "./show-dialog-ozw-refresh-node";
@customElement("ozw-node-config")
class OZWNodeConfig extends LitElement {
@property({ type: Object }) public hass!: HomeAssistant;
@property({ type: Object }) public route!: Route;
@property({ type: Boolean }) public narrow!: boolean;
@property({ type: Boolean }) public isWide!: boolean;
@property() public configEntryId?: string;
@property() public ozwInstance?;
@property() public nodeId?;
@state() private _node?: OZWDevice;
@state() private _metadata?: OZWDeviceMetaDataResponse;
@state() private _config?: OZWDeviceConfig[];
@state() private _error?: string;
protected firstUpdated() {
if (!this.ozwInstance) {
navigate("/config/ozw/dashboard", { replace: true });
} else if (!this.nodeId) {
navigate(`/config/ozw/network/${this.ozwInstance}/nodes`, {
replace: true,
});
} else {
this._fetchData();
}
}
protected render(): TemplateResult {
if (this._error) {
return html`
<hass-error-screen
.hass=${this.hass}
.error=${this.hass.localize(
"ui.panel.config.ozw.node." + this._error
)}
></hass-error-screen>
`;
}
return html`
<hass-tabs-subpage
.hass=${this.hass}
.narrow=${this.narrow}
.route=${this.route}
.tabs=${ozwNodeTabs(this.ozwInstance, this.nodeId)}
>
<ha-config-section .narrow=${this.narrow} .isWide=${this.isWide}>
<div slot="header">
${this.hass.localize("ui.panel.config.ozw.node_config.header")}
</div>
<div slot="introduction">
${this.hass.localize(
"ui.panel.config.ozw.node_config.introduction"
)}
<p>
<em>
${this.hass.localize(
"ui.panel.config.ozw.node_config.help_source"
)}
</em>
</p>
<p>
Note: This panel is currently read-only. The ability to change
values will come in a later update.
</p>
</div>
${this._node
? html`
<ha-card class="content">
<div class="card-content">
<b>
${this._node.node_manufacturer_name}
${this._node.node_product_name} </b
><br />
${this.hass.localize("ui.panel.config.ozw.common.node_id")}:
${this._node.node_id}<br />
${this.hass.localize(
"ui.panel.config.ozw.common.query_stage"
)}:
${this._node.node_query_stage}
${this._metadata?.metadata.ProductManualURL
? html` <a
href=${this._metadata.metadata.ProductManualURL}
>
<p>
${this.hass.localize(
"ui.panel.config.ozw.node_metadata.product_manual"
)}
</p>
</a>`
: ``}
</div>
<div class="card-actions">
<mwc-button @click=${this._refreshNodeClicked}>
${this.hass.localize(
"ui.panel.config.ozw.refresh_node.button"
)}
</mwc-button>
</div>
</ha-card>
${this._metadata?.metadata.WakeupHelp
? html`
<ha-card
class="content"
header=${this.hass.localize(
"ui.panel.config.ozw.common.wakeup_instructions"
)}
>
<div class="card-content">
<span class="secondary">
${this.hass.localize(
"ui.panel.config.ozw.node_config.wakeup_help"
)}
</span>
<p>${this._metadata.metadata.WakeupHelp}</p>
</div>
</ha-card>
`
: ``}
${this._config
? html`
${this._config.map(
(item) => html`
<ha-card class="content">
<div class="card-content">
<b>${item.label}</b><br />
<span class="secondary">${item.help}</span>
<p>${item.value}</p>
</div>
</ha-card>
`
)}
`
: ``}
`
: ``}
</ha-config-section>
</hass-tabs-subpage>
`;
}
private async _fetchData() {
if (!this.ozwInstance || !this.nodeId) {
return;
}
try {
const nodeProm = fetchOZWNodeStatus(
this.hass!,
this.ozwInstance,
this.nodeId
);
const metadataProm = fetchOZWNodeMetadata(
this.hass!,
this.ozwInstance,
this.nodeId
);
const configProm = fetchOZWNodeConfig(
this.hass!,
this.ozwInstance,
this.nodeId
);
[this._node, this._metadata, this._config] = await Promise.all([
nodeProm,
metadataProm,
configProm,
]);
} catch (err: any) {
if (err.code === ERR_NOT_FOUND) {
this._error = ERR_NOT_FOUND;
return;
}
throw err;
}
}
private async _refreshNodeClicked() {
showOZWRefreshNodeDialog(this, {
node_id: this.nodeId,
ozw_instance: this.ozwInstance,
});
}
static get styles(): CSSResultGroup {
return [
haStyle,
css`
.secondary {
color: var(--secondary-text-color);
font-size: 0.9em;
}
.content {
margin-top: 24px;
}
.sectionHeader {
position: relative;
padding-right: 40px;
}
ha-card {
margin: 0 auto;
max-width: 600px;
}
[hidden] {
display: none;
}
blockquote {
display: block;
background-color: #ddd;
padding: 8px;
margin: 8px 0;
font-size: 0.9em;
}
blockquote em {
font-size: 0.9em;
margin-top: 6px;
}
`,
];
}
}
declare global {
interface HTMLElementTagNameMap {
"ozw-node-config": OZWNodeConfig;
}
}

View File

@@ -1,254 +0,0 @@
import "@material/mwc-button/mwc-button";
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { customElement, property, state } from "lit/decorators";
import { navigate } from "../../../../../common/navigate";
import "../../../../../components/buttons/ha-call-service-button";
import "../../../../../components/ha-card";
import {
fetchOZWNodeMetadata,
fetchOZWNodeStatus,
OZWDevice,
OZWDeviceMetaDataResponse,
} from "../../../../../data/ozw";
import { ERR_NOT_FOUND } from "../../../../../data/websocket_api";
import "../../../../../layouts/hass-tabs-subpage";
import { haStyle } from "../../../../../resources/styles";
import type { HomeAssistant, Route } from "../../../../../types";
import "../../../ha-config-section";
import { ozwNodeTabs } from "./ozw-node-router";
import { showOZWRefreshNodeDialog } from "./show-dialog-ozw-refresh-node";
@customElement("ozw-node-dashboard")
class OZWNodeDashboard extends LitElement {
@property({ type: Object }) public hass!: HomeAssistant;
@property({ type: Object }) public route!: Route;
@property({ type: Boolean }) public narrow!: boolean;
@property({ type: Boolean }) public isWide!: boolean;
@property() public configEntryId?: string;
@property() public ozwInstance?;
@property() public nodeId?;
@state() private _node?: OZWDevice;
@state() private _metadata?: OZWDeviceMetaDataResponse;
@state() private _not_found = false;
protected firstUpdated() {
if (!this.ozwInstance) {
navigate("/config/ozw/dashboard", { replace: true });
} else if (!this.nodeId) {
navigate(`/config/ozw/network/${this.ozwInstance}/nodes`, {
replace: true,
});
} else if (this.hass) {
this._fetchData();
}
}
protected render(): TemplateResult {
if (this._not_found) {
return html`
<hass-error-screen
.hass=${this.hass}
.error=${this.hass.localize("ui.panel.config.ozw.node.not_found")}
></hass-error-screen>
`;
}
return html`
<hass-tabs-subpage
.hass=${this.hass}
.narrow=${this.narrow}
.route=${this.route}
.tabs=${ozwNodeTabs(this.ozwInstance, this.nodeId)}
>
<ha-config-section .narrow=${this.narrow} .isWide=${this.isWide}>
<div slot="header">Node Management</div>
<div slot="introduction">
View the status of a node and manage its configuration.
</div>
${this._node
? html`
<ha-card class="content">
<div class="card-content flex">
<div class="node-details">
<b>
${this._node.node_manufacturer_name}
${this._node.node_product_name}
</b>
<br />
Node ID: ${this._node.node_id}<br />
Query Stage: ${this._node.node_query_stage}
${this._metadata?.metadata.ProductManualURL
? html` <a
href=${this._metadata.metadata.ProductManualURL}
>
<p>Product Manual</p>
</a>`
: ``}
</div>
${this._metadata?.metadata.ProductPicBase64
? html`<img
class="product-image"
src="data:image/png;base64,${this._metadata?.metadata
.ProductPicBase64}"
/>`
: ``}
</div>
<div class="card-actions">
<mwc-button @click=${this._refreshNodeClicked}>
Refresh Node
</mwc-button>
</div>
</ha-card>
${this._metadata
? html`
<ha-card class="content" header="Description">
<div class="card-content">
${this._metadata.metadata.Description}
</div>
</ha-card>
<ha-card class="content" header="Inclusion">
<div class="card-content">
${this._metadata.metadata.InclusionHelp}
</div>
</ha-card>
<ha-card class="content" header="Exclusion">
<div class="card-content">
${this._metadata.metadata.ExclusionHelp}
</div>
</ha-card>
<ha-card class="content" header="Reset">
<div class="card-content">
${this._metadata.metadata.ResetHelp}
</div>
</ha-card>
${this._metadata.metadata.WakeupHelp
? html`
<ha-card class="content" header="WakeUp">
<div class="card-content">
${this._metadata.metadata.WakeupHelp}
</div>
</ha-card>
`
: ``}
`
: ``}
`
: ``}
</ha-config-section>
</hass-tabs-subpage>
`;
}
private async _fetchData() {
if (!this.ozwInstance || !this.nodeId) {
return;
}
try {
this._node = await fetchOZWNodeStatus(
this.hass!,
this.ozwInstance,
this.nodeId
);
this._metadata = await fetchOZWNodeMetadata(
this.hass!,
this.ozwInstance,
this.nodeId
);
} catch (err: any) {
if (err.code === ERR_NOT_FOUND) {
this._not_found = true;
return;
}
throw err;
}
}
private async _refreshNodeClicked() {
showOZWRefreshNodeDialog(this, {
node_id: this.nodeId,
ozw_instance: this.ozwInstance,
});
}
static get styles(): CSSResultGroup {
return [
haStyle,
css`
.secondary {
color: var(--secondary-text-color);
}
.content {
margin-top: 24px;
}
.content:last-child {
margin-bottom: 24px;
}
.sectionHeader {
position: relative;
padding-right: 40px;
}
ha-card {
margin: 0 auto;
max-width: 600px;
}
.flex {
display: flex;
justify-content: space-between;
}
.card-actions.warning ha-call-service-button {
color: var(--error-color);
}
.toggle-help-icon {
position: absolute;
top: -6px;
right: 0;
color: var(--primary-color);
}
ha-service-description {
display: block;
color: grey;
padding: 0 8px 12px;
}
[hidden] {
display: none;
}
.product-image {
padding: 12px;
max-height: 140px;
max-width: 140px;
}
.card-actions {
clear: right;
}
`,
];
}
}
declare global {
interface HTMLElementTagNameMap {
"ozw-node-dashboard": OZWNodeDashboard;
}
}

View File

@@ -1,84 +0,0 @@
import { mdiNetwork, mdiWrench } from "@mdi/js";
import { customElement, property } from "lit/decorators";
import { navigate } from "../../../../../common/navigate";
import {
HassRouterPage,
RouterOptions,
} from "../../../../../layouts/hass-router-page";
import { PageNavigation } from "../../../../../layouts/hass-tabs-subpage";
import { HomeAssistant } from "../../../../../types";
export const ozwNodeTabs = (
instance: number,
node: number
): PageNavigation[] => [
{
translationKey: "ui.panel.config.ozw.navigation.node.dashboard",
path: `/config/ozw/network/${instance}/node/${node}/dashboard`,
iconPath: mdiNetwork,
},
{
translationKey: "ui.panel.config.ozw.navigation.node.config",
path: `/config/ozw/network/${instance}/node/${node}/config`,
iconPath: mdiWrench,
},
];
@customElement("ozw-node-router")
class OZWNodeRouter extends HassRouterPage {
@property({ attribute: false }) public hass!: HomeAssistant;
@property() public isWide!: boolean;
@property() public narrow!: boolean;
@property() public ozwInstance!: number;
@property() public nodeId!: number;
private _configEntry = new URLSearchParams(window.location.search).get(
"config_entry"
);
protected routerOptions: RouterOptions = {
defaultPage: "dashboard",
showLoading: true,
routes: {
dashboard: {
tag: "ozw-node-dashboard",
load: () => import("./ozw-node-dashboard"),
},
config: {
tag: "ozw-node-config",
load: () => import("./ozw-node-config"),
},
},
};
protected updatePageEl(el): void {
el.route = this.routeTail;
el.hass = this.hass;
el.isWide = this.isWide;
el.narrow = this.narrow;
el.configEntryId = this._configEntry;
el.ozwInstance = this.ozwInstance;
el.nodeId = this.nodeId;
const searchParams = new URLSearchParams(window.location.search);
if (this._configEntry && !searchParams.has("config_entry")) {
searchParams.append("config_entry", this._configEntry);
navigate(
`${this.routeTail.prefix}${
this.routeTail.path
}?${searchParams.toString()}`,
{ replace: true }
);
}
}
}
declare global {
interface HTMLElementTagNameMap {
"ozw-node-router": OZWNodeRouter;
}
}

View File

@@ -1,19 +0,0 @@
import { fireEvent } from "../../../../../common/dom/fire_event";
export interface OZWRefreshNodeDialogParams {
ozw_instance: number;
node_id: number;
}
export const loadRefreshNodeDialog = () => import("./dialog-ozw-refresh-node");
export const showOZWRefreshNodeDialog = (
element: HTMLElement,
refreshNodeDialogParams: OZWRefreshNodeDialogParams
): void => {
fireEvent(element, "show-dialog", {
dialogTag: "dialog-ozw-refresh-node",
dialogImport: loadRefreshNodeDialog,
dialogParams: refreshNodeDialogParams,
});
};

View File

@@ -1,765 +0,0 @@
import "@polymer/app-layout/app-header/app-header";
import "@polymer/app-layout/app-toolbar/app-toolbar";
import "@polymer/paper-dropdown-menu/paper-dropdown-menu";
import "@polymer/paper-input/paper-input";
import "@polymer/paper-item/paper-item";
import "@polymer/paper-listbox/paper-listbox";
import { setCancelSyntheticClickEvents } from "@polymer/polymer/lib/utils/settings";
import { html } from "@polymer/polymer/lib/utils/html-tag";
/* eslint-plugin-disable lit */
import { PolymerElement } from "@polymer/polymer/polymer-element";
import { computeStateDomain } from "../../../../../common/entity/compute_state_domain";
import { computeStateName } from "../../../../../common/entity/compute_state_name";
import { sortStatesByName } from "../../../../../common/entity/states_sort_by_name";
import "../../../../../components/buttons/ha-call-service-button";
import "../../../../../components/ha-card";
import "../../../../../components/ha-alert";
import "../../../../../components/ha-icon";
import "../../../../../components/ha-icon-button";
import "../../../../../components/ha-icon-button-arrow-prev";
import "../../../../../components/ha-menu-button";
import "../../../../../components/ha-service-description";
import "../../../../../layouts/ha-app-layout";
import { EventsMixin } from "../../../../../mixins/events-mixin";
import LocalizeMixin from "../../../../../mixins/localize-mixin";
import "../../../../../styles/polymer-ha-style";
import "../../../ha-config-section";
import "../../../ha-form-style";
import "./zwave-groups";
import "./zwave-log";
import "./zwave-network";
import "./zwave-node-config";
import "./zwave-node-protection";
import "./zwave-usercodes";
import "./zwave-values";
/*
* @appliesMixin LocalizeMixin
* @appliesMixin EventsMixin
*/
class HaConfigZwave extends LocalizeMixin(EventsMixin(PolymerElement)) {
static get template() {
return html`
<style include="iron-flex ha-style ha-form-style">
app-toolbar {
border-bottom: 1px solid var(--divider-color);
}
ha-alert {
display: block;
margin: 16px;
}
ha-alert a {
text-decoration: none;
}
.content {
margin-top: 24px;
}
.sectionHeader {
position: relative;
padding-right: 40px;
}
.node-info {
margin-left: 16px;
}
.help-text {
padding-left: 24px;
padding-right: 24px;
}
ha-card {
margin: 0 auto;
max-width: 600px;
}
.device-picker {
@apply --layout-horizontal;
@apply --layout-center-center;
padding-left: 24px;
padding-right: 24px;
padding-bottom: 24px;
}
ha-service-description {
display: block;
color: grey;
}
ha-service-description[hidden] {
display: none;
}
.toggle-help-icon {
position: absolute;
top: -6px;
right: 0;
color: var(--primary-color);
}
</style>
<ha-app-layout>
<app-header slot="header" fixed="">
<app-toolbar>
<ha-icon-button-arrow-prev
hass="[[hass]]"
on-click="_backTapped"
></ha-icon-button-arrow-prev>
<div main-title="">[[localize('component.zwave.title')]]</div>
</app-toolbar>
</app-header>
<ha-alert
alert-type="warning"
title="This integration will stop working soon"
>
This Z-Wave integration is deprecated and will no longer receive any
updates. The technical dependencies will render this integration
unusable in the near future. We strongly advise you to migrate to the
new
<a
href="https://www.home-assistant.io/integrations/zwave_js"
target="_blank"
rel="noreferrer"
>Z-Wave JS integration</a
>.
<a
slot="action"
href="https://alerts.home-assistant.io/#zwave.markdown"
target="_blank"
rel="noreferrer"
>
<mwc-button>learn more</mwc-button>
</a>
</ha-alert>
<ha-config-section is-wide="[[isWide]]">
<ha-card
class="content"
header="[[localize('ui.panel.config.zwave.migration.zwave_js.header')]]"
>
<div class="card-content">
[[localize('ui.panel.config.zwave.migration.zwave_js.introduction')]]
</div>
<div class="card-actions">
<a href="/config/zwave/migration"
><mwc-button>Start Migration to Z-Wave JS</mwc-button></a
>
</div>
</ha-card>
</ha-config-section>
<zwave-network
id="zwave-network"
is-wide="[[isWide]]"
hass="[[hass]]"
></zwave-network>
<!-- Node card -->
<ha-config-section is-wide="[[isWide]]">
<div class="sectionHeader" slot="header">
<span
>[[localize('ui.panel.config.zwave.node_management.header')]]</span
>
<ha-icon-button
class="toggle-help-icon"
on-click="toggleHelp"
label="[[localize('ui.common.help')]]"
>
<ha-icon icon="hass:help-circle"></ha-icon>
</ha-icon-button>
</div>
<span slot="introduction">
[[localize('ui.panel.config.zwave.node_management.introduction')]]
</span>
<ha-card class="content">
<div class="device-picker">
<paper-dropdown-menu
dynamic-align=""
label="[[localize('ui.panel.config.zwave.node_management.nodes')]]"
class="flex"
>
<paper-listbox
slot="dropdown-content"
selected="{{selectedNode}}"
>
<template is="dom-repeat" items="[[nodes]]" as="state">
<paper-item>[[computeSelectCaption(state)]]</paper-item>
</template>
</paper-listbox>
</paper-dropdown-menu>
</div>
<template is="dom-if" if="[[!computeIsNodeSelected(selectedNode)]]">
<template is="dom-if" if="[[showHelp]]">
<div style="color: grey; padding: 12px">
[[localize('ui.panel.config.zwave.node_management.introduction')]]
</div>
</template>
</template>
<template is="dom-if" if="[[computeIsNodeSelected(selectedNode)]]">
<div class="card-actions">
<ha-call-service-button
hass="[[hass]]"
domain="zwave"
service="refresh_node"
service-data="[[computeNodeServiceData(selectedNode)]]"
>
[[localize('ui.panel.config.zwave.services.refresh_node')]]
</ha-call-service-button>
<ha-service-description
hass="[[hass]]"
domain="zwave"
service="refresh_node"
hidden$="[[!showHelp]]"
>
</ha-service-description>
<template is="dom-if" if="[[nodeFailed]]">
<ha-call-service-button
hass="[[hass]]"
domain="zwave"
service="remove_failed_node"
service-data="[[computeNodeServiceData(selectedNode)]]"
>
[[localize('ui.panel.config.zwave.services.remove_failed_node')]]
</ha-call-service-button>
<ha-service-description
hass="[[hass]]"
domain="zwave"
service="remove_failed_node"
hidden$="[[!showHelp]]"
>
</ha-service-description>
<ha-call-service-button
hass="[[hass]]"
domain="zwave"
service="replace_failed_node"
service-data="[[computeNodeServiceData(selectedNode)]]"
>
[[localize('ui.panel.config.zwave.services.replace_failed_node')]]
</ha-call-service-button>
<ha-service-description
hass="[[hass]]"
domain="zwave"
service="replace_failed_node"
hidden$="[[!showHelp]]"
>
</ha-service-description>
</template>
<ha-call-service-button
hass="[[hass]]"
domain="zwave"
service="print_node"
service-data="[[computeNodeServiceData(selectedNode)]]"
>
[[localize('ui.panel.config.zwave.services.print_node')]]
</ha-call-service-button>
<ha-service-description
hass="[[hass]]"
domain="zwave"
service="print_node"
hidden$="[[!showHelp]]"
>
</ha-service-description>
<ha-call-service-button
hass="[[hass]]"
domain="zwave"
service="heal_node"
service-data="[[computeHealNodeServiceData(selectedNode)]]"
>
[[localize('ui.panel.config.zwave.services.heal_node')]]
</ha-call-service-button>
<ha-service-description
hass="[[hass]]"
domain="zwave"
service="heal_node"
hidden$="[[!showHelp]]"
>
</ha-service-description>
<ha-call-service-button
hass="[[hass]]"
domain="zwave"
service="test_node"
service-data="[[computeNodeServiceData(selectedNode)]]"
>
[[localize('ui.panel.config.zwave.services.test_node')]]
</ha-call-service-button>
<ha-service-description
hass="[[hass]]"
domain="zwave"
service="test_node"
hidden$="[[!showHelp]]"
>
</ha-service-description>
<mwc-button on-click="_nodeMoreInfo"
>[[localize('ui.panel.config.zwave.services.node_info')]]</mwc-button
>
</div>
<div class="device-picker">
<paper-dropdown-menu
label="[[localize('ui.panel.config.zwave.node_management.entities')]]"
dynamic-align=""
class="flex"
>
<paper-listbox
slot="dropdown-content"
selected="{{selectedEntity}}"
>
<template is="dom-repeat" items="[[entities]]" as="state">
<paper-item>[[state.entity_id]]</paper-item>
</template>
</paper-listbox>
</paper-dropdown-menu>
</div>
<template
is="dom-if"
if="[[!computeIsEntitySelected(selectedEntity)]]"
>
<div class="card-actions">
<ha-call-service-button
hass="[[hass]]"
domain="zwave"
service="refresh_entity"
service-data="[[computeRefreshEntityServiceData(selectedEntity)]]"
>
[[localize('ui.panel.config.zwave.services.refresh_entity')]]
</ha-call-service-button>
<ha-service-description
hass="[[hass]]"
domain="zwave"
service="refresh_entity"
hidden$="[[!showHelp]]"
>
</ha-service-description>
<mwc-button on-click="_entityMoreInfo"
>[[localize('ui.panel.config.zwave.node_management.entity_info')]]</mwc-button
>
</div>
<div class="form-group">
<ha-formfield
label="[[localize('ui.panel.config.zwave.node_management.exclude_entity')]]"
>
<ha-checkbox
checked="[[entityIgnored]]"
class="form-control"
on-change="entityIgnoredChanged"
>
</ha-checkbox>
</ha-formfield>
<paper-input
disabled="{{entityIgnored}}"
label="[[localize('ui.panel.config.zwave.node_management.pooling_intensity')]]"
type="number"
min="0"
value="{{entityPollingIntensity}}"
>
</paper-input>
</div>
<div class="card-actions">
<ha-call-service-button
hass="[[hass]]"
domain="zwave"
service="set_poll_intensity"
service-data="[[computePollIntensityServiceData(entityPollingIntensity)]]"
>
[[localize('ui.common.save')]]
</ha-call-service-button>
</div>
</template>
</template>
</ha-card>
<template is="dom-if" if="[[computeIsNodeSelected(selectedNode)]]">
<!-- Value card -->
<zwave-values
hass="[[hass]]"
nodes="[[nodes]]"
selected-node="[[selectedNode]]"
values="[[values]]"
></zwave-values>
<!-- Group card -->
<zwave-groups
hass="[[hass]]"
nodes="[[nodes]]"
selected-node="[[selectedNode]]"
groups="[[groups]]"
></zwave-groups>
<!-- Config card -->
<zwave-node-config
hass="[[hass]]"
nodes="[[nodes]]"
selected-node="[[selectedNode]]"
config="[[config]]"
></zwave-node-config>
</template>
<!-- Protection card -->
<template is="dom-if" if="{{_protectionNode}}">
<zwave-node-protection
hass="[[hass]]"
nodes="[[nodes]]"
selected-node="[[selectedNode]]"
protection="[[_protection]]"
></zwave-node-protection>
</template>
<!-- User Codes -->
<template is="dom-if" if="{{hasNodeUserCodes}}">
<zwave-usercodes
id="zwave-usercodes"
hass="[[hass]]"
nodes="[[nodes]]"
user-codes="[[userCodes]]"
selected-node="[[selectedNode]]"
></zwave-usercodes>
</template>
</ha-config-section>
<!-- Ozw log -->
<ozw-log is-wide="[[isWide]]" hass="[[hass]]"></ozw-log>
</ha-app-layout>
`;
}
static get properties() {
return {
hass: Object,
isWide: Boolean,
nodes: {
type: Array,
computed: "computeNodes(hass)",
},
selectedNode: {
type: Number,
value: -1,
observer: "selectedNodeChanged",
},
nodeFailed: {
type: Boolean,
value: false,
},
config: {
type: Array,
value: () => [],
},
entities: {
type: Array,
computed: "computeEntities(selectedNode)",
},
selectedEntity: {
type: Number,
value: -1,
observer: "selectedEntityChanged",
},
values: {
type: Array,
},
groups: {
type: Array,
},
userCodes: {
type: Array,
value: () => [],
},
hasNodeUserCodes: {
type: Boolean,
value: false,
},
showHelp: {
type: Boolean,
value: false,
},
entityIgnored: Boolean,
entityPollingIntensity: {
type: Number,
value: 0,
},
_protection: {
type: Array,
value: () => [],
},
_protectionNode: {
type: Boolean,
value: false,
},
};
}
ready() {
super.ready();
import("web-animations-js/web-animations-next-lite.min");
this.addEventListener("hass-service-called", (ev) =>
this.serviceCalled(ev)
);
}
attached() {
setCancelSyntheticClickEvents(true);
}
detached() {
setCancelSyntheticClickEvents(false);
}
serviceCalled(ev) {
if (ev.detail.success && ev.detail.service === "set_poll_intensity") {
this._saveEntity();
}
}
computeNodes(hass) {
return Object.keys(hass.states)
.map((key) => hass.states[key])
.filter((ent) => ent.entity_id.match("zwave[.]"))
.sort(sortStatesByName);
}
computeEntities(selectedNode) {
if (!this.nodes || selectedNode === -1) {
return -1;
}
const nodeid = this.nodes[this.selectedNode].attributes.node_id;
const hass = this.hass;
return Object.keys(this.hass.states)
.map((key) => hass.states[key])
.filter((ent) => {
if (ent.attributes.node_id === undefined) {
return false;
}
return (
"node_id" in ent.attributes &&
ent.attributes.node_id === nodeid &&
!ent.entity_id.match("zwave[.]")
);
})
.sort(sortStatesByName);
}
selectedNodeChanged(selectedNode) {
if (selectedNode === -1) {
return;
}
this.selectedEntity = -1;
this.hass
.callApi(
"GET",
`zwave/config/${this.nodes[selectedNode].attributes.node_id}`
)
.then((configs) => {
this.config = this._objToArray(configs);
});
this.hass
.callApi(
"GET",
`zwave/values/${this.nodes[selectedNode].attributes.node_id}`
)
.then((values) => {
this.values = this._objToArray(values);
});
this.hass
.callApi(
"GET",
`zwave/groups/${this.nodes[selectedNode].attributes.node_id}`
)
.then((groups) => {
this.groups = this._objToArray(groups);
});
this.hasNodeUserCodes = false;
this.notifyPath("hasNodeUserCodes");
this.hass
.callApi(
"GET",
`zwave/usercodes/${this.nodes[selectedNode].attributes.node_id}`
)
.then((usercodes) => {
this.userCodes = this._objToArray(usercodes);
this.hasNodeUserCodes = this.userCodes.length > 0;
this.notifyPath("hasNodeUserCodes");
});
this.hass
.callApi(
"GET",
`zwave/protection/${this.nodes[selectedNode].attributes.node_id}`
)
.then((protections) => {
this._protection = this._objToArray(protections);
if (this._protection) {
if (this._protection.length === 0) {
return;
}
this._protectionNode = true;
}
});
this.nodeFailed = this.nodes[selectedNode].attributes.is_failed;
}
selectedEntityChanged(selectedEntity) {
if (selectedEntity === -1) {
return;
}
this.hass
.callApi(
"GET",
`zwave/values/${this.nodes[this.selectedNode].attributes.node_id}`
)
.then((values) => {
this.values = this._objToArray(values);
});
const valueId = this.entities[selectedEntity].attributes.value_id;
const valueData = this.values.find((obj) => obj.key === valueId);
const valueIndex = this.values.indexOf(valueData);
this.hass
.callApi(
"GET",
`config/zwave/device_config/${this.entities[selectedEntity].entity_id}`
)
.then((data) => {
this.setProperties({
entityIgnored: data.ignored || false,
entityPollingIntensity: this.values[valueIndex].value.poll_intensity,
});
})
.catch(() => {
this.setProperties({
entityIgnored: false,
entityPollingIntensity: this.values[valueIndex].value.poll_intensity,
});
});
}
computeSelectCaption(stateObj) {
return (
computeStateName(stateObj) +
" (Node:" +
stateObj.attributes.node_id +
" " +
stateObj.attributes.query_stage +
")"
);
}
computeSelectCaptionEnt(stateObj) {
return computeStateDomain(stateObj) + "." + computeStateName(stateObj);
}
computeIsNodeSelected() {
return this.nodes && this.selectedNode !== -1;
}
computeIsEntitySelected(selectedEntity) {
return selectedEntity === -1;
}
computeNodeServiceData(selectedNode) {
return { node_id: this.nodes[selectedNode].attributes.node_id };
}
computeHealNodeServiceData(selectedNode) {
return {
node_id: this.nodes[selectedNode].attributes.node_id,
return_routes: true,
};
}
computeRefreshEntityServiceData(selectedEntity) {
if (selectedEntity === -1) {
return -1;
}
return { entity_id: this.entities[selectedEntity].entity_id };
}
computePollIntensityServiceData(entityPollingIntensity) {
if (this.selectedNode === -1 || this.selectedEntity === -1) {
return -1;
}
return {
node_id: this.nodes[this.selectedNode].attributes.node_id,
value_id: this.entities[this.selectedEntity].attributes.value_id,
poll_intensity: parseInt(entityPollingIntensity),
};
}
_nodeMoreInfo() {
this.fire("hass-more-info", {
entityId: this.nodes[this.selectedNode].entity_id,
});
}
_entityMoreInfo() {
this.fire("hass-more-info", {
entityId: this.entities[this.selectedEntity].entity_id,
});
}
_saveEntity() {
const data = {
ignored: this.entityIgnored,
polling_intensity: parseInt(this.entityPollingIntensity),
};
return this.hass.callApi(
"POST",
`config/zwave/device_config/${
this.entities[this.selectedEntity].entity_id
}`,
data
);
}
toggleHelp() {
this.showHelp = !this.showHelp;
}
_objToArray(obj) {
const array = [];
Object.keys(obj).forEach((key) => {
array.push({
key,
value: obj[key],
});
});
return array;
}
_backTapped() {
history.back();
}
entityIgnoredChanged(ev) {
this.entityIgnored = ev.target.checked;
}
}
customElements.define("ha-config-zwave", HaConfigZwave);

View File

@@ -1,62 +0,0 @@
import { customElement, property } from "lit/decorators";
import { navigate } from "../../../../../common/navigate";
import {
HassRouterPage,
RouterOptions,
} from "../../../../../layouts/hass-router-page";
import { HomeAssistant } from "../../../../../types";
@customElement("zwave-config-router")
class ZWaveConfigRouter extends HassRouterPage {
@property({ attribute: false }) public hass!: HomeAssistant;
@property() public isWide!: boolean;
@property() public narrow!: boolean;
private _configEntry = new URLSearchParams(window.location.search).get(
"config_entry"
);
protected routerOptions: RouterOptions = {
defaultPage: "dashboard",
showLoading: true,
routes: {
dashboard: {
tag: "ha-config-zwave",
load: () =>
import(/* webpackChunkName: "ha-config-zwave" */ "./ha-config-zwave"),
},
migration: {
tag: "zwave-migration",
load: () =>
import(/* webpackChunkName: "zwave-migration" */ "./zwave-migration"),
},
},
};
protected updatePageEl(el): void {
el.route = this.routeTail;
el.hass = this.hass;
el.isWide = this.isWide;
el.narrow = this.narrow;
el.configEntryId = this._configEntry;
const searchParams = new URLSearchParams(window.location.search);
if (this._configEntry && !searchParams.has("config_entry")) {
searchParams.append("config_entry", this._configEntry);
navigate(
`${this.routeTail.prefix}${
this.routeTail.path
}?${searchParams.toString()}`,
{ replace: true }
);
}
}
}
declare global {
interface HTMLElementTagNameMap {
"zwave-config-router": ZWaveConfigRouter;
}
}

View File

@@ -1,380 +0,0 @@
import "@polymer/paper-dropdown-menu/paper-dropdown-menu";
import "@polymer/paper-item/paper-item";
import "@polymer/paper-listbox/paper-listbox";
import { html } from "@polymer/polymer/lib/utils/html-tag";
/* eslint-plugin-disable lit */
import { PolymerElement } from "@polymer/polymer/polymer-element";
import { computeStateName } from "../../../../../common/entity/compute_state_name";
import "../../../../../components/buttons/ha-call-service-button";
import "../../../../../components/ha-card";
import LocalizeMixin from "../../../../../mixins/localize-mixin";
import "../../../../../styles/polymer-ha-style";
class ZwaveGroups extends LocalizeMixin(PolymerElement) {
static get template() {
return html`
<style include="iron-flex ha-style">
.content {
margin-top: 24px;
}
ha-card {
margin: 0 auto;
max-width: 600px;
}
.device-picker {
@apply --layout-horizontal;
@apply --layout-center-center;
padding-left: 24px;
padding-right: 24px;
padding-bottom: 24px;
}
.help-text {
padding-left: 24px;
padding-right: 24px;
padding-bottom: 12px;
}
</style>
<ha-card
class="content"
header="[[localize('ui.panel.config.zwave.node_management.node_group_associations')]]"
>
<!-- TODO make api for getting groups and members -->
<div class="device-picker">
<paper-dropdown-menu
label="[[localize('ui.panel.config.zwave.node_management.group')]]"
dynamic-align=""
class="flex"
>
<paper-listbox
slot="dropdown-content"
selected="{{_selectedGroup}}"
>
<template is="dom-repeat" items="[[groups]]" as="state">
<paper-item>[[_computeSelectCaptionGroup(state)]]</paper-item>
</template>
</paper-listbox>
</paper-dropdown-menu>
</div>
<template is="dom-if" if="[[_computeIsGroupSelected(_selectedGroup)]]">
<div class="device-picker">
<paper-dropdown-menu
label="[[localize('ui.panel.config.zwave.node_management.node_to_control')]]"
dynamic-align=""
class="flex"
>
<paper-listbox
slot="dropdown-content"
selected="{{_selectedTargetNode}}"
>
<template is="dom-repeat" items="[[nodes]]" as="state">
<paper-item>[[_computeSelectCaption(state)]]</paper-item>
</template>
</paper-listbox>
</paper-dropdown-menu>
</div>
<div class="help-text">
<span
>[[localize('ui.panel.config.zwave.node_management.nodes_in_group')]]</span
>
<template is="dom-repeat" items="[[_otherGroupNodes]]" as="state">
<div>[[state]]</div>
</template>
</div>
<div class="help-text">
<span
>[[localize('ui.panel.config.zwave.node_management.max_associations')]]</span
>
<span>[[_maxAssociations]]</span>
</div>
</template>
<template
is="dom-if"
if="[[_computeIsTargetNodeSelected(_selectedTargetNode)]]"
>
<div class="card-actions">
<template is="dom-if" if="[[!_noAssociationsLeft]]">
<ha-call-service-button
hass="[[hass]]"
domain="zwave"
service="change_association"
service-data="[[_addAssocServiceData]]"
>
[[localize('ui.panel.config.zwave.node_management.add_to_group')]]
</ha-call-service-button>
</template>
<template
is="dom-if"
if="[[_computeTargetInGroup(_selectedGroup, _selectedTargetNode)]]"
>
<ha-call-service-button
hass="[[hass]]"
domain="zwave"
service="change_association"
service-data="[[_removeAssocServiceData]]"
>
[[localize('ui.panel.config.zwave.node_management.remove_from_group')]]
</ha-call-service-button>
</template>
<template is="dom-if" if="[[_isBroadcastNodeInGroup]]">
<ha-call-service-button
hass="[[hass]]"
domain="zwave"
service="change_association"
service-data="[[_removeBroadcastNodeServiceData]]"
>
[[localize('ui.panel.config.zwave.node_management.remove_broadcast')]]
</ha-call-service-button>
</template>
</div>
</template>
</ha-card>
`;
}
static get properties() {
return {
hass: Object,
nodes: Array,
groups: Array,
selectedNode: {
type: Number,
observer: "_selectedNodeChanged",
},
_selectedTargetNode: {
type: Number,
value: -1,
observer: "_selectedTargetNodeChanged",
},
_selectedGroup: {
type: Number,
value: -1,
},
_otherGroupNodes: {
type: Array,
value: -1,
computed: "_computeOtherGroupNodes(_selectedGroup)",
},
_maxAssociations: {
type: String,
value: "",
computed: "_computeMaxAssociations(_selectedGroup)",
},
_noAssociationsLeft: {
type: Boolean,
value: true,
computed: "_computeAssociationsLeft(_selectedGroup)",
},
_addAssocServiceData: {
type: String,
value: "",
},
_removeAssocServiceData: {
type: String,
value: "",
},
_removeBroadcastNodeServiceData: {
type: String,
value: "",
},
_isBroadcastNodeInGroup: {
type: Boolean,
value: false,
},
};
}
static get observers() {
return ["_selectedGroupChanged(groups, _selectedGroup)"];
}
ready() {
super.ready();
this.addEventListener("hass-service-called", (ev) =>
this.serviceCalled(ev)
);
}
serviceCalled(ev) {
if (ev.detail.success) {
setTimeout(() => {
this._refreshGroups(this.selectedNode);
}, 5000);
}
}
_computeAssociationsLeft(selectedGroup) {
if (selectedGroup === -1) return true;
return this._maxAssociations === this._otherGroupNodes.length;
}
_computeMaxAssociations(selectedGroup) {
if (selectedGroup === -1) return -1;
const maxAssociations = this.groups[selectedGroup].value.max_associations;
if (!maxAssociations) return "None";
return maxAssociations;
}
_computeOtherGroupNodes(selectedGroup) {
if (selectedGroup === -1) return -1;
this.setProperties({ _isBroadcastNodeInGroup: false });
const associations = Object.values(
this.groups[selectedGroup].value.association_instances
);
if (!associations.length) return ["None"];
return associations.map((assoc) => {
if (!assoc.length || assoc.length !== 2) {
return `Unknown Node: ${assoc}`;
}
const id = assoc[0];
const instance = assoc[1];
const node = this.nodes.find((n) => n.attributes.node_id === id);
if (id === 255) {
this.setProperties({
_isBroadcastNodeInGroup: true,
_removeBroadcastNodeServiceData: {
node_id: this.nodes[this.selectedNode].attributes.node_id,
association: "remove",
target_node_id: 255,
group: this.groups[selectedGroup].key,
},
});
}
if (!node) {
return `Unknown Node (${id}: (${instance} ? ${id}.${instance} : ${id}))`;
}
let caption = this._computeSelectCaption(node);
if (instance) {
caption += `/ Instance: ${instance}`;
}
return caption;
});
}
_computeTargetInGroup(selectedGroup, selectedTargetNode) {
if (selectedGroup === -1 || selectedTargetNode === -1) return false;
const associations = Object.values(
this.groups[selectedGroup].value.associations
);
if (!associations.length) return false;
return (
associations.indexOf(
this.nodes[selectedTargetNode].attributes.node_id
) !== -1
);
}
_computeSelectCaption(stateObj) {
return `${computeStateName(stateObj)}
(Node: ${stateObj.attributes.node_id}
${stateObj.attributes.query_stage})`;
}
_computeSelectCaptionGroup(stateObj) {
return `${stateObj.key}: ${stateObj.value.label}`;
}
_computeIsTargetNodeSelected(selectedTargetNode) {
return this.nodes && selectedTargetNode !== -1;
}
_computeIsGroupSelected(selectedGroup) {
return this.nodes && this.selectedNode !== -1 && selectedGroup !== -1;
}
_computeAssocServiceData(selectedGroup, type) {
if (
!this.groups ||
selectedGroup === -1 ||
this.selectedNode === -1 ||
this._selectedTargetNode === -1
) {
return -1;
}
return {
node_id: this.nodes[this.selectedNode].attributes.node_id,
association: type,
target_node_id: this.nodes[this._selectedTargetNode].attributes.node_id,
group: this.groups[selectedGroup].key,
};
}
async _refreshGroups(selectedNode) {
const groupData = [];
const groups = await this.hass.callApi(
"GET",
`zwave/groups/${this.nodes[selectedNode].attributes.node_id}`
);
Object.keys(groups).forEach((key) => {
groupData.push({
key,
value: groups[key],
});
});
this.setProperties({
groups: groupData,
_maxAssociations: groupData[this._selectedGroup].value.max_associations,
_otherGroupNodes: Object.values(
groupData[this._selectedGroup].value.associations
),
_isBroadcastNodeInGroup: false,
});
const oldGroup = this._selectedGroup;
this.setProperties({ _selectedGroup: -1 });
this.setProperties({ _selectedGroup: oldGroup });
}
_selectedGroupChanged() {
if (this._selectedGroup === -1) return;
this.setProperties({
_maxAssociations: this.groups[this._selectedGroup].value.max_associations,
_otherGroupNodes: Object.values(
this.groups[this._selectedGroup].value.associations
),
});
}
_selectedTargetNodeChanged() {
if (this._selectedGroup === -1) return;
if (
this._computeTargetInGroup(this._selectedGroup, this._selectedTargetNode)
) {
this.setProperties({
_removeAssocServiceData: this._computeAssocServiceData(
this._selectedGroup,
"remove"
),
});
} else {
this.setProperties({
_addAssocServiceData: this._computeAssocServiceData(
this._selectedGroup,
"add"
),
});
}
}
_selectedNodeChanged() {
if (this.selectedNode === -1) return;
this.setProperties({ _selectedTargetNode: -1, _selectedGroup: -1 });
}
}
customElements.define("zwave-groups", ZwaveGroups);

View File

@@ -1,83 +0,0 @@
import { html } from "@polymer/polymer/lib/utils/html-tag";
/* eslint-plugin-disable lit */
import { PolymerElement } from "@polymer/polymer/polymer-element";
import { EventsMixin } from "../../../../../mixins/events-mixin";
import "../../../../../styles/polymer-ha-style-dialog";
import "../../../../../components/ha-dialog";
class ZwaveLogDialog extends EventsMixin(PolymerElement) {
static get template() {
return html`
<style include="ha-style-dialog">
pre {
font-family: var(--code-font-family, monospace);
}
</style>
<ha-dialog open="[[_opened]]" heading="OpenZwave internal logfile" on-closed="closeDialog">
<div>
<pre>[[_ozwLog]]</pre>
<div>
</ha-dialog>
`;
}
static get properties() {
return {
hass: Object,
_ozwLog: String,
_dialogClosedCallback: Function,
_opened: {
type: Boolean,
value: false,
},
_intervalId: String,
_numLogLines: {
type: Number,
},
};
}
ready() {
super.ready();
this.addEventListener("iron-overlay-closed", (ev) =>
this._dialogClosed(ev)
);
}
showDialog({ _ozwLog, hass, _tail, _numLogLines, dialogClosedCallback }) {
this.hass = hass;
this._ozwLog = _ozwLog;
this._opened = true;
this._dialogClosedCallback = dialogClosedCallback;
this._numLogLines = _numLogLines;
if (_tail) {
this.setProperties({
_intervalId: setInterval(() => {
this._refreshLog();
}, 1500),
});
}
}
closeDialog() {
clearInterval(this._intervalId);
this._opened = false;
const closedEvent = true;
this._dialogClosedCallback({ closedEvent });
this._dialogClosedCallback = null;
}
async _refreshLog() {
const info = await this.hass.callApi(
"GET",
"zwave/ozwlog?lines=" + this._numLogLines
);
this.setProperties({ _ozwLog: info });
}
}
customElements.define("zwave-log-dialog", ZwaveLogDialog);

View File

@@ -1,160 +0,0 @@
import "@material/mwc-button";
import "@polymer/paper-input/paper-input";
import { html } from "@polymer/polymer/lib/utils/html-tag";
/* eslint-plugin-disable lit */
import { PolymerElement } from "@polymer/polymer/polymer-element";
import isPwa from "../../../../../common/config/is_pwa";
import "../../../../../components/ha-card";
import { EventsMixin } from "../../../../../mixins/events-mixin";
import LocalizeMixin from "../../../../../mixins/localize-mixin";
import "../../../../../styles/polymer-ha-style";
import "../../../ha-config-section";
let registeredDialog = false;
class OzwLog extends LocalizeMixin(EventsMixin(PolymerElement)) {
static get template() {
return html`
<style include="iron-flex ha-style">
.content {
margin-top: 24px;
}
ha-card {
margin: 0 auto;
max-width: 600px;
}
.device-picker {
padding-left: 24px;
padding-right: 24px;
padding-bottom: 24px;
}
</style>
<ha-config-section is-wide="[[isWide]]">
<span slot="header">
[[localize('ui.panel.config.zwave.ozw_log.header')]]
</span>
<span slot="introduction">
[[localize('ui.panel.config.zwave.ozw_log.introduction')]]
</span>
<ha-card class="content">
<div class="device-picker">
<paper-input label="[[localize('ui.panel.config.zwave.ozw_log.last_log_lines')]]" type="number" min="0" max="1000" step="10" value="{{numLogLines}}">
</paper-input>
</div>
<div class="card-actions">
<mwc-button raised="true" on-click="_openLogWindow">[[localize('ui.panel.config.zwave.ozw_log.load')]]</mwc-button>
<mwc-button raised="true" on-click="_tailLog" disabled="{{_completeLog}}">[[localize('ui.panel.config.zwave.ozw_log.tail')]]</mwc-button>
</ha-card>
</ha-config-section>
`;
}
static get properties() {
return {
hass: Object,
isWide: {
type: Boolean,
value: false,
},
_ozwLogs: String,
_completeLog: {
type: Boolean,
value: true,
},
numLogLines: {
type: Number,
value: 0,
observer: "_isCompleteLog",
},
_intervalId: String,
tail: Boolean,
};
}
async _tailLog() {
this.setProperties({ tail: true });
const ozwWindow = await this._openLogWindow();
if (!isPwa()) {
this.setProperties({
_intervalId: setInterval(() => {
this._refreshLog(ozwWindow);
}, 1500),
});
}
}
async _openLogWindow() {
const info = await this.hass.callApi(
"GET",
"zwave/ozwlog?lines=" + this.numLogLines
);
this.setProperties({ _ozwLogs: info });
if (isPwa()) {
this._showOzwlogDialog();
return -1;
}
const ozwWindow = open("", "ozwLog", "toolbar");
ozwWindow.document.body.innerHTML = `<pre>${this._ozwLogs}</pre>`;
return ozwWindow;
}
async _refreshLog(ozwWindow) {
if (ozwWindow.closed === true) {
clearInterval(this._intervalId);
this.setProperties({ _intervalId: null });
} else {
const info = await this.hass.callApi(
"GET",
"zwave/ozwlog?lines=" + this.numLogLines
);
this.setProperties({ _ozwLogs: info });
ozwWindow.document.body.innerHTML = `<pre>${this._ozwLogs}</pre>`;
}
}
_isCompleteLog() {
if (this.numLogLines !== "0") {
this.setProperties({ _completeLog: false });
} else {
this.setProperties({ _completeLog: true });
}
}
connectedCallback() {
super.connectedCallback();
if (!registeredDialog) {
registeredDialog = true;
this.fire("register-dialog", {
dialogShowEvent: "show-ozwlog-dialog",
dialogTag: "zwave-log-dialog",
dialogImport: () => import("./zwave-log-dialog"),
});
}
}
_showOzwlogDialog() {
this.fire("show-ozwlog-dialog", {
hass: this.hass,
_numLogLines: this.numLogLines,
_ozwLog: this._ozwLogs,
_tail: this.tail,
dialogClosedCallback: () => this._dialogClosed(),
});
}
_dialogClosed() {
this.setProperties({
tail: false,
});
}
}
customElements.define("ozw-log", OzwLog);

View File

@@ -1,573 +0,0 @@
import "@material/mwc-button/mwc-button";
import "@polymer/app-layout/app-header/app-header";
import "@polymer/app-layout/app-toolbar/app-toolbar";
import { UnsubscribeFunc } from "home-assistant-js-websocket";
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { customElement, property, state } from "lit/decorators";
import { isComponentLoaded } from "../../../../../common/config/is_component_loaded";
import { computeStateDomain } from "../../../../../common/entity/compute_state_domain";
import { computeStateName } from "../../../../../common/entity/compute_state_name";
import "../../../../../components/buttons/ha-call-api-button";
import "../../../../../components/buttons/ha-call-service-button";
import "../../../../../components/ha-alert";
import "../../../../../components/ha-card";
import "../../../../../components/ha-circular-progress";
import "../../../../../components/ha-icon";
import "../../../../../components/ha-icon-button";
import {
computeDeviceName,
DeviceRegistryEntry,
fetchDeviceRegistry,
subscribeDeviceRegistry,
} from "../../../../../data/device_registry";
import {
fetchMigrationConfig,
fetchNetworkStatus,
startZwaveJsConfigFlow,
ZWaveMigrationConfig,
ZWaveNetworkStatus,
ZWAVE_NETWORK_STATE_STOPPED,
} from "../../../../../data/zwave";
import {
fetchZwaveNetworkStatus as fetchZwaveJsNetworkStatus,
fetchZwaveNodeStatus,
getZwaveJsIdentifiersFromDevice,
migrateZwave,
subscribeZwaveNodeReady,
ZWaveJsMigrationData,
} from "../../../../../data/zwave_js";
import { showConfigFlowDialog } from "../../../../../dialogs/config-flow/show-dialog-config-flow";
import { showAlertDialog } from "../../../../../dialogs/generic/show-dialog-box";
import "../../../../../layouts/hass-subpage";
import { haStyle } from "../../../../../resources/styles";
import type { HomeAssistant, Route } from "../../../../../types";
import "../../../ha-config-section";
@customElement("zwave-migration")
export class ZwaveMigration extends LitElement {
@property({ type: Object }) public hass!: HomeAssistant;
@property({ type: Object }) public route!: Route;
@property({ type: Boolean }) public narrow!: boolean;
@property({ type: Boolean }) public isWide!: boolean;
@state() private _networkStatus?: ZWaveNetworkStatus;
@state() private _step = 0;
@state() private _stoppingNetwork = false;
@state() private _migrationConfig?: ZWaveMigrationConfig;
@state() private _migrationData?: ZWaveJsMigrationData;
@state() private _migratedZwaveEntities?: string[];
@state() private _deviceNameLookup: { [id: string]: string } = {};
@state() private _waitingOnDevices?: DeviceRegistryEntry[];
private _zwaveJsEntryId?: string;
private _nodeReadySubscriptions?: Promise<UnsubscribeFunc>[];
private _unsub?: Promise<UnsubscribeFunc>;
private _unsubDevices?: UnsubscribeFunc;
public disconnectedCallback(): void {
this._unsubscribe();
if (this._unsubDevices) {
this._unsubDevices();
this._unsubDevices = undefined;
}
}
protected render(): TemplateResult {
return html`
<hass-subpage
.hass=${this.hass}
.narrow=${this.narrow}
.route=${this.route}
back-path="/config/zwave"
>
<ha-config-section .narrow=${this.narrow} .isWide=${this.isWide}>
<div slot="header">
${this.hass.localize(
"ui.panel.config.zwave.migration.zwave_js.header"
)}
</div>
<div slot="introduction">
${this.hass.localize(
"ui.panel.config.zwave.migration.zwave_js.introduction"
)}
</div>
${html`
${this._step === 0
? html`
<ha-card class="content" header="Introduction">
<div class="card-content">
<p>
This wizard will walk through the following steps to
migrate from the legacy Z-Wave integration to Z-Wave JS.
</p>
<ol>
<li>Stop the Z-Wave network</li>
${!isComponentLoaded(this.hass, "hassio")
? html`<li>Configure and start Z-Wave JS</li>`
: ""}
<li>Set up the Z-Wave JS integration</li>
<li>
Migrate entities and devices to the new integration
</li>
<li>Remove legacy Z-Wave integration</li>
</ol>
<p>
<b>
${isComponentLoaded(this.hass, "hassio")
? html`Please
<a href="/hassio/backups">make a backup</a>
before proceeding.`
: "Please make a backup of your installation before proceeding."}
</b>
</p>
</div>
<div class="card-actions">
<mwc-button @click=${this._continue}>
Continue
</mwc-button>
</div>
</ha-card>
`
: this._step === 1
? html`
<ha-card class="content" header="Stop Z-Wave Network">
<div class="card-content">
<p>
We need to stop the Z-Wave network to perform the
migration. Home Assistant will not be able to control
Z-Wave devices while the network is stopped.
</p>
${Object.values(this.hass.states)
.filter(
(entityState) =>
computeStateDomain(entityState) === "zwave" &&
!["ready", "sleeping"].includes(entityState.state)
)
.map(
(entityState) =>
html`<ha-alert alert-type="warning">
Device ${computeStateName(entityState)}
(${entityState.entity_id}) is not ready yet! For
the best result, wake the device up if it is
battery powered and wait for this device to become
ready.
</ha-alert>`
)}
${this._stoppingNetwork
? html`
<div class="flex-container">
<ha-circular-progress
active
></ha-circular-progress>
<div><p>Stopping Z-Wave Network...</p></div>
</div>
`
: ``}
</div>
<div class="card-actions">
<mwc-button @click=${this._stopNetwork}>
Stop Network
</mwc-button>
</div>
</ha-card>
`
: this._step === 2
? html`
<ha-card class="content" header="Set up Z-Wave JS">
<div class="card-content">
<p>Now it's time to set up the Z-Wave JS integration.</p>
${isComponentLoaded(this.hass, "hassio")
? html`
<p>
Z-Wave JS runs as a Home Assistant add-on that
will be setup next. Make sure to check the
checkbox to use the add-on.
</p>
`
: html`
<p>
You are not running Home Assistant OS (the default
installation type) or Home Assistant Supervised,
so we can not setup Z-Wave JS automaticaly. Follow
the
<a
href="https://www.home-assistant.io/integrations/zwave_js/#advanced-installation-instructions"
target="_blank"
rel="noreferrer"
>advanced installation instructions</a
>
to install Z-Wave JS.
</p>
<p>
Here's the current Z-Wave configuration. You'll
need these values when setting up Z-Wave JS.
</p>
${this._migrationConfig
? html`<blockquote>
USB Path: ${this._migrationConfig.usb_path}<br />
Network Key:
${this._migrationConfig.network_key}
</blockquote>`
: ``}
<p>
Once Z-Wave JS is installed and running, click
'Continue' to set up the Z-Wave JS integration and
migrate your devices and entities.
</p>
`}
</div>
<div class="card-actions">
<mwc-button @click=${this._setupZwaveJs}>
Continue
</mwc-button>
</div>
</ha-card>
`
: this._step === 3
? html`
<ha-card
class="content"
header="Migrate devices and entities"
>
<div class="card-content">
<p>
Now it's time to migrate your devices and entities from
the legacy Z-Wave integration to the Z-Wave JS
integration, to make sure all your UI's and automations
keep working.
</p>
${this._waitingOnDevices?.map(
(device) =>
html`<ha-alert alert-type="warning">
Device ${computeDeviceName(device, this.hass)} is
not ready yet! For the best result, wake the device
up if it is battery powered and wait for this device
to become ready.
</ha-alert>`
)}
${this._migrationData
? html`
<p>Below is a list of what will be migrated.</p>
${this._migratedZwaveEntities!.length !==
this._migrationData.zwave_entity_ids.length
? html`<ha-alert
alert-type="warning"
title="Not all entities can be migrated!"
>
The following entities will not be migrated
and might need manual adjustments to your
config:
</ha-alert>
<ul>
${this._migrationData.zwave_entity_ids.map(
(entity_id) =>
!this._migratedZwaveEntities!.includes(
entity_id
)
? html`<li>
${entity_id in this.hass.states
? computeStateName(
this.hass.states[entity_id]
)
: ""}
(${entity_id})
</li>`
: ""
)}
</ul>`
: ""}
${Object.keys(
this._migrationData.migration_device_map
).length
? html`<h3>Devices that will be migrated:</h3>
<ul>
${Object.keys(
this._migrationData.migration_device_map
).map(
(device_id) =>
html`<li>
${this._deviceNameLookup[device_id] ||
device_id}
</li>`
)}
</ul>`
: ""}
${Object.keys(
this._migrationData.migration_entity_map
).length
? html`<h3>Entities that will be migrated:</h3>
<ul>
${Object.keys(
this._migrationData.migration_entity_map
).map(
(entity_id) => html`<li>
${entity_id in this.hass.states
? computeStateName(
this.hass.states[entity_id]
)
: ""}
(${entity_id})
</li>`
)}
</ul>`
: ""}
`
: html` <div class="flex-container">
<p>Loading migration data...</p>
<ha-circular-progress active>
</ha-circular-progress>
</div>`}
</div>
<div class="card-actions">
<mwc-button @click=${this._doMigrate}>
Migrate
</mwc-button>
</div>
</ha-card>
`
: this._step === 4
? html`<ha-card class="content" header="Done!">
<div class="card-content">
That was all! You are now migrated to the new Z-Wave JS
integration, check if all your devices and entities are back
the way they where, if not all entities could be migrated
you might have to change those manually.
<p>
If you have 'zwave' in your configurtion.yaml file, you
should remove it now.
</p>
</div>
<div class="card-actions">
<a
href=${`/config/zwave_js?config_entry=${this._zwaveJsEntryId}`}
>
<mwc-button> Go to Z-Wave JS config panel </mwc-button>
</a>
</div>
</ha-card>`
: ""}
`}
</ha-config-section>
</hass-subpage>
`;
}
private async _getMigrationConfig(): Promise<void> {
this._migrationConfig = await fetchMigrationConfig(this.hass!);
}
private async _unsubscribe(): Promise<void> {
if (this._unsub) {
(await this._unsub)();
this._unsub = undefined;
}
}
private _continue(): void {
this._step++;
}
private async _stopNetwork(): Promise<void> {
this._stoppingNetwork = true;
await this._getNetworkStatus();
if (this._networkStatus?.state === ZWAVE_NETWORK_STATE_STOPPED) {
this._networkStopped();
return;
}
this._unsub = this.hass!.connection.subscribeEvents(
() => this._networkStopped(),
"zwave.network_stop"
);
this.hass!.callService("zwave", "stop_network");
}
private async _setupZwaveJs() {
const zwaveJsConfigFlow = await startZwaveJsConfigFlow(this.hass);
showConfigFlowDialog(this, {
continueFlowId: zwaveJsConfigFlow.flow_id,
dialogClosedCallback: (params) => {
if (params.entryId) {
this._zwaveJsEntryId = params.entryId;
this._getZwaveJSNodesStatus();
this._step = 3;
}
},
showAdvanced: this.hass.userData?.showAdvanced,
});
this.hass.loadBackendTranslation("title", "zwave_js", true);
}
private async _getZwaveJSNodesStatus() {
if (this._nodeReadySubscriptions?.length) {
const unsubs = await Promise.all(this._nodeReadySubscriptions);
unsubs.forEach((unsub) => {
unsub();
});
}
this._nodeReadySubscriptions = [];
const networkStatus = await fetchZwaveJsNetworkStatus(
this.hass,
this._zwaveJsEntryId!
);
const nodeStatePromisses = networkStatus.controller.nodes.map((nodeId) =>
fetchZwaveNodeStatus(this.hass, this._zwaveJsEntryId!, nodeId)
);
const nodesNotReady = (await Promise.all(nodeStatePromisses)).filter(
(node) => !node.ready
);
// eslint-disable-next-line no-console
console.log("waiting for nodes to be ready", nodesNotReady);
this._getMigrationData();
if (nodesNotReady.length === 0) {
this._waitingOnDevices = [];
return;
}
this._nodeReadySubscriptions = nodesNotReady.map((node) =>
subscribeZwaveNodeReady(
this.hass,
this._zwaveJsEntryId!,
node.node_id,
() => {
this._getZwaveJSNodesStatus();
}
)
);
const deviceReg: DeviceRegistryEntry[] = await fetchDeviceRegistry(
this.hass.connection
);
this._waitingOnDevices = deviceReg.filter((device) => {
const identifiers = getZwaveJsIdentifiersFromDevice(device);
if (
!identifiers ||
Number(identifiers.home_id) !== networkStatus.controller.home_id
) {
return false;
}
return nodesNotReady.some((node) => identifiers.node_id === node.node_id);
});
}
private async _getMigrationData() {
try {
this._migrationData = await migrateZwave(
this.hass,
this._zwaveJsEntryId!,
true
);
} catch (err: any) {
showAlertDialog(this, {
title: "Failed to get migration data!",
text:
err.code === "unknown_command"
? "Restart Home Assistant and try again."
: err.message,
});
return;
}
this._migratedZwaveEntities = Object.keys(
this._migrationData.migration_entity_map
);
if (Object.keys(this._migrationData.migration_device_map).length) {
this._fetchDevices();
}
}
private _fetchDevices() {
this._unsubDevices = subscribeDeviceRegistry(
this.hass.connection,
(devices) => {
if (!this._migrationData) {
return;
}
const migrationDevices = Object.keys(
this._migrationData.migration_device_map
);
const deviceNameLookup = {};
devices.forEach((device) => {
if (migrationDevices.includes(device.id)) {
deviceNameLookup[device.id] = computeDeviceName(device, this.hass);
}
});
this._deviceNameLookup = deviceNameLookup;
}
);
}
private async _doMigrate() {
const data = await migrateZwave(this.hass, this._zwaveJsEntryId!, false);
if (!data.migrated) {
showAlertDialog(this, { title: "Migration failed!" });
return;
}
this._step = 4;
}
private _networkStopped(): void {
this._unsubscribe();
this._getMigrationConfig();
this._stoppingNetwork = false;
this._step = 2;
}
private async _getNetworkStatus(): Promise<void> {
this._networkStatus = await fetchNetworkStatus(this.hass!);
}
static get styles(): CSSResultGroup {
return [
haStyle,
css`
.content {
margin-top: 24px;
}
.flex-container {
display: flex;
align-items: center;
}
.flex-container ha-circular-progress {
margin-right: 20px;
}
blockquote {
display: block;
background-color: var(--secondary-background-color);
color: var(--primary-text-color);
padding: 8px;
margin: 8px 0;
font-size: 0.9em;
font-family: monospace;
}
ha-card {
margin: 0 auto;
max-width: 600px;
}
`,
];
}
}
declare global {
interface HTMLElementTagNameMap {
"zwave-migration": ZwaveMigration;
}
}

View File

@@ -1,302 +0,0 @@
import { mdiCheckboxMarkedCircle, mdiClose, mdiHelpCircle } from "@mdi/js";
import { UnsubscribeFunc } from "home-assistant-js-websocket";
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { customElement, property, state } from "lit/decorators";
import "../../../../../components/buttons/ha-call-api-button";
import "../../../../../components/buttons/ha-call-service-button";
import "../../../../../components/ha-card";
import "../../../../../components/ha-circular-progress";
import "../../../../../components/ha-svg-icon";
import "../../../../../components/ha-icon-button";
import "../../../../../components/ha-service-description";
import {
fetchNetworkStatus,
ZWaveNetworkStatus,
ZWAVE_NETWORK_STATE_AWAKED,
ZWAVE_NETWORK_STATE_READY,
ZWAVE_NETWORK_STATE_STARTED,
ZWAVE_NETWORK_STATE_STOPPED,
} from "../../../../../data/zwave";
import { haStyle } from "../../../../../resources/styles";
import { HomeAssistant } from "../../../../../types";
import { documentationUrl } from "../../../../../util/documentation-url";
import "../../../ha-config-section";
@customElement("zwave-network")
export class ZwaveNetwork extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property() public isWide!: boolean;
@state() private _showHelp = false;
@state() private _networkStatus?: ZWaveNetworkStatus;
@state() private _unsubs: Array<Promise<UnsubscribeFunc>> = [];
public disconnectedCallback(): void {
this._unsubscribe();
}
protected firstUpdated(changedProps): void {
super.firstUpdated(changedProps);
this._getNetworkStatus();
this._subscribe();
}
protected render(): TemplateResult {
return html`
<ha-config-section .isWide=${this.isWide}>
<div class="sectionHeader" slot="header">
<span>
${this.hass!.localize(
"ui.panel.config.zwave.network_management.header"
)}
</span>
<ha-icon-button
class="toggle-help-icon"
@click=${this._onHelpTap}
.path=${mdiHelpCircle}
.label=${this.hass!.localize("ui.common.help")}
></ha-icon-button>
</div>
<div slot="introduction">
${this.hass!.localize(
"ui.panel.config.zwave.network_management.introduction"
)}
<p>
<a
href=${documentationUrl(this.hass, "/docs/z-wave/control-panel/")}
target="_blank"
rel="noreferrer"
>
${this.hass!.localize("ui.panel.config.zwave.learn_more")}
</a>
</p>
</div>
${this._networkStatus
? html`
<ha-card class="content network-status">
<div class="details">
${this._networkStatus.state === ZWAVE_NETWORK_STATE_STOPPED
? html`
<ha-svg-icon .path=${mdiClose}></ha-svg-icon>
${this.hass!.localize(
"ui.panel.config.zwave.network_status.network_stopped"
)}
`
: this._networkStatus.state === ZWAVE_NETWORK_STATE_STARTED
? html`
<ha-circular-progress active></ha-circular-progress>
${this.hass!.localize(
"ui.panel.config.zwave.network_status.network_starting"
)}<br />
<small>
${this.hass!.localize(
"ui.panel.config.zwave.network_status.network_starting_note"
)}
</small>
`
: this._networkStatus.state === ZWAVE_NETWORK_STATE_AWAKED
? html`
<ha-svg-icon
.path=${mdiCheckboxMarkedCircle}
></ha-svg-icon>
${this.hass!.localize(
"ui.panel.config.zwave.network_status.network_started"
)}<br />
<small>
${this.hass!.localize(
"ui.panel.config.zwave.network_status.network_started_note_some_queried"
)}
</small>
`
: this._networkStatus.state === ZWAVE_NETWORK_STATE_READY
? html`
${this.hass!.localize(
"ui.panel.config.zwave.network_status.network_started"
)}<br />
<small>
${this.hass!.localize(
"ui.panel.config.zwave.network_status.network_started_note_all_queried"
)}
</small>
`
: ""}
</div>
<div class="card-actions">
${this._networkStatus.state >= ZWAVE_NETWORK_STATE_AWAKED
? html`
${this._generateServiceButton("stop_network")}
${this._generateServiceButton("heal_network")}
${this._generateServiceButton("test_network")}
`
: html` ${this._generateServiceButton("start_network")} `}
</div>
${this._networkStatus.state >= ZWAVE_NETWORK_STATE_AWAKED
? html`
<div class="card-actions">
${this._generateServiceButton("soft_reset")}
<ha-call-api-button
.hass=${this.hass}
path="zwave/saveconfig"
>
${this.hass!.localize(
"ui.panel.config.zwave.services.save_config"
)}
</ha-call-api-button>
</div>
`
: ""}
</ha-card>
${this._networkStatus.state >= ZWAVE_NETWORK_STATE_AWAKED
? html`
<ha-card class="content">
<div class="card-actions">
${this._generateServiceButton("add_node_secure")}
${this._generateServiceButton("add_node")}
${this._generateServiceButton("remove_node")}
</div>
<div class="card-actions">
${this._generateServiceButton("cancel_command")}
</div>
</ha-card>
`
: ""}
`
: ""}
</ha-config-section>
`;
}
private async _getNetworkStatus(): Promise<void> {
this._networkStatus = await fetchNetworkStatus(this.hass!);
}
private _subscribe(): void {
this._unsubs = [
"zwave.network_start",
"zwave.network_stop",
"zwave.network_ready",
"zwave.network_complete",
"zwave.network_complete_some_dead",
].map((e) =>
this.hass!.connection.subscribeEvents(
(event) => this._handleEvent(event),
e
)
);
}
private _unsubscribe(): void {
while (this._unsubs.length) {
this._unsubs.pop()!.then((unsub) => unsub());
}
}
private _handleEvent(event) {
if (event.event_type === "zwave.network_start") {
// Optimistically set the state, wait 1s and poll the backend
// The backend will still report a state of 0 when the 'network_start'
// event is first fired.
if (this._networkStatus) {
this._networkStatus = { ...this._networkStatus, state: 5 };
}
setTimeout(() => this._getNetworkStatus, 1000);
} else {
this._getNetworkStatus();
}
}
private _onHelpTap(): void {
this._showHelp = !this._showHelp;
}
private _generateServiceButton(service: string) {
return html`
<ha-call-service-button
.hass=${this.hass}
domain="zwave"
service=${service}
>
${this.hass!.localize("ui.panel.config.zwave.services." + service)}
</ha-call-service-button>
<ha-service-description
.hass=${this.hass}
domain="zwave"
service=${service}
?hidden=${!this._showHelp}
>
</ha-service-description>
`;
}
static get styles(): CSSResultGroup {
return [
haStyle,
css`
.content {
margin-top: 24px;
}
.sectionHeader {
position: relative;
padding-right: 40px;
}
.network-status {
text-align: center;
}
.network-status div.details {
font-size: 1.5rem;
padding: 24px;
}
.network-status ha-svg-icon {
display: block;
margin: 0px auto 16px;
width: 48px;
height: 48px;
}
.network-status small {
font-size: 1rem;
}
ha-card {
margin: 0 auto;
max-width: 600px;
}
.card-actions.warning ha-call-service-button {
color: var(--error-color);
}
.toggle-help-icon {
position: absolute;
top: -6px;
right: 0;
color: var(--primary-color);
}
ha-service-description {
display: block;
color: grey;
padding: 0 8px 12px;
}
[hidden] {
display: none;
}
`,
];
}
}
declare global {
interface HTMLElementTagNameMap {
"zwave-network": ZwaveNetwork;
}
}

View File

@@ -1,388 +0,0 @@
import "@polymer/paper-dropdown-menu/paper-dropdown-menu";
import "@polymer/paper-input/paper-input";
import "@polymer/paper-item/paper-item";
import "@polymer/paper-listbox/paper-listbox";
import {
css,
CSSResultGroup,
html,
LitElement,
PropertyValues,
TemplateResult,
} from "lit";
import { customElement, property, state } from "lit/decorators";
import "../../../../../components/buttons/ha-call-service-button";
import "../../../../../components/ha-card";
import {
fetchNodeConfig,
ZWaveConfigItem,
ZWaveConfigServiceData,
ZWaveNode,
} from "../../../../../data/zwave";
import { haStyle } from "../../../../../resources/styles";
import { HomeAssistant } from "../../../../../types";
@customElement("zwave-node-config")
export class ZwaveNodeConfig extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property() public nodes: ZWaveNode[] = [];
@property() public config: ZWaveConfigItem[] = [];
@property() public selectedNode = -1;
@state() private _configItem?: ZWaveConfigItem;
@state() private _wakeupInput = -1;
@state() private _selectedConfigParameter = -1;
@state() private _selectedConfigValue: number | string = -1;
protected render(): TemplateResult {
return html`
<div class="content">
<ha-card
.header=${this.hass!.localize(
"ui.panel.config.zwave.node_config.header"
)}
>
${"wake_up_interval" in this.nodes[this.selectedNode].attributes
? html`
<div class="card-actions">
<paper-input
.floatLabel=${this.hass!.localize(
"ui.panel.config.zwave.common.wakeup_interval"
)}
type="number"
.value=${this._wakeupInput !== -1
? this._wakeupInput
: this.hass!.localize(
"ui.panel.config.zwave.common.unknown"
)}
@value-changed=${this._onWakeupIntervalChanged}
.placeholder=${this.nodes[this.selectedNode].attributes
.wake_up_interval
? this.nodes[this.selectedNode].attributes
.wake_up_interval
: this.hass!.localize(
"ui.panel.config.zwave.common.unknown"
)}
>
<div suffix>
${this.hass!.localize(
"ui.panel.config.zwave.node_config.seconds"
)}
</div>
</paper-input>
<ha-call-service-button
.hass=${this.hass}
domain="zwave"
service="set_wakeup"
.serviceData=${this._computeWakeupServiceData(
this._wakeupInput
)}
>
${this.hass!.localize(
"ui.panel.config.zwave.node_config.set_wakeup"
)}
</ha-call-service-button>
</div>
`
: ""}
<div class="device-picker">
<paper-dropdown-menu
.label=${this.hass!.localize(
"ui.panel.config.zwave.node_config.config_parameter"
)}
dynamic-align
class="flex"
>
<paper-listbox
slot="dropdown-content"
.selected=${this._selectedConfigParameter}
@iron-select=${this._selectedConfigParameterChanged}
>
${this.config.map(
(entityState) => html`
<paper-item>
${entityState.key}: ${entityState.value.label}
</paper-item>
`
)}
</paper-listbox>
</paper-dropdown-menu>
</div>
${this._configItem
? html`
${this._configItem.value.type === "List"
? html`
<div class="device-picker">
<paper-dropdown-menu
.label=${this.hass!.localize(
"ui.panel.config.zwave.node_config.config_value"
)}
dynamic-align
class="flex"
.placeholder=${this._configItem.value.data}
>
<paper-listbox
slot="dropdown-content"
.selected=${this._configItem.value.data}
@iron-select=${this._configValueSelectChanged}
>
${this._configItem.value.data_items.map(
(entityState) => html`
<paper-item>${entityState}</paper-item>
`
)}
</paper-listbox>
</paper-dropdown-menu>
</div>
`
: ""}
${["Byte", "Short", "Int"].includes(this._configItem.value.type)
? html`
<div class="card-actions">
<paper-input
.label=${this._configItem.value.data_items}
type="number"
.value=${this._configItem.value.data}
.max=${this._configItem.value.max}
.min=${this._configItem.value.min}
@value-changed=${this._configValueInputChanged}
>
</paper-input>
</div>
`
: ""}
${["Bool", "Button"].includes(this._configItem.value.type)
? html`
<div class="device-picker">
<paper-dropdown-menu
.label=${this.hass!.localize(
"ui.panel.config.zwave.node_config.config_value"
)}
class="flex"
dynamic-align
.placeholder=${this._configItem.value.data}
>
<paper-listbox
slot="dropdown-content"
.selected=${this._configItem.value.data}
@iron-select=${this._configValueSelectChanged}
>
<paper-item>
${this.hass!.localize(
"ui.panel.config.zwave.node_config.true"
)}
</paper-item>
<paper-item>
${this.hass!.localize(
"ui.panel.config.zwave.node_config.false"
)}
</paper-item>
</paper-listbox>
</paper-dropdown-menu>
</div>
`
: ""}
<div class="help-text">
<span>${this._configItem.value.help}</span>
</div>
${["Bool", "Button", "Byte", "Short", "Int", "List"].includes(
this._configItem.value.type
)
? html`
<div class="card-actions">
<ha-call-service-button
.hass=${this.hass}
domain="zwave"
service="set_config_parameter"
.serviceData=${this._computeSetConfigParameterServiceData()}
>
${this.hass!.localize(
"ui.panel.config.zwave.node_config.set_config_parameter"
)}
</ha-call-service-button>
</div>
`
: ""}
`
: ""}
</ha-card>
</div>
`;
}
static get styles(): CSSResultGroup {
return [
haStyle,
css`
.content {
margin-top: 24px;
}
ha-card {
margin: 0 auto;
max-width: 600px;
}
.device-picker {
@apply --layout-horizontal;
@apply --layout-center-center;
display: -ms-flexbox;
display: -webkit-flex;
display: flex;
-ms-flex-direction: row;
-webkit-flex-direction: row;
flex-direction: row;
-ms-flex-align: center;
-webkit-align-items: center;
align-items: center;
padding-left: 24px;
padding-right: 24px;
padding-bottom: 24px;
}
.help-text {
padding-left: 24px;
padding-right: 24px;
}
.flex {
-ms-flex: 1 1 0.000000001px;
-webkit-flex: 1;
flex: 1;
-webkit-flex-basis: 0.000000001px;
flex-basis: 0.000000001px;
}
`,
];
}
protected firstUpdated(changedProps: PropertyValues): void {
super.firstUpdated(changedProps);
this.addEventListener("hass-service-called", (ev) =>
this.serviceCalled(ev)
);
}
protected updated(changedProps: PropertyValues): void {
super.updated(changedProps);
if (changedProps.has("selectedNode")) {
this._nodesChanged();
}
}
private serviceCalled(ev): void {
if (ev.detail.success) {
setTimeout(() => {
this._refreshConfig(this.selectedNode);
}, 5000);
}
}
private _nodesChanged(): void {
if (!this.nodes) {
return;
}
this._configItem = undefined;
this._wakeupInput =
this.nodes[this.selectedNode].attributes.wake_up_interval || -1;
}
private _onWakeupIntervalChanged(value: ChangeEvent): void {
this._wakeupInput = value.detail!.value;
}
private _computeWakeupServiceData(wakeupInput: number) {
return {
node_id: this.nodes[this.selectedNode].attributes.node_id,
value: wakeupInput,
};
}
private _computeSetConfigParameterServiceData():
| ZWaveConfigServiceData
| boolean {
if (this.selectedNode === -1 || typeof this._configItem === "undefined") {
return false;
}
let valueData: number | string = "";
if (["Short", "Byte", "Int"].includes(this._configItem!.value.type)) {
valueData =
typeof this._selectedConfigValue === "string"
? parseInt(this._selectedConfigValue, 10)
: this._selectedConfigValue;
}
if (["Bool", "Button", "List"].includes(this._configItem!.value.type)) {
valueData = this._selectedConfigValue;
}
return {
node_id: this.nodes[this.selectedNode].attributes.node_id,
parameter: this._configItem.key,
value: valueData,
};
}
private _selectedConfigParameterChanged(event: ItemSelectedEvent): void {
if (event.target!.selected === -1) {
return;
}
this._selectedConfigParameter = event.target!.selected;
this._configItem = this.config[event.target!.selected];
}
private _configValueSelectChanged(event: ItemSelectedEvent): void {
if (event.target!.selected === -1) {
return;
}
this._selectedConfigValue = event.target!.selectedItem.textContent;
}
private _configValueInputChanged(value: ChangeEvent): void {
this._selectedConfigValue = value.detail!.value;
}
private async _refreshConfig(selectedNode): Promise<void> {
const configData: ZWaveConfigItem[] = [];
const config = await fetchNodeConfig(
this.hass,
this.nodes[selectedNode].attributes.node_id
);
Object.keys(config).forEach((key) => {
configData.push({
key: parseInt(key, 10),
value: config[key],
});
});
this.config = configData;
this._configItem = this.config[this._selectedConfigParameter];
}
}
export interface ChangeEvent {
detail?: {
value?: any;
};
target?: EventTarget;
}
export interface PickerTarget extends EventTarget {
selected: number;
selectedItem?: any;
}
export interface ItemSelectedEvent {
target?: PickerTarget;
}
declare global {
interface HTMLElementTagNameMap {
"zwave-node-config": ZwaveNodeConfig;
}
}

View File

@@ -1,179 +0,0 @@
import "@polymer/paper-dropdown-menu/paper-dropdown-menu";
import "@polymer/paper-input/paper-input";
import "@polymer/paper-item/paper-item";
import "@polymer/paper-listbox/paper-listbox";
import { html } from "@polymer/polymer/lib/utils/html-tag";
/* eslint-plugin-disable lit */
import { PolymerElement } from "@polymer/polymer/polymer-element";
import "../../../../../components/buttons/ha-call-api-button";
import "../../../../../components/ha-card";
import LocalizeMixin from "../../../../../mixins/localize-mixin";
import "../../../../../styles/polymer-ha-style";
class ZwaveNodeProtection extends LocalizeMixin(PolymerElement) {
static get template() {
return html`
<style include="iron-flex ha-style">
.card-actions.warning ha-call-api-button {
color: var(--error-color);
}
.content {
margin-top: 24px;
}
ha-card {
margin: 0 auto;
max-width: 600px;
}
.device-picker {
@apply --layout-horizontal;
@apply --layout-center-center;
padding: 0 24px 24px 24px;
}
</style>
<div class="content">
<ha-card header="[[localize('ui.panel.config.zwave.node_management.node_protection')]]">
<div class="device-picker">
<paper-dropdown-menu label="[[localize('ui.panel.config.zwave.node_management.protection')]]" dynamic-align class="flex" placeholder="{{_loadedProtectionValue}}">
<paper-listbox slot="dropdown-content" selected="{{_selectedProtectionParameter}}">
<template is="dom-repeat" items="[[_protectionOptions]]" as="state">
<paper-item>[[state]]</paper-item>
</template>
</paper-listbox>
</paper-dropdown-menu>
</div>
<div class="card-actions">
<ha-call-api-button
hass="[[hass]]"
path="[[_nodePath]]"
data="[[_protectionData]]">
[[localize('ui.panel.config.zwave.node_management.set_protection')]]
</ha-call-service-button>
</div>
</ha-card>
</div>
`;
}
static get properties() {
return {
hass: Object,
nodes: Array,
selectedNode: {
type: Number,
value: -1,
},
protectionNode: {
type: Boolean,
value: false,
},
_protectionValueID: {
type: Number,
value: -1,
},
_selectedProtectionParameter: {
type: Number,
value: -1,
observer: "_computeProtectionData",
},
_protectionOptions: Array,
_protection: {
type: Array,
value: () => [],
},
_loadedProtectionValue: {
type: String,
value: "",
},
_protectionData: {
type: Object,
value: {},
},
_nodePath: String,
};
}
static get observers() {
return ["_nodesChanged(nodes, selectedNode)"];
}
ready() {
super.ready();
this.addEventListener("hass-api-called", (ev) => this.apiCalled(ev));
}
apiCalled(ev) {
if (ev.detail.success) {
setTimeout(() => {
this._refreshProtection(this.selectedNode);
}, 5000);
}
}
_nodesChanged() {
if (!this.nodes) return;
if (this.protection) {
if (this.protection.length === 0) {
return;
}
let options = [];
let value_id = -1;
let selected = -1;
this.protection.forEach((item) => {
if (item.key === "options") options = item.value;
else if (item.key === "value_id") value_id = item.value;
else if (item.key === "selected") selected = item.value;
});
this.setProperties({
protectionNode: true,
_protectionOptions: options,
_loadedProtectionValue: selected,
_protectionValueID: value_id,
});
}
}
async _refreshProtection(selectedNode) {
const protectionValues = [];
const protections = await this.hass.callApi(
"GET",
`zwave/protection/${this.nodes[selectedNode].attributes.node_id}`
);
Object.keys(protections).forEach((key) => {
protectionValues.push({
key,
value: protections[key],
});
});
this.setProperties({
_protection: protectionValues,
_selectedProtectionParameter: -1,
_loadedProtectionValue: this.protection[1].value,
});
}
_computeProtectionData(selectedProtectionParameter) {
if (this.selectedNode === -1 || selectedProtectionParameter === -1) return;
this._protectionData = {
selection: this._protectionOptions[selectedProtectionParameter],
value_id: this._protectionValueID,
};
this._nodePath = `zwave/protection/${
this.nodes[this.selectedNode].attributes.node_id
}`;
}
}
customElements.define("zwave-node-protection", ZwaveNodeProtection);

View File

@@ -1,226 +0,0 @@
import "@polymer/paper-dropdown-menu/paper-dropdown-menu";
import "@polymer/paper-input/paper-input";
import "@polymer/paper-item/paper-item";
import "@polymer/paper-listbox/paper-listbox";
import { html } from "@polymer/polymer/lib/utils/html-tag";
/* eslint-plugin-disable lit */
import { PolymerElement } from "@polymer/polymer/polymer-element";
import "../../../../../components/buttons/ha-call-service-button";
import "../../../../../components/ha-card";
import "../../../../../styles/polymer-ha-style";
class ZwaveUsercodes extends PolymerElement {
static get template() {
return html`
<style include="iron-flex ha-style">
.content {
margin-top: 24px;
}
ha-card {
margin: 0 auto;
max-width: 600px;
}
.device-picker {
@apply --layout-horizontal;
@apply --layout-center-center;
padding-left: 24px;
padding-right: 24px;
padding-bottom: 24px;
}
pre {
font-family: var(--code-font-family, monospace);
}
</style>
<div class="content">
<ha-card header="Node user codes">
<div class="device-picker">
<paper-dropdown-menu
label="Code slot"
dynamic-align=""
class="flex"
>
<paper-listbox
slot="dropdown-content"
selected="{{_selectedUserCode}}"
>
<template is="dom-repeat" items="[[userCodes]]" as="state">
<paper-item
>[[_computeSelectCaptionUserCodes(state)]]</paper-item
>
</template>
</paper-listbox>
</paper-dropdown-menu>
</div>
<template is="dom-if" if="[[_isUserCodeSelected(_selectedUserCode)]]">
<div class="card-actions">
<paper-input
label="User code"
type="text"
allowed-pattern="[0-9,a-f,x,\\\\]"
maxlength="40"
minlength="16"
value="{{_selectedUserCodeValue}}"
>
</paper-input>
<pre>Ascii: [[_computedCodeOutput]]</pre>
</div>
<div class="card-actions">
<ha-call-service-button
hass="[[hass]]"
domain="lock"
service="set_usercode"
service-data='[[_computeUserCodeServiceData(_selectedUserCodeValue, "Add")]]'
>
Set Usercode
</ha-call-service-button>
<ha-call-service-button
hass="[[hass]]"
domain="lock"
service="clear_usercode"
service-data='[[_computeUserCodeServiceData(_selectedUserCode, "Delete")]]'
>
Delete Usercode
</ha-call-service-button>
</div>
</template>
</ha-card>
</div>
`;
}
static get properties() {
return {
hass: Object,
nodes: Array,
selectedNode: {
type: Number,
observer: "_selectedNodeChanged",
},
userCodes: Object,
_selectedUserCode: {
type: Number,
value: -1,
observer: "_selectedUserCodeChanged",
},
_selectedUserCodeValue: String,
_computedCodeOutput: {
type: String,
value: "",
},
};
}
ready() {
super.ready();
this.addEventListener("hass-service-called", (ev) =>
this.serviceCalled(ev)
);
}
serviceCalled(ev) {
if (ev.detail.success) {
setTimeout(() => {
this._refreshUserCodes(this.selectedNode);
}, 5000);
}
}
_isUserCodeSelected(selectedUserCode) {
if (selectedUserCode === -1) return false;
return true;
}
_computeSelectCaptionUserCodes(stateObj) {
return `${stateObj.key}: ${stateObj.value.label}`;
}
_selectedUserCodeChanged(selectedUserCode) {
if (this._selectedUserCode === -1 || selectedUserCode === -1) return;
const value = this.userCodes[selectedUserCode].value.code;
this.setProperties({
_selectedUserCodeValue: this._a2hex(value),
_computedCodeOutput: `[${this._hex2a(this._a2hex(value))}]`,
});
}
_computeUserCodeServiceData(selectedUserCodeValue, type) {
if (this.selectedNode === -1 || !selectedUserCodeValue) return -1;
let serviceData = null;
let valueData = null;
if (type === "Add") {
valueData = this._hex2a(selectedUserCodeValue);
this._computedCodeOutput = `[${valueData}]`;
serviceData = {
node_id: this.nodes[this.selectedNode].attributes.node_id,
code_slot: this._selectedUserCode,
usercode: valueData,
};
}
if (type === "Delete") {
serviceData = {
node_id: this.nodes[this.selectedNode].attributes.node_id,
code_slot: this._selectedUserCode,
};
}
return serviceData;
}
async _refreshUserCodes(selectedNode) {
this.setProperties({ _selectedUserCodeValue: "" });
const userCodes = [];
const userCodeData = await this.hass.callApi(
"GET",
`zwave/usercodes/${this.nodes[selectedNode].attributes.node_id}`
);
Object.keys(userCodeData).forEach((key) => {
userCodes.push({
key,
value: userCodeData[key],
});
});
this.setProperties({ userCodes: userCodes });
this._selectedUserCodeChanged(this._selectedUserCode);
}
_a2hex(str) {
const arr = [];
let output = "";
for (let i = 0, l = str.length; i < l; i++) {
const hex = Number(str.charCodeAt(i)).toString(16);
if (hex === "0") {
output = "00";
} else {
output = hex;
}
arr.push("\\x" + output);
}
return arr.join("");
}
_hex2a(hexx) {
const hex = hexx.toString();
const hexMod = hex.replace(/\\x/g, "");
let str = "";
for (let i = 0; i < hexMod.length; i += 2) {
str += String.fromCharCode(parseInt(hexMod.substr(i, 2), 16));
}
return str;
}
_selectedNodeChanged() {
if (this.selectedNode === -1) return;
this.setProperties({ _selecteduserCode: -1 });
}
}
customElements.define("zwave-usercodes", ZwaveUsercodes);

View File

@@ -1,109 +0,0 @@
import "@polymer/paper-dropdown-menu/paper-dropdown-menu";
import "@polymer/paper-item/paper-item";
import "@polymer/paper-listbox/paper-listbox";
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { customElement, property, state } from "lit/decorators";
import "../../../../../components/buttons/ha-call-service-button";
import "../../../../../components/ha-card";
import { ZWaveValue } from "../../../../../data/zwave";
import { haStyle } from "../../../../../resources/styles";
import { HomeAssistant } from "../../../../../types";
@customElement("zwave-values")
export class ZwaveValues extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property() public values: ZWaveValue[] = [];
@state() private _selectedValue = -1;
protected render(): TemplateResult {
return html`
<div class="content">
<ha-card
.header=${this.hass.localize("ui.panel.config.zwave.values.header")}
>
<div class="device-picker">
<paper-dropdown-menu
.label=${this.hass.localize("ui.panel.config.zwave.common.value")}
dynamic-align
class="flex"
>
<paper-listbox
slot="dropdown-content"
.selected=${this._selectedValue}
>
${this.values.map(
(item) => html`
<paper-item> ${this._computeCaption(item)} </paper-item>
`
)}
</paper-listbox>
</paper-dropdown-menu>
</div>
</ha-card>
</div>
`;
}
static get styles(): CSSResultGroup {
return [
haStyle,
css`
.content {
margin-top: 24px;
}
ha-card {
margin: 0 auto;
max-width: 600px;
}
.device-picker {
@apply --layout-horizontal;
@apply --layout-center-center;
display: -ms-flexbox;
display: -webkit-flex;
display: flex;
-ms-flex-direction: row;
-webkit-flex-direction: row;
flex-direction: row;
-ms-flex-align: center;
-webkit-align-items: center;
align-items: center;
padding-left: 24px;
padding-right: 24px;
padding-bottom: 24px;
}
.flex {
-ms-flex: 1 1 0.000000001px;
-webkit-flex: 1;
flex: 1;
-webkit-flex-basis: 0.000000001px;
flex-basis: 0.000000001px;
}
.help-text {
padding-left: 24px;
padding-right: 24px;
}
`,
];
}
private _computeCaption(item) {
let out = `${item.value.label}`;
out += ` (${this.hass.localize("ui.panel.config.zwave.common.instance")}:`;
out += ` ${item.value.instance},`;
out += ` ${this.hass.localize("ui.panel.config.zwave.common.index")}:`;
out += ` ${item.value.index})`;
return out;
}
}
declare global {
interface HTMLElementTagNameMap {
"zwave-values": ZwaveValues;
}
}

View File

@@ -237,8 +237,10 @@ class ZWaveJSConfigDashboard extends LitElement {
<mwc-button
@click=${this._removeNodeClicked}
.disabled=${this._status !== "connected" ||
this._network?.controller.inclusion_state !==
InclusionState.Idle}
(this._network?.controller.inclusion_state !==
InclusionState.Idle &&
this._network?.controller.inclusion_state !==
InclusionState.SmartStart)}
>
${this.hass.localize(
"ui.panel.config.zwave_js.common.remove_node"
@@ -304,7 +306,9 @@ class ZWaveJSConfigDashboard extends LitElement {
?rtl=${computeRTL(this.hass)}
@click=${this._addNodeClicked}
.disabled=${this._status !== "connected" ||
this._network?.controller.inclusion_state !== InclusionState.Idle}
(this._network?.controller.inclusion_state !== InclusionState.Idle &&
this._network?.controller.inclusion_state !==
InclusionState.SmartStart)}
>
<ha-svg-icon slot="icon" .path=${mdiPlus}></ha-svg-icon>
</ha-fab>

View File

@@ -289,7 +289,7 @@ class ZWaveJSNodeConfig extends SubscribeMixin(LitElement) {
<div class="flex">
<ha-select
.disabled=${!item.metadata.writeable}
.value=${item.value}
.value=${item.value?.toString()}
.key=${id}
.property=${item.property}
.propertyKey=${item.property_key}
@@ -345,7 +345,7 @@ class ZWaveJSNodeConfig extends SubscribeMixin(LitElement) {
if (ev.target === undefined || this._config![ev.target.key] === undefined) {
return;
}
if (this._config![ev.target.key].value === ev.target.value) {
if (this._config![ev.target.key].value?.toString() === ev.target.value) {
return;
}
this.setResult(ev.target.key, undefined);

View File

@@ -11,6 +11,7 @@ import { CoreFrontendUserData } from "../../../../data/frontend";
import {
LovelaceDashboard,
LovelaceDashboardCreateParams,
LovelaceDashboardMutableParams,
} from "../../../../data/lovelace";
import { DEFAULT_PANEL, setDefaultPanel } from "../../../../data/panel";
import { haStyleDialog } from "../../../../resources/styles";
@@ -40,7 +41,7 @@ export class DialogLovelaceDashboardDetail extends LitElement {
} else {
this._data = {
show_in_sidebar: true,
icon: "",
icon: undefined,
title: "",
require_admin: false,
mode: "storage",
@@ -264,7 +265,13 @@ export class DialogLovelaceDashboardDetail extends LitElement {
this._submitting = true;
try {
if (this._params!.dashboard) {
await this._params!.updateDashboard(this._data as LovelaceDashboard);
const values: Partial<LovelaceDashboardMutableParams> = {
require_admin: this._data!.require_admin,
show_in_sidebar: this._data!.show_in_sidebar,
icon: this._data!.icon || undefined,
title: this._data!.title,
};
await this._params!.updateDashboard(values);
} else {
await this._params!.createDashboard(
this._data as LovelaceDashboardCreateParams

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