mirror of
https://github.com/home-assistant/frontend.git
synced 2025-08-19 14:19:30 +00:00
Compare commits
83 Commits
update-int
...
20220316.0
Author | SHA1 | Date | |
---|---|---|---|
![]() |
8a5090684e | ||
![]() |
1784ba5e68 | ||
![]() |
4fbe9a7b10 | ||
![]() |
1ca9c7838a | ||
![]() |
4fc2c3ef05 | ||
![]() |
73ff8e28a8 | ||
![]() |
dde1c5e03c | ||
![]() |
01eed22592 | ||
![]() |
94ebb63589 | ||
![]() |
29119db5ce | ||
![]() |
9908162ac2 | ||
![]() |
1e929ae78a | ||
![]() |
ab5df0fe6e | ||
![]() |
d5010dda9e | ||
![]() |
4ac097f32b | ||
![]() |
5d3d15072f | ||
![]() |
5c53bc4225 | ||
![]() |
d5a307f8f4 | ||
![]() |
a27dd1e7f1 | ||
![]() |
c86ed1fb3e | ||
![]() |
7fa7a48072 | ||
![]() |
4e0fc8ee08 | ||
![]() |
5f6490e54e | ||
![]() |
db78b046a2 | ||
![]() |
c37fe1e7ff | ||
![]() |
f1ec479d41 | ||
![]() |
e01cb3ca82 | ||
![]() |
b8d3c68a7a | ||
![]() |
641003bb2a | ||
![]() |
3358fc2b18 | ||
![]() |
dcf50e055b | ||
![]() |
1fa04baa16 | ||
![]() |
84ffa2369a | ||
![]() |
cc27ddb362 | ||
![]() |
c4dc6bfb0d | ||
![]() |
4fbcc30a37 | ||
![]() |
4916527e5f | ||
![]() |
fad8a27232 | ||
![]() |
a993d3a753 | ||
![]() |
5dfe17a43a | ||
![]() |
9b6c935ffb | ||
![]() |
f4e28da0a3 | ||
![]() |
294a69d7e4 | ||
![]() |
f89b8cffcf | ||
![]() |
99fd3a1b6f | ||
![]() |
246e426182 | ||
![]() |
9f1e9b43fe | ||
![]() |
d968fe41ee | ||
![]() |
db830e9014 | ||
![]() |
fc6b594a27 | ||
![]() |
68e7ce1883 | ||
![]() |
e9003ac35e | ||
![]() |
1dd5214b42 | ||
![]() |
96738350bb | ||
![]() |
5bdecf57cf | ||
![]() |
ec12282f8c | ||
![]() |
552dbca201 | ||
![]() |
0bbc0ebb3c | ||
![]() |
ac7acc5802 | ||
![]() |
64e1d160d1 | ||
![]() |
8e51878b6d | ||
![]() |
7c94ced303 | ||
![]() |
a040e1d5e0 | ||
![]() |
87c7407857 | ||
![]() |
d0d0c44ec7 | ||
![]() |
4cdff3faea | ||
![]() |
0dac10aa23 | ||
![]() |
4b8b14a69d | ||
![]() |
9d28df31bd | ||
![]() |
8258641443 | ||
![]() |
dfcb0f6ba0 | ||
![]() |
2e10eb04b6 | ||
![]() |
b4b52d3872 | ||
![]() |
3873203721 | ||
![]() |
ccb91e0b49 | ||
![]() |
bd20c15a55 | ||
![]() |
0936fd9ae4 | ||
![]() |
adefc7a4e2 | ||
![]() |
8f8017ecff | ||
![]() |
7f086c0900 | ||
![]() |
8882624618 | ||
![]() |
ffac3d055e | ||
![]() |
09f8f816d1 |
@@ -1,4 +1,3 @@
|
||||
import "web-animations-js/web-animations-next-lite.min";
|
||||
import "../../../src/resources/ha-style";
|
||||
import "../../../src/resources/roboto";
|
||||
import "./layout/hc-lovelace";
|
||||
|
BIN
gallery/public/images/logo-with-text.png
Normal file
BIN
gallery/public/images/logo-with-text.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 67 KiB |
@@ -36,6 +36,10 @@ module.exports = [
|
||||
category: "misc",
|
||||
header: "Miscelaneous",
|
||||
},
|
||||
{
|
||||
category: "brand",
|
||||
header: "Brand",
|
||||
},
|
||||
{
|
||||
category: "user-test",
|
||||
header: "User Tests",
|
||||
|
@@ -78,6 +78,9 @@ class DemoCards extends LitElement {
|
||||
ha-formfield {
|
||||
margin-right: 16px;
|
||||
}
|
||||
#container {
|
||||
background-color: var(--primary-background-color);
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
|
@@ -12,7 +12,14 @@ class PageDescription extends HaMarkdown {
|
||||
if (!PAGES[this.page].description) {
|
||||
return html``;
|
||||
}
|
||||
|
||||
return html`
|
||||
<div class="heading">
|
||||
<div class="title">
|
||||
${PAGES[this.page].metadata.title || this.page.split("/")[1]}
|
||||
</div>
|
||||
<div class="subtitle">${PAGES[this.page].metadata.subtitle}</div>
|
||||
</div>
|
||||
${until(
|
||||
PAGES[this.page]
|
||||
.description()
|
||||
@@ -25,9 +32,22 @@ class PageDescription extends HaMarkdown {
|
||||
static styles = [
|
||||
HaMarkdown.styles,
|
||||
css`
|
||||
.heading {
|
||||
padding: 16px;
|
||||
border-bottom: 1px solid var(--secondary-background-color);
|
||||
}
|
||||
.title {
|
||||
font-size: 42px;
|
||||
line-height: 56px;
|
||||
padding-bottom: 8px;
|
||||
}
|
||||
.subtitle {
|
||||
font-size: 18px;
|
||||
line-height: 24px;
|
||||
}
|
||||
.root {
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
margin: 16px auto;
|
||||
}
|
||||
.root > *:first-child {
|
||||
margin-top: 0;
|
||||
|
@@ -5,6 +5,7 @@ import { html, css, LitElement, PropertyValues } from "lit";
|
||||
import { customElement, property, query } from "lit/decorators";
|
||||
import "../../src/components/ha-icon-button";
|
||||
import "../../src/managers/notification-manager";
|
||||
import "../../src/components/ha-expansion-panel";
|
||||
import { haStyle } from "../../src/resources/styles";
|
||||
import { PAGES, SIDEBAR } from "../build/import-pages";
|
||||
import { dynamicElement } from "../../src/common/dom/dynamic-element-directive";
|
||||
@@ -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;
|
||||
}
|
||||
`,
|
||||
];
|
||||
|
41
gallery/src/pages/brand/our-story.markdown
Normal file
41
gallery/src/pages/brand/our-story.markdown
Normal file
@@ -0,0 +1,41 @@
|
||||
---
|
||||
title: "Our story"
|
||||
---
|
||||
|
||||
## Open source home automation that puts local control and privacy first
|
||||
|
||||
Home Assistant is a free and open-source software for home automation that is designed to be the central control system for smart home devices with a focus on local control and privacy. It can be accessed via a web-based user interface, via apps for Android and iOS, or using voice commands via a supported virtual assistant like Google Assistant and Amazon Alexa.
|
||||
|
||||
IoT devices and services are supported by modular support for controlling proprietary ecosystems if they provide public access via an Open API for third-party integrations and protocols like Bluetooth, MQTT, Zigbee, and Z-Wave, After the Home Assistant software application is installed as a computer appliance it will act as a central control system for home automation. Information from all entities it sees can be used and controlled from within scripts trigger automations using scheduling and "blueprint" subroutines, e.g. for controlling lighting, climate, entertainment systems, and appliances.
|
||||
|
||||
# Open Home
|
||||
|
||||
The Open Home is our vision for the smart home. It defines the values that we put at the heart of every decision we make at Home Assistant. It’s woven into our architecture, licensing, community, and everything else.
|
||||
|
||||
The Open Home is about privacy, choice, and durability.
|
||||
|
||||
## Privacy
|
||||
|
||||
Your home should be your safe space. A place where you can be your true self without having to bother about what the world thinks of you. A place where you don’t need to act differently to avoid an algorithm categorizing your behavior. Privacy for the Open Home means that devices need to work locally. No one else needs to know if you turn on a light bulb or change the thermostat.
|
||||
|
||||
It is okay for a product to offer a cloud connection, but it should be extra and opt-in.
|
||||
|
||||
## Choice
|
||||
|
||||
Devices in your home gather data about themselves and their surroundings. Your data. Vendors shouldn’t be able to limit your access to your data or limit the interoperability of your devices with the rest of your smart home.
|
||||
|
||||
Choice for the Open Home means that devices need to make the gathered data available through local APIs. This avoids vendor lock-in and allows users to create their own smart home with devices from different manufacturers.
|
||||
|
||||
## Durability
|
||||
|
||||
If there is one thing that technology firms are very good at, it is launching new products. However, maintaining the products and making sure they keep working is an afterthought for most. The result is that vendors can decide to no longer support your device, crippling its features or even preventing it from working at all. As we install more and more devices in our home, durability is becoming more and more important. We shouldn’t have to buy everything new every couple of years because the manufacturer decided to move on.
|
||||
|
||||
Durability for the Open Home means that devices are designed and built to keep working. Not just this year, but for the next decade.
|
||||
|
||||
# Our history
|
||||
|
||||
The project was started as a Python application by Paulus Schoutsen in September 2013 and first published publicly on GitHub in November 2013. In July 2017, a managed operating system called Hass.io was initially introduced to make it easier use to use Home Assistant on single-board computers like the Raspberry Pi series. Its bundled "supervisor" management system allowed users to manage, backup, and update the local installation and introduced the option to extend the functionality of the software with add-ons.
|
||||
|
||||
An optional subscription service was introduced in December 2017 for $5/month to solve the complexities associated with secured remote access, as well as linking to Amazon Alexa and Google Assistant. Nabu Casa, Inc. was formed in September 2018 to take over the subscription service. The company's funding is based solely on revenue from the subscription service. It is used to finance the project's infrastructure and to pay for full-time employees contributing to the project.
|
||||
|
||||
In January 2020, branding was adjusted to make it easier to refer to different parts of the project. The main piece of software was renamed to Home Assistant Core, while the full suite of software with the embedded operating system and bundled "supervisor" management system was renamed to Home Assistant.
|
@@ -1,5 +1,6 @@
|
||||
---
|
||||
title: Alerts
|
||||
subtitle: An alert displays a short, important message in a way that attracts the user's attention without interrupting the user's task.
|
||||
---
|
||||
|
||||
# Alert `<ha-alert>`
|
||||
|
@@ -12,6 +12,98 @@ import { mockEntityRegistry } from "../../../../demo/src/stubs/entity_registry";
|
||||
import { mockHassioSupervisor } from "../../../../demo/src/stubs/hassio_supervisor";
|
||||
import { provideHass } from "../../../../src/fake_data/provide_hass";
|
||||
import { HomeAssistant } from "../../../../src/types";
|
||||
import { getEntity } from "../../../../src/fake_data/entity";
|
||||
|
||||
const ENTITIES = [
|
||||
getEntity("alarm_control_panel", "alarm", "disarmed", {
|
||||
friendly_name: "Alarm",
|
||||
}),
|
||||
getEntity("media_player", "livingroom", "playing", {
|
||||
friendly_name: "Livingroom",
|
||||
}),
|
||||
getEntity("media_player", "lounge", "idle", {
|
||||
friendly_name: "Lounge",
|
||||
supported_features: 444983,
|
||||
}),
|
||||
getEntity("light", "bedroom", "on", {
|
||||
friendly_name: "Bedroom",
|
||||
}),
|
||||
getEntity("switch", "coffee", "off", {
|
||||
friendly_name: "Coffee",
|
||||
}),
|
||||
];
|
||||
|
||||
const DEVICES = [
|
||||
{
|
||||
area_id: "bedroom",
|
||||
configuration_url: null,
|
||||
config_entries: ["config_entry_1"],
|
||||
connections: [],
|
||||
disabled_by: null,
|
||||
entry_type: null,
|
||||
id: "device_1",
|
||||
identifiers: [["demo", "volume1"] as [string, string]],
|
||||
manufacturer: null,
|
||||
model: null,
|
||||
name_by_user: null,
|
||||
name: "Dishwasher",
|
||||
sw_version: null,
|
||||
hw_version: null,
|
||||
via_device_id: null,
|
||||
},
|
||||
{
|
||||
area_id: "backyard",
|
||||
configuration_url: null,
|
||||
config_entries: ["config_entry_2"],
|
||||
connections: [],
|
||||
disabled_by: null,
|
||||
entry_type: null,
|
||||
id: "device_2",
|
||||
identifiers: [["demo", "pwm1"] as [string, string]],
|
||||
manufacturer: null,
|
||||
model: null,
|
||||
name_by_user: null,
|
||||
name: "Lamp",
|
||||
sw_version: null,
|
||||
hw_version: null,
|
||||
via_device_id: null,
|
||||
},
|
||||
{
|
||||
area_id: null,
|
||||
configuration_url: null,
|
||||
config_entries: ["config_entry_3"],
|
||||
connections: [],
|
||||
disabled_by: null,
|
||||
entry_type: null,
|
||||
id: "device_3",
|
||||
identifiers: [["demo", "pwm1"] as [string, string]],
|
||||
manufacturer: null,
|
||||
model: null,
|
||||
name_by_user: "User name",
|
||||
name: "Technical name",
|
||||
sw_version: null,
|
||||
hw_version: null,
|
||||
via_device_id: null,
|
||||
},
|
||||
];
|
||||
|
||||
const AREAS = [
|
||||
{
|
||||
area_id: "backyard",
|
||||
name: "Backyard",
|
||||
picture: null,
|
||||
},
|
||||
{
|
||||
area_id: "bedroom",
|
||||
name: "Bedroom",
|
||||
picture: null,
|
||||
},
|
||||
{
|
||||
area_id: "livingroom",
|
||||
name: "Livingroom",
|
||||
picture: null,
|
||||
},
|
||||
];
|
||||
|
||||
const SCHEMAS: {
|
||||
title: string;
|
||||
@@ -38,6 +130,8 @@ const SCHEMAS: {
|
||||
select: "Select",
|
||||
icon: "Icon",
|
||||
media: "Media",
|
||||
location: "Location",
|
||||
entities: "Entities",
|
||||
},
|
||||
schema: [
|
||||
{ name: "addon", selector: { addon: {} } },
|
||||
@@ -45,6 +139,7 @@ const SCHEMAS: {
|
||||
{
|
||||
name: "Attribute",
|
||||
selector: { attribute: { entity_id: "" } },
|
||||
context: { filter_entity: "entity" },
|
||||
},
|
||||
{ name: "Device", selector: { device: {} } },
|
||||
{ name: "Duration", selector: { duration: {} } },
|
||||
@@ -75,6 +170,14 @@ const SCHEMAS: {
|
||||
media: {},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "location",
|
||||
selector: { location: { radius: true, icon: "mdi:home" } },
|
||||
},
|
||||
{
|
||||
name: "entities",
|
||||
selector: { entity: { multiple: true } },
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
@@ -315,9 +418,10 @@ class DemoHaForm extends LitElement {
|
||||
const hass = provideHass(this);
|
||||
hass.updateTranslations(null, "en");
|
||||
hass.updateTranslations("config", "en");
|
||||
hass.addEntities(ENTITIES);
|
||||
mockEntityRegistry(hass);
|
||||
mockDeviceRegistry(hass);
|
||||
mockAreaRegistry(hass);
|
||||
mockDeviceRegistry(hass, DEVICES);
|
||||
mockAreaRegistry(hass, AREAS);
|
||||
mockHassioSupervisor(hass);
|
||||
}
|
||||
|
||||
|
@@ -146,6 +146,7 @@ const SCHEMAS: {
|
||||
},
|
||||
boolean: { name: "Boolean", selector: { boolean: {} } },
|
||||
time: { name: "Time", selector: { time: {} } },
|
||||
date: { name: "Date", selector: { date: {} } },
|
||||
action: { name: "Action", selector: { action: {} } },
|
||||
text: {
|
||||
name: "Text",
|
||||
@@ -168,6 +169,23 @@ const SCHEMAS: {
|
||||
},
|
||||
icon: { name: "Icon", selector: { icon: {} } },
|
||||
media: { name: "Media", selector: { media: {} } },
|
||||
location: { name: "Location", selector: { location: {} } },
|
||||
location_radius: {
|
||||
name: "Location with radius",
|
||||
selector: { location: { radius: true, icon: "mdi:home" } },
|
||||
},
|
||||
color_temp: {
|
||||
name: "Color Temperature",
|
||||
selector: { color_temp: {} },
|
||||
},
|
||||
color_rgb: { name: "Color", selector: { color_rgb: {} } },
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Multiples",
|
||||
input: {
|
||||
entity: { name: "Entity", selector: { entity: { multiple: true } } },
|
||||
device: { name: "Device", selector: { device: { multiple: true } } },
|
||||
},
|
||||
},
|
||||
];
|
||||
|
@@ -2,6 +2,8 @@
|
||||
title: Editing design.home-assistant.io
|
||||
---
|
||||
|
||||

|
||||
|
||||
# How to edit design.home-assistant.io
|
||||
|
||||
All pages are stored in [the pages folder][pages-folder] on GitHub. Pages are grouped in a folder per sidebar section. Each page can contain a `<page name>.markdown` description file, a `<page name>.ts` demo file or both. If both are defined the description is rendered first. The description can contain metadata to specify the title of the page.
|
||||
@@ -41,15 +43,12 @@ import { html, css, LitElement } from "lit";
|
||||
import { customElement } from "lit/decorators";
|
||||
import "../../../../src/components/ha-card";
|
||||
|
||||
|
||||
@customElement("demo-user-experience-usability")
|
||||
export class DemoUserExperienceUsability extends LitElement {
|
||||
protected render() {
|
||||
return html`
|
||||
<ha-card>
|
||||
<div class="card-content">
|
||||
Hello world!
|
||||
</div>
|
||||
<div class="card-content">Hello world!</div>
|
||||
</ha-card>
|
||||
`;
|
||||
}
|
||||
|
@@ -188,6 +188,7 @@ const createEntityRegistryEntries = (
|
||||
device_id: "mock-device-id",
|
||||
area_id: null,
|
||||
disabled_by: null,
|
||||
hidden_by: null,
|
||||
entity_category: null,
|
||||
entity_id: "binary_sensor.updater",
|
||||
name: null,
|
||||
|
@@ -1,5 +1,4 @@
|
||||
import { mdiFolderUpload } from "@mdi/js";
|
||||
import "@polymer/paper-input/paper-input-container";
|
||||
import { html, LitElement, TemplateResult } from "lit";
|
||||
import { customElement, state } from "lit/decorators";
|
||||
import { fireEvent } from "../../../src/common/dom/fire_event";
|
||||
|
@@ -121,7 +121,8 @@ export class HassioMain extends SupervisorBaseElement {
|
||||
this.parentElement,
|
||||
this.hass.themes,
|
||||
themeName,
|
||||
themeSettings
|
||||
themeSettings,
|
||||
true
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@@ -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": "^6.1.1",
|
||||
"idb-keyval": "^5.1.3",
|
||||
"intl-messageformat": "^9.9.1",
|
||||
"js-yaml": "^4.1.0",
|
||||
@@ -136,7 +135,6 @@
|
||||
"vis-network": "^8.5.4",
|
||||
"vue": "^2.6.12",
|
||||
"vue2-daterange-picker": "^0.5.1",
|
||||
"web-animations-js": "^2.3.2",
|
||||
"workbox-cacheable-response": "^6.4.2",
|
||||
"workbox-core": "^6.4.2",
|
||||
"workbox-expiration": "^6.4.2",
|
||||
|
@@ -1,6 +1,6 @@
|
||||
[metadata]
|
||||
name = home-assistant-frontend
|
||||
version = 20220301.0
|
||||
version = 20220316.0
|
||||
author = The Home Assistant Authors
|
||||
author_email = hello@home-assistant.io
|
||||
license = Apache-2.0
|
||||
|
@@ -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) {
|
||||
|
@@ -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 };
|
||||
}
|
||||
|
@@ -9,7 +9,6 @@ import {
|
||||
mdiCast,
|
||||
mdiCastConnected,
|
||||
mdiClock,
|
||||
mdiEmoticonDead,
|
||||
mdiFlash,
|
||||
mdiGestureTapButton,
|
||||
mdiLanConnect,
|
||||
@@ -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";
|
||||
/**
|
||||
@@ -115,18 +111,6 @@ export const domainIcon = (
|
||||
return mdiFlash;
|
||||
}
|
||||
|
||||
case "zwave":
|
||||
switch (compareState) {
|
||||
case "dead":
|
||||
return mdiEmoticonDead;
|
||||
case "sleeping":
|
||||
return mdiSleep;
|
||||
case "initializing":
|
||||
return mdiTimerSand;
|
||||
default:
|
||||
return mdiZWave;
|
||||
}
|
||||
|
||||
case "sensor": {
|
||||
const icon = sensorIcon(stateObj);
|
||||
if (icon) {
|
||||
|
4
src/common/string/is_ip_address.ts
Normal file
4
src/common/string/is_ip_address.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
const regexp =
|
||||
/^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/;
|
||||
|
||||
export const isIPAddress = (input: string): boolean => regexp.test(input);
|
@@ -70,9 +70,6 @@ export const iconColorCSS = css`
|
||||
}
|
||||
|
||||
ha-state-icon[data-domain="plant"][data-state="problem"],
|
||||
ha-state-icon[data-domain="zwave"][data-state="dead"] {
|
||||
color: var(--state-icon-error-color);
|
||||
}
|
||||
|
||||
/* Color the icon if unavailable */
|
||||
ha-state-icon[data-state="unavailable"] {
|
||||
|
@@ -1,8 +1,9 @@
|
||||
import "@material/mwc-button";
|
||||
import type { Button } from "@material/mwc-button";
|
||||
import { mdiAlertOctagram, mdiCheckBold } from "@mdi/js";
|
||||
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
|
||||
import { customElement, property, query } from "lit/decorators";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import "../ha-circular-progress";
|
||||
import "../ha-svg-icon";
|
||||
|
||||
@customElement("ha-progress-button")
|
||||
export class HaProgressButton extends LitElement {
|
||||
@@ -12,38 +13,53 @@ export class HaProgressButton extends LitElement {
|
||||
|
||||
@property({ type: Boolean }) public raised = false;
|
||||
|
||||
@query("mwc-button", true) private _button?: Button;
|
||||
@state() private _result?: "success" | "error";
|
||||
|
||||
public render(): TemplateResult {
|
||||
const overlay = this._result || this.progress;
|
||||
return html`
|
||||
<mwc-button
|
||||
?raised=${this.raised}
|
||||
.disabled=${this.disabled || this.progress}
|
||||
@click=${this._buttonTapped}
|
||||
class=${this._result || ""}
|
||||
>
|
||||
<slot></slot>
|
||||
</mwc-button>
|
||||
${this.progress
|
||||
? html`<div class="progress">
|
||||
<ha-circular-progress size="small" active></ha-circular-progress>
|
||||
</div>`
|
||||
: ""}
|
||||
${!overlay
|
||||
? ""
|
||||
: html`
|
||||
<div class="progress">
|
||||
${this._result === "success"
|
||||
? html`<ha-svg-icon .path=${mdiCheckBold}></ha-svg-icon>`
|
||||
: this._result === "error"
|
||||
? html`<ha-svg-icon .path=${mdiAlertOctagram}></ha-svg-icon>`
|
||||
: this.progress
|
||||
? html`
|
||||
<ha-circular-progress
|
||||
size="small"
|
||||
active
|
||||
></ha-circular-progress>
|
||||
`
|
||||
: ""}
|
||||
</div>
|
||||
`}
|
||||
`;
|
||||
}
|
||||
|
||||
public actionSuccess(): void {
|
||||
this._tempClass("success");
|
||||
this._setResult("success");
|
||||
}
|
||||
|
||||
public actionError(): void {
|
||||
this._tempClass("error");
|
||||
this._setResult("error");
|
||||
}
|
||||
|
||||
private _tempClass(className: string): void {
|
||||
this._button!.classList.add(className);
|
||||
private _setResult(result: "success" | "error"): void {
|
||||
this._result = result;
|
||||
setTimeout(() => {
|
||||
this._button!.classList.remove(className);
|
||||
}, 1000);
|
||||
this._result = undefined;
|
||||
}, 2000);
|
||||
}
|
||||
|
||||
private _buttonTapped(ev: Event): void {
|
||||
@@ -69,6 +85,7 @@ export class HaProgressButton extends LitElement {
|
||||
background-color: var(--success-color);
|
||||
transition: none;
|
||||
border-radius: 4px;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
mwc-button[raised].success {
|
||||
@@ -81,6 +98,7 @@ export class HaProgressButton extends LitElement {
|
||||
background-color: var(--error-color);
|
||||
transition: none;
|
||||
border-radius: 4px;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
mwc-button[raised].error {
|
||||
@@ -89,13 +107,21 @@ export class HaProgressButton extends LitElement {
|
||||
}
|
||||
|
||||
.progress {
|
||||
bottom: 0;
|
||||
margin-top: 4px;
|
||||
bottom: 4px;
|
||||
position: absolute;
|
||||
text-align: center;
|
||||
top: 0;
|
||||
top: 4px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
ha-svg-icon {
|
||||
color: white;
|
||||
}
|
||||
|
||||
mwc-button.success slot,
|
||||
mwc-button.error slot {
|
||||
visibility: hidden;
|
||||
}
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
@@ -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 {
|
||||
|
@@ -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 || [];
|
||||
|
@@ -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"
|
||||
),
|
||||
|
@@ -41,7 +41,7 @@ export class HaDateInput extends LitElement {
|
||||
return html`<ha-textfield
|
||||
.label=${this.label}
|
||||
.disabled=${this.disabled}
|
||||
iconTrailing="calendar"
|
||||
iconTrailing
|
||||
@click=${this._openDialog}
|
||||
.value=${this.value
|
||||
? formatDateNumeric(new Date(this.value), this.locale)
|
||||
|
@@ -1,6 +1,13 @@
|
||||
import { mdiChevronDown } from "@mdi/js";
|
||||
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
|
||||
import { customElement, property, query } from "lit/decorators";
|
||||
import {
|
||||
css,
|
||||
CSSResultGroup,
|
||||
html,
|
||||
LitElement,
|
||||
PropertyValues,
|
||||
TemplateResult,
|
||||
} from "lit";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import { classMap } from "lit/directives/class-map";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import { nextRender } from "../common/util/render-status";
|
||||
@@ -16,11 +23,21 @@ class HaExpansionPanel extends LitElement {
|
||||
|
||||
@property() secondary?: string;
|
||||
|
||||
@state() _showContent = this.expanded;
|
||||
|
||||
@query(".container") private _container!: HTMLDivElement;
|
||||
|
||||
protected render(): TemplateResult {
|
||||
return html`
|
||||
<div class="summary" @click=${this._toggleContainer}>
|
||||
<div
|
||||
id="summary"
|
||||
@click=${this._toggleContainer}
|
||||
@keydown=${this._toggleContainer}
|
||||
role="button"
|
||||
tabindex="0"
|
||||
aria-expanded=${this.expanded}
|
||||
aria-controls="sect1"
|
||||
>
|
||||
<slot class="header" name="header">
|
||||
${this.header}
|
||||
<slot class="secondary" name="secondary">${this.secondary}</slot>
|
||||
@@ -33,21 +50,37 @@ class HaExpansionPanel extends LitElement {
|
||||
<div
|
||||
class="container ${classMap({ expanded: this.expanded })}"
|
||||
@transitionend=${this._handleTransitionEnd}
|
||||
role="region"
|
||||
aria-labelledby="summary"
|
||||
aria-hidden=${!this.expanded}
|
||||
tabindex="-1"
|
||||
>
|
||||
<slot></slot>
|
||||
${this._showContent ? html`<slot></slot>` : ""}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
private _handleTransitionEnd() {
|
||||
this._container.style.removeProperty("height");
|
||||
protected willUpdate(changedProps: PropertyValues) {
|
||||
if (changedProps.has("expanded") && this.expanded) {
|
||||
this._showContent = this.expanded;
|
||||
}
|
||||
}
|
||||
|
||||
private async _toggleContainer(): Promise<void> {
|
||||
private _handleTransitionEnd() {
|
||||
this._container.style.removeProperty("height");
|
||||
this._showContent = this.expanded;
|
||||
}
|
||||
|
||||
private async _toggleContainer(ev): Promise<void> {
|
||||
if (ev.type === "keydown" && ev.key !== "Enter" && ev.key !== " ") {
|
||||
return;
|
||||
}
|
||||
ev.preventDefault();
|
||||
const newExpanded = !this.expanded;
|
||||
fireEvent(this, "expanded-will-change", { expanded: newExpanded });
|
||||
|
||||
if (newExpanded) {
|
||||
this._showContent = true;
|
||||
// allow for dynamic content to be rendered
|
||||
await nextRender();
|
||||
}
|
||||
@@ -80,17 +113,21 @@ class HaExpansionPanel extends LitElement {
|
||||
var(--divider-color, #e0e0e0)
|
||||
);
|
||||
border-radius: var(--ha-card-border-radius, 4px);
|
||||
padding: 0 8px;
|
||||
}
|
||||
|
||||
.summary {
|
||||
#summary {
|
||||
display: flex;
|
||||
padding: var(--expansion-panel-summary-padding, 0);
|
||||
padding: var(--expansion-panel-summary-padding, 0 8px);
|
||||
min-height: 48px;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
overflow: hidden;
|
||||
font-weight: 500;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
#summary:focus {
|
||||
background: var(--input-fill-color);
|
||||
}
|
||||
|
||||
.summary-icon {
|
||||
@@ -103,6 +140,7 @@ class HaExpansionPanel extends LitElement {
|
||||
}
|
||||
|
||||
.container {
|
||||
padding: var(--expansion-panel-content-padding, 0 8px);
|
||||
overflow: hidden;
|
||||
transition: height 300ms cubic-bezier(0.4, 0, 0.2, 1);
|
||||
height: 0px;
|
||||
|
@@ -1,6 +1,5 @@
|
||||
import { styles } from "@material/mwc-textfield/mwc-textfield.css";
|
||||
import { mdiClose } from "@mdi/js";
|
||||
import "@polymer/iron-input/iron-input";
|
||||
import "@polymer/paper-input/paper-input-container";
|
||||
import { css, html, LitElement, PropertyValues, TemplateResult } from "lit";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import { classMap } from "lit/directives/class-map";
|
||||
@@ -21,7 +20,7 @@ export class HaFileUpload extends LitElement {
|
||||
|
||||
@property() public accept!: string;
|
||||
|
||||
@property() public icon!: string;
|
||||
@property() public icon?: string;
|
||||
|
||||
@property() public label!: string;
|
||||
|
||||
@@ -39,15 +38,7 @@ export class HaFileUpload extends LitElement {
|
||||
protected firstUpdated(changedProperties: PropertyValues) {
|
||||
super.firstUpdated(changedProperties);
|
||||
if (this.autoOpenFileDialog) {
|
||||
this._input?.click();
|
||||
}
|
||||
}
|
||||
|
||||
protected updated(changedProperties: PropertyValues) {
|
||||
if (changedProperties.has("_drag") && !this.uploading) {
|
||||
(
|
||||
this.shadowRoot!.querySelector("paper-input-container") as any
|
||||
)._setFocused(this._drag);
|
||||
this._openFilePicker();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -60,51 +51,75 @@ export class HaFileUpload extends LitElement {
|
||||
active
|
||||
></ha-circular-progress>`
|
||||
: html`
|
||||
<label for="input">
|
||||
<paper-input-container
|
||||
.alwaysFloatLabel=${Boolean(this.value)}
|
||||
@drop=${this._handleDrop}
|
||||
@dragenter=${this._handleDragStart}
|
||||
@dragover=${this._handleDragStart}
|
||||
@dragleave=${this._handleDragEnd}
|
||||
@dragend=${this._handleDragEnd}
|
||||
class=${classMap({
|
||||
dragged: this._drag,
|
||||
})}
|
||||
<label
|
||||
for="input"
|
||||
class="mdc-text-field mdc-text-field--filled ${classMap({
|
||||
"mdc-text-field--focused": this._drag,
|
||||
"mdc-text-field--with-leading-icon": Boolean(this.icon),
|
||||
"mdc-text-field--with-trailing-icon": Boolean(this.value),
|
||||
})}"
|
||||
@drop=${this._handleDrop}
|
||||
@dragenter=${this._handleDragStart}
|
||||
@dragover=${this._handleDragStart}
|
||||
@dragleave=${this._handleDragEnd}
|
||||
@dragend=${this._handleDragEnd}
|
||||
>
|
||||
<span class="mdc-text-field__ripple"></span>
|
||||
<span
|
||||
class="mdc-floating-label ${this.value || this._drag
|
||||
? "mdc-floating-label--float-above"
|
||||
: ""}"
|
||||
id="label"
|
||||
>${this.label}</span
|
||||
>
|
||||
<label for="input" slot="label"> ${this.label} </label>
|
||||
<iron-input slot="input">
|
||||
<input
|
||||
id="input"
|
||||
type="file"
|
||||
class="file"
|
||||
accept=${this.accept}
|
||||
@change=${this._handleFilePicked}
|
||||
/>
|
||||
${this.value}
|
||||
</iron-input>
|
||||
${this.value
|
||||
? html`
|
||||
<ha-icon-button
|
||||
slot="suffix"
|
||||
@click=${this._clearValue}
|
||||
.label=${this.hass?.localize("ui.common.close") ||
|
||||
"close"}
|
||||
.path=${mdiClose}
|
||||
></ha-icon-button>
|
||||
`
|
||||
: html`
|
||||
<ha-icon-button
|
||||
slot="suffix"
|
||||
.path=${this.icon}
|
||||
></ha-icon-button>
|
||||
`}
|
||||
</paper-input-container>
|
||||
${this.icon
|
||||
? html`<span
|
||||
class="mdc-text-field__icon mdc-text-field__icon--leading"
|
||||
tabindex="-1"
|
||||
>
|
||||
<ha-icon-button
|
||||
@click=${this._openFilePicker}
|
||||
.path=${this.icon}
|
||||
></ha-icon-button>
|
||||
</span>`
|
||||
: ""}
|
||||
<div class="value">${this.value}</div>
|
||||
<input
|
||||
id="input"
|
||||
type="file"
|
||||
class="mdc-text-field__input file"
|
||||
accept=${this.accept}
|
||||
@change=${this._handleFilePicked}
|
||||
aria-labelledby="label"
|
||||
/>
|
||||
${this.value
|
||||
? html`<span
|
||||
class="mdc-text-field__icon mdc-text-field__icon--trailing"
|
||||
tabindex="1"
|
||||
>
|
||||
<ha-icon-button
|
||||
slot="suffix"
|
||||
@click=${this._clearValue}
|
||||
.label=${this.hass?.localize("ui.common.close") ||
|
||||
"close"}
|
||||
.path=${mdiClose}
|
||||
></ha-icon-button>
|
||||
</span>`
|
||||
: ""}
|
||||
<span
|
||||
class="mdc-line-ripple ${this._drag
|
||||
? "mdc-line-ripple--active"
|
||||
: ""}"
|
||||
></span>
|
||||
</label>
|
||||
`}
|
||||
`;
|
||||
}
|
||||
|
||||
private _openFilePicker() {
|
||||
this._input?.click();
|
||||
}
|
||||
|
||||
private _handleDrop(ev: DragEvent) {
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
@@ -137,40 +152,66 @@ export class HaFileUpload extends LitElement {
|
||||
}
|
||||
|
||||
static get styles() {
|
||||
return css`
|
||||
paper-input-container {
|
||||
position: relative;
|
||||
padding: 8px;
|
||||
margin: 0 -8px;
|
||||
}
|
||||
paper-input-container.dragged:before {
|
||||
position: var(--layout-fit_-_position);
|
||||
top: var(--layout-fit_-_top);
|
||||
right: var(--layout-fit_-_right);
|
||||
bottom: var(--layout-fit_-_bottom);
|
||||
left: var(--layout-fit_-_left);
|
||||
background: currentColor;
|
||||
content: "";
|
||||
opacity: var(--dark-divider-opacity);
|
||||
pointer-events: none;
|
||||
border-radius: 4px;
|
||||
}
|
||||
input.file {
|
||||
display: none;
|
||||
}
|
||||
img {
|
||||
max-width: 125px;
|
||||
max-height: 125px;
|
||||
}
|
||||
ha-icon-button {
|
||||
--mdc-icon-button-size: 24px;
|
||||
--mdc-icon-size: 20px;
|
||||
}
|
||||
ha-circular-progress {
|
||||
display: block;
|
||||
text-align-last: center;
|
||||
}
|
||||
`;
|
||||
return [
|
||||
styles,
|
||||
css`
|
||||
:host {
|
||||
display: block;
|
||||
}
|
||||
.mdc-text-field--filled {
|
||||
height: auto;
|
||||
padding-top: 16px;
|
||||
cursor: pointer;
|
||||
}
|
||||
.mdc-text-field--filled.mdc-text-field--with-trailing-icon {
|
||||
padding-top: 28px;
|
||||
}
|
||||
.mdc-text-field:not(.mdc-text-field--disabled) .mdc-text-field__icon {
|
||||
color: var(--secondary-text-color);
|
||||
}
|
||||
.mdc-text-field--filled.mdc-text-field--with-trailing-icon
|
||||
.mdc-text-field__icon {
|
||||
align-self: flex-end;
|
||||
}
|
||||
.mdc-text-field__icon--leading {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
.mdc-text-field--filled .mdc-floating-label--float-above {
|
||||
transform: scale(0.75);
|
||||
top: 8px;
|
||||
}
|
||||
.dragged:before {
|
||||
position: var(--layout-fit_-_position);
|
||||
top: var(--layout-fit_-_top);
|
||||
right: var(--layout-fit_-_right);
|
||||
bottom: var(--layout-fit_-_bottom);
|
||||
left: var(--layout-fit_-_left);
|
||||
background: currentColor;
|
||||
content: "";
|
||||
opacity: var(--dark-divider-opacity);
|
||||
pointer-events: none;
|
||||
border-radius: 4px;
|
||||
}
|
||||
.value {
|
||||
width: 100%;
|
||||
}
|
||||
input.file {
|
||||
display: none;
|
||||
}
|
||||
img {
|
||||
max-width: 100%;
|
||||
max-height: 125px;
|
||||
}
|
||||
ha-icon-button {
|
||||
--mdc-icon-button-size: 24px;
|
||||
--mdc-icon-size: 20px;
|
||||
}
|
||||
ha-circular-progress {
|
||||
display: block;
|
||||
text-align-last: center;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -9,7 +9,9 @@ export class HaFormConstant extends LitElement implements HaFormElement {
|
||||
@property() public label!: string;
|
||||
|
||||
protected render(): TemplateResult {
|
||||
return html`<span class="label">${this.label}</span>: ${this.schema.value}`;
|
||||
return html`<span class="label">${this.label}</span>${this.schema.value
|
||||
? `: ${this.schema.value}`
|
||||
: ""}`;
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
|
@@ -25,6 +25,8 @@ import { HomeAssistant } from "../../types";
|
||||
const getValue = (obj, item) =>
|
||||
obj ? (!item.name ? obj : obj[item.name]) : null;
|
||||
|
||||
const getError = (obj, item) => (obj && item.name ? obj[item.name] : null);
|
||||
|
||||
let selectorImported = false;
|
||||
|
||||
@customElement("ha-form")
|
||||
@@ -84,7 +86,7 @@ export class HaForm extends LitElement implements HaFormElement {
|
||||
`
|
||||
: ""}
|
||||
${this.schema.map((item) => {
|
||||
const error = getValue(this.error, item);
|
||||
const error = getError(this.error, item);
|
||||
|
||||
return html`
|
||||
${error
|
||||
@@ -104,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,
|
||||
@@ -113,6 +116,7 @@ export class HaForm extends LitElement implements HaFormElement {
|
||||
hass: this.hass,
|
||||
computeLabel: this.computeLabel,
|
||||
computeHelper: this.computeHelper,
|
||||
context: this._generateContext(item),
|
||||
})}
|
||||
`;
|
||||
})}
|
||||
@@ -120,6 +124,20 @@ export class HaForm extends LitElement implements HaFormElement {
|
||||
`;
|
||||
}
|
||||
|
||||
private _generateContext(
|
||||
schema: HaFormSchema
|
||||
): Record<string, any> | undefined {
|
||||
if (!schema.context) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const context = {};
|
||||
for (const [context_key, data_key] of Object.entries(schema.context)) {
|
||||
context[context_key] = this.data[data_key];
|
||||
}
|
||||
return context;
|
||||
}
|
||||
|
||||
protected createRenderRoot() {
|
||||
const root = super.createRenderRoot();
|
||||
// attach it as soon as possible to make sure we fetch all events.
|
||||
|
@@ -24,6 +24,7 @@ export interface HaFormBaseSchema {
|
||||
// This value will be set initially when form is loaded
|
||||
suggested_value?: HaFormData;
|
||||
};
|
||||
context?: Record<string, string>;
|
||||
}
|
||||
|
||||
export interface HaFormGridSchema extends HaFormBaseSchema {
|
||||
@@ -40,7 +41,7 @@ export interface HaFormSelector extends HaFormBaseSchema {
|
||||
|
||||
export interface HaFormConstantSchema extends HaFormBaseSchema {
|
||||
type: "constant";
|
||||
value: string;
|
||||
value?: string;
|
||||
}
|
||||
|
||||
export interface HaFormIntegerSchema extends HaFormBaseSchema {
|
||||
|
@@ -44,6 +44,9 @@ export class HaSelect extends SelectBase {
|
||||
.mdc-select:not(.mdc-select--disabled) .mdc-select__icon {
|
||||
color: var(--secondary-text-color);
|
||||
}
|
||||
.mdc-select__anchor {
|
||||
width: var(--ha-select-min-width, 200px);
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
@@ -1,9 +1,10 @@
|
||||
import "../entity/ha-entity-attribute-picker";
|
||||
import { html, LitElement } from "lit";
|
||||
import { html, LitElement, PropertyValues } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { AttributeSelector } from "../../data/selector";
|
||||
import { SubscribeMixin } from "../../mixins/subscribe-mixin";
|
||||
import { HomeAssistant } from "../../types";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
|
||||
@customElement("ha-selector-attribute")
|
||||
export class HaSelectorAttribute extends SubscribeMixin(LitElement) {
|
||||
@@ -17,11 +18,16 @@ export class HaSelectorAttribute extends SubscribeMixin(LitElement) {
|
||||
|
||||
@property({ type: Boolean }) public disabled = false;
|
||||
|
||||
@property() public context?: {
|
||||
filter_entity?: string;
|
||||
};
|
||||
|
||||
protected render() {
|
||||
return html`
|
||||
<ha-entity-attribute-picker
|
||||
.hass=${this.hass}
|
||||
.entityId=${this.selector.attribute.entity_id}
|
||||
.entityId=${this.selector.attribute.entity_id ||
|
||||
this.context?.filter_entity}
|
||||
.value=${this.value}
|
||||
.label=${this.label}
|
||||
.disabled=${this.disabled}
|
||||
@@ -29,6 +35,47 @@ export class HaSelectorAttribute extends SubscribeMixin(LitElement) {
|
||||
></ha-entity-attribute-picker>
|
||||
`;
|
||||
}
|
||||
|
||||
protected updated(changedProps: PropertyValues): void {
|
||||
super.updated(changedProps);
|
||||
if (
|
||||
// No need to filter value if no value
|
||||
!this.value ||
|
||||
// Only adjust value if we used the context
|
||||
this.selector.attribute.entity_id ||
|
||||
// Only check if context has changed
|
||||
!changedProps.has("context")
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const oldContext = changedProps.get("context") as this["context"];
|
||||
|
||||
if (
|
||||
!this.context ||
|
||||
oldContext?.filter_entity === this.context.filter_entity
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Validate that that the attribute is still valid for this entity, else unselect.
|
||||
let invalid = false;
|
||||
if (this.context.filter_entity) {
|
||||
const stateObj = this.hass.states[this.context.filter_entity];
|
||||
|
||||
if (!(stateObj && this.value in stateObj.attributes)) {
|
||||
invalid = true;
|
||||
}
|
||||
} else {
|
||||
invalid = this.value !== undefined;
|
||||
}
|
||||
|
||||
if (invalid) {
|
||||
fireEvent(this, "value-changed", {
|
||||
value: undefined,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
|
58
src/components/ha-selector/ha-selector-color-rgb.ts
Normal file
58
src/components/ha-selector/ha-selector-color-rgb.ts
Normal file
@@ -0,0 +1,58 @@
|
||||
import { css, html, LitElement } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import type { ColorRGBSelector } from "../../data/selector";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import { hex2rgb, rgb2hex } from "../../common/color/convert-color";
|
||||
import "../ha-textfield";
|
||||
|
||||
@customElement("ha-selector-color_rgb")
|
||||
export class HaColorRGBSelector extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public selector!: ColorRGBSelector;
|
||||
|
||||
@property() public value?: string;
|
||||
|
||||
@property() public label?: string;
|
||||
|
||||
@property({ type: Boolean, reflect: true }) public disabled = false;
|
||||
|
||||
protected render() {
|
||||
return html`
|
||||
<ha-textfield
|
||||
type="color"
|
||||
.value=${this.value ? rgb2hex(this.value as any) : ""}
|
||||
.label=${this.label || ""}
|
||||
@change=${this._valueChanged}
|
||||
></ha-textfield>
|
||||
`;
|
||||
}
|
||||
|
||||
private _valueChanged(ev: CustomEvent) {
|
||||
const value = (ev.target as any).value;
|
||||
fireEvent(this, "value-changed", {
|
||||
value: hex2rgb(value),
|
||||
});
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
:host {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
align-items: center;
|
||||
}
|
||||
ha-textfield {
|
||||
--text-field-padding: 8px;
|
||||
min-width: 75px;
|
||||
flex-grow: 1;
|
||||
margin: 0 4px;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-selector-color_rgb": HaColorRGBSelector;
|
||||
}
|
||||
}
|
58
src/components/ha-selector/ha-selector-color-temp.ts
Normal file
58
src/components/ha-selector/ha-selector-color-temp.ts
Normal file
@@ -0,0 +1,58 @@
|
||||
import { css, html, LitElement } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import type { ColorTempSelector } from "../../data/selector";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import "../ha-labeled-slider";
|
||||
|
||||
@customElement("ha-selector-color_temp")
|
||||
export class HaColorTempSelector extends LitElement {
|
||||
@property() public hass!: HomeAssistant;
|
||||
|
||||
@property() public selector!: ColorTempSelector;
|
||||
|
||||
@property() public value?: string;
|
||||
|
||||
@property() public label?: string;
|
||||
|
||||
@property({ type: Boolean, reflect: true }) public disabled = false;
|
||||
|
||||
protected render() {
|
||||
return html`
|
||||
<ha-labeled-slider
|
||||
pin
|
||||
icon="hass:thermometer"
|
||||
.caption=${this.label}
|
||||
.min=${this.selector.color_temp.min_mireds ?? 153}
|
||||
.max=${this.selector.color_temp.max_mireds ?? 500}
|
||||
.value=${this.value}
|
||||
@change=${this._valueChanged}
|
||||
></ha-labeled-slider>
|
||||
`;
|
||||
}
|
||||
|
||||
private _valueChanged(ev: CustomEvent) {
|
||||
fireEvent(this, "value-changed", {
|
||||
value: Number((ev.target as any).value),
|
||||
});
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
ha-labeled-slider {
|
||||
--ha-slider-background: -webkit-linear-gradient(
|
||||
right,
|
||||
rgb(255, 160, 0) 0%,
|
||||
white 50%,
|
||||
rgb(166, 209, 255) 100%
|
||||
);
|
||||
/* The color temp minimum value shouldn't be rendered differently. It's not "off". */
|
||||
--paper-slider-knob-start-border-color: var(--primary-color);
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-selector-color_temp": HaColorTempSelector;
|
||||
}
|
||||
}
|
36
src/components/ha-selector/ha-selector-date.ts
Normal file
36
src/components/ha-selector/ha-selector-date.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
import { html, LitElement } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import type { DateSelector } from "../../data/selector";
|
||||
import "../ha-date-input";
|
||||
|
||||
@customElement("ha-selector-date")
|
||||
export class HaDateSelector extends LitElement {
|
||||
@property() public hass!: HomeAssistant;
|
||||
|
||||
@property() public selector!: DateSelector;
|
||||
|
||||
@property() public value?: string;
|
||||
|
||||
@property() public label?: string;
|
||||
|
||||
@property({ type: Boolean, reflect: true }) public disabled = false;
|
||||
|
||||
protected render() {
|
||||
return html`
|
||||
<ha-date-input
|
||||
.label=${this.label}
|
||||
.locale=${this.hass.locale}
|
||||
.disabled=${this.disabled}
|
||||
.value=${this.value}
|
||||
>
|
||||
</ha-date-input>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-selector-date": HaDateSelector;
|
||||
}
|
||||
}
|
@@ -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 => {
|
||||
|
@@ -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[] {
|
||||
|
80
src/components/ha-selector/ha-selector-location.ts
Normal file
80
src/components/ha-selector/ha-selector-location.ts
Normal file
@@ -0,0 +1,80 @@
|
||||
import { html, LitElement } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import type {
|
||||
LocationSelector,
|
||||
LocationSelectorValue,
|
||||
} from "../../data/selector";
|
||||
import "../../panels/lovelace/components/hui-theme-select-editor";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import type { MarkerLocation } from "../map/ha-locations-editor";
|
||||
import "../map/ha-locations-editor";
|
||||
|
||||
@customElement("ha-selector-location")
|
||||
export class HaLocationSelector extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public selector!: LocationSelector;
|
||||
|
||||
@property() public value?: LocationSelectorValue;
|
||||
|
||||
@property() public label?: string;
|
||||
|
||||
@property({ type: Boolean, reflect: true }) public disabled = false;
|
||||
|
||||
protected render() {
|
||||
return html`
|
||||
<ha-locations-editor
|
||||
class="flex"
|
||||
.hass=${this.hass}
|
||||
.locations=${this._location(this.selector, this.value)}
|
||||
@location-updated=${this._locationChanged}
|
||||
@radius-updated=${this._radiusChanged}
|
||||
></ha-locations-editor>
|
||||
`;
|
||||
}
|
||||
|
||||
private _location = memoizeOne(
|
||||
(
|
||||
selector: LocationSelector,
|
||||
value?: LocationSelectorValue
|
||||
): MarkerLocation[] => {
|
||||
const computedStyles = getComputedStyle(this);
|
||||
const zoneRadiusColor = selector.location.radius
|
||||
? computedStyles.getPropertyValue("--zone-radius-color") ||
|
||||
computedStyles.getPropertyValue("--accent-color")
|
||||
: undefined;
|
||||
return [
|
||||
{
|
||||
id: "location",
|
||||
latitude: value?.latitude || this.hass.config.latitude,
|
||||
longitude: value?.longitude || this.hass.config.longitude,
|
||||
radius: selector.location.radius ? value?.radius || 1000 : undefined,
|
||||
radius_color: zoneRadiusColor,
|
||||
icon: selector.location.icon,
|
||||
location_editable: true,
|
||||
radius_editable: true,
|
||||
},
|
||||
];
|
||||
}
|
||||
);
|
||||
|
||||
private _locationChanged(ev: CustomEvent) {
|
||||
const [latitude, longitude] = ev.detail.location;
|
||||
fireEvent(this, "value-changed", {
|
||||
value: { ...this.value, latitude, longitude },
|
||||
});
|
||||
}
|
||||
|
||||
private _radiusChanged(ev: CustomEvent) {
|
||||
const radius = ev.detail.radius;
|
||||
fireEvent(this, "value-changed", { value: { ...this.value, radius } });
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-selector-location": HaLocationSelector;
|
||||
}
|
||||
}
|
@@ -8,6 +8,8 @@ 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-device";
|
||||
import "./ha-selector-duration";
|
||||
import "./ha-selector-entity";
|
||||
@@ -20,6 +22,8 @@ import "./ha-selector-time";
|
||||
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 {
|
||||
@@ -39,6 +43,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();
|
||||
}
|
||||
@@ -58,6 +64,7 @@ export class HaSelector extends LitElement {
|
||||
disabled: this.disabled,
|
||||
required: this.required,
|
||||
helper: this.helper,
|
||||
context: this.context,
|
||||
id: "selector",
|
||||
})}
|
||||
`;
|
||||
|
@@ -9,6 +9,12 @@ export class HaTextField extends TextFieldBase {
|
||||
|
||||
@property({ attribute: "error-message" }) public errorMessage?: string;
|
||||
|
||||
// @ts-ignore
|
||||
@property({ type: Boolean }) public icon?: boolean;
|
||||
|
||||
// @ts-ignore
|
||||
@property({ type: Boolean }) public iconTrailing?: boolean;
|
||||
|
||||
override updated(changedProperties: PropertyValues) {
|
||||
super.updated(changedProperties);
|
||||
if (
|
||||
@@ -53,6 +59,11 @@ export class HaTextField extends TextFieldBase {
|
||||
padding-right: var(--text-field-suffix-padding-right, 0px);
|
||||
}
|
||||
|
||||
.mdc-text-field:not(.mdc-text-field--disabled)
|
||||
.mdc-text-field__affix--suffix {
|
||||
color: var(--secondary-text-color);
|
||||
}
|
||||
|
||||
.mdc-text-field__icon {
|
||||
color: var(--secondary-text-color);
|
||||
}
|
||||
|
@@ -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}
|
||||
|
@@ -35,7 +35,7 @@ class SearchInput extends LitElement {
|
||||
.autofocus=${this.autofocus}
|
||||
.label=${this.label || "Search"}
|
||||
.value=${this.filter || ""}
|
||||
.icon=${true}
|
||||
icon
|
||||
.iconTrailing=${this.filter || this.suffix}
|
||||
@input=${this._filterInputChanged}
|
||||
>
|
||||
|
@@ -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;
|
||||
|
@@ -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,
|
||||
|
@@ -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;
|
||||
}
|
||||
|
||||
|
@@ -29,7 +29,7 @@ export const createImage = async (
|
||||
body: fd,
|
||||
});
|
||||
if (resp.status === 413) {
|
||||
throw new Error("Uploaded image is too large");
|
||||
throw new Error(`Uploaded image is too large (${file.name})`);
|
||||
} else if (resp.status !== 200) {
|
||||
throw new Error("Unknown error");
|
||||
}
|
||||
|
@@ -43,7 +43,7 @@ export const uploadLocalMedia = async (
|
||||
}
|
||||
);
|
||||
if (resp.status === 413) {
|
||||
throw new Error("Uploaded image is too large");
|
||||
throw new Error(`Uploaded file is too large (${file.name})`);
|
||||
} else if (resp.status !== 200) {
|
||||
throw new Error("Unknown error");
|
||||
}
|
||||
|
213
src/data/ozw.ts
213
src/data/ozw.ts
@@ -1,213 +0,0 @@
|
||||
import { HomeAssistant } from "../types";
|
||||
import { DeviceRegistryEntry } from "./device_registry";
|
||||
|
||||
export interface OZWNodeIdentifiers {
|
||||
ozw_instance: number;
|
||||
node_id: number;
|
||||
}
|
||||
|
||||
export interface OZWDevice {
|
||||
node_id: number;
|
||||
node_query_stage: string;
|
||||
is_awake: boolean;
|
||||
is_failed: boolean;
|
||||
is_zwave_plus: boolean;
|
||||
ozw_instance: number;
|
||||
event: string;
|
||||
node_manufacturer_name: string;
|
||||
node_product_name: string;
|
||||
}
|
||||
|
||||
export interface OZWDeviceMetaDataResponse {
|
||||
node_id: number;
|
||||
ozw_instance: number;
|
||||
metadata: OZWDeviceMetaData;
|
||||
}
|
||||
|
||||
export interface OZWDeviceMetaData {
|
||||
OZWInfoURL: string;
|
||||
ZWAProductURL: string;
|
||||
ProductPic: string;
|
||||
Description: string;
|
||||
ProductManualURL: string;
|
||||
ProductPageURL: string;
|
||||
InclusionHelp: string;
|
||||
ExclusionHelp: string;
|
||||
ResetHelp: string;
|
||||
WakeupHelp: string;
|
||||
ProductSupportURL: string;
|
||||
Frequency: string;
|
||||
Name: string;
|
||||
ProductPicBase64: string;
|
||||
}
|
||||
|
||||
export interface OZWInstance {
|
||||
ozw_instance: number;
|
||||
OZWDaemon_Version: string;
|
||||
OpenZWave_Version: string;
|
||||
QTOpenZWave_Version: string;
|
||||
Status: string;
|
||||
getControllerPath: string;
|
||||
homeID: string;
|
||||
}
|
||||
|
||||
export interface OZWNetworkStatistics {
|
||||
ozw_instance: number;
|
||||
node_count: number;
|
||||
readCnt: number;
|
||||
writeCnt: number;
|
||||
ACKCnt: number;
|
||||
CANCnt: number;
|
||||
NAKCnt: number;
|
||||
dropped: number;
|
||||
retries: number;
|
||||
}
|
||||
|
||||
export interface OZWDeviceConfig {
|
||||
label: string;
|
||||
type: string;
|
||||
value: string | number;
|
||||
parameter: number;
|
||||
min: number;
|
||||
max: number;
|
||||
help: string;
|
||||
}
|
||||
|
||||
export const nodeQueryStages = [
|
||||
"ProtocolInfo",
|
||||
"Probe",
|
||||
"WakeUp",
|
||||
"ManufacturerSpecific1",
|
||||
"NodeInfo",
|
||||
"NodePlusInfo",
|
||||
"ManufacturerSpecific2",
|
||||
"Versions",
|
||||
"Instances",
|
||||
"Static",
|
||||
"CacheLoad",
|
||||
"Associations",
|
||||
"Neighbors",
|
||||
"Session",
|
||||
"Dynamic",
|
||||
"Configuration",
|
||||
"Complete",
|
||||
];
|
||||
|
||||
export const networkOnlineStatuses = [
|
||||
"driverAllNodesQueried",
|
||||
"driverAllNodesQueriedSomeDead",
|
||||
"driverAwakeNodesQueried",
|
||||
];
|
||||
export const networkStartingStatuses = [
|
||||
"starting",
|
||||
"started",
|
||||
"Ready",
|
||||
"driverReady",
|
||||
];
|
||||
export const networkOfflineStatuses = [
|
||||
"Offline",
|
||||
"stopped",
|
||||
"driverFailed",
|
||||
"driverReset",
|
||||
"driverRemoved",
|
||||
"driverAllNodesOnFire",
|
||||
];
|
||||
|
||||
export const getIdentifiersFromDevice = function (
|
||||
device: DeviceRegistryEntry
|
||||
): OZWNodeIdentifiers | undefined {
|
||||
if (!device) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const ozwIdentifier = device.identifiers.find(
|
||||
(identifier) => identifier[0] === "ozw"
|
||||
);
|
||||
if (!ozwIdentifier) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const identifiers = ozwIdentifier[1].split(".");
|
||||
return {
|
||||
node_id: parseInt(identifiers[1]),
|
||||
ozw_instance: parseInt(identifiers[0]),
|
||||
};
|
||||
};
|
||||
|
||||
export const fetchOZWInstances = (
|
||||
hass: HomeAssistant
|
||||
): Promise<OZWInstance[]> =>
|
||||
hass.callWS({
|
||||
type: "ozw/get_instances",
|
||||
});
|
||||
|
||||
export const fetchOZWNetworkStatus = (
|
||||
hass: HomeAssistant,
|
||||
ozw_instance: number
|
||||
): Promise<OZWInstance> =>
|
||||
hass.callWS({
|
||||
type: "ozw/network_status",
|
||||
ozw_instance,
|
||||
});
|
||||
|
||||
export const fetchOZWNetworkStatistics = (
|
||||
hass: HomeAssistant,
|
||||
ozw_instance: number
|
||||
): Promise<OZWNetworkStatistics> =>
|
||||
hass.callWS({
|
||||
type: "ozw/network_statistics",
|
||||
ozw_instance,
|
||||
});
|
||||
|
||||
export const fetchOZWNodes = (
|
||||
hass: HomeAssistant,
|
||||
ozw_instance: number
|
||||
): Promise<OZWDevice[]> =>
|
||||
hass.callWS({
|
||||
type: "ozw/get_nodes",
|
||||
ozw_instance,
|
||||
});
|
||||
|
||||
export const fetchOZWNodeStatus = (
|
||||
hass: HomeAssistant,
|
||||
ozw_instance: number,
|
||||
node_id: number
|
||||
): Promise<OZWDevice> =>
|
||||
hass.callWS({
|
||||
type: "ozw/node_status",
|
||||
ozw_instance,
|
||||
node_id,
|
||||
});
|
||||
|
||||
export const fetchOZWNodeMetadata = (
|
||||
hass: HomeAssistant,
|
||||
ozw_instance: number,
|
||||
node_id: number
|
||||
): Promise<OZWDeviceMetaDataResponse> =>
|
||||
hass.callWS({
|
||||
type: "ozw/node_metadata",
|
||||
ozw_instance,
|
||||
node_id,
|
||||
});
|
||||
|
||||
export const fetchOZWNodeConfig = (
|
||||
hass: HomeAssistant,
|
||||
ozw_instance: number,
|
||||
node_id: number
|
||||
): Promise<OZWDeviceConfig[]> =>
|
||||
hass.callWS({
|
||||
type: "ozw/get_config_parameters",
|
||||
ozw_instance,
|
||||
node_id,
|
||||
});
|
||||
|
||||
export const refreshNodeInfo = (
|
||||
hass: HomeAssistant,
|
||||
ozw_instance: number,
|
||||
node_id: number
|
||||
): Promise<OZWDevice> =>
|
||||
hass.callWS({
|
||||
type: "ozw/refresh_node_info",
|
||||
ozw_instance,
|
||||
node_id,
|
||||
});
|
@@ -2,6 +2,7 @@ export type Selector =
|
||||
| AddonSelector
|
||||
| AttributeSelector
|
||||
| EntitySelector
|
||||
| DateSelector
|
||||
| DeviceSelector
|
||||
| DurationSelector
|
||||
| AreaSelector
|
||||
@@ -15,22 +16,36 @@ export type Selector =
|
||||
| SelectSelector
|
||||
| IconSelector
|
||||
| MediaSelector
|
||||
| ThemeSelector;
|
||||
| ThemeSelector
|
||||
| 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 DeviceSelector {
|
||||
device: {
|
||||
integration?: string;
|
||||
@@ -40,6 +55,7 @@ export interface DeviceSelector {
|
||||
domain?: EntitySelector["entity"]["domain"];
|
||||
device_class?: EntitySelector["entity"]["device_class"];
|
||||
};
|
||||
multiple?: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -95,6 +111,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: {};
|
||||
@@ -164,6 +187,16 @@ export interface MediaSelector {
|
||||
media: {};
|
||||
}
|
||||
|
||||
export interface LocationSelector {
|
||||
location: { radius?: boolean; icon?: string };
|
||||
}
|
||||
|
||||
export interface LocationSelectorValue {
|
||||
latitude: number;
|
||||
longitude: number;
|
||||
radius?: number;
|
||||
}
|
||||
|
||||
export interface MediaSelectorValue {
|
||||
entity_id?: string;
|
||||
media_content_id?: string;
|
||||
|
@@ -12,12 +12,12 @@ export interface Zone {
|
||||
}
|
||||
|
||||
export interface ZoneMutableParams {
|
||||
name: string;
|
||||
icon?: string;
|
||||
latitude: number;
|
||||
longitude: number;
|
||||
name: string;
|
||||
passive: boolean;
|
||||
radius: number;
|
||||
passive?: boolean;
|
||||
radius?: number;
|
||||
}
|
||||
|
||||
export const fetchZones = (hass: HomeAssistant) =>
|
||||
|
@@ -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}`);
|
@@ -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`
|
||||
@@ -377,13 +386,20 @@ class DataEntryFlowDialog extends LitElement {
|
||||
step = await this._params!.flowConfig.createFlow(this.hass, handler);
|
||||
} catch (err: any) {
|
||||
this.closeDialog();
|
||||
const message =
|
||||
err?.status_code === 404
|
||||
? this.hass.localize(
|
||||
"ui.panel.config.integrations.config_flow.no_config_flow"
|
||||
)
|
||||
: `${this.hass.localize(
|
||||
"ui.panel.config.integrations.config_flow.could_not_load"
|
||||
)}: ${err?.body?.message || err?.message}`;
|
||||
|
||||
showAlertDialog(this, {
|
||||
title: this.hass.localize(
|
||||
"ui.panel.config.integrations.config_flow.error"
|
||||
),
|
||||
text: `${this.hass.localize(
|
||||
"ui.panel.config.integrations.config_flow.could_not_load"
|
||||
)}: ${err.message || err.body}`,
|
||||
text: message,
|
||||
});
|
||||
return;
|
||||
} finally {
|
||||
@@ -414,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 {
|
||||
|
@@ -181,6 +181,21 @@ export const showConfigFlowDialog = (
|
||||
: "";
|
||||
},
|
||||
|
||||
renderMenuHeader(hass, step) {
|
||||
return (
|
||||
hass.localize(
|
||||
`component.${step.handler}.config.step.${step.step_id}.title`
|
||||
) || hass.localize(`component.${step.handler}.title`)
|
||||
);
|
||||
},
|
||||
|
||||
renderMenuOption(hass, step, option) {
|
||||
return hass.localize(
|
||||
`component.${step.handler}.config.step.${step.step_id}.menu_options.${option}`,
|
||||
step.description_placeholders
|
||||
);
|
||||
},
|
||||
|
||||
renderLoadingDescription(hass, reason, handler, step) {
|
||||
if (!["loading_flow", "loading_step"].includes(reason)) {
|
||||
return "";
|
||||
|
@@ -7,6 +7,7 @@ import {
|
||||
DataEntryFlowStepCreateEntry,
|
||||
DataEntryFlowStepExternal,
|
||||
DataEntryFlowStepForm,
|
||||
DataEntryFlowStepMenu,
|
||||
DataEntryFlowStepProgress,
|
||||
} from "../../data/data_entry_flow";
|
||||
import { HomeAssistant } from "../../types";
|
||||
@@ -80,6 +81,14 @@ export interface FlowConfig {
|
||||
step: DataEntryFlowStepProgress
|
||||
): TemplateResult | "";
|
||||
|
||||
renderMenuHeader(hass: HomeAssistant, step: DataEntryFlowStepMenu): string;
|
||||
|
||||
renderMenuOption(
|
||||
hass: HomeAssistant,
|
||||
step: DataEntryFlowStepMenu,
|
||||
option: string
|
||||
): string;
|
||||
|
||||
renderLoadingDescription(
|
||||
hass: HomeAssistant,
|
||||
loadingReason: LoadingReason,
|
||||
|
@@ -134,6 +134,21 @@ export const showOptionsFlowDialog = (
|
||||
: "";
|
||||
},
|
||||
|
||||
renderMenuHeader(hass, step) {
|
||||
return (
|
||||
hass.localize(
|
||||
`component.${step.handler}.option.step.${step.step_id}.title`
|
||||
) || hass.localize(`component.${step.handler}.title`)
|
||||
);
|
||||
},
|
||||
|
||||
renderMenuOption(hass, step, option) {
|
||||
return hass.localize(
|
||||
`component.${step.handler}.options.step.${step.step_id}.menu_options.${option}`,
|
||||
step.description_placeholders
|
||||
);
|
||||
},
|
||||
|
||||
renderLoadingDescription(hass, reason) {
|
||||
return (
|
||||
hass.localize(`component.${configEntry.domain}.options.loading`) ||
|
||||
|
80
src/dialogs/config-flow/step-flow-menu.ts
Normal file
80
src/dialogs/config-flow/step-flow-menu.ts
Normal file
@@ -0,0 +1,80 @@
|
||||
import "@material/mwc-list/mwc-list-item";
|
||||
import { css, html, LitElement, TemplateResult } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import type { DataEntryFlowStepMenu } from "../../data/data_entry_flow";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import type { FlowConfig } from "./show-dialog-data-entry-flow";
|
||||
import "../../components/ha-icon-next";
|
||||
import { configFlowContentStyles } from "./styles";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
|
||||
@customElement("step-flow-menu")
|
||||
class StepFlowMenu extends LitElement {
|
||||
@property({ attribute: false }) public flowConfig!: FlowConfig;
|
||||
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public step!: DataEntryFlowStepMenu;
|
||||
|
||||
protected render(): TemplateResult {
|
||||
let options: string[];
|
||||
let translations: Record<string, string>;
|
||||
|
||||
if (Array.isArray(this.step.menu_options)) {
|
||||
options = this.step.menu_options;
|
||||
translations = {};
|
||||
for (const option of options) {
|
||||
translations[option] = this.flowConfig.renderMenuOption(
|
||||
this.hass,
|
||||
this.step,
|
||||
option
|
||||
);
|
||||
}
|
||||
} else {
|
||||
options = Object.keys(this.step.menu_options);
|
||||
translations = this.step.menu_options;
|
||||
}
|
||||
|
||||
return html`
|
||||
<h2>${this.flowConfig.renderMenuHeader(this.hass, this.step)}</h2>
|
||||
<div class="options">
|
||||
${options.map(
|
||||
(option) => html`
|
||||
<mwc-list-item hasMeta .step=${option} @click=${this._handleStep}>
|
||||
<span>${translations[option]}</span>
|
||||
<ha-icon-next slot="meta"></ha-icon-next>
|
||||
</mwc-list-item>
|
||||
`
|
||||
)}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
private _handleStep(ev) {
|
||||
fireEvent(this, "flow-update", {
|
||||
stepPromise: this.flowConfig.handleFlowStep(
|
||||
this.hass,
|
||||
this.step.flow_id,
|
||||
{
|
||||
next_step_id: ev.currentTarget.step,
|
||||
}
|
||||
),
|
||||
});
|
||||
}
|
||||
|
||||
static styles = [
|
||||
configFlowContentStyles,
|
||||
css`
|
||||
.options {
|
||||
margin-top: 20px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"step-flow-menu": StepFlowMenu;
|
||||
}
|
||||
}
|
@@ -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")
|
||||
|
@@ -1,3 +1,4 @@
|
||||
import "@material/mwc-list/mwc-list-item";
|
||||
import {
|
||||
css,
|
||||
CSSResultGroup,
|
||||
@@ -12,6 +13,7 @@ import { fireEvent } from "../../../common/dom/fire_event";
|
||||
import { stopPropagation } from "../../../common/dom/stop_propagation";
|
||||
import { supportsFeature } from "../../../common/entity/supports-feature";
|
||||
import { computeRTLDirection } from "../../../common/util/compute_rtl";
|
||||
import "../../../components/ha-select";
|
||||
import "../../../components/ha-slider";
|
||||
import "../../../components/ha-switch";
|
||||
import {
|
||||
@@ -19,8 +21,6 @@ import {
|
||||
HUMIDIFIER_SUPPORT_MODES,
|
||||
} from "../../../data/humidifier";
|
||||
import { HomeAssistant } from "../../../types";
|
||||
import "@material/mwc-list/mwc-list";
|
||||
import "@material/mwc-list/mwc-list-item";
|
||||
|
||||
class MoreInfoHumidifier extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
@@ -67,26 +67,24 @@ class MoreInfoHumidifier extends LitElement {
|
||||
|
||||
${supportModes
|
||||
? html`
|
||||
<div class="container-modes">
|
||||
<mwc-list
|
||||
.label=${hass.localize("ui.card.humidifier.mode")}
|
||||
.value=${stateObj.attributes.mode}
|
||||
fixedMenuPosition
|
||||
naturalMenuWidth
|
||||
@selected=${this._handleModeChanged}
|
||||
@closed=${stopPropagation}
|
||||
>
|
||||
${stateObj.attributes.available_modes!.map(
|
||||
(mode) => html`
|
||||
<mwc-list-item .value=${mode}>
|
||||
${hass.localize(
|
||||
`state_attributes.humidifier.mode.${mode}`
|
||||
) || mode}
|
||||
</mwc-list-item>
|
||||
`
|
||||
)}
|
||||
</mwc-list>
|
||||
</div>
|
||||
<ha-select
|
||||
.label=${hass.localize("ui.card.humidifier.mode")}
|
||||
.value=${stateObj.attributes.mode}
|
||||
fixedMenuPosition
|
||||
naturalMenuWidth
|
||||
@selected=${this._handleModeChanged}
|
||||
@closed=${stopPropagation}
|
||||
>
|
||||
${stateObj.attributes.available_modes!.map(
|
||||
(mode) => html`
|
||||
<mwc-list-item .value=${mode}>
|
||||
${hass.localize(
|
||||
`state_attributes.humidifier.mode.${mode}`
|
||||
) || mode}
|
||||
</mwc-list-item>
|
||||
`
|
||||
)}
|
||||
</ha-select>
|
||||
`
|
||||
: ""}
|
||||
</div>
|
||||
|
@@ -162,7 +162,7 @@ export class QuickBar extends LitElement {
|
||||
"ui.dialogs.quick-bar.filter_placeholder"
|
||||
)}
|
||||
.value=${this._commandMode ? `>${this._search}` : this._search}
|
||||
.icon=${true}
|
||||
icon
|
||||
.iconTrailing=${this._search !== undefined || this._narrow}
|
||||
@input=${this._handleSearchChange}
|
||||
@keydown=${this._handleInputKeyDown}
|
||||
|
@@ -1,7 +1,6 @@
|
||||
/* eslint-disable lit/prefer-static-styles */
|
||||
import "@material/mwc-button/mwc-button";
|
||||
import { mdiMicrophone } from "@mdi/js";
|
||||
import "@polymer/paper-input/paper-input";
|
||||
import type { PaperInputElement } from "@polymer/paper-input/paper-input";
|
||||
import {
|
||||
css,
|
||||
CSSResultGroup,
|
||||
@@ -10,12 +9,16 @@ import {
|
||||
PropertyValues,
|
||||
TemplateResult,
|
||||
} from "lit";
|
||||
import { customElement, property, state, query } from "lit/decorators";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import { classMap } from "lit/directives/class-map";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import { SpeechRecognition } from "../../common/dom/speech-recognition";
|
||||
import { uid } from "../../common/util/uid";
|
||||
import "../../components/ha-dialog";
|
||||
import type { HaDialog } from "../../components/ha-dialog";
|
||||
import "../../components/ha-icon-button";
|
||||
import "../../components/ha-textfield";
|
||||
import type { HaTextField } from "../../components/ha-textfield";
|
||||
import {
|
||||
AgentInfo,
|
||||
getAgentInfo,
|
||||
@@ -24,9 +27,6 @@ import {
|
||||
} from "../../data/conversation";
|
||||
import { haStyleDialog } from "../../resources/styles";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import "../../components/ha-dialog";
|
||||
import type { HaDialog } from "../../components/ha-dialog";
|
||||
import "@material/mwc-button/mwc-button";
|
||||
|
||||
interface Message {
|
||||
who: string;
|
||||
@@ -127,18 +127,19 @@ export class HaVoiceCommandDialog extends LitElement {
|
||||
: ""}
|
||||
</div>
|
||||
<div class="input" slot="primaryAction">
|
||||
<paper-input
|
||||
<ha-textfield
|
||||
@keyup=${this._handleKeyUp}
|
||||
.label=${this.hass.localize(
|
||||
`ui.dialogs.voice_command.${
|
||||
SpeechRecognition ? "label_voice" : "label"
|
||||
}`
|
||||
)}
|
||||
autofocus
|
||||
dialogInitialFocus
|
||||
iconTrailing
|
||||
>
|
||||
${SpeechRecognition
|
||||
? html`
|
||||
<span suffix="" slot="suffix">
|
||||
<span slot="trailingIcon">
|
||||
${this.results
|
||||
? html`
|
||||
<div class="bouncer">
|
||||
@@ -155,7 +156,7 @@ export class HaVoiceCommandDialog extends LitElement {
|
||||
</span>
|
||||
`
|
||||
: ""}
|
||||
</paper-input>
|
||||
</ha-textfield>
|
||||
${this._agentInfo && this._agentInfo.attribution
|
||||
? html`
|
||||
<a
|
||||
@@ -195,7 +196,7 @@ export class HaVoiceCommandDialog extends LitElement {
|
||||
}
|
||||
|
||||
private _handleKeyUp(ev: KeyboardEvent) {
|
||||
const input = ev.target as PaperInputElement;
|
||||
const input = ev.target as HaTextField;
|
||||
if (ev.keyCode === 13 && input.value) {
|
||||
this._processText(input.value);
|
||||
input.value = "";
|
||||
@@ -327,6 +328,7 @@ export class HaVoiceCommandDialog extends LitElement {
|
||||
css`
|
||||
ha-icon-button {
|
||||
color: var(--secondary-text-color);
|
||||
margin-right: -24px;
|
||||
}
|
||||
|
||||
ha-icon-button[active] {
|
||||
@@ -338,7 +340,9 @@ export class HaVoiceCommandDialog extends LitElement {
|
||||
--secondary-action-button-flex: 0;
|
||||
--mdc-dialog-max-width: 450px;
|
||||
}
|
||||
|
||||
ha-textfield {
|
||||
display: block;
|
||||
}
|
||||
a.button {
|
||||
text-decoration: none;
|
||||
}
|
||||
@@ -406,7 +410,6 @@ export class HaVoiceCommandDialog extends LitElement {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
}
|
||||
.double-bounce1,
|
||||
.double-bounce2 {
|
||||
|
@@ -300,7 +300,9 @@ export const provideHass = (
|
||||
applyThemesOnElement(
|
||||
document.documentElement,
|
||||
themes,
|
||||
selectedTheme!.theme
|
||||
selectedTheme!.theme,
|
||||
undefined,
|
||||
true
|
||||
);
|
||||
},
|
||||
|
||||
|
@@ -101,7 +101,8 @@ class SupervisorErrorScreen extends LitElement {
|
||||
this.parentElement,
|
||||
this.hass.themes,
|
||||
themeName,
|
||||
themeSettings
|
||||
themeSettings,
|
||||
true
|
||||
);
|
||||
}
|
||||
|
||||
|
@@ -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
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -1,7 +1,6 @@
|
||||
import "@material/mwc-button/mwc-button";
|
||||
import "@polymer/paper-input/paper-textarea";
|
||||
import { HassEntity } from "home-assistant-js-websocket";
|
||||
import { css, CSSResultGroup, html, LitElement } from "lit";
|
||||
import { css, CSSResultGroup, html, LitElement, PropertyValues } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { fireEvent } from "../../../common/dom/fire_event";
|
||||
import "../../../components/entity/ha-entity-toggle";
|
||||
@@ -11,6 +10,7 @@ import "../../../components/ha-circular-progress";
|
||||
import "../../../components/ha-markdown";
|
||||
import "../../../components/ha-selector/ha-selector";
|
||||
import "../../../components/ha-settings-row";
|
||||
import "../../../components/ha-textfield";
|
||||
import {
|
||||
BlueprintAutomationConfig,
|
||||
triggerAutomationActions,
|
||||
@@ -38,6 +38,8 @@ export class HaBlueprintAutomationEditor extends LitElement {
|
||||
|
||||
@state() private _blueprints?: Blueprints;
|
||||
|
||||
@state() private _showDescription = false;
|
||||
|
||||
protected firstUpdated(changedProps) {
|
||||
super.firstUpdated(changedProps);
|
||||
this._getBlueprints();
|
||||
@@ -50,6 +52,17 @@ export class HaBlueprintAutomationEditor extends LitElement {
|
||||
return this._blueprints[this.config.use_blueprint.path];
|
||||
}
|
||||
|
||||
protected willUpdate(changedProps: PropertyValues): void {
|
||||
super.willUpdate(changedProps);
|
||||
if (
|
||||
!this._showDescription &&
|
||||
changedProps.has("config") &&
|
||||
this.config.description
|
||||
) {
|
||||
this._showDescription = true;
|
||||
}
|
||||
}
|
||||
|
||||
protected render() {
|
||||
const blueprint = this._blueprint;
|
||||
return html`
|
||||
@@ -64,26 +77,39 @@ export class HaBlueprintAutomationEditor extends LitElement {
|
||||
</span>
|
||||
<ha-card>
|
||||
<div class="card-content">
|
||||
<paper-input
|
||||
<ha-textfield
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.automation.editor.alias"
|
||||
)}
|
||||
name="alias"
|
||||
.value=${this.config.alias}
|
||||
@value-changed=${this._valueChanged}
|
||||
.value=${this.config.alias || ""}
|
||||
@change=${this._valueChanged}
|
||||
>
|
||||
</paper-input>
|
||||
<paper-textarea
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.automation.editor.description.label"
|
||||
)}
|
||||
.placeholder=${this.hass.localize(
|
||||
"ui.panel.config.automation.editor.description.placeholder"
|
||||
)}
|
||||
name="description"
|
||||
.value=${this.config.description}
|
||||
@value-changed=${this._valueChanged}
|
||||
></paper-textarea>
|
||||
</ha-textfield>
|
||||
${this._showDescription
|
||||
? html`
|
||||
<ha-textarea
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.automation.editor.description.label"
|
||||
)}
|
||||
.placeholder=${this.hass.localize(
|
||||
"ui.panel.config.automation.editor.description.placeholder"
|
||||
)}
|
||||
name="description"
|
||||
autogrow
|
||||
.value=${this.config.description || ""}
|
||||
@change=${this._valueChanged}
|
||||
></ha-textarea>
|
||||
`
|
||||
: html`
|
||||
<div class="link-button-row">
|
||||
<button class="link" @click=${this._addDescription}>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.automation.editor.description.add"
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
`}
|
||||
</div>
|
||||
${this.stateObj
|
||||
? html`
|
||||
@@ -173,15 +199,14 @@ export class HaBlueprintAutomationEditor extends LitElement {
|
||||
value?.default}
|
||||
@value-changed=${this._inputChanged}
|
||||
></ha-selector>`
|
||||
: html`<paper-input
|
||||
: html`<ha-textfield
|
||||
.key=${key}
|
||||
required
|
||||
.value=${(this.config.use_blueprint.input &&
|
||||
this.config.use_blueprint.input[key]) ??
|
||||
value?.default}
|
||||
@value-changed=${this._inputChanged}
|
||||
no-label-float
|
||||
></paper-input>`}
|
||||
@input=${this._inputChanged}
|
||||
></ha-textfield>`}
|
||||
</ha-settings-row>`
|
||||
)
|
||||
: html`<p class="padding">
|
||||
@@ -221,7 +246,7 @@ export class HaBlueprintAutomationEditor extends LitElement {
|
||||
ev.stopPropagation();
|
||||
const target = ev.target as any;
|
||||
const key = target.key;
|
||||
const value = ev.detail.value;
|
||||
const value = ev.detail?.value || target.value;
|
||||
if (
|
||||
(this.config.use_blueprint.input &&
|
||||
this.config.use_blueprint.input[key] === value) ||
|
||||
@@ -253,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;
|
||||
}
|
||||
@@ -262,6 +287,10 @@ export class HaBlueprintAutomationEditor extends LitElement {
|
||||
});
|
||||
}
|
||||
|
||||
private _addDescription() {
|
||||
this._showDescription = true;
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return [
|
||||
haStyle,
|
||||
@@ -273,9 +302,16 @@ export class HaBlueprintAutomationEditor extends LitElement {
|
||||
.padding {
|
||||
padding: 16px;
|
||||
}
|
||||
.link-button-row {
|
||||
padding: 14px;
|
||||
}
|
||||
.blueprint-picker-container {
|
||||
padding: 0 16px 16px;
|
||||
}
|
||||
ha-textarea,
|
||||
ha-textfield {
|
||||
display: block;
|
||||
}
|
||||
h3 {
|
||||
margin: 16px;
|
||||
}
|
||||
@@ -292,9 +328,7 @@ export class HaBlueprintAutomationEditor extends LitElement {
|
||||
--paper-time-input-justify-content: flex-end;
|
||||
border-top: 1px solid var(--divider-color);
|
||||
}
|
||||
:host(:not([narrow])) ha-settings-row paper-input {
|
||||
width: 60%;
|
||||
}
|
||||
:host(:not([narrow])) ha-settings-row ha-textfield,
|
||||
:host(:not([narrow])) ha-settings-row ha-selector {
|
||||
width: 60%;
|
||||
}
|
||||
|
@@ -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;
|
||||
|
@@ -1,10 +1,11 @@
|
||||
import "@material/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 { customElement, property, state, query } from "lit/decorators";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||
import "../../../../components/ha-circular-progress";
|
||||
import "../../../../components/ha-dialog";
|
||||
import "../../../../components/ha-textfield";
|
||||
import type { HaTextField } from "../../../../components/ha-textfield";
|
||||
import type { AutomationConfig } from "../../../../data/automation";
|
||||
import { convertThingTalk } from "../../../../data/cloud";
|
||||
import { haStyle, haStyleDialog } from "../../../../resources/styles";
|
||||
@@ -12,7 +13,6 @@ import type { HomeAssistant } from "../../../../types";
|
||||
import "./ha-thingtalk-placeholders";
|
||||
import type { PlaceholderValues } from "./ha-thingtalk-placeholders";
|
||||
import type { ThingtalkDialogParams } from "./show-dialog-thingtalk";
|
||||
import "../../../../components/ha-dialog";
|
||||
|
||||
export interface Placeholder {
|
||||
name: string;
|
||||
@@ -38,7 +38,7 @@ class DialogThingtalk extends LitElement {
|
||||
|
||||
@state() private _placeholders?: PlaceholderContainer;
|
||||
|
||||
@query("#input") private _input?: PaperInputElement;
|
||||
@query("#input") private _input?: HaTextField;
|
||||
|
||||
private _value?: string;
|
||||
|
||||
@@ -58,7 +58,7 @@ class DialogThingtalk extends LitElement {
|
||||
this._placeholders = undefined;
|
||||
this._params = undefined;
|
||||
if (this._input) {
|
||||
this._input.value = null;
|
||||
this._input.value = "";
|
||||
}
|
||||
fireEvent(this, "dialog-closed", { dialog: this.localName });
|
||||
}
|
||||
@@ -127,13 +127,13 @@ class DialogThingtalk extends LitElement {
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
<paper-input
|
||||
<ha-textfield
|
||||
id="input"
|
||||
label="What should this automation do?"
|
||||
.value=${this._value}
|
||||
autofocus
|
||||
@keyup=${this._handleKeyUp}
|
||||
></paper-input>
|
||||
></ha-textfield>
|
||||
<a
|
||||
href="https://almond.stanford.edu/"
|
||||
target="_blank"
|
||||
|
@@ -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));
|
||||
|
@@ -88,7 +88,7 @@ export class HaWebhookTrigger extends LitElement {
|
||||
.helper=${this.hass.localize(
|
||||
"ui.panel.config.automation.editor.triggers.type.webhook.webhook_id_helper"
|
||||
)}
|
||||
.iconTrailing=${true}
|
||||
iconTrailing
|
||||
.value=${webhookId || ""}
|
||||
@input=${this._valueChanged}
|
||||
>
|
||||
|
@@ -1,11 +1,12 @@
|
||||
import "@material/mwc-button";
|
||||
import "@polymer/paper-input/paper-input";
|
||||
import type { PaperInputElement } from "@polymer/paper-input/paper-input";
|
||||
import { css, CSSResultGroup, html, LitElement } from "lit";
|
||||
import { state } from "lit/decorators";
|
||||
import { query, state } from "lit/decorators";
|
||||
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||
import { copyToClipboard } from "../../../../common/util/copy-clipboard";
|
||||
import type { HaTextField } from "../../../../components/ha-textfield";
|
||||
import "../../../../components/ha-textfield";
|
||||
import { showConfirmationDialog } from "../../../../dialogs/generic/show-dialog-box";
|
||||
import { haStyle } from "../../../../resources/styles";
|
||||
import { haStyle, haStyleDialog } from "../../../../resources/styles";
|
||||
import { HomeAssistant } from "../../../../types";
|
||||
import { documentationUrl } from "../../../../util/documentation-url";
|
||||
import { WebhookDialogParams } from "./show-dialog-manage-cloudhook";
|
||||
@@ -17,6 +18,8 @@ export class DialogManageCloudhook extends LitElement {
|
||||
|
||||
@state() private _params?: WebhookDialogParams;
|
||||
|
||||
@query("ha-textfield") _input!: HaTextField;
|
||||
|
||||
public showDialog(params: WebhookDialogParams) {
|
||||
this._params = params;
|
||||
}
|
||||
@@ -53,12 +56,12 @@ export class DialogManageCloudhook extends LitElement {
|
||||
"ui.panel.config.cloud.dialog_cloudhook.available_at"
|
||||
)}
|
||||
</p>
|
||||
<paper-input
|
||||
label=${inputLabel}
|
||||
value=${cloudhook.cloudhook_url}
|
||||
<ha-textfield
|
||||
.label=${inputLabel}
|
||||
.value=${cloudhook.cloudhook_url}
|
||||
@click=${this._copyClipboard}
|
||||
@blur=${this._restoreLabel}
|
||||
></paper-input>
|
||||
></ha-textfield>
|
||||
<p>
|
||||
${cloudhook.managed
|
||||
? html`
|
||||
@@ -98,10 +101,6 @@ export class DialogManageCloudhook extends LitElement {
|
||||
`;
|
||||
}
|
||||
|
||||
private get _paperInput(): PaperInputElement {
|
||||
return this.shadowRoot!.querySelector("paper-input")!;
|
||||
}
|
||||
|
||||
private async _disableWebhook() {
|
||||
showConfirmationDialog(this, {
|
||||
text: this.hass!.localize(
|
||||
@@ -117,14 +116,10 @@ export class DialogManageCloudhook extends LitElement {
|
||||
}
|
||||
|
||||
private _copyClipboard(ev: FocusEvent) {
|
||||
// paper-input -> iron-input -> input
|
||||
const paperInput = ev.currentTarget as PaperInputElement;
|
||||
const input = (paperInput.inputElement as any)
|
||||
.inputElement as HTMLInputElement;
|
||||
input.setSelectionRange(0, input.value.length);
|
||||
const textField = ev.currentTarget as HaTextField;
|
||||
try {
|
||||
document.execCommand("copy");
|
||||
paperInput.label = this.hass!.localize(
|
||||
copyToClipboard(textField.value);
|
||||
textField.label = this.hass!.localize(
|
||||
"ui.panel.config.cloud.dialog_cloudhook.copied_to_clipboard"
|
||||
);
|
||||
} catch (err: any) {
|
||||
@@ -133,18 +128,19 @@ export class DialogManageCloudhook extends LitElement {
|
||||
}
|
||||
|
||||
private _restoreLabel() {
|
||||
this._paperInput.label = inputLabel;
|
||||
this._input.label = inputLabel;
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return [
|
||||
haStyle,
|
||||
haStyleDialog,
|
||||
css`
|
||||
ha-dialog {
|
||||
width: 650px;
|
||||
}
|
||||
paper-input {
|
||||
margin-top: -8px;
|
||||
ha-textfield {
|
||||
display: block;
|
||||
}
|
||||
button.link {
|
||||
color: var(--primary-color);
|
||||
|
@@ -1,12 +1,25 @@
|
||||
import "@material/mwc-button/mwc-button";
|
||||
import "@polymer/paper-input/paper-input";
|
||||
import type { PaperInputElement } from "@polymer/paper-input/paper-input";
|
||||
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
|
||||
import {
|
||||
css,
|
||||
CSSResultGroup,
|
||||
html,
|
||||
LitElement,
|
||||
PropertyValues,
|
||||
TemplateResult,
|
||||
} from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { isComponentLoaded } from "../../../common/config/is_component_loaded";
|
||||
import "../../../components/ha-card";
|
||||
import "../../../components/ha-switch";
|
||||
import "../../../components/ha-alert";
|
||||
import "../../../components/ha-formfield";
|
||||
import "../../../components/ha-textfield";
|
||||
import type { HaTextField } from "../../../components/ha-textfield";
|
||||
import { CloudStatus, fetchCloudStatus } from "../../../data/cloud";
|
||||
import { saveCoreConfig } from "../../../data/core";
|
||||
import type { PolymerChangedEvent } from "../../../polymer-types";
|
||||
import type { HomeAssistant } from "../../../types";
|
||||
import { isIPAddress } from "../../../common/string/is_ip_address";
|
||||
|
||||
@customElement("ha-config-url-form")
|
||||
class ConfigUrlForm extends LitElement {
|
||||
@@ -20,18 +33,48 @@ class ConfigUrlForm extends LitElement {
|
||||
|
||||
@state() private _internal_url?: string;
|
||||
|
||||
@state() private _cloudStatus?: CloudStatus | null;
|
||||
|
||||
@state() private _showCustomExternalUrl = false;
|
||||
|
||||
@state() private _showCustomInternalUrl = false;
|
||||
|
||||
protected render(): TemplateResult {
|
||||
const canEdit = ["storage", "default"].includes(
|
||||
this.hass.config.config_source
|
||||
);
|
||||
const disabled = this._working || !canEdit;
|
||||
|
||||
if (!this.hass.userData?.showAdvanced) {
|
||||
if (!this.hass.userData?.showAdvanced || this._cloudStatus === undefined) {
|
||||
return html``;
|
||||
}
|
||||
|
||||
const internalUrl = this._internalUrlValue;
|
||||
const externalUrl = this._externalUrlValue;
|
||||
let hasCloud: boolean;
|
||||
let remoteEnabled: boolean;
|
||||
let httpUseHttps: boolean;
|
||||
|
||||
if (this._cloudStatus === null) {
|
||||
hasCloud = false;
|
||||
remoteEnabled = false;
|
||||
httpUseHttps = false;
|
||||
} else {
|
||||
httpUseHttps = this._cloudStatus.http_use_ssl;
|
||||
|
||||
if (this._cloudStatus.logged_in) {
|
||||
hasCloud = true;
|
||||
remoteEnabled =
|
||||
this._cloudStatus.active_subscription &&
|
||||
this._cloudStatus.prefs.remote_enabled;
|
||||
} else {
|
||||
hasCloud = false;
|
||||
remoteEnabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
return html`
|
||||
<ha-card>
|
||||
<ha-card .header=${this.hass.localize("ui.panel.config.url.caption")}>
|
||||
<div class="card-content">
|
||||
${!canEdit
|
||||
? html`
|
||||
@@ -43,46 +86,147 @@ class ConfigUrlForm extends LitElement {
|
||||
`
|
||||
: ""}
|
||||
${this._error ? html`<div class="error">${this._error}</div>` : ""}
|
||||
<div class="row">
|
||||
<div class="flex">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.core.section.core.core_config.external_url"
|
||||
)}
|
||||
</div>
|
||||
|
||||
<paper-input
|
||||
class="flex"
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.core.section.core.core_config.external_url"
|
||||
)}
|
||||
name="external_url"
|
||||
type="url"
|
||||
.disabled=${disabled}
|
||||
.value=${this._externalUrlValue}
|
||||
@value-changed=${this._handleChange}
|
||||
>
|
||||
</paper-input>
|
||||
<div class="description">
|
||||
${this.hass.localize("ui.panel.config.url.description")}
|
||||
</div>
|
||||
|
||||
${hasCloud
|
||||
? html`
|
||||
<div class="row">
|
||||
<div class="flex">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.url.external_url_label"
|
||||
)}
|
||||
</div>
|
||||
<ha-formfield
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.url.external_use_ha_cloud"
|
||||
)}
|
||||
>
|
||||
<ha-switch
|
||||
.disabled=${disabled}
|
||||
.checked=${externalUrl === null}
|
||||
@change=${this._toggleCloud}
|
||||
></ha-switch>
|
||||
</ha-formfield>
|
||||
</div>
|
||||
`
|
||||
: ""}
|
||||
${!this._showCustomExternalUrl
|
||||
? ""
|
||||
: html`
|
||||
<div class="row">
|
||||
<div class="flex">
|
||||
${hasCloud
|
||||
? ""
|
||||
: this.hass.localize(
|
||||
"ui.panel.config.url.external_url_label"
|
||||
)}
|
||||
</div>
|
||||
<ha-textfield
|
||||
class="flex"
|
||||
name="external_url"
|
||||
type="url"
|
||||
.disabled=${disabled}
|
||||
.value=${externalUrl || ""}
|
||||
@change=${this._handleChange}
|
||||
placeholder="https://example.duckdns.org:8123"
|
||||
>
|
||||
</ha-textfield>
|
||||
</div>
|
||||
`}
|
||||
${hasCloud || !isComponentLoaded(this.hass, "cloud")
|
||||
? ""
|
||||
: html`
|
||||
<div class="row">
|
||||
<div class="flex"></div>
|
||||
<a href="/config/cloud"
|
||||
>${this.hass.localize(
|
||||
"ui.panel.config.url.external_get_ha_cloud"
|
||||
)}</a
|
||||
>
|
||||
</div>
|
||||
`}
|
||||
${!this._showCustomExternalUrl && hasCloud
|
||||
? html`
|
||||
${remoteEnabled
|
||||
? ""
|
||||
: html`
|
||||
<ha-alert alert-type="error">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.url.ha_cloud_remote_not_enabled"
|
||||
)}
|
||||
<a href="/config/cloud" slot="action"
|
||||
><mwc-button
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.url.enable_remote"
|
||||
)}
|
||||
></mwc-button
|
||||
></a>
|
||||
</ha-alert>
|
||||
`}
|
||||
`
|
||||
: ""}
|
||||
|
||||
<div class="row">
|
||||
<div class="flex">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.core.section.core.core_config.internal_url"
|
||||
)}
|
||||
${this.hass.localize("ui.panel.config.url.internal_url_label")}
|
||||
</div>
|
||||
<paper-input
|
||||
class="flex"
|
||||
|
||||
<ha-formfield
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.core.section.core.core_config.internal_url"
|
||||
"ui.panel.config.url.internal_url_automatic"
|
||||
)}
|
||||
name="internal_url"
|
||||
type="url"
|
||||
.disabled=${disabled}
|
||||
.value=${this._internalUrlValue}
|
||||
@value-changed=${this._handleChange}
|
||||
>
|
||||
</paper-input>
|
||||
<ha-switch
|
||||
.checked=${internalUrl === null}
|
||||
@change=${this._toggleInternalAutomatic}
|
||||
></ha-switch>
|
||||
</ha-formfield>
|
||||
</div>
|
||||
|
||||
${!this._showCustomInternalUrl
|
||||
? ""
|
||||
: html`
|
||||
<div class="row">
|
||||
<div class="flex"></div>
|
||||
<ha-textfield
|
||||
class="flex"
|
||||
name="internal_url"
|
||||
type="url"
|
||||
placeholder="http://<some IP address>:8123"
|
||||
.disabled=${disabled}
|
||||
.value=${internalUrl || ""}
|
||||
@change=${this._handleChange}
|
||||
>
|
||||
</ha-textfield>
|
||||
</div>
|
||||
`}
|
||||
${
|
||||
// If the user has configured a cert, show an error if
|
||||
httpUseHttps && // there is no internal url configured
|
||||
(!internalUrl ||
|
||||
// the internal url does not start with https
|
||||
!internalUrl.startsWith("https://") ||
|
||||
// the internal url points at an IP address
|
||||
isIPAddress(new URL(internalUrl).hostname))
|
||||
? html`
|
||||
<ha-alert
|
||||
.alertType=${this._showCustomInternalUrl
|
||||
? "info"
|
||||
: "warning"}
|
||||
.title=${this.hass.localize(
|
||||
"ui.panel.config.url.intenral_url_https_error_title"
|
||||
)}
|
||||
>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.url.internal_url_https_error_description"
|
||||
)}
|
||||
</ha-alert>
|
||||
`
|
||||
: ""
|
||||
}
|
||||
</div>
|
||||
<div class="card-actions">
|
||||
<mwc-button @click=${this._save} .disabled=${disabled}>
|
||||
@@ -95,6 +239,24 @@ class ConfigUrlForm extends LitElement {
|
||||
`;
|
||||
}
|
||||
|
||||
protected override firstUpdated(changedProps: PropertyValues) {
|
||||
super.firstUpdated(changedProps);
|
||||
|
||||
this._showCustomInternalUrl = this._internalUrlValue !== null;
|
||||
|
||||
if (isComponentLoaded(this.hass, "cloud")) {
|
||||
fetchCloudStatus(this.hass).then((cloudStatus) => {
|
||||
if (cloudStatus.logged_in) {
|
||||
this._cloudStatus = cloudStatus;
|
||||
this._showCustomExternalUrl = this._externalUrlValue !== null;
|
||||
}
|
||||
});
|
||||
} else {
|
||||
this._cloudStatus = null;
|
||||
this._showCustomExternalUrl = true;
|
||||
}
|
||||
}
|
||||
|
||||
private get _internalUrlValue() {
|
||||
return this._internal_url !== undefined
|
||||
? this._internal_url
|
||||
@@ -107,9 +269,17 @@ class ConfigUrlForm extends LitElement {
|
||||
: this.hass.config.external_url;
|
||||
}
|
||||
|
||||
private _toggleCloud(ev) {
|
||||
this._showCustomExternalUrl = !ev.currentTarget.checked;
|
||||
}
|
||||
|
||||
private _toggleInternalAutomatic(ev) {
|
||||
this._showCustomInternalUrl = !ev.currentTarget.checked;
|
||||
}
|
||||
|
||||
private _handleChange(ev: PolymerChangedEvent<string>) {
|
||||
const target = ev.currentTarget as PaperInputElement;
|
||||
this[`_${target.name}`] = target.value;
|
||||
const target = ev.currentTarget as HaTextField;
|
||||
this[`_${target.name}`] = target.value || null;
|
||||
}
|
||||
|
||||
private async _save() {
|
||||
@@ -117,8 +287,12 @@ class ConfigUrlForm extends LitElement {
|
||||
this._error = undefined;
|
||||
try {
|
||||
await saveCoreConfig(this.hass, {
|
||||
external_url: this._external_url || null,
|
||||
internal_url: this._internal_url || null,
|
||||
external_url: this._showCustomExternalUrl
|
||||
? this._external_url || null
|
||||
: null,
|
||||
internal_url: this._showCustomInternalUrl
|
||||
? this._internal_url || null
|
||||
: null,
|
||||
});
|
||||
} catch (err: any) {
|
||||
this._error = err.message || err;
|
||||
@@ -129,11 +303,15 @@ class ConfigUrlForm extends LitElement {
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return css`
|
||||
.description {
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
.row {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
margin: 0 -8px;
|
||||
align-items: center;
|
||||
padding: 8px 0;
|
||||
}
|
||||
|
||||
.secondary {
|
||||
@@ -154,6 +332,10 @@ class ConfigUrlForm extends LitElement {
|
||||
.card-actions {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
a {
|
||||
color: var(--primary-color);
|
||||
}
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
@@ -40,7 +40,7 @@ export class HaDeviceEntitiesCard extends LitElement {
|
||||
|
||||
@property() public entities!: EntityRegistryStateEntry[];
|
||||
|
||||
@property() public showDisabled = false;
|
||||
@property() public showHidden = false;
|
||||
|
||||
@state() private _extDisabledEntityEntries?: Record<
|
||||
string,
|
||||
@@ -60,77 +60,77 @@ export class HaDeviceEntitiesCard extends LitElement {
|
||||
}
|
||||
|
||||
protected render(): TemplateResult {
|
||||
const disabledEntities: EntityRegistryStateEntry[] = [];
|
||||
if (!this.entities.length) {
|
||||
return html`
|
||||
<ha-card .header=${this.header}>
|
||||
<div class="empty card-content">
|
||||
${this.hass.localize("ui.panel.config.devices.entities.none")}
|
||||
</div>
|
||||
</ha-card>
|
||||
`;
|
||||
}
|
||||
|
||||
const shownEntities: EntityRegistryStateEntry[] = [];
|
||||
const hiddenEntities: EntityRegistryStateEntry[] = [];
|
||||
this._entityRows = [];
|
||||
|
||||
this.entities.forEach((entry) => {
|
||||
if (entry.disabled_by || entry.hidden_by) {
|
||||
if (this._extDisabledEntityEntries) {
|
||||
hiddenEntities.push(
|
||||
this._extDisabledEntityEntries[entry.entity_id] || entry
|
||||
);
|
||||
} else {
|
||||
hiddenEntities.push(entry);
|
||||
}
|
||||
} else {
|
||||
shownEntities.push(entry);
|
||||
}
|
||||
});
|
||||
|
||||
return html`
|
||||
<ha-card .header=${this.header}>
|
||||
${this.entities.length
|
||||
? html`
|
||||
<div id="entities" @hass-more-info=${this._overrideMoreInfo}>
|
||||
${this.entities.map((entry: EntityRegistryStateEntry) => {
|
||||
if (entry.disabled_by) {
|
||||
if (this._extDisabledEntityEntries) {
|
||||
disabledEntities.push(
|
||||
this._extDisabledEntityEntries[entry.entity_id] || entry
|
||||
);
|
||||
} else {
|
||||
disabledEntities.push(entry);
|
||||
}
|
||||
return "";
|
||||
}
|
||||
return this.hass.states[entry.entity_id]
|
||||
? this._renderEntity(entry)
|
||||
: this._renderEntry(entry);
|
||||
})}
|
||||
</div>
|
||||
${disabledEntities.length
|
||||
? !this.showDisabled
|
||||
? html`
|
||||
<button
|
||||
class="show-more"
|
||||
@click=${this._toggleShowDisabled}
|
||||
>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.devices.entities.disabled_entities",
|
||||
"count",
|
||||
disabledEntities.length
|
||||
)}
|
||||
</button>
|
||||
`
|
||||
: html`
|
||||
${disabledEntities.map((entry) =>
|
||||
this._renderEntry(entry)
|
||||
)}
|
||||
<button
|
||||
class="show-more"
|
||||
@click=${this._toggleShowDisabled}
|
||||
>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.devices.entities.hide_disabled"
|
||||
)}
|
||||
</button>
|
||||
`
|
||||
: ""}
|
||||
<div class="card-actions">
|
||||
<mwc-button @click=${this._addToLovelaceView}>
|
||||
<div id="entities" @hass-more-info=${this._overrideMoreInfo}>
|
||||
${shownEntities.map((entry) =>
|
||||
this.hass.states[entry.entity_id]
|
||||
? this._renderEntity(entry)
|
||||
: this._renderEntry(entry)
|
||||
)}
|
||||
</div>
|
||||
${hiddenEntities.length
|
||||
? !this.showHidden
|
||||
? html`
|
||||
<button class="show-more" @click=${this._toggleShowHidden}>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.devices.entities.add_entities_lovelace"
|
||||
"ui.panel.config.devices.entities.hidden_entities",
|
||||
"count",
|
||||
hiddenEntities.length
|
||||
)}
|
||||
</mwc-button>
|
||||
</div>
|
||||
`
|
||||
: html`
|
||||
<div class="empty card-content">
|
||||
${this.hass.localize("ui.panel.config.devices.entities.none")}
|
||||
</div>
|
||||
`}
|
||||
</button>
|
||||
`
|
||||
: html`
|
||||
${hiddenEntities.map((entry) => this._renderEntry(entry))}
|
||||
<button class="show-more" @click=${this._toggleShowHidden}>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.devices.entities.hide_disabled"
|
||||
)}
|
||||
</button>
|
||||
`
|
||||
: ""}
|
||||
<div class="card-actions">
|
||||
<mwc-button @click=${this._addToLovelaceView}>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.devices.entities.add_entities_lovelace"
|
||||
)}
|
||||
</mwc-button>
|
||||
</div>
|
||||
</ha-card>
|
||||
`;
|
||||
}
|
||||
|
||||
private _toggleShowDisabled() {
|
||||
this.showDisabled = !this.showDisabled;
|
||||
if (!this.showDisabled || this._extDisabledEntityEntries !== undefined) {
|
||||
private _toggleShowHidden() {
|
||||
this.showHidden = !this.showHidden;
|
||||
if (!this.showHidden || this._extDisabledEntityEntries !== undefined) {
|
||||
return;
|
||||
}
|
||||
this._extDisabledEntityEntries = {};
|
||||
|
@@ -1,84 +0,0 @@
|
||||
import "@material/mwc-button/mwc-button";
|
||||
import {
|
||||
css,
|
||||
CSSResultGroup,
|
||||
html,
|
||||
LitElement,
|
||||
PropertyValues,
|
||||
TemplateResult,
|
||||
} from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { navigate } from "../../../../../../common/navigate";
|
||||
import { DeviceRegistryEntry } from "../../../../../../data/device_registry";
|
||||
import {
|
||||
getIdentifiersFromDevice,
|
||||
OZWNodeIdentifiers,
|
||||
} from "../../../../../../data/ozw";
|
||||
import { haStyle } from "../../../../../../resources/styles";
|
||||
import { HomeAssistant } from "../../../../../../types";
|
||||
import { showOZWRefreshNodeDialog } from "../../../../integrations/integration-panels/ozw/show-dialog-ozw-refresh-node";
|
||||
|
||||
@customElement("ha-device-actions-ozw")
|
||||
export class HaDeviceActionsOzw extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property() public device!: DeviceRegistryEntry;
|
||||
|
||||
@property()
|
||||
private node_id = 0;
|
||||
|
||||
@property()
|
||||
private ozw_instance = 1;
|
||||
|
||||
protected updated(changedProperties: PropertyValues) {
|
||||
if (changedProperties.has("device")) {
|
||||
const identifiers: OZWNodeIdentifiers | undefined =
|
||||
getIdentifiersFromDevice(this.device);
|
||||
if (!identifiers) {
|
||||
return;
|
||||
}
|
||||
this.ozw_instance = identifiers.ozw_instance;
|
||||
this.node_id = identifiers.node_id;
|
||||
}
|
||||
}
|
||||
|
||||
protected render(): TemplateResult {
|
||||
if (!this.ozw_instance || !this.node_id) {
|
||||
return html``;
|
||||
}
|
||||
return html`
|
||||
<mwc-button @click=${this._nodeDetailsClicked}>
|
||||
${this.hass.localize("ui.panel.config.ozw.node.button")}
|
||||
</mwc-button>
|
||||
<mwc-button @click=${this._refreshNodeClicked}>
|
||||
${this.hass.localize("ui.panel.config.ozw.refresh_node.button")}
|
||||
</mwc-button>
|
||||
`;
|
||||
}
|
||||
|
||||
private async _refreshNodeClicked() {
|
||||
showOZWRefreshNodeDialog(this, {
|
||||
node_id: this.node_id,
|
||||
ozw_instance: this.ozw_instance,
|
||||
});
|
||||
}
|
||||
|
||||
private async _nodeDetailsClicked() {
|
||||
navigate(
|
||||
`/config/ozw/network/${this.ozw_instance}/node/${this.node_id}/dashboard`
|
||||
);
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return [
|
||||
haStyle,
|
||||
css`
|
||||
:host {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
}
|
@@ -1,99 +0,0 @@
|
||||
import {
|
||||
css,
|
||||
CSSResultGroup,
|
||||
html,
|
||||
LitElement,
|
||||
PropertyValues,
|
||||
TemplateResult,
|
||||
} from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { DeviceRegistryEntry } from "../../../../../../data/device_registry";
|
||||
import {
|
||||
fetchOZWNodeStatus,
|
||||
getIdentifiersFromDevice,
|
||||
OZWDevice,
|
||||
OZWNodeIdentifiers,
|
||||
} from "../../../../../../data/ozw";
|
||||
import { haStyle } from "../../../../../../resources/styles";
|
||||
import { HomeAssistant } from "../../../../../../types";
|
||||
|
||||
@customElement("ha-device-info-ozw")
|
||||
export class HaDeviceInfoOzw extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property() public device!: DeviceRegistryEntry;
|
||||
|
||||
@property()
|
||||
private node_id = 0;
|
||||
|
||||
@property()
|
||||
private ozw_instance = 1;
|
||||
|
||||
@state() private _ozwDevice?: OZWDevice;
|
||||
|
||||
protected updated(changedProperties: PropertyValues) {
|
||||
if (changedProperties.has("device")) {
|
||||
const identifiers: OZWNodeIdentifiers | undefined =
|
||||
getIdentifiersFromDevice(this.device);
|
||||
if (!identifiers) {
|
||||
return;
|
||||
}
|
||||
this.ozw_instance = identifiers.ozw_instance;
|
||||
this.node_id = identifiers.node_id;
|
||||
|
||||
this._fetchNodeDetails();
|
||||
}
|
||||
}
|
||||
|
||||
protected async _fetchNodeDetails() {
|
||||
this._ozwDevice = await fetchOZWNodeStatus(
|
||||
this.hass,
|
||||
this.ozw_instance,
|
||||
this.node_id
|
||||
);
|
||||
}
|
||||
|
||||
protected render(): TemplateResult {
|
||||
if (!this._ozwDevice) {
|
||||
return html``;
|
||||
}
|
||||
return html`
|
||||
<h4>
|
||||
${this.hass.localize("ui.panel.config.ozw.device_info.zwave_info")}
|
||||
</h4>
|
||||
<div>
|
||||
${this.hass.localize("ui.panel.config.ozw.common.node_id")}:
|
||||
${this._ozwDevice.node_id}
|
||||
</div>
|
||||
<div>
|
||||
${this.hass.localize("ui.panel.config.ozw.device_info.stage")}:
|
||||
${this._ozwDevice.node_query_stage}
|
||||
</div>
|
||||
<div>
|
||||
${this.hass.localize("ui.panel.config.ozw.common.ozw_instance")}:
|
||||
${this._ozwDevice.ozw_instance}
|
||||
</div>
|
||||
<div>
|
||||
${this.hass.localize("ui.panel.config.ozw.device_info.node_failed")}:
|
||||
${this._ozwDevice.is_failed
|
||||
? this.hass.localize("ui.common.yes")
|
||||
: this.hass.localize("ui.common.no")}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return [
|
||||
haStyle,
|
||||
css`
|
||||
h4 {
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
div {
|
||||
word-break: break-all;
|
||||
margin-top: 2px;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
}
|
@@ -27,7 +27,7 @@ import { HomeAssistant } from "../../../../../../types";
|
||||
export class HaDeviceInfoZWaveJS extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property() public device!: DeviceRegistryEntry;
|
||||
@property({ attribute: false }) public device!: DeviceRegistryEntry;
|
||||
|
||||
@state() private _entryId?: string;
|
||||
|
||||
@@ -173,3 +173,9 @@ export class HaDeviceInfoZWaveJS extends LitElement {
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-device-info-zwave_js": HaDeviceInfoZWaveJS;
|
||||
}
|
||||
}
|
||||
|
@@ -557,7 +557,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 +902,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");
|
||||
|
@@ -19,6 +19,7 @@ import "../../../../components/entity/ha-statistic-picker";
|
||||
import "../../../../components/entity/ha-entity-picker";
|
||||
import "../../../../components/ha-radio";
|
||||
import "../../../../components/ha-formfield";
|
||||
import "../../../../components/ha-textfield";
|
||||
import type { HaRadio } from "../../../../components/ha-radio";
|
||||
|
||||
@customElement("dialog-energy-gas-settings")
|
||||
@@ -188,20 +189,19 @@ export class DialogEnergyGasSettings
|
||||
></ha-radio>
|
||||
</ha-formfield>
|
||||
${this._costs === "number"
|
||||
? html`<paper-input
|
||||
? html`<ha-textfield
|
||||
.label=${this.hass.localize(
|
||||
`ui.panel.config.energy.gas.dialog.cost_number_input`,
|
||||
{ unit }
|
||||
)}
|
||||
no-label-float
|
||||
class="price-options"
|
||||
step=".01"
|
||||
type="number"
|
||||
.value=${this._source.number_energy_price}
|
||||
@value-changed=${this._numberPriceChanged}
|
||||
@change=${this._numberPriceChanged}
|
||||
.suffix=${`${this.hass.config.currency}/${unit}`}
|
||||
>
|
||||
<span slot="suffix">${this.hass.config.currency}/${unit}</span>
|
||||
</paper-input>`
|
||||
</ha-textfield>`
|
||||
: ""}
|
||||
|
||||
<mwc-button @click=${this.closeDialog} slot="secondaryAction">
|
||||
@@ -223,10 +223,10 @@ export class DialogEnergyGasSettings
|
||||
this._costs = input.value as any;
|
||||
}
|
||||
|
||||
private _numberPriceChanged(ev: CustomEvent) {
|
||||
private _numberPriceChanged(ev) {
|
||||
this._source = {
|
||||
...this._source!,
|
||||
number_energy_price: Number(ev.detail.value),
|
||||
number_energy_price: Number(ev.target.value),
|
||||
entity_energy_price: null,
|
||||
stat_cost: null,
|
||||
};
|
||||
@@ -295,13 +295,10 @@ export class DialogEnergyGasSettings
|
||||
ha-formfield {
|
||||
display: block;
|
||||
}
|
||||
ha-statistic-picker {
|
||||
width: 100%;
|
||||
}
|
||||
.price-options {
|
||||
display: block;
|
||||
padding-left: 52px;
|
||||
margin-top: -16px;
|
||||
margin-top: -8px;
|
||||
}
|
||||
`,
|
||||
];
|
||||
|
@@ -190,24 +190,21 @@ export class DialogEnergyGridFlowSettings
|
||||
></ha-radio>
|
||||
</ha-formfield>
|
||||
${this._costs === "number"
|
||||
? html`<paper-input
|
||||
? html`<ha-textfield
|
||||
.label=${this.hass.localize(
|
||||
`ui.panel.config.energy.grid.flow_dialog.${this._params.direction}.cost_number_input`
|
||||
)}
|
||||
no-label-float
|
||||
class="price-options"
|
||||
step=".01"
|
||||
type="number"
|
||||
.value=${this._source.number_energy_price}
|
||||
@value-changed=${this._numberPriceChanged}
|
||||
.suffix=${this.hass.localize(
|
||||
`ui.panel.config.energy.grid.flow_dialog.${this._params.direction}.cost_number_suffix`,
|
||||
{ currency: this.hass.config.currency }
|
||||
)}
|
||||
@change=${this._numberPriceChanged}
|
||||
>
|
||||
<span slot="suffix"
|
||||
>${this.hass.localize(
|
||||
`ui.panel.config.energy.grid.flow_dialog.${this._params.direction}.cost_number_suffix`,
|
||||
{ currency: this.hass.config.currency }
|
||||
)}</span
|
||||
>
|
||||
</paper-input>`
|
||||
</ha-textfield>`
|
||||
: ""}
|
||||
|
||||
<mwc-button @click=${this.closeDialog} slot="secondaryAction">
|
||||
@@ -243,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,
|
||||
};
|
||||
}
|
||||
@@ -302,13 +299,10 @@ export class DialogEnergyGridFlowSettings
|
||||
ha-formfield {
|
||||
display: block;
|
||||
}
|
||||
ha-statistic-picker {
|
||||
width: 100%;
|
||||
}
|
||||
.price-options {
|
||||
display: block;
|
||||
padding-left: 52px;
|
||||
margin-top: -16px;
|
||||
margin-top: -8px;
|
||||
}
|
||||
`,
|
||||
];
|
||||
|
@@ -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;
|
||||
}
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
@@ -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;
|
||||
}
|
||||
`,
|
||||
];
|
||||
|
@@ -3,6 +3,7 @@ import {
|
||||
mdiAlertCircle,
|
||||
mdiCancel,
|
||||
mdiDelete,
|
||||
mdiEyeOff,
|
||||
mdiFilterVariant,
|
||||
mdiPencilOff,
|
||||
mdiPlus,
|
||||
@@ -101,6 +102,8 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) {
|
||||
|
||||
@state() private _showDisabled = false;
|
||||
|
||||
@state() private _showHidden = false;
|
||||
|
||||
@state() private _showUnavailable = true;
|
||||
|
||||
@state() private _showReadOnly = true;
|
||||
@@ -249,7 +252,10 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) {
|
||||
filterable: true,
|
||||
width: "68px",
|
||||
template: (_status, entity: EntityRow) =>
|
||||
entity.unavailable || entity.disabled_by || entity.readonly
|
||||
entity.unavailable ||
|
||||
entity.disabled_by ||
|
||||
entity.hidden_by ||
|
||||
entity.readonly
|
||||
? html`
|
||||
<div
|
||||
tabindex="0"
|
||||
@@ -265,6 +271,8 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) {
|
||||
? mdiAlertCircle
|
||||
: entity.disabled_by
|
||||
? mdiCancel
|
||||
: entity.hidden_by
|
||||
? mdiEyeOff
|
||||
: mdiPencilOff}
|
||||
></ha-svg-icon>
|
||||
<paper-tooltip animation-delay="0" position="left">
|
||||
@@ -280,6 +288,10 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) {
|
||||
? this.hass.localize(
|
||||
"ui.panel.config.entities.picker.status.disabled"
|
||||
)
|
||||
: entity.hidden_by
|
||||
? this.hass.localize(
|
||||
"ui.panel.config.entities.picker.status.hidden"
|
||||
)
|
||||
: this.hass.localize(
|
||||
"ui.panel.config.entities.picker.status.readonly"
|
||||
)}
|
||||
@@ -301,6 +313,7 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) {
|
||||
showDisabled: boolean,
|
||||
showUnavailable: boolean,
|
||||
showReadOnly: boolean,
|
||||
showHidden: boolean,
|
||||
entries?: ConfigEntry[]
|
||||
) => {
|
||||
const result: EntityRow[] = [];
|
||||
@@ -362,6 +375,12 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) {
|
||||
);
|
||||
}
|
||||
|
||||
if (!showHidden) {
|
||||
filteredEntities = filteredEntities.filter(
|
||||
(entity) => !entity.hidden_by
|
||||
);
|
||||
}
|
||||
|
||||
for (const entry of filteredEntities) {
|
||||
const entity = this.hass.states[entry.entity_id];
|
||||
const unavailable = entity?.state === UNAVAILABLE;
|
||||
@@ -465,6 +484,7 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) {
|
||||
this._showDisabled,
|
||||
this._showUnavailable,
|
||||
this._showReadOnly,
|
||||
this._showHidden,
|
||||
this._entries
|
||||
);
|
||||
|
||||
@@ -533,6 +553,11 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) {
|
||||
"ui.panel.config.entities.picker.disable_selected.button"
|
||||
)}</mwc-button
|
||||
>
|
||||
<mwc-button @click=${this._hideSelected}
|
||||
>${this.hass.localize(
|
||||
"ui.panel.config.entities.picker.hide_selected.button"
|
||||
)}</mwc-button
|
||||
>
|
||||
<mwc-button @click=${this._removeSelected} class="warning"
|
||||
>${this.hass.localize(
|
||||
"ui.panel.config.entities.picker.remove_selected.button"
|
||||
@@ -562,6 +587,17 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) {
|
||||
"ui.panel.config.entities.picker.disable_selected.button"
|
||||
)}
|
||||
</paper-tooltip>
|
||||
<ha-icon-button
|
||||
id="hide-btn"
|
||||
@click=${this._hideSelected}
|
||||
.path=${mdiCancel}
|
||||
.label=${this.hass.localize("ui.common.hide")}
|
||||
></ha-icon-button>
|
||||
<paper-tooltip animation-delay="0" for="hide-btn">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.entities.picker.hide_selected.button"
|
||||
)}
|
||||
</paper-tooltip>
|
||||
<ha-icon-button
|
||||
class="warning"
|
||||
id="remove-btn"
|
||||
@@ -603,6 +639,15 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) {
|
||||
"ui.panel.config.entities.picker.filter.show_disabled"
|
||||
)}
|
||||
</ha-check-list-item>
|
||||
<ha-check-list-item
|
||||
@request-selected=${this._showHiddenChanged}
|
||||
.selected=${this._showHidden}
|
||||
left
|
||||
>
|
||||
${this.hass!.localize(
|
||||
"ui.panel.config.entities.picker.filter.show_hidden"
|
||||
)}
|
||||
</ha-check-list-item>
|
||||
<ha-check-list-item
|
||||
@request-selected=${this._showRestoredChanged}
|
||||
graphic="control"
|
||||
@@ -671,6 +716,7 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) {
|
||||
entity_id: entityId,
|
||||
platform: computeDomain(entityId),
|
||||
disabled_by: null,
|
||||
hidden_by: null,
|
||||
area_id: null,
|
||||
config_entry_id: null,
|
||||
device_id: null,
|
||||
@@ -693,6 +739,13 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) {
|
||||
this._showDisabled = ev.detail.selected;
|
||||
}
|
||||
|
||||
private _showHiddenChanged(ev: CustomEvent<RequestSelectedDetail>) {
|
||||
if (ev.detail.source !== "property") {
|
||||
return;
|
||||
}
|
||||
this._showHidden = ev.detail.selected;
|
||||
}
|
||||
|
||||
private _showRestoredChanged(ev: CustomEvent<RequestSelectedDetail>) {
|
||||
if (ev.detail.source !== "property") {
|
||||
return;
|
||||
@@ -791,6 +844,29 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) {
|
||||
});
|
||||
}
|
||||
|
||||
private _hideSelected() {
|
||||
showConfirmationDialog(this, {
|
||||
title: this.hass.localize(
|
||||
"ui.panel.config.entities.picker.hide_selected.confirm_title",
|
||||
"number",
|
||||
this._selectedEntities.length
|
||||
),
|
||||
text: this.hass.localize(
|
||||
"ui.panel.config.entities.picker.hide_selected.confirm_text"
|
||||
),
|
||||
confirmText: this.hass.localize("ui.common.hide"),
|
||||
dismissText: this.hass.localize("ui.common.cancel"),
|
||||
confirm: () => {
|
||||
this._selectedEntities.forEach((entity) =>
|
||||
updateEntityRegistryEntry(this.hass, entity, {
|
||||
hidden_by: "user",
|
||||
})
|
||||
);
|
||||
this._clearSelection();
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
private _removeSelected() {
|
||||
const removeableEntities = this._selectedEntities.filter((entity) => {
|
||||
const stateObj = this.hass.states[entity];
|
||||
|
@@ -376,21 +376,11 @@ class HaPanelConfig extends HassRouterPage {
|
||||
"./integrations/integration-panels/zha/zha-config-dashboard-router"
|
||||
),
|
||||
},
|
||||
zwave: {
|
||||
tag: "zwave-config-router",
|
||||
load: () =>
|
||||
import("./integrations/integration-panels/zwave/zwave-config-router"),
|
||||
},
|
||||
mqtt: {
|
||||
tag: "mqtt-config-panel",
|
||||
load: () =>
|
||||
import("./integrations/integration-panels/mqtt/mqtt-config-panel"),
|
||||
},
|
||||
ozw: {
|
||||
tag: "ozw-config-router",
|
||||
load: () =>
|
||||
import("./integrations/integration-panels/ozw/ozw-config-router"),
|
||||
},
|
||||
zwave_js: {
|
||||
tag: "zwave_js-config-router",
|
||||
load: () =>
|
||||
|
@@ -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"
|
||||
|
@@ -30,6 +30,7 @@ import "../../../components/ha-check-list-item";
|
||||
import { isComponentLoaded } from "../../../common/config/is_component_loaded";
|
||||
import { ConfigEntry, getConfigEntries } from "../../../data/config_entries";
|
||||
import {
|
||||
getConfigFlowHandlers,
|
||||
getConfigFlowInProgressCollection,
|
||||
localizeConfigFlowTitle,
|
||||
subscribeConfigFlowInProgress,
|
||||
@@ -51,7 +52,10 @@ import {
|
||||
} from "../../../data/integration";
|
||||
import { scanUSBDevices } from "../../../data/usb";
|
||||
import { showConfigFlowDialog } from "../../../dialogs/config-flow/show-dialog-config-flow";
|
||||
import { showConfirmationDialog } from "../../../dialogs/generic/show-dialog-box";
|
||||
import {
|
||||
showAlertDialog,
|
||||
showConfirmationDialog,
|
||||
} from "../../../dialogs/generic/show-dialog-box";
|
||||
import "../../../layouts/hass-loading-screen";
|
||||
import "../../../layouts/hass-tabs-subpage";
|
||||
import { SubscribeMixin } from "../../../mixins/subscribe-mixin";
|
||||
@@ -652,6 +656,19 @@ class HaConfigIntegrations extends SubscribeMixin(LitElement) {
|
||||
if (!domain) {
|
||||
return;
|
||||
}
|
||||
const handlers = await getConfigFlowHandlers(this.hass);
|
||||
|
||||
if (!handlers.includes(domain)) {
|
||||
showAlertDialog(this, {
|
||||
title: this.hass.localize(
|
||||
"ui.panel.config.integrations.config_flow.error"
|
||||
),
|
||||
text: this.hass.localize(
|
||||
"ui.panel.config.integrations.config_flow.no_config_flow"
|
||||
),
|
||||
});
|
||||
return;
|
||||
}
|
||||
const localize = await localizePromise;
|
||||
if (
|
||||
!(await showConfirmationDialog(this, {
|
||||
|
@@ -55,8 +55,6 @@ const integrationsWithPanel = {
|
||||
hassio: "/hassio/dashboard",
|
||||
mqtt: "/config/mqtt",
|
||||
zha: "/config/zha/dashboard",
|
||||
ozw: "/config/ozw/dashboard",
|
||||
zwave: "/config/zwave",
|
||||
zwave_js: "/config/zwave_js/dashboard",
|
||||
};
|
||||
|
||||
|
@@ -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;
|
||||
}
|
||||
}
|
@@ -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;
|
||||
}
|
||||
}
|
@@ -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;
|
||||
}
|
||||
}
|
@@ -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`
|
||||
•
|
||||
${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;
|
||||
}
|
||||
}
|
@@ -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;
|
||||
}
|
||||
}
|
@@ -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;
|
||||
}
|
||||
}
|
@@ -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;
|
||||
}
|
||||
}
|
@@ -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;
|
||||
}
|
||||
}
|
@@ -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;
|
||||
}
|
||||
}
|
@@ -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,
|
||||
});
|
||||
};
|
@@ -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);
|
@@ -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;
|
||||
}
|
||||
}
|
@@ -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);
|
@@ -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);
|
@@ -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);
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user