20220330.0 (#12165)

* Add a docs icon to the config flow dialog

* Use same help icon everywhere

* Fix quickbar overlaying, fix click handling (#11900)

* Supervisor mobile click accessibility (#11915)

* Convert objects to string in config flow error (#11908)

* Fix datepicker triangle (#11920)

* Always show tab labels (#11919)

* Remove zwave and ozw panels (#11911)

Remove zwave and ozw panels

* Convert lovelace config dialogs to ha-form (#11910)

* Guard setting up config flow for an unsupported domain (#11937)

* Show triggered vars on click (#11924)

* Allow marking YAML editor as read only (#11960)

* Convert inputs (#11907)

* Convert inputs

* Update dialog-thingtalk.ts

* imports

* Remove some additional old zwave code (#11941)

* Correct media upload error + add file name (#11949)

Co-authored-by: Bram Kragten <mail@bramkragten.nl>

* Fix humidifier more info mode dropdown (#11964)

* Make min width of select configurable (#11965)

* Fix for Statistics Editor (#11942)

Co-authored-by: Bram Kragten <mail@bramkragten.nl>

* A11y expansion panel (#11967)

* Convert file upload to mdc (#11906)

* Add location selector, convert zone editor (#11902)

* Add systemd_resolved unsupported reason (#11971)

* replace default switch icon 

to  make it stand out against a power entity which uses the same mdiFlash https://github.com/home-assistant/core/issues/67620#issuecomment-1061949527

suggest the Outline version, so create a subtle difference with the on/off icons.

* Allow selecting multiple entities (#11986)

* Fix theme setting (#11977)

* Update Style of Design Page (#11982)

* change icon to mimic physical device

and follow comments

* Use entities-picker in entity selector (#11990)

* #11971 Change order of alarm panel buttons (#11998)

* Fix zwave_js 'add/remove device' disabled bug (#12000)

* Fix zwave_js 'add/remove device' disabled bug

* revert extra change

* Fix zwave_js set config dropdown default value (#11974)

Co-authored-by: Paulus Schoutsen <paulus@home-assistant.io>

* Fix changing cost number in energy settings (#12009)

* Fix Dashboard Editing (#12011)

* Fix For Selecting Device Class (#12010)

* Fix: Allow for deleting Input_select options (#12007)

* Script ID update with Alias (#12008)

* HAWS 6.1 (#12016)

* Bumped version to 20220301.1

* Bumped version to 20220301.2

* Bumped version to 20220312.0

* Add shade to device class overrides (#11874)

* Fix: Changing Blueprint Automation Name (#12036)

* Fix @changed where using ev.detail (#12043)

* Add all cover device classes (#12042)

* Rename Lovelace Dashboard to just Dashboard (#12044)

Co-authored-by: Zack Barett <zackbarett@hey.com>

* Add Color Temp Selector (#12041)

* Utilize Hide Hidden Entities

* Reviews

* add to demo

* Add `Brand` folder and `Our story` page (#11978)

Co-authored-by: Zack Barett <zackbarett@hey.com>

* Add HA to public folder and show in markdown

* Update Translations

* Disabled by

* remove 1

* Add Description of chosen

* Add icons and buttons

* Add Color RGB Selector (#12039)

* Add Date Selector

* Add ha-form context (#12062)

* test condition (#11925)

* Revamp URL form (#12060)

* Add support for menu data entry flow option (#12055)

* Add translation

* add to basic editor and update advanced style

* clean up

* Entity Status

* Add Devices Picker (#12056)

* Remvoe redunency

* Bumped version to 20220316.0

* Bump HAWS to 7.0.0 (#12067)

* Create new Logo page

* Add files via upload

* Ignore diagnostics not found exceptions (#12066)

* Bump HAWS to 7.0.1

* Update lock

* Add Date Time Selector (#12070)

* Add radio Form Logic to Select Selector (#12063)

* Bumped version to 20220317.0 (#12074)

* Update gallery/src/pages/brand/logo.markdown

Co-authored-by: Zack Barett <zackbarett@hey.com>

* Update gallery/src/pages/brand/logo.markdown

Co-authored-by: Zack Barett <zackbarett@hey.com>

* Update logo.markdown

* Fetch history with `no_attributes` for entities that do not need them (#12082)

* Update required version of MDI to 6.6.95

* Upload release assets (#11566)

Co-authored-by: Joakim Sørensen <hi@ludeeus.dev>

* Update styles for hui-editor

Update the background-color and text-color of the app-toolbar in
hui-editor to match the styles of hui-root while in edit-mode.

Previously, these properties were set using undefined css variables that
could not be changed via themes (--dark-background-color and
--dark-text-color).

* Fix gas energy graph units if stats added by external source (#11892)

* Change Netlify preview URL (#12095)

* Update src/dialogs/config-flow/dialog-data-entry-flow.ts

* Stack Action Inputs in the Button Editor (#12076)

* Stack Action Inputs in the Button Editor

* update style

* Update for other editors

* Add support for update entities (#12059)

* Add support for update entities

* Apply suggestions from code review

Co-authored-by: Zack Barett <zackbarett@hey.com>

* Add to gallery

* implement xx%

* Adjustments for skipped

* Add progress bar

* Add UPDATE_SUPPORT_INSTALL

* Allow skipping without install support

* Add version to service call if supported

* Adjust changelog link

* Use Installing

* adjustments

* Use unavailable

Co-authored-by: Zack Barett <zackbarett@hey.com>

* Add support for integration type (#12077)

* Update When entity can change enabled or hidden (#12096)

* Add entity include and exclude to selector (#12078)

Co-authored-by: Paulus Schoutsen <balloob@gmail.com>

* change from hidden to not shown (#12097)

* Add statistic adjust dialog (#12101)

Co-authored-by: Zack Barett <zackbarett@hey.com>

* Fix Duration Selector Default (#12098)

* Fix Duration Default

* USe initial form data function

* Bumped version to 20220322.0 (#12102)

* Create user types page and rename the category (#12089)

Co-authored-by: Zack Barett <zackbarett@hey.com>
Co-authored-by: Paulus Schoutsen <balloob@gmail.com>

* Remove `setup.py` (#11593)

* Fix selecting 0 with number selector

* Update lock file with MDI updates

* Use update entities for showing updates on configuration panel (#12100)

Co-authored-by: Paulus Schoutsen <balloob@gmail.com>

* Fix loading traces for automation with custom id (#12112)

* Only show docs link when showing a form

* Exclude restored automations from dashboard (#12113)

* Support descriptions in flow menu steps (#12108)

Co-authored-by: Paulus Schoutsen <balloob@gmail.com>

* Sort selectors (#12120)

* Update type for backend (#12122)

* Fix issue where theme select does not appear when user's theme is deleted (#12104)

* Fix possibility to enable entity disabled by integration (#12121)

Co-authored-by: Zack Barett <zackbarett@hey.com>

* Allow rendering helper text from strings.json (#12119)

* Allow rendering helper text from strings.json

* Persistent helpers

* Update src/components/ha-base-time-input.ts

Co-authored-by: Zack Barett <zackbarett@hey.com>

* Update src/components/ha-base-time-input.ts

Co-authored-by: Zack Barett <zackbarett@hey.com>

* Add Day to duration selector (#12125)

* Add variables to automation trigger type

* Fix z-index map, always set icon for location selector (#12137)

* Make padding on settings row content consistent (#12139)

* Add Area Multiple Selector option (#12138)

* break theme picker out of lovelace (#12140)

* Allow binary sensor device class updates (#12124)

* Add selector initial values (#12142)

* Add badge to configuration sidebar to indicate pending updates (#12146)

* Bumped version to 20220329.0 (#12152)

* Add entity source API (#12149)

* Update adjust statistic dialog (#12118)

* Update text for adjust statistic dialog

* Change everything

* Import type

* Max show 5

* Revert back the API change

* Hide adjust button if no sum

* Adjustments

* Update src/panels/developer-tools/statistics/developer-tools-statistics.ts

* Render optional

Co-authored-by: Zack <zackbarett@hey.com>

* Fetch release notes for update entities that provides it (#12148)

* Fetch release notes for update entities that provides it

* lint

* Add support for new timer properties (#11940)

* Fix theme settings on design page (#12154)

* Allow ha-alert to be used in our markdown render (#12153)

* Allow device_tracker entities to use state_color (#12127)

* Automation description text overflow (#12040)

Co-authored-by: Bram Kragten <mail@bramkragten.nl>

* Update Pickers and selectors with required (#12151)

* Update Pickers and selectors with required

* Use native * for device and entity

* Add support for my links to create a helper config entry (#12155)

* Use brand icon instead of domain icon for helpers (#12157)

* Import components that are allowed to be defined in markdown (#12158)

* Add options to selectors gallery (#12156)

* Add helpers to list when searching in add integration (#12159)

* List Selector (#12099)

Co-authored-by: Bram Kragten <mail@bramkragten.nl>

* Add shuffle and repeat-mode of media_player to UI (#12052)

Co-authored-by: Paulus Schoutsen <balloob@gmail.com>

* Add panel to Backup integration (#11671)

Co-authored-by: Zack Barett <zackbarett@hey.com>
Co-authored-by: Paulus Schoutsen <balloob@gmail.com>

* Fix for Mobile View of Entities Table (#12160)

* Allow Sensor Units to be updated via Entity Registry (#12143)

* Add switch as x to entity settings (#12161)

Co-authored-by: Zack <zackbarett@hey.com>

* Bumped version to 20220330.0 (#12164)

Co-authored-by: Paulus Schoutsen <balloob@gmail.com>
Co-authored-by: Bram Kragten <mail@bramkragten.nl>
Co-authored-by: Steve Repsher <steverep@users.noreply.github.com>
Co-authored-by: Robin Wittebol <robinwittebol@live.nl>
Co-authored-by: Raman Gupta <7243222+raman325@users.noreply.github.com>
Co-authored-by: Philip Allgaier <philip.allgaier@gmx.de>
Co-authored-by: Joakim Sørensen <ludeeus@ludeeus.dev>
Co-authored-by: Marius <33354141+Mariusthvdb@users.noreply.github.com>
Co-authored-by: Emil Stjerneman <emil@stjerneman.com>
Co-authored-by: Charles Garwood <cgarwood@gmail.com>
Co-authored-by: Paulus Schoutsen <paulus@home-assistant.io>
Co-authored-by: jpearl <jpearl@users.noreply.github.com>
Co-authored-by: Matthias de Baat <matthias.debaat@nabucasa.com>
Co-authored-by: Matthias de Baat <hello@matthiasdebaat.com>
Co-authored-by: J. Nick Koston <nick@koston.org>
Co-authored-by: Michael Irigoyen <michael@irigoyen.dev>
Co-authored-by: Marc Mueller <30130371+cdce8p@users.noreply.github.com>
Co-authored-by: Joakim Sørensen <hi@ludeeus.dev>
Co-authored-by: Nick Iacullo <duckycrayfish@gmail.com>
Co-authored-by: Pawel <pszafer@gmail.com>
Co-authored-by: Erik <erik@montnemery.com>
Co-authored-by: Brynley McDonald <brynley+github@zephire.nz>
Co-authored-by: blair <1585872+blairun@users.noreply.github.com>
Co-authored-by: NachtaktiverHalbaffe <57433516+NachtaktiverHalbaffe@users.noreply.github.com>
This commit is contained in:
Bram Kragten 2022-03-30 20:49:00 +02:00 committed by GitHub
commit 24484d0e74
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
215 changed files with 6493 additions and 7496 deletions

View File

@ -10,10 +10,18 @@ env:
NODE_VERSION: 14
NODE_OPTIONS: --max_old_space_size=6144
# Set default workflow permissions
# All scopes not mentioned here are set to no access
# https://docs.github.com/en/actions/security-guides/automatic-token-authentication#permissions-for-the-github_token
permissions:
actions: none
jobs:
release:
name: Release
runs-on: ubuntu-latest
permissions:
contents: write # Required to upload release assets
steps:
- name: Checkout the repository
uses: actions/checkout@v2
@ -47,6 +55,13 @@ jobs:
script/release
- name: Upload release assets
uses: softprops/action-gh-release@v0.1.14
with:
files: |
dist/*.whl
dist/*.tar.gz
wheels-init:
name: Init wheels build
needs: release

View File

@ -1,4 +1,3 @@
import "web-animations-js/web-animations-next-lite.min";
import "../../../src/resources/ha-style";
import "../../../src/resources/roboto";
import "./layout/hc-lovelace";

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 67 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

View File

@ -23,7 +23,7 @@ if [[ "${PULL_REQUEST}" == "true" ]]; then
createStatus "pending" "Building design preview" "https://app.netlify.com/sites/home-assistant-gallery/deploys/$BUILD_ID"
gulp build-gallery
if [ $? -eq 0 ]; then
createStatus "success" "Build complete" "$DEPLOY_URL"
createStatus "success" "Build complete" "$DEPLOY_PRIME_URL"
else
createStatus "error" "Build failed" "https://app.netlify.com/sites/home-assistant-gallery/deploys/$BUILD_ID"
fi

View File

@ -36,12 +36,17 @@ module.exports = [
category: "misc",
header: "Miscelaneous",
},
{
category: "brand",
header: "Brand",
},
{
category: "user-test",
header: "User Tests",
header: "Users",
pages: ["user-types", "configuration-menu"],
},
{
category: "design.home-assistant.io",
header: "Design Documentation",
header: "About",
},
];

View File

@ -53,13 +53,19 @@ class DemoBlackWhiteRow extends LitElement {
firstUpdated(changedProps) {
super.firstUpdated(changedProps);
applyThemesOnElement(this.shadowRoot!.querySelector(".dark"), {
default_theme: "default",
default_dark_theme: "default",
themes: {},
darkMode: true,
theme: "default",
});
applyThemesOnElement(
this.shadowRoot!.querySelector(".dark"),
{
default_theme: "default",
default_dark_theme: "default",
themes: {},
darkMode: true,
theme: "default",
},
undefined,
undefined,
true
);
}
handleSubmit(ev) {

View File

@ -12,7 +12,14 @@ class PageDescription extends HaMarkdown {
if (!PAGES[this.page].description) {
return html``;
}
return html`
<div class="heading">
<div class="title">
${PAGES[this.page].metadata.title || this.page.split("/")[1]}
</div>
<div class="subtitle">${PAGES[this.page].metadata.subtitle}</div>
</div>
${until(
PAGES[this.page]
.description()
@ -25,9 +32,22 @@ class PageDescription extends HaMarkdown {
static styles = [
HaMarkdown.styles,
css`
.heading {
padding: 16px;
border-bottom: 1px solid var(--secondary-background-color);
}
.title {
font-size: 42px;
line-height: 56px;
padding-bottom: 8px;
}
.subtitle {
font-size: 18px;
line-height: 24px;
}
.root {
max-width: 800px;
margin: 0 auto;
margin: 16px auto;
}
.root > *:first-child {
margin-top: 0;

11
gallery/src/data/text.ts Normal file
View File

@ -0,0 +1,11 @@
export const LONG_TEXT = `
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nunc laoreet velit ut elit volutpat, eget ultrices odio lacinia. In imperdiet malesuada est, nec sagittis metus ultricies quis. Sed nisl ex, convallis porttitor ante quis, hendrerit tristique justo. Mauris pharetra venenatis augue, eu maximus sem cursus in. Quisque sed consequat risus. Suspendisse facilisis ligula a odio consectetur condimentum. Curabitur vehicula elit nec augue mollis, et volutpat massa dictum.
Nam pellentesque auctor rutrum. Suspendisse elit est, sodales vel diam nec, porttitor faucibus massa. Ut pretium ac orci eu pharetra. Praesent in nibh at magna viverra rutrum eu vitae tortor. Etiam eget sem ex. Fusce tristique odio nec lacus mattis, vitae tempor nunc malesuada. Maecenas faucibus magna vel libero maximus egestas. Vestibulum luctus semper velit, in lobortis risus tempus non. Curabitur bibendum ornare commodo. Quisque commodo neque sit amet tincidunt lacinia. Proin elementum ante velit, eu congue nulla semper quis. Pellentesque consequat vel nunc at scelerisque. Mauris sit amet venenatis diam, blandit viverra leo. Integer commodo laoreet orci.
Curabitur ipsum tortor, sodales ut augue sed, commodo porttitor libero. Pellentesque molestie vitae mi consectetur tempor. In sed lectus consequat, lobortis neque non, semper ipsum. Etiam eget ex et nibh sagittis pulvinar lacinia ac mauris. Aenean ligula eros, viverra ac nibh at, venenatis semper quam. Sed interdum ligula sit amet massa tincidunt tincidunt. Suspendisse potenti. Aliquam egestas facilisis est, sed faucibus erat scelerisque id. Duis dolor quam, viverra vitae orci euismod, laoreet pellentesque justo. Nunc malesuada non erat at ullamcorper. Mauris eget posuere odio. Vestibulum turpis nunc, pharetra eget ante in, feugiat mollis justo. Proin porttitor, diam nec vulputate pretium, tellus arcu rhoncus turpis, a blandit nisi nulla quis arcu. Nunc ac ullamcorper ligula, nec facilisis leo.
In vitae eros sollicitudin, iaculis ex eget, egestas orci. Etiam sed pretium lorem. Nam nisi enim, consectetur sit amet semper ac, semper pharetra diam. In pulvinar neque sapien, ac ullamcorper est lacinia a. Etiam tincidunt velit sed diam malesuada, eu ornare ex consectetur. Phasellus in imperdiet tellus. Sed bibendum, dui sit amet fringilla aliquet, enim odio sollicitudin lorem, vel semper turpis mauris vel mauris. Aenean congue magna ac massa cursus, in dictum orci commodo. Pellentesque mollis velit in sollicitudin tincidunt. Vestibulum et efficitur nulla.
Quisque posuere, velit sed porttitor dapibus, neque augue fringilla felis, eu luctus nisi nisl nec ipsum. Curabitur pellentesque ac lectus eget ultricies. Vestibulum est dolor, lacinia pharetra vulputate a, facilisis a magna. Nam vitae arcu nibh. Praesent finibus blandit ante, ac gravida ex mollis eget. Donec quam est, pulvinar vitae neque ut, bibendum aliquam erat. Nullam mollis arcu at sem tincidunt, in tristique lectus facilisis. Aenean ut lacus vel nisl finibus iaculis non a turpis. Integer eget ipsum ante. Donec nunc neque, vestibulum ac magna ac, posuere scelerisque dui. Pellentesque massa nibh, rhoncus id dolor quis, placerat posuere turpis. Donec aliquet augue nisi, eu finibus dui auctor et. Vestibulum eu varius lorem. Quisque lectus ante, malesuada pretium risus eget, interdum mattis enim.
`;

View File

@ -5,6 +5,7 @@ import { html, css, LitElement, PropertyValues } from "lit";
import { customElement, property, query } from "lit/decorators";
import "../../src/components/ha-icon-button";
import "../../src/managers/notification-manager";
import "../../src/components/ha-expansion-panel";
import { haStyle } from "../../src/resources/styles";
import { PAGES, SIDEBAR } from "../build/import-pages";
import { dynamicElement } from "../../src/common/dom/dynamic-element-directive";
@ -44,6 +45,10 @@ class HaGallery extends LitElement {
for (const page of group.pages!) {
const key = `${group.category}/${page}`;
const active = this._page === key;
if (!(key in PAGES)) {
console.error("Undefined page referenced in sidebar.js:", key);
continue;
}
const title = PAGES[key].metadata.title || page;
links.push(html`
<a ?active=${active} href=${`#${group.category}/${page}`}>${title}</a>
@ -53,10 +58,9 @@ class HaGallery extends LitElement {
sidebar.push(
group.header
? html`
<details>
<summary class="section">${group.header}</summary>
<ha-expansion-panel .header=${group.header}>
${links}
</details>
</ha-expansion-panel>
`
: links
);
@ -92,27 +96,34 @@ class HaGallery extends LitElement {
${dynamicElement(`demo-${this._page.replace("/", "-")}`)}
</div>
<div class="page-footer">
${PAGES[this._page].description ||
Object.keys(PAGES[this._page].metadata).length > 0
? html`
<a
href=${`${GITHUB_DEMO_URL}${this._page}.markdown`}
target="_blank"
>
Edit text
</a>
`
: ""}
${PAGES[this._page].demo
? html`
<a
href=${`${GITHUB_DEMO_URL}${this._page}.ts`}
target="_blank"
>
Edit demo
</a>
`
: ""}
<div class="header">Help us to improve our documentation</div>
<div class="secondary">
Suggest an edit to this page, or provide/view feedback for this
page.
</div>
<div>
${PAGES[this._page].description ||
Object.keys(PAGES[this._page].metadata).length > 0
? html`
<a
href=${`${GITHUB_DEMO_URL}${this._page}.markdown`}
target="_blank"
>
Edit text
</a>
`
: ""}
${PAGES[this._page].demo
? html`
<a
href=${`${GITHUB_DEMO_URL}${this._page}.ts`}
target="_blank"
>
Edit demo
</a>
`
: ""}
</div>
</div>
</div>
</mwc-drawer>
@ -186,27 +197,16 @@ class HaGallery extends LitElement {
padding: 4px;
}
.sidebar details {
margin-top: 1em;
margin-left: 1em;
}
.sidebar summary {
cursor: pointer;
font-weight: bold;
margin-bottom: 8px;
}
.sidebar a {
color: var(--primary-text-color);
display: block;
padding: 4px 12px;
padding: 12px;
text-decoration: none;
position: relative;
}
.sidebar a[active]::before {
border-radius: 4px;
border-radius: 12px;
position: absolute;
top: 0;
right: 2px;
@ -237,14 +237,32 @@ class HaGallery extends LitElement {
.page-footer {
text-align: center;
margin: 16px 0;
padding-top: 16px;
border-top: 1px solid rgba(0, 0, 0, 0.12);
margin: 16px;
padding: 16px;
border-radius: 12px;
background-color: var(--primary-background-color);
}
.page-footer div {
margin-top: 4px;
}
.page-footer .header {
font-size: 16px;
font-weight: 500;
line-height: 28px;
text-align: center;
}
.page-footer .secondary {
line-height: 23px;
text-align: center;
}
.page-footer a {
display: inline-block;
margin: 0 8px;
text-decoration: none;
}
`,
];

View File

@ -0,0 +1,34 @@
---
title: "Logo"
---
![Using our logo](/images/using-our-logo.png)
# Using our logo
As a community, we are proud of our logo. Follow these guidelines to ensure it always looks its best. Our logo follows Google's material design spec and uses the blue interface color.
[Download Logo](https://github.com/home-assistant/assets/tree/master/logo)
![Logo](/images/logo.png)
## Using the icon
Our icon is a shorter and most used version of our logo. The icon can exist without the wordmark, the wordmark should never exist without the icon.
![Logo variants](/images/logo-variants.png)
## Using the right variant
The pretty blue logo with a background shadow, pictured top left, is our primary logo. It should only be used with black, white, and non-duotone photography.
When needed you can use our logo without a shadow, as seen as the second variant.
The outlined logo should only be used on packaging.
## Exclusion zone
The logo needs some personal space. It's exclusion zone is equal to a quarter the height of the icon.
![Clearspace](/images/clearspace.png)

View File

@ -0,0 +1,41 @@
---
title: "Our story"
---
## Open source home automation that puts local control and privacy first
Home Assistant is a free and open-source software for home automation that is designed to be the central control system for smart home devices with a focus on local control and privacy. It can be accessed via a web-based user interface, via apps for Android and iOS, or using voice commands via a supported virtual assistant like Google Assistant and Amazon Alexa.
IoT devices and services are supported by modular support for controlling proprietary ecosystems if they provide public access via an Open API for third-party integrations and protocols like Bluetooth, MQTT, Zigbee, and Z-Wave, After the Home Assistant software application is installed as a computer appliance it will act as a central control system for home automation. Information from all entities it sees can be used and controlled from within scripts trigger automations using scheduling and "blueprint" subroutines, e.g. for controlling lighting, climate, entertainment systems, and appliances.
# Open Home
The Open Home is our vision for the smart home. It defines the values that we put at the heart of every decision we make at Home Assistant. Its woven into our architecture, licensing, community, and everything else.
The Open Home is about privacy, choice, and durability.
## Privacy
Your home should be your safe space. A place where you can be your true self without having to bother about what the world thinks of you. A place where you dont need to act differently to avoid an algorithm categorizing your behavior. Privacy for the Open Home means that devices need to work locally. No one else needs to know if you turn on a light bulb or change the thermostat.
It is okay for a product to offer a cloud connection, but it should be extra and opt-in.
## Choice
Devices in your home gather data about themselves and their surroundings. Your data. Vendors shouldnt be able to limit your access to your data or limit the interoperability of your devices with the rest of your smart home.
Choice for the Open Home means that devices need to make the gathered data available through local APIs. This avoids vendor lock-in and allows users to create their own smart home with devices from different manufacturers.
## Durability
If there is one thing that technology firms are very good at, it is launching new products. However, maintaining the products and making sure they keep working is an afterthought for most. The result is that vendors can decide to no longer support your device, crippling its features or even preventing it from working at all. As we install more and more devices in our home, durability is becoming more and more important. We shouldnt have to buy everything new every couple of years because the manufacturer decided to move on.
Durability for the Open Home means that devices are designed and built to keep working. Not just this year, but for the next decade.
# Our history
The project was started as a Python application by Paulus Schoutsen in September 2013 and first published publicly on GitHub in November 2013. In July 2017, a managed operating system called Hass.io was initially introduced to make it easier use to use Home Assistant on single-board computers like the Raspberry Pi series. Its bundled "supervisor" management system allowed users to manage, backup, and update the local installation and introduced the option to extend the functionality of the software with add-ons.
An optional subscription service was introduced in December 2017 for $5/month to solve the complexities associated with secured remote access, as well as linking to Amazon Alexa and Google Assistant. Nabu Casa, Inc. was formed in September 2018 to take over the subscription service. The company's funding is based solely on revenue from the subscription service. It is used to finance the project's infrastructure and to pay for full-time employees contributing to the project.
In January 2020, branding was adjusted to make it easier to refer to different parts of the project. The main piece of software was renamed to Home Assistant Core, while the full suite of software with the embedded operating system and bundled "supervisor" management system was renamed to Home Assistant.

View File

@ -1,5 +1,6 @@
---
title: Alerts
subtitle: An alert displays a short, important message in a way that attracts the user's attention without interrupting the user's task.
---
# Alert `<ha-alert>`

View File

@ -3,18 +3,7 @@ import { customElement } from "lit/decorators";
import "../../../../src/components/ha-card";
import "../../../../src/components/ha-faded";
import "../../../../src/components/ha-markdown";
const LONG_TEXT = `
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nunc laoreet velit ut elit volutpat, eget ultrices odio lacinia. In imperdiet malesuada est, nec sagittis metus ultricies quis. Sed nisl ex, convallis porttitor ante quis, hendrerit tristique justo. Mauris pharetra venenatis augue, eu maximus sem cursus in. Quisque sed consequat risus. Suspendisse facilisis ligula a odio consectetur condimentum. Curabitur vehicula elit nec augue mollis, et volutpat massa dictum.
Nam pellentesque auctor rutrum. Suspendisse elit est, sodales vel diam nec, porttitor faucibus massa. Ut pretium ac orci eu pharetra. Praesent in nibh at magna viverra rutrum eu vitae tortor. Etiam eget sem ex. Fusce tristique odio nec lacus mattis, vitae tempor nunc malesuada. Maecenas faucibus magna vel libero maximus egestas. Vestibulum luctus semper velit, in lobortis risus tempus non. Curabitur bibendum ornare commodo. Quisque commodo neque sit amet tincidunt lacinia. Proin elementum ante velit, eu congue nulla semper quis. Pellentesque consequat vel nunc at scelerisque. Mauris sit amet venenatis diam, blandit viverra leo. Integer commodo laoreet orci.
Curabitur ipsum tortor, sodales ut augue sed, commodo porttitor libero. Pellentesque molestie vitae mi consectetur tempor. In sed lectus consequat, lobortis neque non, semper ipsum. Etiam eget ex et nibh sagittis pulvinar lacinia ac mauris. Aenean ligula eros, viverra ac nibh at, venenatis semper quam. Sed interdum ligula sit amet massa tincidunt tincidunt. Suspendisse potenti. Aliquam egestas facilisis est, sed faucibus erat scelerisque id. Duis dolor quam, viverra vitae orci euismod, laoreet pellentesque justo. Nunc malesuada non erat at ullamcorper. Mauris eget posuere odio. Vestibulum turpis nunc, pharetra eget ante in, feugiat mollis justo. Proin porttitor, diam nec vulputate pretium, tellus arcu rhoncus turpis, a blandit nisi nulla quis arcu. Nunc ac ullamcorper ligula, nec facilisis leo.
In vitae eros sollicitudin, iaculis ex eget, egestas orci. Etiam sed pretium lorem. Nam nisi enim, consectetur sit amet semper ac, semper pharetra diam. In pulvinar neque sapien, ac ullamcorper est lacinia a. Etiam tincidunt velit sed diam malesuada, eu ornare ex consectetur. Phasellus in imperdiet tellus. Sed bibendum, dui sit amet fringilla aliquet, enim odio sollicitudin lorem, vel semper turpis mauris vel mauris. Aenean congue magna ac massa cursus, in dictum orci commodo. Pellentesque mollis velit in sollicitudin tincidunt. Vestibulum et efficitur nulla.
Quisque posuere, velit sed porttitor dapibus, neque augue fringilla felis, eu luctus nisi nisl nec ipsum. Curabitur pellentesque ac lectus eget ultricies. Vestibulum est dolor, lacinia pharetra vulputate a, facilisis a magna. Nam vitae arcu nibh. Praesent finibus blandit ante, ac gravida ex mollis eget. Donec quam est, pulvinar vitae neque ut, bibendum aliquam erat. Nullam mollis arcu at sem tincidunt, in tristique lectus facilisis. Aenean ut lacus vel nisl finibus iaculis non a turpis. Integer eget ipsum ante. Donec nunc neque, vestibulum ac magna ac, posuere scelerisque dui. Pellentesque massa nibh, rhoncus id dolor quis, placerat posuere turpis. Donec aliquet augue nisi, eu finibus dui auctor et. Vestibulum eu varius lorem. Quisque lectus ante, malesuada pretium risus eget, interdum mattis enim.
`;
import { LONG_TEXT } from "../../data/text";
const SMALL_TEXT = "Lorem ipsum dolor sit amet, consectetur adipiscing elit.";

View File

@ -1,17 +1,109 @@
/* eslint-disable lit/no-template-arrow */
import "@material/mwc-button";
import { LitElement, TemplateResult, html } from "lit";
import { html, LitElement, TemplateResult } from "lit";
import { customElement, state } from "lit/decorators";
import { computeInitialHaFormData } from "../../../../src/components/ha-form/compute-initial-ha-form-data";
import type { HaFormSchema } from "../../../../src/components/ha-form/types";
import "../../../../src/components/ha-form/ha-form";
import "../../components/demo-black-white-row";
import { mockAreaRegistry } from "../../../../demo/src/stubs/area_registry";
import { mockDeviceRegistry } from "../../../../demo/src/stubs/device_registry";
import { mockEntityRegistry } from "../../../../demo/src/stubs/entity_registry";
import { mockHassioSupervisor } from "../../../../demo/src/stubs/hassio_supervisor";
import { computeInitialHaFormData } from "../../../../src/components/ha-form/compute-initial-ha-form-data";
import "../../../../src/components/ha-form/ha-form";
import type { HaFormSchema } from "../../../../src/components/ha-form/types";
import { getEntity } from "../../../../src/fake_data/entity";
import { provideHass } from "../../../../src/fake_data/provide_hass";
import { HomeAssistant } from "../../../../src/types";
import "../../components/demo-black-white-row";
const ENTITIES = [
getEntity("alarm_control_panel", "alarm", "disarmed", {
friendly_name: "Alarm",
}),
getEntity("media_player", "livingroom", "playing", {
friendly_name: "Livingroom",
}),
getEntity("media_player", "lounge", "idle", {
friendly_name: "Lounge",
supported_features: 444983,
}),
getEntity("light", "bedroom", "on", {
friendly_name: "Bedroom",
}),
getEntity("switch", "coffee", "off", {
friendly_name: "Coffee",
}),
];
const DEVICES = [
{
area_id: "bedroom",
configuration_url: null,
config_entries: ["config_entry_1"],
connections: [],
disabled_by: null,
entry_type: null,
id: "device_1",
identifiers: [["demo", "volume1"] as [string, string]],
manufacturer: null,
model: null,
name_by_user: null,
name: "Dishwasher",
sw_version: null,
hw_version: null,
via_device_id: null,
},
{
area_id: "backyard",
configuration_url: null,
config_entries: ["config_entry_2"],
connections: [],
disabled_by: null,
entry_type: null,
id: "device_2",
identifiers: [["demo", "pwm1"] as [string, string]],
manufacturer: null,
model: null,
name_by_user: null,
name: "Lamp",
sw_version: null,
hw_version: null,
via_device_id: null,
},
{
area_id: null,
configuration_url: null,
config_entries: ["config_entry_3"],
connections: [],
disabled_by: null,
entry_type: null,
id: "device_3",
identifiers: [["demo", "pwm1"] as [string, string]],
manufacturer: null,
model: null,
name_by_user: "User name",
name: "Technical name",
sw_version: null,
hw_version: null,
via_device_id: null,
},
];
const AREAS = [
{
area_id: "backyard",
name: "Backyard",
picture: null,
},
{
area_id: "bedroom",
name: "Bedroom",
picture: null,
},
{
area_id: "livingroom",
name: "Livingroom",
picture: null,
},
];
const SCHEMAS: {
title: string;
@ -39,6 +131,7 @@ const SCHEMAS: {
icon: "Icon",
media: "Media",
location: "Location",
entities: "Entities",
},
schema: [
{ name: "addon", selector: { addon: {} } },
@ -46,6 +139,7 @@ const SCHEMAS: {
{
name: "Attribute",
selector: { attribute: { entity_id: "" } },
context: { filter_entity: "entity" },
},
{ name: "Device", selector: { device: {} } },
{ name: "Duration", selector: { duration: {} } },
@ -53,7 +147,9 @@ const SCHEMAS: {
{ name: "target", selector: { target: {} } },
{ name: "number", selector: { number: { min: 0, max: 10 } } },
{ name: "boolean", selector: { boolean: {} } },
{ name: "time", selector: { time: {} } },
{ name: "time", required: true, selector: { time: {} } },
{ name: "datetime", required: true, selector: { datetime: {} } },
{ name: "date", required: true, selector: { date: {} } },
{ name: "action", selector: { action: {} } },
{ name: "text", selector: { text: { multiline: false } } },
{ name: "text_multiline", selector: { text: { multiline: true } } },
@ -80,6 +176,10 @@ const SCHEMAS: {
name: "location",
selector: { location: { radius: true, icon: "mdi:home" } },
},
{
name: "entities",
selector: { entity: { multiple: true } },
},
],
},
{
@ -320,9 +420,10 @@ class DemoHaForm extends LitElement {
const hass = provideHass(this);
hass.updateTranslations(null, "en");
hass.updateTranslations("config", "en");
hass.addEntities(ENTITIES);
mockEntityRegistry(hass);
mockDeviceRegistry(hass);
mockAreaRegistry(hass);
mockDeviceRegistry(hass, DEVICES);
mockAreaRegistry(hass, AREAS);
mockHassioSupervisor(hass);
}

View File

@ -1,20 +1,20 @@
/* eslint-disable lit/no-template-arrow */
import "@material/mwc-button";
import { LitElement, TemplateResult, css, html } from "lit";
import { css, html, LitElement, TemplateResult } from "lit";
import { customElement, state } from "lit/decorators";
import { mockAreaRegistry } from "../../../../demo/src/stubs/area_registry";
import { mockDeviceRegistry } from "../../../../demo/src/stubs/device_registry";
import { mockEntityRegistry } from "../../../../demo/src/stubs/entity_registry";
import { mockHassioSupervisor } from "../../../../demo/src/stubs/hassio_supervisor";
import "../../../../src/components/ha-selector/ha-selector";
import "../../../../src/components/ha-settings-row";
import { BlueprintInput } from "../../../../src/data/blueprint";
import { showDialog } from "../../../../src/dialogs/make-dialog-manager";
import { getEntity } from "../../../../src/fake_data/entity";
import { provideHass } from "../../../../src/fake_data/provide_hass";
import { ProvideHassElement } from "../../../../src/mixins/provide-hass-lit-mixin";
import type { HomeAssistant } from "../../../../src/types";
import "../../components/demo-black-white-row";
import { BlueprintInput } from "../../../../src/data/blueprint";
import { mockEntityRegistry } from "../../../../demo/src/stubs/entity_registry";
import { mockDeviceRegistry } from "../../../../demo/src/stubs/device_registry";
import { mockAreaRegistry } from "../../../../demo/src/stubs/area_registry";
import { mockHassioSupervisor } from "../../../../demo/src/stubs/hassio_supervisor";
import { getEntity } from "../../../../src/fake_data/entity";
import { ProvideHassElement } from "../../../../src/mixins/provide-hass-lit-mixin";
import { showDialog } from "../../../../src/dialogs/make-dialog-manager";
const ENTITIES = [
getEntity("alarm_control_panel", "alarm", "disarmed", {
@ -109,7 +109,7 @@ const AREAS = [
const SCHEMAS: {
name: string;
input: Record<string, BlueprintInput | null>;
input: Record<string, (BlueprintInput & { required?: boolean }) | null>;
}[] = [
{
name: "One of each",
@ -146,6 +146,8 @@ const SCHEMAS: {
},
boolean: { name: "Boolean", selector: { boolean: {} } },
time: { name: "Time", selector: { time: {} } },
date: { name: "Date", selector: { date: {} } },
datetime: { name: "Date Time", selector: { datetime: {} } },
action: { name: "Action", selector: { action: {} } },
text: {
name: "Text",
@ -162,9 +164,42 @@ const SCHEMAS: {
},
},
object: { name: "Object", selector: { object: {} } },
select_radio: {
name: "Select (Radio)",
selector: {
select: { options: ["Option 1", "Option 2"], mode: "list" },
},
},
select: {
name: "Select",
selector: { select: { options: ["Option 1", "Option 2"] } },
selector: {
select: {
options: [
"Option 1",
"Option 2",
"Option 3",
"Option 4",
"Option 5",
"Option 6",
],
},
},
},
select_custom: {
name: "Select (Custom)",
selector: {
select: {
custom_value: true,
options: [
"Option 1",
"Option 2",
"Option 3",
"Option 4",
"Option 5",
"Option 6",
],
},
},
},
icon: { name: "Icon", selector: { icon: {} } },
media: { name: "Media", selector: { media: {} } },
@ -173,6 +208,47 @@ const SCHEMAS: {
name: "Location with radius",
selector: { location: { radius: true, icon: "mdi:home" } },
},
color_temp: {
name: "Color Temperature",
selector: { color_temp: {} },
},
color_rgb: { name: "Color", selector: { color_rgb: {} } },
},
},
{
name: "Multiples",
input: {
entity: { name: "Entity", selector: { entity: { multiple: true } } },
device: { name: "Device", selector: { device: { multiple: true } } },
area: { name: "Area", selector: { area: { multiple: true } } },
select: {
name: "Select Multiple",
selector: {
select: {
multiple: true,
custom_value: true,
options: [
"Option 1",
"Option 2",
"Option 3",
"Option 4",
"Option 5",
"Option 6",
],
},
},
},
select_checkbox: {
name: "Select Multiple (Checkbox)",
required: false,
selector: {
select: {
mode: "list",
multiple: true,
options: ["Option 1", "Option 2", "Option 3", "Option 4"],
},
},
},
},
},
];
@ -181,6 +257,12 @@ const SCHEMAS: {
class DemoHaSelector extends LitElement implements ProvideHassElement {
@state() public hass!: HomeAssistant;
@state() private _disabled = false;
@state() private _required = false;
@state() private _label = true;
private data = SCHEMAS.map(() => ({}));
constructor() {
@ -314,6 +396,29 @@ class DemoHaSelector extends LitElement implements ProvideHassElement {
protected render(): TemplateResult {
return html`
<div class="options">
<ha-formfield label="Labels">
<ha-switch
.name=${"label"}
.checked=${this._label}
@change=${this._handleOptionChange}
></ha-switch>
</ha-formfield>
<ha-formfield label="Required">
<ha-switch
.name=${"required"}
.checked=${this._required}
@change=${this._handleOptionChange}
></ha-switch>
</ha-formfield>
<ha-formfield label="Disabled">
<ha-switch
.name=${"disabled"}
.checked=${this._disabled}
@change=${this._handleOptionChange}
></ha-switch>
</ha-formfield>
</div>
${SCHEMAS.map((info, idx) => {
const data = this.data[idx];
const valueChanged = (ev) => {
@ -336,7 +441,10 @@ class DemoHaSelector extends LitElement implements ProvideHassElement {
.hass=${this.hass}
.selector=${value!.selector}
.key=${key}
.label=${this._label ? value!.name : undefined}
.value=${data[key] ?? value!.default}
.disabled=${this._disabled}
.required=${this._required}
@value-changed=${valueChanged}
></ha-selector>
</ha-settings-row>
@ -349,10 +457,20 @@ class DemoHaSelector extends LitElement implements ProvideHassElement {
`;
}
private _handleOptionChange(ev) {
this[`_${ev.target.name}`] = ev.target.checked;
}
static styles = css`
ha-selector {
width: 60;
}
.options {
padding: 16px 48px;
}
.options ha-formfield {
margin-right: 16px;
}
`;
}

View File

@ -2,6 +2,8 @@
title: Editing design.home-assistant.io
---
![Home Assistant Logo](/images/logo-with-text.png)
# How to edit design.home-assistant.io
All pages are stored in [the pages folder][pages-folder] on GitHub. Pages are grouped in a folder per sidebar section. Each page can contain a `<page name>.markdown` description file, a `<page name>.ts` demo file or both. If both are defined the description is rendered first. The description can contain metadata to specify the title of the page.
@ -41,15 +43,12 @@ import { html, css, LitElement } from "lit";
import { customElement } from "lit/decorators";
import "../../../../src/components/ha-card";
@customElement("demo-user-experience-usability")
export class DemoUserExperienceUsability extends LitElement {
protected render() {
return html`
<ha-card>
<div class="card-content">
Hello world!
</div>
<div class="card-content">Hello world!</div>
</ha-card>
`;
}

View File

@ -9,7 +9,7 @@ const CONFIGS = [
heading: "markdown-it demo",
config: `
- type: markdown
content: >
content: >-
# h1 Heading 8-)
## h2 Heading
@ -249,6 +249,17 @@ const CONFIGS = [
::: warning
*here be dragons*
:::
### ha-alert
You can use our [\`ha-alert\`](https://design.home-assistant.io/#components/ha-alert) component in markdown content rendered in the Home Assistant Frontend.
<ha-alert alert-type="error">This is an error alert check it out!</ha-alert>
<ha-alert alert-type="warning">This is a warning alert check it out!</ha-alert>
<ha-alert alert-type="info">This is an info alert check it out!</ha-alert>
<ha-alert alert-type="success">This is a success alert check it out!</ha-alert>
<ha-alert title="Test alert">This is an alert with a title</ha-alert>
`,
},
];

View File

@ -188,6 +188,7 @@ const createEntityRegistryEntries = (
device_id: "mock-device-id",
area_id: null,
disabled_by: null,
hidden_by: null,
entity_category: null,
entity_id: "binary_sensor.updater",
name: null,

View File

@ -0,0 +1,3 @@
---
title: Update
---

View File

@ -0,0 +1,178 @@
import { html, LitElement, PropertyValues, TemplateResult } from "lit";
import { customElement, property, query } from "lit/decorators";
import "../../../../src/components/ha-card";
import {
UPDATE_SUPPORT_BACKUP,
UPDATE_SUPPORT_PROGRESS,
UPDATE_SUPPORT_INSTALL,
UPDATE_SUPPORT_RELEASE_NOTES,
} from "../../../../src/data/update";
import "../../../../src/dialogs/more-info/more-info-content";
import { getEntity } from "../../../../src/fake_data/entity";
import {
MockHomeAssistant,
provideHass,
} from "../../../../src/fake_data/provide_hass";
import "../../components/demo-more-infos";
import { LONG_TEXT } from "../../data/text";
const base_attributes = {
title: "Awesome",
current_version: "1.2.2",
latest_version: "1.2.3",
release_url: "https://home-assistant.io",
supported_features: UPDATE_SUPPORT_INSTALL,
skipped_version: null,
in_progress: false,
release_summary:
"Lorem ipsum dolor sit amet, consectetur adipiscing elit. In nec metus aliquet, porta mi ut, ultrices odio. Etiam egestas orci tellus, non semper metus blandit tincidunt. Praesent elementum turpis vel tempor pharetra. Sed quis cursus diam. Proin sem justo.",
};
const ENTITIES = [
getEntity("update", "update1", "on", {
...base_attributes,
friendly_name: "Update",
}),
getEntity("update", "update2", "on", {
...base_attributes,
title: null,
friendly_name: "Update without title",
}),
getEntity("update", "update3", "on", {
...base_attributes,
release_url: null,
friendly_name: "Update without release_url",
}),
getEntity("update", "update4", "on", {
...base_attributes,
release_summary: null,
friendly_name: "Update without release_summary",
}),
getEntity("update", "update5", "off", {
...base_attributes,
current_version: "1.2.3",
friendly_name: "No update",
}),
getEntity("update", "update6", "off", {
...base_attributes,
skipped_version: "1.2.3",
friendly_name: "Skipped version",
}),
getEntity("update", "update7", "on", {
...base_attributes,
supported_features:
base_attributes.supported_features + UPDATE_SUPPORT_BACKUP,
friendly_name: "With backup support",
}),
getEntity("update", "update8", "on", {
...base_attributes,
in_progress: true,
friendly_name: "With true in_progress",
}),
getEntity("update", "update9", "on", {
...base_attributes,
in_progress: 25,
supported_features:
base_attributes.supported_features + UPDATE_SUPPORT_PROGRESS,
friendly_name: "With 25 in_progress",
}),
getEntity("update", "update10", "on", {
...base_attributes,
in_progress: 50,
supported_features:
base_attributes.supported_features + UPDATE_SUPPORT_PROGRESS,
friendly_name: "With 50 in_progress",
}),
getEntity("update", "update11", "on", {
...base_attributes,
in_progress: 75,
supported_features:
base_attributes.supported_features + UPDATE_SUPPORT_PROGRESS,
friendly_name: "With 75 in_progress",
}),
getEntity("update", "update12", "unavailable", {
...base_attributes,
in_progress: 50,
friendly_name: "Unavailable",
}),
getEntity("update", "update13", "on", {
...base_attributes,
supported_features: 0,
friendly_name: "No install support",
}),
getEntity("update", "update14", "off", {
...base_attributes,
current_version: null,
friendly_name: "Update without current_version",
}),
getEntity("update", "update15", "off", {
...base_attributes,
latest_version: null,
friendly_name: "Update without latest_version",
}),
getEntity("update", "update16", "off", {
...base_attributes,
friendly_name: "Update with release notes",
supported_features:
base_attributes.supported_features + UPDATE_SUPPORT_RELEASE_NOTES,
}),
getEntity("update", "update17", "off", {
...base_attributes,
friendly_name: "Update with release notes error",
supported_features:
base_attributes.supported_features + UPDATE_SUPPORT_RELEASE_NOTES,
}),
getEntity("update", "update18", "off", {
...base_attributes,
friendly_name: "Update with release notes loading",
supported_features:
base_attributes.supported_features + UPDATE_SUPPORT_RELEASE_NOTES,
}),
];
@customElement("demo-more-info-update")
class DemoMoreInfoUpdate extends LitElement {
@property() public hass!: MockHomeAssistant;
@query("demo-more-infos") private _demoRoot!: HTMLElement;
protected render(): TemplateResult {
return html`
<demo-more-infos
.hass=${this.hass}
.entities=${ENTITIES.map((ent) => ent.entityId)}
></demo-more-infos>
`;
}
protected firstUpdated(changedProperties: PropertyValues) {
super.firstUpdated(changedProperties);
const hass = provideHass(this._demoRoot);
hass.updateTranslations(null, "en");
hass.addEntities(ENTITIES);
hass.mockWS(
"update/release_notes",
(msg: { type: string; entity_id: string }) => {
if (msg.entity_id === "update.update16") {
return LONG_TEXT;
}
if (msg.entity_id === "update.update17") {
return Promise.reject({
code: "error",
message: "Could not fetch release notes",
});
}
if (msg.entity_id === "update.update18") {
return undefined;
}
return null;
}
);
}
}
declare global {
interface HTMLElementTagNameMap {
"demo-more-info-update": DemoMoreInfoUpdate;
}
}

View File

@ -0,0 +1,17 @@
---
title: "User types"
---
We have defined three user types for Home Assistant. They are a lean segmentation of users that helps us make decisions throughout the product. User types differ from traditional personas in that the segmentation criteria arent demographic and dont personify a group into a single character with a fictitious background story.
# Outgrowers
Users that outgrow big tech smart home solutions. It just needs to work with easy setup via an app.
# Tinkerers
Technoid users in home networking and development that know how to code.
# Questioner
Users who want more advanced home automation, but need support to make it work.

View File

@ -1,9 +1,12 @@
// Compat needs to be first import
import "../../src/resources/compatibility";
import { setCancelSyntheticClickEvents } from "@polymer/polymer/lib/utils/settings";
import "../../src/resources/roboto";
import "../../src/resources/safari-14-attachshadow-patch";
import "./hassio-main";
setCancelSyntheticClickEvents(false);
const styleEl = document.createElement("style");
styleEl.innerHTML = `
body {

View File

@ -45,7 +45,6 @@ import { showAlertDialog } from "../../../src/dialogs/generic/show-dialog-box";
import "../../../src/layouts/hass-loading-screen";
import "../../../src/layouts/hass-subpage";
import "../../../src/layouts/hass-tabs-subpage";
import { SUPERVISOR_UPDATE_NAMES } from "../../../src/panels/config/dashboard/ha-config-updates";
import { HomeAssistant, Route } from "../../../src/types";
import { addonArchIsSupported, extractChangelog } from "../util/addon";
@ -55,6 +54,12 @@ declare global {
}
}
const SUPERVISOR_UPDATE_NAMES = {
core: "Home Assistant Core",
os: "Home Assistant Operating System",
supervisor: "Home Assistant Supervisor",
};
type updateType = "os" | "supervisor" | "core" | "addon";
const changelogUrl = (

View File

@ -72,14 +72,13 @@
"@material/mwc-textfield": "0.25.3",
"@material/mwc-top-app-bar-fixed": "^0.25.3",
"@material/top-app-bar": "14.0.0-canary.261f2db59.0",
"@mdi/js": "6.5.95",
"@mdi/svg": "6.5.95",
"@mdi/js": "6.6.95",
"@mdi/svg": "6.6.95",
"@polymer/app-layout": "^3.1.0",
"@polymer/iron-flex-layout": "^3.0.1",
"@polymer/iron-icon": "^3.0.1",
"@polymer/iron-input": "^3.0.1",
"@polymer/iron-resizable-behavior": "^3.0.1",
"@polymer/paper-dropdown-menu": "^3.2.0",
"@polymer/paper-input": "^3.2.1",
"@polymer/paper-item": "^3.0.1",
"@polymer/paper-listbox": "^3.0.1",
@ -109,7 +108,7 @@
"fuse.js": "^6.0.0",
"google-timezones-json": "^1.0.2",
"hls.js": "^1.1.5",
"home-assistant-js-websocket": "^6.0.1",
"home-assistant-js-websocket": "^7.0.1",
"idb-keyval": "^5.1.3",
"intl-messageformat": "^9.9.1",
"js-yaml": "^4.1.0",
@ -136,7 +135,6 @@
"vis-network": "^8.5.4",
"vue": "^2.6.12",
"vue2-daterange-picker": "^0.5.1",
"web-animations-js": "^2.3.2",
"workbox-cacheable-response": "^6.4.2",
"workbox-core": "^6.4.2",
"workbox-expiration": "^6.4.2",

View File

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

View File

@ -1,7 +0,0 @@
"""
Entry point for setuptools. Required for editable installs.
TODO: Remove file after updating to pip 21.3
"""
from setuptools import setup
setup()

View File

@ -187,6 +187,7 @@ export const DOMAINS_WITH_MORE_INFO = [
"scene",
"sun",
"timer",
"update",
"vacuum",
"water_heater",
"weather",
@ -200,6 +201,7 @@ export const DOMAINS_HIDE_DEFAULT_MORE_INFO = [
"input_text",
"number",
"scene",
"update",
"select",
];

View File

@ -1,12 +1,18 @@
import { HassEntity } from "home-assistant-js-websocket";
import { UNAVAILABLE, UNKNOWN } from "../../data/entity";
import { FrontendLocaleData } from "../../data/translation";
import {
updateIsInstalling,
UpdateEntity,
UPDATE_SUPPORT_PROGRESS,
} from "../../data/update";
import { formatDate } from "../datetime/format_date";
import { formatDateTime } from "../datetime/format_date_time";
import { formatTime } from "../datetime/format_time";
import { formatNumber, isNumericState } from "../number/format_number";
import { LocalizeFunc } from "../translations/localize";
import { computeStateDomain } from "./compute_state_domain";
import { supportsFeature } from "./supports-feature";
export const computeStateDisplay = (
localize: LocalizeFunc,
@ -130,6 +136,28 @@ export const computeStateDisplay = (
}
}
if (domain === "update") {
// When updating, and entity does not support % show "Installing"
// When updating, and entity does support % show "Installing (xx%)"
// When update available, show the version
// When the latest version is skipped, show the latest version
// When update is not available, show "Up-to-date"
// When update is not available and there is no latest_version show "Unavailable"
return compareState === "on"
? updateIsInstalling(stateObj as UpdateEntity)
? supportsFeature(stateObj, UPDATE_SUPPORT_PROGRESS)
? localize("ui.card.update.installing_with_progress", {
progress: stateObj.attributes.in_progress,
})
: localize("ui.card.update.installing")
: stateObj.attributes.latest_version
: stateObj.attributes.skipped_version ===
stateObj.attributes.latest_version
? stateObj.attributes.latest_version ??
localize("state.default.unavailable")
: localize("ui.card.update.up_to_date");
}
return (
// Return device class translation
(stateObj.attributes.device_class &&

View File

@ -1,4 +1,4 @@
import { HassEntity } from "home-assistant-js-websocket";
import type { HassEntity } from "home-assistant-js-websocket";
import { computeDomain } from "./compute_domain";
export const computeStateDomain = (stateObj: HassEntity) =>

View File

@ -9,11 +9,10 @@ import {
mdiCast,
mdiCastConnected,
mdiClock,
mdiEmoticonDead,
mdiFlash,
mdiGestureTapButton,
mdiLanConnect,
mdiLanDisconnect,
mdiLightSwitch,
mdiLock,
mdiLockAlert,
mdiLockClock,
@ -22,16 +21,16 @@ import {
mdiPowerPlug,
mdiPowerPlugOff,
mdiRestart,
mdiSleep,
mdiTimerSand,
mdiToggleSwitch,
mdiToggleSwitchOff,
mdiCheckCircleOutline,
mdiCloseCircleOutline,
mdiWeatherNight,
mdiZWave,
mdiPackage,
mdiPackageDown,
} from "@mdi/js";
import { HassEntity } from "home-assistant-js-websocket";
import { updateIsInstalling, UpdateEntity } from "../../data/update";
/**
* Return the icon to be used for a domain.
*
@ -112,19 +111,7 @@ export const domainIcon = (
case "switch":
return compareState === "on" ? mdiToggleSwitch : mdiToggleSwitchOff;
default:
return mdiFlash;
}
case "zwave":
switch (compareState) {
case "dead":
return mdiEmoticonDead;
case "sleeping":
return mdiSleep;
case "initializing":
return mdiTimerSand;
default:
return mdiZWave;
return mdiLightSwitch;
}
case "sensor": {
@ -149,6 +136,13 @@ export const domainIcon = (
return stateObj?.state === "above_horizon"
? FIXED_DOMAIN_ICONS[domain]
: mdiWeatherNight;
case "update":
return compareState === "on"
? updateIsInstalling(stateObj as UpdateEntity)
? mdiPackageDown
: mdiPackageUp
: mdiPackage;
}
if (domain in FIXED_DOMAIN_ICONS) {

View File

@ -0,0 +1,4 @@
const regexp =
/^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/;
export const isIPAddress = (input: string): boolean => regexp.test(input);

View File

@ -7,6 +7,7 @@ export const iconColorCSS = css`
ha-state-icon[data-domain="calendar"][data-state="on"],
ha-state-icon[data-domain="camera"][data-state="streaming"],
ha-state-icon[data-domain="cover"][data-state="open"],
ha-state-icon[data-domain="device_tracker"][data-state="home"],
ha-state-icon[data-domain="fan"][data-state="on"],
ha-state-icon[data-domain="humidifier"][data-state="on"],
ha-state-icon[data-domain="light"][data-state="on"],
@ -70,9 +71,6 @@ export const iconColorCSS = css`
}
ha-state-icon[data-domain="plant"][data-state="problem"],
ha-state-icon[data-domain="zwave"][data-state="dead"] {
color: var(--state-icon-error-color);
}
/* Color the icon if unavailable */
ha-state-icon[data-state="unavailable"] {

View File

@ -0,0 +1,53 @@
import { HomeAssistant } from "../../types";
interface ResultCache<T> {
[entityId: string]: Promise<T> | undefined;
}
/**
* Call a function with result caching per entity.
* @param cacheKey key to store the cache on hass object
* @param cacheTime time to cache the results
* @param func function to fetch the data
* @param hass Home Assistant object
* @param entityId entity to fetch data for
* @param args extra arguments to pass to the function to fetch the data
* @returns
*/
export const timeCacheEntityPromiseFunc = async <T>(
cacheKey: string,
cacheTime: number,
func: (hass: HomeAssistant, entityId: string, ...args: any[]) => Promise<T>,
hass: HomeAssistant,
entityId: string,
...args: any[]
): Promise<T> => {
let cache: ResultCache<T> | undefined = (hass as any)[cacheKey];
if (!cache) {
cache = hass[cacheKey] = {};
}
const lastResult = cache[entityId];
if (lastResult) {
return lastResult;
}
const result = func(hass, entityId, ...args);
cache[entityId] = result;
result.then(
// When successful, set timer to clear cache
() =>
setTimeout(() => {
cache![entityId] = undefined;
}, cacheTime),
// On failure, clear cache right away
() => {
cache![entityId] = undefined;
}
);
return result;
};

View File

@ -1,43 +1,80 @@
import { HomeAssistant } from "../../types";
interface ResultCache<T> {
[entityId: string]: Promise<T> | undefined;
interface CacheResult<T> {
result: T;
cacheKey: any;
}
/**
* Caches a result of a promise for X time. Allows optional extra validation
* check to invalidate the cache.
* @param cacheKey the key to store the cache
* @param cacheTime the time to cache the result
* @param func the function to fetch the data
* @param generateCacheKey optional function to generate a cache key based on current hass + cached result. Cache is invalid if generates a different cache key.
* @param hass Home Assistant object
* @param args extra arguments to pass to the function to fetch the data
* @returns
*/
export const timeCachePromiseFunc = async <T>(
cacheKey: string,
cacheTime: number,
func: (hass: HomeAssistant, entityId: string, ...args: any[]) => Promise<T>,
func: (hass: HomeAssistant, ...args: any[]) => Promise<T>,
generateCacheKey:
| ((hass: HomeAssistant, lastResult: T) => unknown)
| undefined,
hass: HomeAssistant,
entityId: string,
...args: any[]
): Promise<T> => {
let cache: ResultCache<T> | undefined = (hass as any)[cacheKey];
const anyHass = hass as any;
const lastResult: Promise<CacheResult<T>> | CacheResult<T> | undefined =
anyHass[cacheKey];
if (!cache) {
cache = hass[cacheKey] = {};
}
const checkCachedResult = (result: CacheResult<T>): T | Promise<T> => {
if (
!generateCacheKey ||
generateCacheKey(hass, result.result) === result.cacheKey
) {
return result.result;
}
const lastResult = cache[entityId];
anyHass[cacheKey] = undefined;
return timeCachePromiseFunc(
cacheKey,
cacheTime,
func,
generateCacheKey,
hass,
...args
);
};
// If we have a cached result, return it if it's still valid
if (lastResult) {
return lastResult;
return lastResult instanceof Promise
? lastResult.then(checkCachedResult)
: checkCachedResult(lastResult);
}
const result = func(hass, entityId, ...args);
cache[entityId] = result;
const resultPromise = func(hass, ...args);
anyHass[cacheKey] = resultPromise;
result.then(
resultPromise.then(
// When successful, set timer to clear cache
() =>
(result) => {
anyHass[cacheKey] = {
result,
cacheKey: generateCacheKey?.(hass, result),
};
setTimeout(() => {
cache![entityId] = undefined;
}, cacheTime),
anyHass[cacheKey] = undefined;
}, cacheTime);
},
// On failure, clear cache right away
() => {
cache![entityId] = undefined;
anyHass[cacheKey] = undefined;
}
);
return result;
return resultPromise;
};

View File

@ -1,8 +1,9 @@
import "@material/mwc-button";
import type { Button } from "@material/mwc-button";
import { mdiAlertOctagram, mdiCheckBold } from "@mdi/js";
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { customElement, property, query } from "lit/decorators";
import { customElement, property, state } from "lit/decorators";
import "../ha-circular-progress";
import "../ha-svg-icon";
@customElement("ha-progress-button")
export class HaProgressButton extends LitElement {
@ -12,38 +13,53 @@ export class HaProgressButton extends LitElement {
@property({ type: Boolean }) public raised = false;
@query("mwc-button", true) private _button?: Button;
@state() private _result?: "success" | "error";
public render(): TemplateResult {
const overlay = this._result || this.progress;
return html`
<mwc-button
?raised=${this.raised}
.disabled=${this.disabled || this.progress}
@click=${this._buttonTapped}
class=${this._result || ""}
>
<slot></slot>
</mwc-button>
${this.progress
? html`<div class="progress">
<ha-circular-progress size="small" active></ha-circular-progress>
</div>`
: ""}
${!overlay
? ""
: html`
<div class="progress">
${this._result === "success"
? html`<ha-svg-icon .path=${mdiCheckBold}></ha-svg-icon>`
: this._result === "error"
? html`<ha-svg-icon .path=${mdiAlertOctagram}></ha-svg-icon>`
: this.progress
? html`
<ha-circular-progress
size="small"
active
></ha-circular-progress>
`
: ""}
</div>
`}
`;
}
public actionSuccess(): void {
this._tempClass("success");
this._setResult("success");
}
public actionError(): void {
this._tempClass("error");
this._setResult("error");
}
private _tempClass(className: string): void {
this._button!.classList.add(className);
private _setResult(result: "success" | "error"): void {
this._result = result;
setTimeout(() => {
this._button!.classList.remove(className);
}, 1000);
this._result = undefined;
}, 2000);
}
private _buttonTapped(ev: Event): void {
@ -69,6 +85,7 @@ export class HaProgressButton extends LitElement {
background-color: var(--success-color);
transition: none;
border-radius: 4px;
pointer-events: none;
}
mwc-button[raised].success {
@ -81,6 +98,7 @@ export class HaProgressButton extends LitElement {
background-color: var(--error-color);
transition: none;
border-radius: 4px;
pointer-events: none;
}
mwc-button[raised].error {
@ -89,13 +107,21 @@ export class HaProgressButton extends LitElement {
}
.progress {
bottom: 0;
margin-top: 4px;
bottom: 4px;
position: absolute;
text-align: center;
top: 0;
top: 4px;
width: 100%;
}
ha-svg-icon {
color: white;
}
mwc-button.success slot,
mwc-button.error slot {
visibility: hidden;
}
`;
}
}

View File

@ -115,7 +115,7 @@ class DateRangePickerElement extends WrappedElement {
color: var(--primary-text-color);
min-width: initial !important;
}
.daterangepicker:after {
.daterangepicker:before {
display: none;
}
.daterangepicker:after {

View File

@ -86,6 +86,8 @@ export class HaDevicePicker extends SubscribeMixin(LitElement) {
@property({ type: Boolean }) public disabled?: boolean;
@property({ type: Boolean }) public required?: boolean;
@state() private _opened?: boolean;
@query("ha-combo-box", true) public comboBox!: HaComboBox;
@ -269,6 +271,7 @@ export class HaDevicePicker extends SubscribeMixin(LitElement) {
.value=${this._value}
.renderer=${rowRenderer}
.disabled=${this.disabled}
.required=${this.required}
item-value-path="id"
item-label-path="name"
@opened-changed=${this._openedChanged}

View File

@ -1,4 +1,4 @@
import { html, LitElement, TemplateResult } from "lit";
import { css, html, LitElement, TemplateResult } from "lit";
import { customElement, property } from "lit/decorators";
import { fireEvent } from "../../common/dom/fire_event";
import { PolymerChangedEvent } from "../../polymer-types";
@ -11,6 +11,8 @@ class HaDevicesPicker extends LitElement {
@property() public value?: string[];
@property({ type: Boolean }) public required?: boolean;
/**
* Show entities from specific domains.
* @type {string}
@ -66,6 +68,7 @@ class HaDevicesPicker extends LitElement {
.excludeDomains=${this.excludeDomains}
.includeDeviceClasses=${this.includeDeviceClasses}
.label=${this.pickDeviceLabel}
.required=${this.required}
@value-changed=${this._addDevice}
></ha-device-picker>
</div>
@ -116,6 +119,12 @@ class HaDevicesPicker extends LitElement {
this._updateDevices([...currentDevices, toAdd]);
}
static override styles = css`
div {
margin-top: 8px;
}
`;
}
declare global {

View File

@ -14,6 +14,8 @@ class HaEntitiesPickerLight extends LitElement {
@property({ type: Array }) public value?: string[];
@property({ type: Boolean }) public required?: boolean;
/**
* Show entities from specific domains.
* @type {string}
@ -46,11 +48,29 @@ class HaEntitiesPickerLight extends LitElement {
@property({ type: Array, attribute: "include-unit-of-measurement" })
public includeUnitOfMeasurement?: string[];
/**
* List of allowed entities to show. Will ignore all other filters.
* @type {Array}
* @attr include-entities
*/
@property({ type: Array, attribute: "include-entities" })
public includeEntities?: string[];
/**
* List of entities to be excluded.
* @type {Array}
* @attr exclude-entities
*/
@property({ type: Array, attribute: "exclude-entities" })
public excludeEntities?: string[];
@property({ attribute: "picked-entity-label" })
public pickedEntityLabel?: string;
@property({ attribute: "pick-entity-label" }) public pickEntityLabel?: string;
@property() public entityFilter?: HaEntityPickerEntityFilterFunc;
protected render(): TemplateResult {
if (!this.hass) {
return html``;
@ -67,6 +87,8 @@ class HaEntitiesPickerLight extends LitElement {
.hass=${this.hass}
.includeDomains=${this.includeDomains}
.excludeDomains=${this.excludeDomains}
.includeEntities=${this.includeEntities}
.excludeEntities=${this.excludeEntities}
.includeDeviceClasses=${this.includeDeviceClasses}
.includeUnitOfMeasurement=${this.includeUnitOfMeasurement}
.entityFilter=${this._entityFilter}
@ -82,10 +104,13 @@ class HaEntitiesPickerLight extends LitElement {
.hass=${this.hass}
.includeDomains=${this.includeDomains}
.excludeDomains=${this.excludeDomains}
.includeEntities=${this.includeEntities}
.excludeEntities=${this.excludeEntities}
.includeDeviceClasses=${this.includeDeviceClasses}
.includeUnitOfMeasurement=${this.includeUnitOfMeasurement}
.entityFilter=${this._entityFilter}
.label=${this.pickEntityLabel}
.required=${this.required}
@value-changed=${this._addEntity}
></ha-entity-picker>
</div>
@ -94,7 +119,9 @@ class HaEntitiesPickerLight extends LitElement {
private _entityFilter: HaEntityPickerEntityFilterFunc = (
stateObj: HassEntity
) => !this.value || !this.value.includes(stateObj.entity_id);
) =>
(!this.value || !this.value.includes(stateObj.entity_id)) &&
(!this.entityFilter || this.entityFilter(stateObj));
private get _currentEntities() {
return this.value || [];

View File

@ -19,6 +19,8 @@ class HaEntityAttributePicker extends LitElement {
@property({ type: Boolean }) public disabled = false;
@property({ type: Boolean }) public required = false;
@property({ type: Boolean, attribute: "allow-custom-value" })
public allowCustomValue;
@ -61,6 +63,7 @@ class HaEntityAttributePicker extends LitElement {
"ui.components.entity.entity-attribute-picker.attribute"
)}
.disabled=${this.disabled || !this.entityId}
.required=${this.required}
.allowCustomValue=${this.allowCustomValue}
item-value-path="value"
item-label-path="label"

View File

@ -7,6 +7,7 @@ import memoizeOne from "memoize-one";
import { fireEvent } from "../../common/dom/fire_event";
import { computeDomain } from "../../common/entity/compute_domain";
import { computeStateName } from "../../common/entity/compute_state_name";
import { caseInsensitiveStringCompare } from "../../common/string/compare";
import { PolymerChangedEvent } from "../../polymer-types";
import { HomeAssistant } from "../../types";
import "../ha-combo-box";
@ -15,18 +16,21 @@ import "../ha-icon-button";
import "../ha-svg-icon";
import "./state-badge";
interface HassEntityWithCachedName extends HassEntity {
friendly_name: string;
}
export type HaEntityPickerEntityFilterFunc = (entityId: HassEntity) => boolean;
// eslint-disable-next-line lit/prefer-static-styles
const rowRenderer: ComboBoxLitRenderer<HassEntity & { friendly_name: string }> =
(item) =>
html`<mwc-list-item graphic="avatar" .twoline=${!!item.entity_id}>
${item.state
? html`<state-badge slot="graphic" .stateObj=${item}></state-badge>`
: ""}
<span>${item.friendly_name}</span>
<span slot="secondary">${item.entity_id}</span>
</mwc-list-item>`;
const rowRenderer: ComboBoxLitRenderer<HassEntityWithCachedName> = (item) =>
html`<mwc-list-item graphic="avatar" .twoline=${!!item.entity_id}>
${item.state
? html`<state-badge slot="graphic" .stateObj=${item}></state-badge>`
: ""}
<span>${item.friendly_name}</span>
<span slot="secondary">${item.entity_id}</span>
</mwc-list-item>`;
@customElement("ha-entity-picker")
export class HaEntityPicker extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@ -35,6 +39,8 @@ export class HaEntityPicker extends LitElement {
@property({ type: Boolean }) public disabled?: boolean;
@property({ type: Boolean }) public required?: boolean;
@property({ type: Boolean, attribute: "allow-custom-entity" })
public allowCustomEntity;
@ -74,6 +80,22 @@ export class HaEntityPicker extends LitElement {
@property({ type: Array, attribute: "include-unit-of-measurement" })
public includeUnitOfMeasurement?: string[];
/**
* List of allowed entities to show. Will ignore all other filters.
* @type {Array}
* @attr include-entities
*/
@property({ type: Array, attribute: "include-entities" })
public includeEntities?: string[];
/**
* List of entities to be excluded.
* @type {Array}
* @attr exclude-entities
*/
@property({ type: Array, attribute: "exclude-entities" })
public excludeEntities?: string[];
@property() public entityFilter?: HaEntityPickerEntityFilterFunc;
@property({ type: Boolean }) public hideClearIcon = false;
@ -96,7 +118,7 @@ export class HaEntityPicker extends LitElement {
private _initedStates = false;
private _states: HassEntity[] = [];
private _states: HassEntityWithCachedName[] = [];
private _getStates = memoizeOne(
(
@ -106,9 +128,11 @@ export class HaEntityPicker extends LitElement {
excludeDomains: this["excludeDomains"],
entityFilter: this["entityFilter"],
includeDeviceClasses: this["includeDeviceClasses"],
includeUnitOfMeasurement: this["includeUnitOfMeasurement"]
) => {
let states: HassEntity[] = [];
includeUnitOfMeasurement: this["includeUnitOfMeasurement"],
includeEntities: this["includeEntities"],
excludeEntities: this["excludeEntities"]
): HassEntityWithCachedName[] => {
let states: HassEntityWithCachedName[] = [];
if (!hass) {
return [];
@ -122,7 +146,7 @@ export class HaEntityPicker extends LitElement {
state: "",
last_changed: "",
last_updated: "",
context: { id: "", user_id: null },
context: { id: "", user_id: null, parent_id: null },
friendly_name: this.hass!.localize(
"ui.components.entity.entity-picker.no_entities"
),
@ -136,6 +160,30 @@ export class HaEntityPicker extends LitElement {
];
}
if (includeEntities) {
entityIds = entityIds.filter((entityId) =>
this.includeEntities!.includes(entityId)
);
return entityIds
.map((key) => ({
...hass!.states[key],
friendly_name: computeStateName(hass!.states[key]) || key,
}))
.sort((entityA, entityB) =>
caseInsensitiveStringCompare(
entityA.friendly_name,
entityB.friendly_name
)
);
}
if (excludeEntities) {
entityIds = entityIds.filter(
(entityId) => !excludeEntities!.includes(entityId)
);
}
if (includeDomains) {
entityIds = entityIds.filter((eid) =>
includeDomains.includes(computeDomain(eid))
@ -148,10 +196,17 @@ export class HaEntityPicker extends LitElement {
);
}
states = entityIds.sort().map((key) => ({
...hass!.states[key],
friendly_name: computeStateName(hass!.states[key]) || key,
}));
states = entityIds
.map((key) => ({
...hass!.states[key],
friendly_name: computeStateName(hass!.states[key]) || key,
}))
.sort((entityA, entityB) =>
caseInsensitiveStringCompare(
entityA.friendly_name,
entityB.friendly_name
)
);
if (includeDeviceClasses) {
states = states.filter(
@ -190,7 +245,7 @@ export class HaEntityPicker extends LitElement {
state: "",
last_changed: "",
last_updated: "",
context: { id: "", user_id: null },
context: { id: "", user_id: null, parent_id: null },
friendly_name: this.hass!.localize(
"ui.components.entity.entity-picker.no_match"
),
@ -228,7 +283,9 @@ export class HaEntityPicker extends LitElement {
this.excludeDomains,
this.entityFilter,
this.includeDeviceClasses,
this.includeUnitOfMeasurement
this.includeUnitOfMeasurement,
this.includeEntities,
this.excludeEntities
);
if (this._initedStates) {
(this.comboBox as any).filteredItems = this._states;
@ -250,6 +307,7 @@ export class HaEntityPicker extends LitElement {
.allowCustomValue=${this.allowCustomEntity}
.filteredItems=${this._states}
.renderer=${rowRenderer}
.required=${this.required}
@opened-changed=${this._openedChanged}
@value-changed=${this._valueChanged}
@filter-changed=${this._filterChanged}

View File

@ -30,6 +30,8 @@ class HaAddonPicker extends LitElement {
@property({ type: Boolean }) public disabled = false;
@property({ type: Boolean }) public required = false;
@query("ha-combo-box") private _comboBox!: HaComboBox;
public open() {
@ -55,6 +57,8 @@ class HaAddonPicker extends LitElement {
? this.hass.localize("ui.components.addon-picker.addon")
: this.label}
.value=${this._value}
.required=${this.required}
.disabled=${this.disabled}
.renderer=${rowRenderer}
.items=${this._addons}
item-value-path="slug"

View File

@ -28,8 +28,8 @@ import { SubscribeMixin } from "../mixins/subscribe-mixin";
import { PolymerChangedEvent } from "../polymer-types";
import { HomeAssistant } from "../types";
import type { HaDevicePickerDeviceFilterFunc } from "./device/ha-device-picker";
import type { HaComboBox } from "./ha-combo-box";
import "./ha-combo-box";
import type { HaComboBox } from "./ha-combo-box";
import "./ha-icon-button";
import "./ha-svg-icon";
@ -84,6 +84,8 @@ export class HaAreaPicker extends SubscribeMixin(LitElement) {
@property({ type: Boolean }) public disabled?: boolean;
@property({ type: Boolean }) public required?: boolean;
@state() private _areas?: AreaRegistryEntry[];
@state() private _devices?: DeviceRegistryEntry[];
@ -315,6 +317,7 @@ export class HaAreaPicker extends SubscribeMixin(LitElement) {
item-label-path="name"
.value=${this.value}
.disabled=${this.disabled}
.required=${this.required}
.label=${this.label === undefined && this.hass
? this.hass.localize("ui.components.area-picker.area")
: this.label}

View File

@ -0,0 +1,163 @@
import { css, html, LitElement, TemplateResult } from "lit";
import { customElement, property } from "lit/decorators";
import { fireEvent } from "../common/dom/fire_event";
import type { EntityRegistryEntry } from "../data/entity_registry";
import { SubscribeMixin } from "../mixins/subscribe-mixin";
import type { HomeAssistant } from "../types";
import type { HaDevicePickerDeviceFilterFunc } from "./device/ha-device-picker";
import "./ha-area-picker";
@customElement("ha-areas-picker")
export class HaAreasPicker extends SubscribeMixin(LitElement) {
@property({ attribute: false }) public hass!: HomeAssistant;
@property() public label?: string;
@property() public value?: string[];
@property() public placeholder?: string;
@property({ type: Boolean, attribute: "no-add" })
public noAdd?: boolean;
/**
* Show only areas with entities from specific domains.
* @type {Array}
* @attr include-domains
*/
@property({ type: Array, attribute: "include-domains" })
public includeDomains?: string[];
/**
* Show no areas with entities of these domains.
* @type {Array}
* @attr exclude-domains
*/
@property({ type: Array, attribute: "exclude-domains" })
public excludeDomains?: string[];
/**
* Show only areas with entities of these device classes.
* @type {Array}
* @attr include-device-classes
*/
@property({ type: Array, attribute: "include-device-classes" })
public includeDeviceClasses?: string[];
@property() public deviceFilter?: HaDevicePickerDeviceFilterFunc;
@property() public entityFilter?: (entity: EntityRegistryEntry) => boolean;
@property({ attribute: "picked-area-label" })
public pickedAreaLabel?: string;
@property({ attribute: "pick-area-label" })
public pickAreaLabel?: string;
@property({ type: Boolean }) public disabled?: boolean;
@property({ type: Boolean }) public required?: boolean;
protected render(): TemplateResult {
if (!this.hass) {
return html``;
}
const currentAreas = this._currentAreas;
return html`
${currentAreas.map(
(area) => html`
<div>
<ha-area-picker
.curValue=${area}
.noAdd=${this.noAdd}
.hass=${this.hass}
.value=${area}
.label=${this.pickedAreaLabel}
.includeDomains=${this.includeDomains}
.excludeDomains=${this.excludeDomains}
.includeDeviceClasses=${this.includeDeviceClasses}
.deviceFilter=${this.deviceFilter}
.entityFilter=${this.entityFilter}
.disabled=${this.disabled}
@value-changed=${this._areaChanged}
></ha-area-picker>
</div>
`
)}
<div>
<ha-area-picker
.noAdd=${this.noAdd}
.hass=${this.hass}
.label=${this.pickAreaLabel}
.includeDomains=${this.includeDomains}
.excludeDomains=${this.excludeDomains}
.includeDeviceClasses=${this.includeDeviceClasses}
.deviceFilter=${this.deviceFilter}
.entityFilter=${this.entityFilter}
.disabled=${this.disabled}
.placeholder=${this.placeholder}
.required=${this.required}
@value-changed=${this._addArea}
></ha-area-picker>
</div>
`;
}
private get _currentAreas(): string[] {
return this.value || [];
}
private async _updateAreas(areas) {
this.value = areas;
fireEvent(this, "value-changed", {
value: areas,
});
}
private _areaChanged(ev: CustomEvent) {
ev.stopPropagation();
const curValue = (ev.currentTarget as any).curValue;
const newValue = ev.detail.value;
if (newValue === curValue) {
return;
}
const currentAreas = this._currentAreas;
if (!newValue || currentAreas.includes(newValue)) {
this._updateAreas(currentAreas.filter((ent) => ent !== curValue));
return;
}
this._updateAreas(
currentAreas.map((ent) => (ent === curValue ? newValue : ent))
);
}
private _addArea(ev: CustomEvent) {
ev.stopPropagation();
const toAdd = ev.detail.value;
if (!toAdd) {
return;
}
(ev.currentTarget as any).value = "";
const currentAreas = this._currentAreas;
if (currentAreas.includes(toAdd)) {
return;
}
this._updateAreas([...currentAreas, toAdd]);
}
static override styles = css`
div {
margin-top: 8px;
}
`;
}
declare global {
interface HTMLElementTagNameMap {
"ha-areas-picker": HaAreasPicker;
}
}

View File

@ -1,12 +1,13 @@
import { LitElement, html, TemplateResult, css } from "lit";
import { customElement, property } from "lit/decorators";
import "./ha-select";
import "@material/mwc-list/mwc-list-item";
import "./ha-textfield";
import { css, html, LitElement, TemplateResult } from "lit";
import { customElement, property } from "lit/decorators";
import { fireEvent } from "../common/dom/fire_event";
import { stopPropagation } from "../common/dom/stop_propagation";
import "./ha-select";
import "./ha-textfield";
export interface TimeChangedEvent {
days?: number;
hours: number;
minutes: number;
seconds: number;
@ -21,6 +22,11 @@ export class HaBaseTimeInput extends LitElement {
*/
@property() label?: string;
/**
* Helper for the input
*/
@property() helper?: string;
/**
* auto validate time inputs
*/
@ -41,6 +47,11 @@ export class HaBaseTimeInput extends LitElement {
*/
@property({ type: Boolean }) disabled = false;
/**
* day
*/
@property({ type: Number }) days = 0;
/**
* hour
*/
@ -61,6 +72,11 @@ export class HaBaseTimeInput extends LitElement {
*/
@property({ type: Number }) milliseconds = 0;
/**
* Label for the day input
*/
@property() dayLabel = "";
/**
* Label for the hour input
*/
@ -91,6 +107,11 @@ export class HaBaseTimeInput extends LitElement {
*/
@property({ type: Boolean }) enableMillisecond = false;
/**
* show the day field
*/
@property({ type: Boolean }) enableDay = false;
/**
* limit hours input
*/
@ -108,8 +129,33 @@ export class HaBaseTimeInput extends LitElement {
protected render(): TemplateResult {
return html`
${this.label ? html`<label>${this.label}</label>` : ""}
${this.label
? html`<label>${this.label}${this.required ? "*" : ""}</label>`
: ""}
<div class="time-input-wrap">
${this.enableDay
? html`
<ha-textfield
id="day"
type="number"
inputmode="numeric"
.value=${this.days}
.label=${this.dayLabel}
name="days"
@input=${this._valueChanged}
@focus=${this._onFocus}
no-spinner
.required=${this.required}
.autoValidate=${this.autoValidate}
min="0"
.disabled=${this.disabled}
suffix=":"
class="hasSuffix"
>
</ha-textfield>
`
: ""}
<ha-textfield
id="hour"
type="number"
@ -207,6 +253,7 @@ export class HaBaseTimeInput extends LitElement {
<mwc-list-item value="PM">PM</mwc-list-item>
</ha-select>`}
</div>
${this.helper ? html`<div class="helper">${this.helper}</div>` : ""}
`;
}
@ -303,6 +350,13 @@ export class HaBaseTimeInput extends LitElement {
color: var(--mdc-theme-text-primary-on-background, rgba(0, 0, 0, 0.87));
padding-left: 4px;
}
.helper {
color: var(--mdc-text-field-label-ink-color, rgba(0, 0, 0, 0.6));
font-size: 0.75rem;
padding-left: 16px;
padding-right: 16px;
}
`;
}

View File

@ -25,13 +25,7 @@ export class HaChipSet extends LitElement {
${unsafeCSS(chipStyles)}
slot::slotted(ha-chip) {
margin: 4px;
}
slot::slotted(ha-chip:first-of-type) {
margin-left: -4px;
}
slot::slotted(ha-chip:last-of-type) {
margin-right: -4px;
margin: 4px 4px 4px 0;
}
`;
}

View File

@ -14,6 +14,8 @@ import { customElement, property } from "lit/decorators";
export class HaChip extends LitElement {
@property({ type: Boolean }) public hasIcon = false;
@property({ type: Boolean }) public hasTrailingIcon = false;
@property({ type: Boolean }) public noText = false;
protected render(): TemplateResult {
@ -30,6 +32,11 @@ export class HaChip extends LitElement {
<span class="mdc-chip__text"><slot></slot></span>
</span>
</span>
${this.hasTrailingIcon
? html`<div class="mdc-chip__icon mdc-chip__icon--trailing">
<slot name="trailing-icon"></slot>
</div>`
: null}
</div>
`;
}
@ -53,14 +60,20 @@ export class HaChip extends LitElement {
color: var(--ha-chip-text-color, var(--primary-text-color));
}
.mdc-chip__icon--leading {
--mdc-icon-size: 20px;
.mdc-chip__icon--leading,
.mdc-chip__icon--trailing {
--mdc-icon-size: 18px;
line-height: 14px;
color: var(--ha-chip-icon-color, var(--ha-chip-text-color));
}
.mdc-chip.no-text
.mdc-chip__icon--leading:not(.mdc-chip__icon--leading-hidden) {
margin-right: -4px;
}
span[role="gridcell"] {
line-height: 14px;
}
`;
}
}

View File

@ -87,6 +87,8 @@ export class HaComboBox extends LitElement {
@property({ type: Boolean }) public disabled?: boolean;
@property({ type: Boolean }) public required?: boolean;
@property({ type: Boolean, reflect: true, attribute: "opened" })
private _opened?: boolean;
@ -108,17 +110,22 @@ export class HaComboBox extends LitElement {
return this._comboBox.selectedItem;
}
public setInputValue(value: string) {
this._comboBox.value = value;
}
protected render(): TemplateResult {
return html`
<vaadin-combo-box-light
.itemValuePath=${this.itemValuePath}
.itemIdPath=${this.itemIdPath}
.itemLabelPath=${this.itemLabelPath}
.value=${this.value || ""}
.items=${this.items}
.value=${this.value || ""}
.filteredItems=${this.filteredItems}
.allowCustomValue=${this.allowCustomValue}
.disabled=${this.disabled}
.required=${this.required}
${comboBoxRenderer(this.renderer || this._defaultRowRenderer)}
@opened-changed=${this._openedChanged}
@filter-changed=${this._filterChanged}
@ -129,6 +136,7 @@ export class HaComboBox extends LitElement {
.label=${this.label}
.placeholder=${this.placeholder}
.disabled=${this.disabled}
.required=${this.required}
.validationMessage=${this.validationMessage}
.errorMessage=${this.errorMessage}
class="input"

View File

@ -35,6 +35,8 @@ export class HaDateInput extends LitElement {
@property({ type: Boolean }) public disabled = false;
@property({ type: Boolean }) public required = false;
@property() public label?: string;
render() {
@ -46,6 +48,7 @@ export class HaDateInput extends LitElement {
.value=${this.value
? formatDateNumeric(new Date(this.value), this.locale)
: ""}
.required=${this.required}
>
<ha-svg-icon slot="trailingIcon" .path=${mdiCalendar}></ha-svg-icon>
</ha-textfield>`;

View File

@ -5,6 +5,7 @@ import "./ha-base-time-input";
import type { TimeChangedEvent } from "./ha-base-time-input";
export interface HaDurationData {
days?: number;
hours?: number;
minutes?: number;
seconds?: number;
@ -17,10 +18,14 @@ class HaDurationInput extends LitElement {
@property() public label?: string;
@property() public helper?: string;
@property({ type: Boolean }) public required?: boolean;
@property({ type: Boolean }) public enableMillisecond?: boolean;
@property({ type: Boolean }) public enableDay?: boolean;
@property({ type: Boolean }) public disabled = false;
@query("paper-time-input", true) private _input?: HTMLElement;
@ -35,19 +40,23 @@ class HaDurationInput extends LitElement {
return html`
<ha-base-time-input
.label=${this.label}
.helper=${this.helper}
.required=${this.required}
.autoValidate=${this.required}
.disabled=${this.disabled}
errorMessage="Required"
enableSecond
.enableMillisecond=${this.enableMillisecond}
.enableDay=${this.enableDay}
format="24"
.days=${this._days}
.hours=${this._hours}
.minutes=${this._minutes}
.seconds=${this._seconds}
.milliseconds=${this._milliseconds}
@value-changed=${this._durationChanged}
noHoursLimit
dayLabel="dd"
hourLabel="hh"
minLabel="mm"
secLabel="ss"
@ -56,6 +65,10 @@ class HaDurationInput extends LitElement {
`;
}
private get _days() {
return this.data?.days ? Number(this.data.days) : 0;
}
private get _hours() {
return this.data?.hours ? Number(this.data.hours) : 0;
}
@ -94,6 +107,11 @@ class HaDurationInput extends LitElement {
value.minutes %= 60;
}
if (this.enableDay && value.hours > 24) {
value.days = (value.days ?? 0) + Math.floor(value.hours / 24);
value.hours %= 24;
}
fireEvent(this, "value-changed", {
value,
});

View File

@ -1,4 +1,5 @@
import { HaFormSchema } from "./types";
import type { Selector } from "../../data/selector";
import type { HaFormSchema } from "./types";
export const computeInitialHaFormData = (
schema: HaFormSchema[]
@ -31,6 +32,55 @@ export const computeInitialHaFormData = (
minutes: 0,
seconds: 0,
};
} else if ("selector" in field) {
const selector: Selector = field.selector;
if ("device" in selector) {
data[field.name] = selector.device.multiple ? [] : "";
} else if ("entity" in selector) {
data[field.name] = selector.entity.multiple ? [] : "";
} else if ("area" in selector) {
data[field.name] = selector.area.multiple ? [] : "";
} else if ("boolean" in selector) {
data[field.name] = false;
} else if (
"text" in selector ||
"addon" in selector ||
"attribute" in selector ||
"icon" in selector ||
"theme" in selector
) {
data[field.name] = "";
} else if ("number" in selector) {
data[field.name] = selector.number.min ?? 0;
} else if ("select" in selector) {
if (selector.select.options.length) {
data[field.name] = selector.select.options[0][0];
}
} else if ("duration" in selector) {
data[field.name] = {
hours: 0,
minutes: 0,
seconds: 0,
};
} else if ("time" in selector) {
data[field.name] = "00:00:00";
} else if ("date" in selector || "datetime" in selector) {
const now = new Date().toISOString().slice(0, 10);
data[field.name] = `${now} 00:00:00`;
} else if ("color_rgb" in selector) {
data[field.name] = [0, 0, 0];
} else if ("color_temp" in selector) {
data[field.name] = selector.color_temp.min_mireds ?? 153;
} else if (
"action" in selector ||
"media" in selector ||
"target" in selector
) {
data[field.name] = {};
} else {
throw new Error("Selector not supported in initial form data");
}
}
});
return data;

View File

@ -1,16 +1,20 @@
import "@material/mwc-list/mwc-list-item";
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { customElement, property, query } from "lit/decorators";
import memoizeOne from "memoize-one";
import { html, LitElement, TemplateResult } from "lit";
import { customElement, property } from "lit/decorators";
import { fireEvent } from "../../common/dom/fire_event";
import { stopPropagation } from "../../common/dom/stop_propagation";
import "../ha-radio";
import type { HaRadio } from "../ha-radio";
import "../ha-select";
import type { HaSelect } from "../ha-select";
import { HaFormElement, HaFormSelectData, HaFormSelectSchema } from "./types";
import type { HomeAssistant } from "../../types";
import type {
HaFormElement,
HaFormSelectData,
HaFormSelectSchema,
} from "./types";
import type { SelectSelector } from "../../data/selector";
import "../ha-selector/ha-selector-select";
@customElement("ha-form-select")
export class HaFormSelect extends LitElement implements HaFormElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public schema!: HaFormSelectSchema;
@property() public data!: HaFormSelectData;
@ -19,60 +23,35 @@ export class HaFormSelect extends LitElement implements HaFormElement {
@property({ type: Boolean }) public disabled = false;
@query("ha-select", true) private _input?: HTMLElement;
public focus() {
if (this._input) {
this._input.focus();
}
}
private _selectSchema = memoizeOne(
(options): SelectSelector => ({
select: {
options: options.map((option) => ({
value: option[0],
label: option[1],
})),
},
})
);
protected render(): TemplateResult {
if (this.schema.required && this.schema.options!.length < 6) {
return html`
<div>
${this.label}
${this.schema.options.map(
([value, label]) => html`
<mwc-formfield .label=${label}>
<ha-radio
.checked=${value === this.data}
.value=${value}
.disabled=${this.disabled}
@change=${this._valueChanged}
></ha-radio>
</mwc-formfield>
`
)}
</div>
`;
}
return html`
<ha-select
fixedMenuPosition
naturalMenuWidth
.label=${this.label}
<ha-selector-select
.hass=${this.hass}
.schema=${this.schema}
.value=${this.data}
.label=${this.label}
.disabled=${this.disabled}
@closed=${stopPropagation}
@selected=${this._valueChanged}
>
${!this.schema.required
? html`<mwc-list-item value=""></mwc-list-item>`
: ""}
${this.schema.options!.map(
([value, label]) => html`
<mwc-list-item .value=${value}>${label}</mwc-list-item>
`
)}
</ha-select>
.required=${this.schema.required}
.selector=${this._selectSchema(this.schema.options)}
@value-changed=${this._valueChanged}
></ha-selector-select>
`;
}
private _valueChanged(ev: CustomEvent) {
ev.stopPropagation();
let value: string | undefined = (ev.target as HaSelect | HaRadio).value;
let value: string | undefined = ev.detail.value;
if (value === this.data) {
return;
@ -86,15 +65,6 @@ export class HaFormSelect extends LitElement implements HaFormElement {
value,
});
}
static get styles(): CSSResultGroup {
return css`
ha-select,
mwc-formfield {
display: block;
}
`;
}
}
declare global {

View File

@ -28,6 +28,8 @@ export class HaFormString extends LitElement implements HaFormElement {
@property() public label!: string;
@property() public helper?: string;
@property({ type: Boolean }) public disabled = false;
@state() private _unmaskedPassword = false;
@ -53,6 +55,8 @@ export class HaFormString extends LitElement implements HaFormElement {
: "password"}
.label=${this.label}
.value=${this.data || ""}
.helper=${this.helper}
helperPersistent
.disabled=${this.disabled}
.required=${this.schema.required}
.autoValidate=${this.schema.required}

View File

@ -106,6 +106,7 @@ export class HaForm extends LitElement implements HaFormElement {
.disabled=${this.disabled}
.helper=${this._computeHelper(item)}
.required=${item.required || false}
.context=${this._generateContext(item)}
></ha-selector>`
: dynamicElement(`ha-form-${item.type}`, {
schema: item,
@ -115,6 +116,7 @@ export class HaForm extends LitElement implements HaFormElement {
hass: this.hass,
computeLabel: this.computeLabel,
computeHelper: this.computeHelper,
context: this._generateContext(item),
})}
`;
})}
@ -122,6 +124,20 @@ export class HaForm extends LitElement implements HaFormElement {
`;
}
private _generateContext(
schema: HaFormSchema
): Record<string, any> | undefined {
if (!schema.context) {
return undefined;
}
const context = {};
for (const [context_key, data_key] of Object.entries(schema.context)) {
context[context_key] = this.data[data_key];
}
return context;
}
protected createRenderRoot() {
const root = super.createRenderRoot();
// attach it as soon as possible to make sure we fetch all events.

View File

@ -24,6 +24,7 @@ export interface HaFormBaseSchema {
// This value will be set initially when form is loaded
suggested_value?: HaFormData;
};
context?: Record<string, string>;
}
export interface HaFormGridSchema extends HaFormBaseSchema {

View File

@ -39,6 +39,8 @@ export class HaIconPicker extends LitElement {
@property({ type: Boolean }) public disabled = false;
@property({ type: Boolean }) public required = false;
@property({ type: Boolean }) public invalid = false;
@state() private _opened = false;
@ -56,6 +58,7 @@ export class HaIconPicker extends LitElement {
.filteredItems=${iconItems}
.label=${this.label}
.disabled=${this.disabled}
.required=${this.required}
.placeholder=${this.placeholder}
.errorMessage=${this.errorMessage}
.invalid=${this.invalid}

View File

@ -33,7 +33,7 @@ class HaLabeledSlider extends PolymerElement {
}
</style>
<div class="title">[[caption]]</div>
<div class="title">[[_getTitle()]]</div>
<div class="extra-container"><slot name="extra"></slot></div>
<div class="slider-container">
<ha-icon icon="[[icon]]" hidden$="[[!icon]]"></ha-icon>
@ -49,10 +49,15 @@ class HaLabeledSlider extends PolymerElement {
`;
}
_getTitle() {
return `${this.caption}${this.required ? "*" : ""}`;
}
static get properties() {
return {
caption: String,
disabled: Boolean,
required: Boolean,
min: Number,
max: Number,
pin: Boolean,

View File

@ -2,6 +2,11 @@ import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { customElement, property } from "lit/decorators";
import "./ha-markdown-element";
// Import components that are allwoed to be defined.
import "./ha-alert";
import "./ha-icon";
import "./ha-svg-icon";
@customElement("ha-markdown")
export class HaMarkdown extends LitElement {
@property() public content?;

View File

@ -14,11 +14,17 @@ export class HaAddonSelector extends LitElement {
@property() public label?: string;
@property({ type: Boolean }) public disabled = false;
@property({ type: Boolean }) public required = true;
protected render() {
return html`<ha-addon-picker
.hass=${this.hass}
.value=${this.value}
.label=${this.label}
.disabled=${this.disabled}
.required=${this.required}
allow-custom-entity
></ha-addon-picker>`;
}

View File

@ -6,6 +6,7 @@ import { EntityRegistryEntry } from "../../data/entity_registry";
import { AreaSelector } from "../../data/selector";
import { HomeAssistant } from "../../types";
import "../ha-area-picker";
import "../ha-areas-picker";
@customElement("ha-selector-area")
export class HaAreaSelector extends LitElement {
@ -21,6 +22,8 @@ export class HaAreaSelector extends LitElement {
@property({ type: Boolean }) public disabled = false;
@property({ type: Boolean }) public required = true;
protected updated(changedProperties) {
if (changedProperties.has("selector")) {
const oldSelector = changedProperties.get("selector");
@ -28,27 +31,55 @@ export class HaAreaSelector extends LitElement {
oldSelector !== this.selector &&
this.selector.area.device?.integration
) {
this._loadConfigEntries();
getConfigEntries(this.hass, {
domain: this.selector.area.device.integration,
}).then((entries) => {
this._configEntries = entries;
});
}
}
}
protected render() {
return html`<ha-area-picker
.hass=${this.hass}
.value=${this.value}
.label=${this.label}
no-add
.deviceFilter=${this._filterDevices}
.entityFilter=${this._filterEntities}
.includeDeviceClasses=${this.selector.area.entity?.device_class
? [this.selector.area.entity.device_class]
: undefined}
.includeDomains=${this.selector.area.entity?.domain
? [this.selector.area.entity.domain]
: undefined}
.disabled=${this.disabled}
></ha-area-picker>`;
if (!this.selector.area.multiple) {
return html`
<ha-area-picker
.hass=${this.hass}
.value=${this.value}
.label=${this.label}
no-add
.deviceFilter=${this._filterDevices}
.entityFilter=${this._filterEntities}
.includeDeviceClasses=${this.selector.area.entity?.device_class
? [this.selector.area.entity.device_class]
: undefined}
.includeDomains=${this.selector.area.entity?.domain
? [this.selector.area.entity.domain]
: undefined}
.disabled=${this.disabled}
.required=${this.required}
></ha-area-picker>
`;
}
return html`
<ha-areas-picker
.hass=${this.hass}
.value=${this.value}
.pickAreaLabel=${this.label}
no-add
.deviceFilter=${this._filterDevices}
.entityFilter=${this._filterEntities}
.includeDeviceClasses=${this.selector.area.entity?.device_class
? [this.selector.area.entity.device_class]
: undefined}
.includeDomains=${this.selector.area.entity?.domain
? [this.selector.area.entity.domain]
: undefined}
.disabled=${this.disabled}
.required=${this.required}
></ha-areas-picker>
`;
}
private _filterEntities = (entity: EntityRegistryEntry): boolean => {
@ -85,12 +116,6 @@ export class HaAreaSelector extends LitElement {
}
return true;
};
private async _loadConfigEntries() {
this._configEntries = (await getConfigEntries(this.hass)).filter(
(entry) => entry.domain === this.selector.area.device?.integration
);
}
}
declare global {

View File

@ -1,9 +1,10 @@
import "../entity/ha-entity-attribute-picker";
import { html, LitElement } from "lit";
import { html, LitElement, PropertyValues } from "lit";
import { customElement, property } from "lit/decorators";
import { fireEvent } from "../../common/dom/fire_event";
import { AttributeSelector } from "../../data/selector";
import { SubscribeMixin } from "../../mixins/subscribe-mixin";
import { HomeAssistant } from "../../types";
import "../entity/ha-entity-attribute-picker";
@customElement("ha-selector-attribute")
export class HaSelectorAttribute extends SubscribeMixin(LitElement) {
@ -17,18 +18,67 @@ export class HaSelectorAttribute extends SubscribeMixin(LitElement) {
@property({ type: Boolean }) public disabled = false;
@property({ type: Boolean }) public required = true;
@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}
.required=${this.required}
allow-custom-value
></ha-entity-attribute-picker>
`;
}
protected updated(changedProps: PropertyValues): void {
super.updated(changedProps);
if (
// No need to filter value if no value
!this.value ||
// Only adjust value if we used the context
this.selector.attribute.entity_id ||
// Only check if context has changed
!changedProps.has("context")
) {
return;
}
const oldContext = changedProps.get("context") as this["context"];
if (
!this.context ||
oldContext?.filter_entity === this.context.filter_entity
) {
return;
}
// Validate that that the attribute is still valid for this entity, else unselect.
let invalid = false;
if (this.context.filter_entity) {
const stateObj = this.hass.states[this.context.filter_entity];
if (!(stateObj && this.value in stateObj.attributes)) {
invalid = true;
}
} else {
invalid = this.value !== undefined;
}
if (invalid) {
fireEvent(this, "value-changed", {
value: undefined,
});
}
}
}
declare global {

View File

@ -0,0 +1,62 @@
import { css, html, LitElement } from "lit";
import { customElement, property } from "lit/decorators";
import { hex2rgb, rgb2hex } from "../../common/color/convert-color";
import { fireEvent } from "../../common/dom/fire_event";
import type { ColorRGBSelector } from "../../data/selector";
import type { HomeAssistant } from "../../types";
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;
@property({ type: Boolean }) public required = true;
protected render() {
return html`
<ha-textfield
type="color"
.value=${this.value ? rgb2hex(this.value as any) : ""}
.label=${this.label || ""}
.required=${this.required}
.disalbled=${this.disabled}
@change=${this._valueChanged}
></ha-textfield>
`;
}
private _valueChanged(ev: CustomEvent) {
const value = (ev.target as any).value;
fireEvent(this, "value-changed", {
value: hex2rgb(value),
});
}
static styles = css`
:host {
display: flex;
justify-content: flex-end;
align-items: center;
}
ha-textfield {
--text-field-padding: 8px;
min-width: 75px;
flex-grow: 1;
margin: 0 4px;
}
`;
}
declare global {
interface HTMLElementTagNameMap {
"ha-selector-color_rgb": HaColorRGBSelector;
}
}

View File

@ -0,0 +1,62 @@
import { css, html, LitElement } from "lit";
import { customElement, property } from "lit/decorators";
import { fireEvent } from "../../common/dom/fire_event";
import type { ColorTempSelector } from "../../data/selector";
import type { HomeAssistant } from "../../types";
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;
@property({ type: Boolean }) public required = true;
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}
.disabled=${this.disabled}
.required=${this.required}
@change=${this._valueChanged}
></ha-labeled-slider>
`;
}
private _valueChanged(ev: CustomEvent) {
fireEvent(this, "value-changed", {
value: Number((ev.target as any).value),
});
}
static styles = css`
ha-labeled-slider {
--ha-slider-background: -webkit-linear-gradient(
right,
rgb(255, 160, 0) 0%,
white 50%,
rgb(166, 209, 255) 100%
);
/* The color temp minimum value shouldn't be rendered differently. It's not "off". */
--paper-slider-knob-start-border-color: var(--primary-color);
}
`;
}
declare global {
interface HTMLElementTagNameMap {
"ha-selector-color_temp": HaColorTempSelector;
}
}

View File

@ -0,0 +1,39 @@
import { html, LitElement } from "lit";
import { customElement, property } from "lit/decorators";
import type { DateSelector } from "../../data/selector";
import type { HomeAssistant } from "../../types";
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;
@property({ type: Boolean }) public required = true;
protected render() {
return html`
<ha-date-input
.label=${this.label}
.locale=${this.hass.locale}
.disabled=${this.disabled}
.value=${this.value}
.required=${this.required}
>
</ha-date-input>
`;
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-selector-date": HaDateSelector;
}
}

View File

@ -0,0 +1,78 @@
import { css, html, LitElement } from "lit";
import { customElement, property, query } from "lit/decorators";
import { fireEvent } from "../../common/dom/fire_event";
import type { DateTimeSelector } from "../../data/selector";
import type { HomeAssistant } from "../../types";
import "../ha-date-input";
import type { HaDateInput } from "../ha-date-input";
import "../ha-time-input";
import type { HaTimeInput } from "../ha-time-input";
@customElement("ha-selector-datetime")
export class HaDateTimeSelector extends LitElement {
@property() public hass!: HomeAssistant;
@property() public selector!: DateTimeSelector;
@property() public value?: string;
@property() public label?: string;
@property({ type: Boolean, reflect: true }) public disabled = false;
@property({ type: Boolean }) public required = true;
@query("ha-date-input") private _dateInput!: HaDateInput;
@query("ha-time-input") private _timeInput!: HaTimeInput;
protected render() {
const values = this.value?.split(" ");
return html`
<ha-date-input
.label=${this.label}
.locale=${this.hass.locale}
.disabled=${this.disabled}
.required=${this.required}
.value=${values?.[0]}
@value-changed=${this._valueChanged}
>
</ha-date-input>
<ha-time-input
enable-second
.value=${values?.[1] || "0:00:00"}
.locale=${this.hass.locale}
.disabled=${this.disabled}
.required=${this.required}
@value-changed=${this._valueChanged}
></ha-time-input>
`;
}
private _valueChanged(ev: CustomEvent): void {
ev.stopPropagation();
fireEvent(this, "value-changed", {
value: `${this._dateInput.value} ${this._timeInput.value}`,
});
}
static styles = css`
:host {
display: flex;
align-items: center;
flex-direction: row;
}
ha-date-input {
min-width: 150px;
margin-right: 4px;
}
`;
}
declare global {
interface HTMLElementTagNameMap {
"ha-selector-datetime": HaDateTimeSelector;
}
}

View File

@ -1,10 +1,11 @@
import { html, LitElement } from "lit";
import { customElement, property, state } from "lit/decorators";
import { ConfigEntry, getConfigEntries } from "../../data/config_entries";
import { DeviceRegistryEntry } from "../../data/device_registry";
import { DeviceSelector } from "../../data/selector";
import { HomeAssistant } from "../../types";
import type { DeviceRegistryEntry } from "../../data/device_registry";
import type { DeviceSelector } from "../../data/selector";
import type { HomeAssistant } from "../../types";
import "../device/ha-device-picker";
import "../device/ha-devices-picker";
@customElement("ha-selector-device")
export class HaDeviceSelector extends LitElement {
@ -20,30 +21,56 @@ export class HaDeviceSelector extends LitElement {
@property({ type: Boolean }) public disabled = false;
@property({ type: Boolean }) public required = true;
protected updated(changedProperties) {
if (changedProperties.has("selector")) {
const oldSelector = changedProperties.get("selector");
if (oldSelector !== this.selector && this.selector.device?.integration) {
this._loadConfigEntries();
getConfigEntries(this.hass, {
domain: this.selector.device.integration,
}).then((entries) => {
this._configEntries = entries;
});
}
}
}
protected render() {
return html`<ha-device-picker
.hass=${this.hass}
.value=${this.value}
.label=${this.label}
.deviceFilter=${this._filterDevices}
.includeDeviceClasses=${this.selector.device.entity?.device_class
? [this.selector.device.entity.device_class]
: undefined}
.includeDomains=${this.selector.device.entity?.domain
? [this.selector.device.entity.domain]
: undefined}
.disabled=${this.disabled}
allow-custom-entity
></ha-device-picker>`;
if (!this.selector.device.multiple) {
return html`
<ha-device-picker
.hass=${this.hass}
.value=${this.value}
.label=${this.label}
.deviceFilter=${this._filterDevices}
.includeDeviceClasses=${this.selector.device.entity?.device_class
? [this.selector.device.entity.device_class]
: undefined}
.includeDomains=${this.selector.device.entity?.domain
? [this.selector.device.entity.domain]
: undefined}
.disabled=${this.disabled}
.required=${this.required}
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}
.required=${this.required}
></ha-devices-picker>
`;
}
private _filterDevices = (device: DeviceRegistryEntry): boolean => {
@ -71,12 +98,6 @@ export class HaDeviceSelector extends LitElement {
}
return true;
};
private async _loadConfigEntries() {
this._configEntries = (await getConfigEntries(this.hass)).filter(
(entry) => entry.domain === this.selector.device.integration
);
}
}
declare global {

View File

@ -1,8 +1,8 @@
import "../ha-duration-input";
import { html, LitElement } from "lit";
import { customElement, property } from "lit/decorators";
import { DurationSelector } from "../../data/selector";
import { HomeAssistant } from "../../types";
import type { DurationSelector } from "../../data/selector";
import type { HomeAssistant } from "../../types";
import "../ha-duration-input";
@customElement("ha-selector-duration")
export class HaTimeDuration extends LitElement {
@ -14,6 +14,8 @@ export class HaTimeDuration extends LitElement {
@property() public label?: string;
@property() public helper?: string;
@property({ type: Boolean }) public disabled = false;
@property({ type: Boolean }) public required = true;
@ -22,9 +24,11 @@ export class HaTimeDuration extends LitElement {
return html`
<ha-duration-input
.label=${this.label}
.helper=${this.helper}
.data=${this.value}
.disabled=${this.disabled}
.required=${this.required}
.enableDay=${this.selector.duration.enable_day}
></ha-duration-input>
`;
}

View File

@ -1,20 +1,23 @@
import { HassEntity, UnsubscribeFunc } from "home-assistant-js-websocket";
import { html, LitElement } from "lit";
import { HassEntity } from "home-assistant-js-websocket";
import { html, LitElement, PropertyValues } from "lit";
import { customElement, property, state } from "lit/decorators";
import { computeStateDomain } from "../../common/entity/compute_state_domain";
import { subscribeEntityRegistry } from "../../data/entity_registry";
import {
EntitySources,
fetchEntitySourcesWithCache,
} from "../../data/entity_sources";
import { EntitySelector } from "../../data/selector";
import { SubscribeMixin } from "../../mixins/subscribe-mixin";
import { HomeAssistant } from "../../types";
import "../entity/ha-entities-picker";
import "../entity/ha-entity-picker";
@customElement("ha-selector-entity")
export class HaEntitySelector extends SubscribeMixin(LitElement) {
export class HaEntitySelector extends LitElement {
@property() public hass!: HomeAssistant;
@property() public selector!: EntitySelector;
@state() private _entityPlaformLookup?: Record<string, string>;
@state() private _entitySources?: EntitySources;
@property() public value?: any;
@ -22,60 +25,77 @@ export class HaEntitySelector extends SubscribeMixin(LitElement) {
@property({ type: Boolean }) public disabled = false;
@property({ type: Boolean }) public required = true;
protected render() {
return html`<ha-entity-picker
.hass=${this.hass}
.value=${this.value}
.label=${this.label}
.entityFilter=${this._filterEntities}
.disabled=${this.disabled}
allow-custom-entity
></ha-entity-picker>`;
if (!this.selector.entity.multiple) {
return html`<ha-entity-picker
.hass=${this.hass}
.value=${this.value}
.label=${this.label}
.includeEntities=${this.selector.entity.include_entities}
.excludeEntities=${this.selector.entity.exclude_entities}
.entityFilter=${this._filterEntities}
.disabled=${this.disabled}
.required=${this.required}
allow-custom-entity
></ha-entity-picker>`;
}
return html`
${this.label ? html`<label>${this.label}</label>` : ""}
<ha-entities-picker
.hass=${this.hass}
.value=${this.value}
.entityFilter=${this._filterEntities}
.includeEntities=${this.selector.entity.include_entities}
.excludeEntities=${this.selector.entity.exclude_entities}
.required=${this.required}
></ha-entities-picker>
`;
}
public hassSubscribe(): UnsubscribeFunc[] {
return [
subscribeEntityRegistry(this.hass.connection!, (entities) => {
const entityLookup = {};
for (const confEnt of entities) {
if (!confEnt.platform) {
continue;
}
entityLookup[confEnt.entity_id] = confEnt.platform;
}
this._entityPlaformLookup = entityLookup;
}),
];
protected updated(changedProps: PropertyValues): void {
super.updated(changedProps);
if (
changedProps.has("selector") &&
this.selector.entity.integration &&
!this._entitySources
) {
fetchEntitySourcesWithCache(this.hass).then((sources) => {
this._entitySources = sources;
});
}
}
private _filterEntities = (entity: HassEntity): boolean => {
if (this.selector.entity?.domain) {
const filterDomain = this.selector.entity.domain;
const filterDomainIsArray = Array.isArray(filterDomain);
const {
domain: filterDomain,
device_class: filterDeviceClass,
integration: filterIntegration,
} = this.selector.entity;
if (filterDomain) {
const entityDomain = computeStateDomain(entity);
if (
(filterDomainIsArray && !filterDomain.includes(entityDomain)) ||
(!filterDomainIsArray && entityDomain !== filterDomain)
Array.isArray(filterDomain)
? !filterDomain.includes(entityDomain)
: entityDomain !== filterDomain
) {
return false;
}
}
if (this.selector.entity?.device_class) {
if (
!entity.attributes.device_class ||
entity.attributes.device_class !== this.selector.entity.device_class
) {
return false;
}
if (
filterDeviceClass &&
entity.attributes.device_class !== filterDeviceClass
) {
return false;
}
if (this.selector.entity?.integration) {
if (
!this._entityPlaformLookup ||
this._entityPlaformLookup[entity.entity_id] !==
this.selector.entity.integration
) {
return false;
}
if (
filterIntegration &&
this._entitySources?.[entity.entity_id]?.domain !== filterIntegration
) {
return false;
}
return true;
};

View File

@ -1,9 +1,9 @@
import "../ha-icon-picker";
import { html, LitElement } from "lit";
import { customElement, property } from "lit/decorators";
import { HomeAssistant } from "../../types";
import { IconSelector } from "../../data/selector";
import { fireEvent } from "../../common/dom/fire_event";
import { IconSelector } from "../../data/selector";
import { HomeAssistant } from "../../types";
import "../ha-icon-picker";
@customElement("ha-selector-icon")
export class HaIconSelector extends LitElement {
@ -17,11 +17,15 @@ export class HaIconSelector extends LitElement {
@property({ type: Boolean, reflect: true }) public disabled = false;
@property({ type: Boolean }) public required = true;
protected render() {
return html`
<ha-icon-picker
.label=${this.label}
.value=${this.value}
.required=${this.required}
.disabled=${this.disabled}
.fallbackPath=${this.selector.icon.fallbackPath}
.placeholder=${this.selector.icon.placeholder}
@value-changed=${this._valueChanged}

View File

@ -6,7 +6,6 @@ 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";
@ -52,7 +51,10 @@ export class HaLocationSelector extends LitElement {
longitude: value?.longitude || this.hass.config.longitude,
radius: selector.location.radius ? value?.radius || 1000 : undefined,
radius_color: zoneRadiusColor,
icon: selector.location.icon,
icon:
selector.location.icon || selector.location.radius
? "mdi:map-marker-radius"
: "mdi:map-marker",
location_editable: true,
radius_editable: true,
},

View File

@ -35,6 +35,8 @@ export class HaMediaSelector extends LitElement {
@property({ type: Boolean, reflect: true }) public disabled = false;
@property({ type: Boolean, reflect: true }) public required = true;
@state() private _thumbnailUrl?: string | null;
willUpdate(changedProps: PropertyValues<this>) {
@ -84,6 +86,7 @@ export class HaMediaSelector extends LitElement {
.label=${this.label ||
this.hass.localize("ui.components.selectors.media.pick_media_player")}
.disabled=${this.disabled}
.required=${this.required}
include-domains='["media_player"]'
allow-custom-entity
@value-changed=${this._entityChanged}

View File

@ -19,13 +19,15 @@ export class HaNumberSelector extends LitElement {
@property() public label?: string;
@property() public helper?: string;
@property({ type: Boolean }) public required = true;
@property({ type: Boolean }) public disabled = false;
protected render() {
return html`${this.selector.number.mode !== "box"
? html`${this.label}<ha-slider
? html`${this.label}${this.required ? "*" : ""}<ha-slider
.min=${this.selector.number.min}
.max=${this.selector.number.max}
.value=${this._value}
@ -46,8 +48,10 @@ export class HaNumberSelector extends LitElement {
class=${classMap({ single: this.selector.number.mode === "box" })}
.min=${this.selector.number.min}
.max=${this.selector.number.max}
.value=${this.value || ""}
.value=${this.value ?? ""}
.step=${this.selector.number.step ?? 1}
helperPersistent
.helper=${this.helper}
.disabled=${this.disabled}
.required=${this.required}
.suffix=${this.selector.number.unit_of_measurement}

View File

@ -16,10 +16,13 @@ export class HaObjectSelector extends LitElement {
@property({ type: Boolean }) public disabled = false;
@property({ type: Boolean }) public required = true;
protected render() {
return html`<ha-yaml-editor
.hass=${this.hass}
.disabled=${this.disabled}
.readonly=${this.disabled}
.required=${this.required}
.placeholder=${this.placeholder}
.defaultValue=${this.value}
@value-changed=${this._handleChange}

View File

@ -1,19 +1,27 @@
import "@material/mwc-formfield/mwc-formfield";
import "@material/mwc-list/mwc-list-item";
import { css, CSSResultGroup, html, LitElement } from "lit";
import { customElement, property } from "lit/decorators";
import { mdiClose } from "@mdi/js";
import { css, html, LitElement } from "lit";
import { customElement, property, query } from "lit/decorators";
import { fireEvent } from "../../common/dom/fire_event";
import { stopPropagation } from "../../common/dom/stop_propagation";
import { SelectOption, SelectSelector } from "../../data/selector";
import { HomeAssistant } from "../../types";
import type { SelectOption, SelectSelector } from "../../data/selector";
import type { HomeAssistant } from "../../types";
import "../ha-checkbox";
import "../ha-chip";
import "../ha-chip-set";
import type { HaComboBox } from "../ha-combo-box";
import "../ha-formfield";
import "../ha-radio";
import "../ha-select";
@customElement("ha-selector-select")
export class HaSelectSelector extends LitElement {
@property() public hass!: HomeAssistant;
@property({ attribute: false }) public hass!: HomeAssistant;
@property() public selector!: SelectSelector;
@property({ attribute: false }) public selector!: SelectSelector;
@property() public value?: string;
@property() public value?: string | string[];
@property() public label?: string;
@ -21,43 +29,254 @@ export class HaSelectSelector extends LitElement {
@property({ type: Boolean }) public disabled = false;
protected render() {
return html`<ha-select
fixedMenuPosition
naturalMenuWidth
.label=${this.label}
.value=${this.value}
.helper=${this.helper}
.disabled=${this.disabled}
@closed=${stopPropagation}
@selected=${this._valueChanged}
>
${this.selector.select.options.map((item: string | SelectOption) => {
const value = typeof item === "object" ? item.value : item;
const label = typeof item === "object" ? item.label : item;
@property({ type: Boolean }) public required = true;
return html`<mwc-list-item .value=${value}>${label}</mwc-list-item>`;
})}
</ha-select>`;
@query("ha-combo-box", true) private comboBox!: HaComboBox;
private _filter = "";
protected render() {
const options = this.selector.select.options.map((option) =>
typeof option === "object" ? option : { value: option, label: option }
);
if (!this.selector.select.custom_value && this._mode === "list") {
if (!this.selector.select.multiple || this.required) {
return html`
<div>
${this.label}
${options.map(
(item: SelectOption) => html`
<mwc-formfield .label=${item.label}>
<ha-radio
.checked=${item.value === this.value}
.value=${item.value}
.disabled=${this.disabled}
@change=${this._valueChanged}
></ha-radio>
</mwc-formfield>
`
)}
</div>
`;
}
return html`
<div>
${this.label}${options.map(
(item: SelectOption) => html`
<ha-formfield .label=${item.label}>
<ha-checkbox
.checked=${this.value?.includes(item.value)}
.value=${item.value}
.disabled=${this.disabled}
@change=${this._checkboxChanged}
></ha-checkbox>
</ha-formfield>
`
)}
</div>
`;
}
if (this.selector.select.multiple) {
const value =
!this.value || this.value === "" ? [] : (this.value as string[]);
return html`
<ha-chip-set>
${value?.map(
(item, idx) =>
html`
<ha-chip hasTrailingIcon>
${options.find((option) => option.value === item)?.label ||
item}
<ha-svg-icon
slot="trailing-icon"
.path=${mdiClose}
.idx=${idx}
@click=${this._removeItem}
></ha-svg-icon>
</ha-chip>
`
)}
</ha-chip-set>
<ha-combo-box
item-value-path="value"
item-label-path="label"
.hass=${this.hass}
.label=${this.label}
.disabled=${this.disabled}
.required=${this.required}
.value=${this._filter}
.items=${options.filter((item) => !this.value?.includes(item.value))}
@filter-changed=${this._filterChanged}
@value-changed=${this._comboBoxValueChanged}
></ha-combo-box>
`;
}
if (this.selector.select.custom_value) {
if (
this.value !== undefined &&
!options.find((option) => option.value === this.value)
) {
options.unshift({ value: this.value, label: this.value });
}
return html`
<ha-combo-box
item-value-path="value"
item-label-path="label"
.hass=${this.hass}
.label=${this.label}
.disabled=${this.disabled}
.required=${this.required}
.items=${options}
.value=${this.value}
@filter-changed=${this._filterChanged}
@value-changed=${this._comboBoxValueChanged}
></ha-combo-box>
`;
}
return html`
<ha-select
fixedMenuPosition
naturalMenuWidth
.label=${this.label}
.value=${this.value}
.helper=${this.helper}
.disabled=${this.disabled}
@closed=${stopPropagation}
@selected=${this._valueChanged}
>
${options.map(
(item: SelectOption) => html`
<mwc-list-item .value=${item.value}>${item.label}</mwc-list-item>
`
)}
</ha-select>
`;
}
private get _mode(): "list" | "dropdown" {
return (
this.selector.select.mode ||
(this.selector.select.options.length < 6 ? "list" : "dropdown")
);
}
private _valueChanged(ev) {
ev.stopPropagation();
if (this.disabled || !ev.target.value) {
const value = ev.detail?.value || ev.target.value;
if (this.disabled || !value) {
return;
}
fireEvent(this, "value-changed", {
value: ev.target.value,
value: value,
});
}
static get styles(): CSSResultGroup {
return css`
ha-select {
width: 100%;
private _checkboxChanged(ev) {
ev.stopPropagation();
if (this.disabled) {
return;
}
let newValue: string[];
const value: string = ev.target.value;
const checked = ev.target.checked;
if (checked) {
if (!this.value) {
newValue = [value];
} else if (this.value.includes(value)) {
return;
} else {
newValue = [...this.value, value];
}
`;
} else {
if (!this.value?.includes(value)) {
return;
}
newValue = (this.value as string[]).filter((v) => v !== value);
}
fireEvent(this, "value-changed", {
value: newValue,
});
}
private async _removeItem(ev) {
const value: string[] = [...(this.value! as string[])];
value.splice(ev.target.idx, 1);
fireEvent(this, "value-changed", {
value,
});
await this.updateComplete;
this._filterChanged();
}
private _comboBoxValueChanged(ev: CustomEvent): void {
ev.stopPropagation();
const newValue = ev.detail.value;
if (this.disabled || newValue === "") {
return;
}
if (!this.selector.select.multiple) {
fireEvent(this, "value-changed", {
value: newValue,
});
return;
}
if (newValue !== undefined && this.value?.includes(newValue)) {
return;
}
setTimeout(() => {
this._filterChanged();
this.comboBox.setInputValue("");
}, 0);
const currentValue =
!this.value || this.value === "" ? [] : (this.value as string[]);
fireEvent(this, "value-changed", {
value: [...currentValue, newValue],
});
}
private _filterChanged(ev?: CustomEvent): void {
this._filter = ev?.detail.value || "";
const filteredItems = this.comboBox.items?.filter((item) => {
if (this.selector.select.multiple && this.value?.includes(item.value)) {
return false;
}
const label = item.label || item.value;
return label.toLowerCase().includes(this._filter?.toLowerCase());
});
if (this._filter && this.selector.select.custom_value) {
filteredItems?.unshift({ label: this._filter, value: this._filter });
}
this.comboBox.filteredItems = filteredItems;
}
static styles = css`
ha-select,
mwc-formfield,
ha-formfield {
display: block;
}
`;
}
declare global {

View File

@ -134,9 +134,8 @@ export class HaTargetSelector extends SubscribeMixin(LitElement) {
private async _loadConfigEntries() {
this._configEntries = (await getConfigEntries(this.hass)).filter(
(entry) =>
entry.domain ===
(this.selector.target.device?.integration ||
this.selector.target.entity?.integration)
entry.domain === this.selector.target.device?.integration ||
entry.domain === this.selector.target.entity?.integration
);
}

View File

@ -18,6 +18,8 @@ export class HaTextSelector extends LitElement {
@property() public placeholder?: string;
@property() public helper?: string;
@property() public selector!: StringSelector;
@property({ type: Boolean }) public disabled = false;
@ -32,6 +34,8 @@ export class HaTextSelector extends LitElement {
.label=${this.label}
.placeholder=${this.placeholder}
.value=${this.value || ""}
.helper=${this.helper}
helperPersistent
.disabled=${this.disabled}
@input=${this._handleChange}
autocapitalize="none"
@ -44,6 +48,8 @@ export class HaTextSelector extends LitElement {
return html`<ha-textfield
.value=${this.value || ""}
.placeholder=${this.placeholder || ""}
.helper=${this.helper}
helperPersistent
.disabled=${this.disabled}
.type=${this._unmaskedPassword ? "text" : this.selector.text?.type}
@input=${this._handleChange}

View File

@ -1,8 +1,8 @@
import "../../panels/lovelace/components/hui-theme-select-editor";
import { html, LitElement } from "lit";
import { customElement, property } from "lit/decorators";
import type { HomeAssistant } from "../../types";
import type { ThemeSelector } from "../../data/selector";
import type { HomeAssistant } from "../../types";
import "../ha-theme-picker";
@customElement("ha-selector-theme")
export class HaThemeSelector extends LitElement {
@ -16,13 +16,17 @@ export class HaThemeSelector extends LitElement {
@property({ type: Boolean, reflect: true }) public disabled = false;
@property({ type: Boolean }) public required = true;
protected render() {
return html`
<hui-theme-select-editor
<ha-theme-picker
.hass=${this.hass}
.value=${this.value}
.label=${this.label}
></hui-theme-select-editor>
.disabled=${this.disabled}
.required=${this.required}
></ha-theme-picker>
`;
}
}

View File

@ -16,12 +16,15 @@ export class HaTimeSelector extends LitElement {
@property({ type: Boolean }) public disabled = false;
@property({ type: Boolean }) public required = false;
protected render() {
return html`
<ha-time-input
.value=${this.value}
.locale=${this.hass.locale}
.disabled=${this.disabled}
.required=${this.required}
.label=${this.label}
enable-second
></ha-time-input>

View File

@ -8,6 +8,9 @@ import "./ha-selector-addon";
import "./ha-selector-area";
import "./ha-selector-attribute";
import "./ha-selector-boolean";
import "./ha-selector-color-rgb";
import "./ha-selector-date";
import "./ha-selector-datetime";
import "./ha-selector-device";
import "./ha-selector-duration";
import "./ha-selector-entity";
@ -21,6 +24,7 @@ import "./ha-selector-icon";
import "./ha-selector-media";
import "./ha-selector-theme";
import "./ha-selector-location";
import "./ha-selector-color-temp";
@customElement("ha-selector")
export class HaSelector extends LitElement {
@ -40,6 +44,8 @@ export class HaSelector extends LitElement {
@property({ type: Boolean }) public required = true;
@property() public context?: Record<string, any>;
public focus() {
this.shadowRoot?.getElementById("selector")?.focus();
}
@ -59,6 +65,7 @@ export class HaSelector extends LitElement {
disabled: this.disabled,
required: this.required,
helper: this.helper,
context: this.context,
id: "selector",
})}
`;

View File

@ -471,6 +471,7 @@ export class HaServiceControl extends LitElement {
}
ha-settings-row {
--paper-time-input-justify-content: flex-end;
--settings-row-content-width: 100%;
border-top: var(
--service-control-items-border-top,
1px solid var(--divider-color)
@ -489,9 +490,6 @@ export class HaServiceControl extends LitElement {
margin: var(--service-control-padding, 0 16px);
padding: 16px 0;
}
:host(:not([narrow])) ha-settings-row ha-selector {
width: 60%;
}
.checkbox-spacer {
width: 32px;
}

View File

@ -21,7 +21,7 @@ export class HaSettingsRow extends LitElement {
<div secondary><slot name="description"></slot></div>
</paper-item-body>
</div>
<slot></slot>
<div class="content"><slot></slot></div>
`;
}
@ -43,6 +43,18 @@ export class HaSettingsRow extends LitElement {
);
flex: 1;
}
.content {
display: contents;
}
:host(:not([narrow])) .content {
display: flex;
justify-content: flex-end;
flex: 1;
padding: 16px 0;
}
.content ::slotted(*) {
width: var(--settings-row-content-width);
}
:host([narrow]) {
align-items: normal;
flex-direction: column;

View File

@ -37,6 +37,7 @@ import { LocalStorage } from "../common/decorators/local-storage";
import { fireEvent } from "../common/dom/fire_event";
import { toggleAttribute } from "../common/dom/toggle_attribute";
import { computeDomain } from "../common/entity/compute_domain";
import { computeStateDomain } from "../common/entity/compute_state_domain";
import { stringCompare } from "../common/string/compare";
import { computeRTL } from "../common/util/compute_rtl";
import { ActionHandlerDetail } from "../data/lovelace";
@ -44,6 +45,7 @@ import {
PersistentNotification,
subscribeNotifications,
} from "../data/persistent_notification";
import { updateCanInstall, UpdateEntity } from "../data/update";
import { actionHandler } from "../panels/lovelace/common/directives/action-handler-directive";
import { haStyleScrollbar } from "../resources/styles";
import type { HomeAssistant, PanelInfo, Route } from "../types";
@ -68,7 +70,6 @@ const SORT_VALUE_URL_PATHS = {
const PANEL_ICONS = {
calendar: mdiCalendar,
config: mdiCog,
"developer-tools": mdiHammer,
energy: mdiLightningBolt,
history: mdiChartBox,
@ -190,6 +191,8 @@ class HaSidebar extends LitElement {
@state() private _notifications?: PersistentNotification[];
@state() private _updatesCount = 0;
@state() private _renderEmptySortable = false;
private _mouseLeaveTimeout?: number;
@ -235,6 +238,7 @@ class HaSidebar extends LitElement {
changedProps.has("narrow") ||
changedProps.has("alwaysExpand") ||
changedProps.has("_externalConfig") ||
changedProps.has("_updatesCount") ||
changedProps.has("_notifications") ||
changedProps.has("editMode") ||
changedProps.has("_renderEmptySortable") ||
@ -290,6 +294,12 @@ class HaSidebar extends LitElement {
toggleAttribute(this, "rtl", computeRTL(this.hass));
}
this._updatesCount = Object.values(this.hass.states).filter(
(entity) =>
computeStateDomain(entity) === "update" &&
updateCanInstall(entity as UpdateEntity)
).length;
if (!SUPPORT_SCROLL_IF_NEEDED) {
return;
}
@ -387,35 +397,37 @@ class HaSidebar extends LitElement {
icon?: string | null,
iconPath?: string | null
) {
return html`
<a
role="option"
href=${`/${urlPath}`}
data-panel=${urlPath}
tabindex="-1"
@mouseenter=${this._itemMouseEnter}
@mouseleave=${this._itemMouseLeave}
>
<paper-icon-item>
${iconPath
? html`<ha-svg-icon
slot="item-icon"
.path=${iconPath}
></ha-svg-icon>`
: html`<ha-icon slot="item-icon" .icon=${icon}></ha-icon>`}
<span class="item-text">${title}</span>
</paper-icon-item>
${this.editMode
? html`<ha-icon-button
.label=${this.hass.localize("ui.sidebar.hide_panel")}
.path=${mdiClose}
class="hide-panel"
.panel=${urlPath}
@click=${this._hidePanel}
></ha-icon-button>`
: ""}
</a>
`;
return urlPath === "config"
? this._renderConfiguration(title)
: html`
<a
role="option"
href=${`/${urlPath}`}
data-panel=${urlPath}
tabindex="-1"
@mouseenter=${this._itemMouseEnter}
@mouseleave=${this._itemMouseLeave}
>
<paper-icon-item>
${iconPath
? html`<ha-svg-icon
slot="item-icon"
.path=${iconPath}
></ha-svg-icon>`
: html`<ha-icon slot="item-icon" .icon=${icon}></ha-icon>`}
<span class="item-text">${title}</span>
</paper-icon-item>
${this.editMode
? html`<ha-icon-button
.label=${this.hass.localize("ui.sidebar.hide_panel")}
.path=${mdiClose}
class="hide-panel"
.panel=${urlPath}
@click=${this._hidePanel}
></ha-icon-button>`
: ""}
</a>
`;
}
private _renderPanelsEdit(beforeSpacer: PanelInfo[]) {
@ -477,6 +489,35 @@ class HaSidebar extends LitElement {
return html`<div class="spacer" disabled></div>`;
}
private _renderConfiguration(title: string | null) {
return html` <a
class="configuration-container"
role="option"
href="/config"
data-panel="config"
tabindex="-1"
@mouseenter=${this._itemMouseEnter}
@mouseleave=${this._itemMouseLeave}
>
<paper-icon-item class="configuration" role="option">
<ha-svg-icon slot="item-icon" .path=${mdiCog}></ha-svg-icon>
${!this.alwaysExpand && this._updatesCount > 0
? html`
<span class="configuration-badge" slot="item-icon">
${this._updatesCount}
</span>
`
: ""}
<span class="item-text">${title}</span>
${this.alwaysExpand && this._updatesCount > 0
? html`
<span class="configuration-badge">${this._updatesCount}</span>
`
: ""}
</paper-icon-item>
</a>`;
}
private _renderNotifications() {
let notificationCount = this._notifications
? this._notifications.length
@ -953,18 +994,21 @@ class HaSidebar extends LitElement {
height: 1px;
background-color: var(--divider-color);
}
.notifications-container {
.notifications-container,
.configuration-container {
display: flex;
margin-left: env(safe-area-inset-left);
}
:host([rtl]) .notifications-container {
:host([rtl]) .notifications-container,
:host([rtl]) .configuration-container {
margin-left: initial;
margin-right: env(safe-area-inset-right);
}
.notifications {
cursor: pointer;
}
.notifications .item-text {
.notifications .item-text,
.configuration .item-text {
flex: 1;
}
.profile {
@ -988,7 +1032,8 @@ class HaSidebar extends LitElement {
margin-right: 8px;
}
.notification-badge {
.notification-badge,
.configuration-badge {
min-width: 20px;
box-sizing: border-box;
border-radius: 50%;
@ -999,7 +1044,11 @@ class HaSidebar extends LitElement {
padding: 0px 6px;
color: var(--text-accent-color, var(--text-primary-color));
}
ha-svg-icon + .notification-badge {
.configuration-badge {
background-color: var(--primary-color);
}
ha-svg-icon + .notification-badge,
ha-svg-icon + .configuration-badge {
position: absolute;
bottom: 14px;
left: 26px;

View File

@ -42,9 +42,7 @@ export class HaTab extends LitElement {
@keydown=${this._handleKeyDown}
>
${this.narrow ? html`<slot name="icon"></slot>` : ""}
${!this.narrow || this.active
? html`<span class="name">${this.name}</span>`
: ""}
<span class="name">${this.name}</span>
${this._shouldRenderRipple ? html`<mwc-ripple></mwc-ripple>` : ""}
</div>
`;

View File

@ -19,13 +19,11 @@ export class HaTextArea extends TextAreaBase {
textfieldStyles,
textareaStyles,
css`
:host([autogrow]) {
max-height: 200px;
}
:host([autogrow]) .mdc-text-field {
position: relative;
min-height: 74px;
min-width: 178px;
max-height: 200px;
}
:host([autogrow]) .mdc-text-field:after {
content: attr(data-value);

View File

@ -2,29 +2,31 @@ import "@material/mwc-button";
import "@material/mwc-list/mwc-list-item";
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { customElement, property } from "lit/decorators";
import { fireEvent } from "../../../common/dom/fire_event";
import { stopPropagation } from "../../../common/dom/stop_propagation";
import "../../../components/ha-select";
import { HomeAssistant } from "../../../types";
import { fireEvent } from "../common/dom/fire_event";
import { stopPropagation } from "../common/dom/stop_propagation";
import { HomeAssistant } from "../types";
import "./ha-select";
@customElement("hui-theme-select-editor")
export class HuiThemeSelectEditor extends LitElement {
@customElement("ha-theme-picker")
export class HaThemePicker extends LitElement {
@property() public value?: string;
@property() public label?: string;
@property({ attribute: false }) public hass?: HomeAssistant;
@property({ type: Boolean, reflect: true }) public disabled = false;
@property({ type: Boolean }) public required = false;
protected render(): TemplateResult {
return html`
<ha-select
.label=${this.label ||
`${this.hass!.localize(
"ui.panel.lovelace.editor.card.generic.theme"
)} (${this.hass!.localize(
"ui.panel.lovelace.editor.card.config.optional"
)})`}
this.hass!.localize("ui.components.theme_picker.theme")}
.value=${this.value}
.required=${this.required}
.disabled=${this.disabled}
@selected=${this._changed}
@closed=${stopPropagation}
fixedMenuPosition
@ -32,7 +34,7 @@ export class HuiThemeSelectEditor extends LitElement {
>
<mwc-list-item value="remove"
>${this.hass!.localize(
"ui.panel.lovelace.editor.card.generic.no_theme"
"ui.components.theme_picker.no_theme"
)}</mwc-list-item
>
${Object.keys(this.hass!.themes.themes)
@ -64,6 +66,6 @@ export class HuiThemeSelectEditor extends LitElement {
declare global {
interface HTMLElementTagNameMap {
"hui-theme-select-editor": HuiThemeSelectEditor;
"ha-theme-picker": HaThemePicker;
}
}

View File

@ -2,8 +2,8 @@ import { html, LitElement } from "lit";
import { customElement, property } from "lit/decorators";
import { useAmPm } from "../common/datetime/use_am_pm";
import { fireEvent } from "../common/dom/fire_event";
import "./ha-base-time-input";
import { FrontendLocaleData } from "../data/translation";
import "./ha-base-time-input";
import type { TimeChangedEvent } from "./ha-base-time-input";
@customElement("ha-time-input")
@ -16,6 +16,8 @@ export class HaTimeInput extends LitElement {
@property({ type: Boolean }) public disabled = false;
@property({ type: Boolean }) public required = false;
@property({ type: Boolean, attribute: "enable-second" })
public enableSecond = false;
@ -43,6 +45,7 @@ export class HaTimeInput extends LitElement {
.disabled=${this.disabled}
@value-changed=${this._timeChanged}
.enableSecond=${this.enableSecond}
.required=${this.required}
></ha-base-time-input>
`;
}

View File

@ -31,6 +31,10 @@ export class HaYamlEditor extends LitElement {
@property() public label?: string;
@property({ type: Boolean }) public readOnly = false;
@property({ type: Boolean }) public required = false;
@state() private _yaml = "";
public setValue(value): void {
@ -57,10 +61,11 @@ export class HaYamlEditor extends LitElement {
return html``;
}
return html`
${this.label ? html`<p>${this.label}</p>` : ""}
${this.label ? html`<p>${this.label}${this.required ? "*" : ""}</p>` : ""}
<ha-code-editor
.hass=${this.hass}
.value=${this._yaml}
.readOnly=${this.readOnly}
mode="yaml"
autocomplete-entities
.error=${this.isValid === false}

View File

@ -488,6 +488,14 @@ export class HaMap extends ReactiveElement {
text-align: center;
color: var(--primary-text-color);
}
.leaflet-pane {
z-index: 0 !important;
}
.leaflet-control,
.leaflet-top,
.leaflet-bottom {
z-index: 1 !important;
}
`;
}
}

View File

@ -62,6 +62,7 @@ export interface ContextConstraint {
export interface BaseTrigger {
platform: string;
id?: string;
variables?: Record<string, unknown>;
}
export interface StateTrigger extends BaseTrigger {

36
src/data/backup.ts Normal file
View File

@ -0,0 +1,36 @@
import { HomeAssistant } from "../types";
export interface BackupContent {
slug: string;
date: string;
name: string;
size: number;
path: string;
}
export interface BackupData {
backing_up: boolean;
backups: BackupContent[];
}
export const getBackupDownloadUrl = (slug: string) =>
`/api/backup/download/${slug}`;
export const fetchBackupInfo = (hass: HomeAssistant): Promise<BackupData> =>
hass.callWS({
type: "backup/info",
});
export const removeBackup = (
hass: HomeAssistant,
slug: string
): Promise<void> =>
hass.callWS({
type: "backup/remove",
slug,
});
export const generateBackup = (hass: HomeAssistant): Promise<BackupContent> =>
hass.callWS({
type: "backup/generate",
});

View File

@ -7,6 +7,7 @@ import {
HistoryResult,
LineChartUnit,
TimelineEntity,
entityIdHistoryNeedsAttributes,
} from "./history";
export interface CacheConfig {
@ -53,7 +54,17 @@ export const getRecent = (
return cache.data;
}
const prom = fetchRecent(hass, entityId, startTime, endTime).then(
const noAttributes = !entityIdHistoryNeedsAttributes(hass, entityId);
const prom = fetchRecent(
hass,
entityId,
startTime,
endTime,
false,
undefined,
true,
noAttributes
).then(
(stateHistory) => computeHistory(hass, stateHistory, localize),
(err) => {
delete RECENT_CACHE[entityId];
@ -120,6 +131,7 @@ export const getRecentWithCache = (
}
const curCacheProm = cache.prom;
const noAttributes = !entityIdHistoryNeedsAttributes(hass, entityId);
const genProm = async () => {
let fetchedHistory: HassEntity[][];
@ -132,7 +144,10 @@ export const getRecentWithCache = (
entityId,
toFetchStartTime,
endTime,
appendingToCache
appendingToCache,
undefined,
true,
noAttributes
),
]);
fetchedHistory = results[1];

View File

@ -2,7 +2,7 @@ import {
HassEntityAttributeBase,
HassEntityBase,
} from "home-assistant-js-websocket";
import { timeCachePromiseFunc } from "../common/util/time-cache-function-promise";
import { timeCacheEntityPromiseFunc } from "../common/util/time-cache-entity-promise-func";
import { HomeAssistant } from "../types";
import { getSignedPath } from "./auth";
@ -50,7 +50,7 @@ export const fetchThumbnailUrlWithCache = async (
width: number,
height: number
) => {
const base_url = await timeCachePromiseFunc(
const base_url = await timeCacheEntityPromiseFunc(
"_cameraTmbUrl",
9000,
fetchThumbnailUrl,

View File

@ -6,6 +6,7 @@ import { AutomationConfig } from "./automation";
interface CloudStatusNotLoggedIn {
logged_in: false;
cloud: "disconnected" | "connecting" | "connected";
http_use_ssl: boolean;
}
export interface GoogleEntityConfig {
@ -59,6 +60,7 @@ export interface CloudStatusLoggedIn {
remote_connected: boolean;
remote_certificate: undefined | CertificateInformation;
http_use_ssl: boolean;
active_subscription: boolean;
}
export type CloudStatus = CloudStatusNotLoggedIn | CloudStatusLoggedIn;

View File

@ -34,8 +34,24 @@ export const ERROR_STATES: ConfigEntry["state"][] = [
"setup_retry",
];
export const getConfigEntries = (hass: HomeAssistant) =>
hass.callApi<ConfigEntry[]>("GET", "config/config_entries/entry");
export const getConfigEntries = (
hass: HomeAssistant,
filters?: { type?: "helper" | "integration"; domain?: string }
): Promise<ConfigEntry[]> => {
const params = new URLSearchParams();
if (filters) {
if (filters.type) {
params.append("type", filters.type);
}
if (filters.domain) {
params.append("domain", filters.domain);
}
}
return hass.callApi<ConfigEntry[]>(
"GET",
`config/config_entries/entry?${params.toString()}`
);
};
export const updateConfigEntry = (
hass: HomeAssistant,

View File

@ -65,8 +65,14 @@ export const ignoreConfigFlow = (
export const deleteConfigFlow = (hass: HomeAssistant, flowId: string) =>
hass.callApi("DELETE", `config/config_entries/flow/${flowId}`);
export const getConfigFlowHandlers = (hass: HomeAssistant) =>
hass.callApi<string[]>("GET", "config/config_entries/flow_handlers");
export const getConfigFlowHandlers = (
hass: HomeAssistant,
type?: "helper" | "integration"
) =>
hass.callApi<string[]>(
"GET",
`config/config_entries/flow_handlers${type ? `?type=${type}` : ""}`
);
export const fetchConfigFlowInProgress = (
conn: Connection

View File

@ -28,7 +28,7 @@ export interface DataEntryFlowStepForm {
step_id: string;
data_schema: HaFormSchema[];
errors: Record<string, string>;
description_placeholders: Record<string, string>;
description_placeholders?: Record<string, string>;
last_step: boolean | null;
}
@ -49,7 +49,7 @@ export interface DataEntryFlowStepCreateEntry {
title: string;
result?: ConfigEntry;
description: string;
description_placeholders: Record<string, string>;
description_placeholders?: Record<string, string>;
}
export interface DataEntryFlowStepAbort {
@ -57,7 +57,7 @@ export interface DataEntryFlowStepAbort {
flow_id: string;
handler: string;
reason: string;
description_placeholders: Record<string, string>;
description_placeholders?: Record<string, string>;
}
export interface DataEntryFlowStepProgress {
@ -66,7 +66,17 @@ export interface DataEntryFlowStepProgress {
handler: string;
step_id: string;
progress_action: string;
description_placeholders: Record<string, string>;
description_placeholders?: Record<string, string>;
}
export interface DataEntryFlowStepMenu {
type: "menu";
flow_id: string;
handler: string;
step_id: string;
/** If array, use value to lookup translations in strings.json */
menu_options: string[] | Record<string, string>;
description_placeholders?: Record<string, string>;
}
export type DataEntryFlowStep =
@ -74,7 +84,8 @@ export type DataEntryFlowStep =
| DataEntryFlowStepExternal
| DataEntryFlowStepCreateEntry
| DataEntryFlowStepAbort
| DataEntryFlowStepProgress;
| DataEntryFlowStepProgress
| DataEntryFlowStepMenu;
export const subscribeDataEntryFlowProgressed = (
conn: Connection,

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