Compare commits

..

187 Commits

Author SHA1 Message Date
Bram Kragten
ff7309f5c4 Merge pull request #4218 from home-assistant/dev
20191114.0
2019-11-14 13:47:21 +01:00
Bram Kragten
1c614c855f Bumped version to 20191114.0 2019-11-14 13:28:28 +01:00
Bram Kragten
6a3238951d Updated translations 2019-11-14 13:28:06 +01:00
Bram Kragten
0dab5828fb Add Thingtalk automation generation (#4216)
* thingtalk

* works

* Add device_class support and get placeholders from api

* Update
2019-11-14 13:22:44 +01:00
Joakim Sørensen
d0b9c09f8f Fix person dialog issues (#4210)
* Fix person dialog issues

* Remove repo (should not have been committed...)

* Trigger Travis
2019-11-12 20:30:11 +01:00
Josh McCarty
55f4629256 Wrap the dialog title when using Entity Registry (#4174)
* Wrap the dialog title when using Entity Registry

* Move updated styles to styles()
2019-11-12 14:52:56 +01:00
Joakim Sørensen
004565217e Restructure/reorder configuration dashboard (#4202)
* Reoreder config dashboard

* Changes base translation for config dashboard items to be pleural

* Convert to lit/ts

* remove unneded stuff

* fix child property passes

* Export page configuration

* Apply suggestion

Co-Authored-By: Bram Kragten <mail@bramkragten.nl>

* Change property to CloudStatus
2019-11-10 23:25:13 +01:00
Bram Kragten
c07b39ebde Fix device and entity pickers clear (#4206)
* Fix device and entity pickers clear

* Prevent combobox to open on clear
2019-11-10 13:25:07 -08:00
Bram Kragten
8b17b6ed1c Add attribution and onboarding to voice (#4190)
* Add attribution and onboarding to voice

* Align with backend changes

* Layout + switch to ws for process

* Don't mutate window

* Move speechRecognition

* Add border

* Update ha-voice-command-dialog.ts
2019-11-10 11:30:41 -08:00
Paulus Schoutsen
1d16bdbe54 Merge pull request #4201 from home-assistant/lokalise
Update lokalise cli to version 2 + fix replacing localise vars
2019-11-10 11:30:08 -08:00
Joakim Sørensen
9e2a0c77d5 Adds clear value handler (#4205)
* Adds clear value handler

* Add the same to device picker
2019-11-10 19:23:27 +01:00
Joakim Sørensen
4f41508110 Adds timestamp device_class for sensor (#4204) 2019-11-10 18:41:33 +01:00
Joakim Sørensen
eaedb2e5ae Checked button colors (#4203)
* split switch-checked-color into 2 vars like the unchecked variant

* Apply new css vars

* revert accidental delete

* revert demo changes

* revert property change
2019-11-10 18:40:36 +01:00
Bram Kragten
75ad1f51a9 Update translations_upload_base 2019-11-09 23:49:26 +01:00
Bram Kragten
142175c6ab Update lokalise cli to version 2 + fix replacing localise vars 2019-11-09 23:47:58 +01:00
Joakim Sørensen
f1980d6bcf Fixes history graph card defaults (#4198) 2019-11-09 17:30:08 +01:00
Joakim Sørensen
5a7b5200fe Change correct localization string (#4197)
* Change correct localization string

* Update en.json
2019-11-09 17:29:14 +01:00
Joakim Sørensen
d284d53b93 Reload buttons (#4194)
* Change base translation for ui.panel.config.server_control.section.reloading.core

* Give each btn a new card-action div
2019-11-08 14:39:21 -08:00
Bram Kragten
bc01df42d8 Merge pull request #4193 from home-assistant/dev
20191108.0
2019-11-08 17:41:58 +01:00
Bram Kragten
901752bec3 Update translations (#4192) 2019-11-08 17:12:48 +01:00
Bram Kragten
e3ef3cfae1 Bumped version to 20191108.0 2019-11-08 17:12:28 +01:00
Paulus Schoutsen
ab476d2f1b Add conversation ID to voice dialog (#4189) 2019-11-07 12:21:37 -08:00
Bram Kragten
5ca82fd39c Fix mobile support for voice dialog (#4154)
* Fix mobile support for voice dialog

* Update ha-voice-command-dialog.ts

* typo

* Add extra data functions

* Start listening for choice

* Remove extra data logic
2019-11-04 12:34:59 -08:00
Bram Kragten
da35c263d2 Add scene editor (#4164)
* Add scene editor

* Update ha-config-scene.ts

* Update en.json

* Update ha-scene-editor.ts

* Partial comments

* Types

* 1 more

* Comments

* Lint

* Update ha-device-picker.ts

* Update ha-device-card.ts
2019-11-04 12:07:09 -08:00
Bram Kragten
2a617a9639 Revert "Add Netatmo specific states" (#4171) 2019-11-01 20:35:35 +01:00
springstan
c730aab28f Translated config flow form and steps (#4140)
* Translated config flow form and steps

* Moved translation from config_entry to config_flow

* Renamed translation key from not_all_fields_required to not_all_required_fields
2019-11-01 20:34:10 +01:00
Ian Richardson
274c2016c0 action-handler (#4115)
* action-handler

* comments

* address comments

* finish conversion

* move haptics

* address comments

* lint

* keyup

* double enter

* address comments

* keyup
2019-11-01 13:15:11 -05:00
Bram Kragten
9b3891f778 Fix state-label-badge override name (#4166) 2019-11-01 12:54:14 -05:00
Bram Kragten
b705de956e Fix hassio build (#4167) 2019-11-01 17:42:36 +01:00
Ian Richardson
e37201f84f enter is registering as double_tap (#4161)
* enter is registering as double_tap

* Update long-press-directive.ts

* Update long-press-directive.ts
2019-11-01 16:39:53 +01:00
springstan
f53eea81c4 Translated a bunch of strings (#4137)
* Translated customization page
 - added translation key-value pairs to en.json
 - translated form-customize, customize-icon and its key-value

* Translated mutliple pages:
 - devices
 - entity-registry
 - config-entry
 - dashboard -> hint to advanced mode toggle

* Translated custom panel confirm message

* Added translation for no entries in logbook

* Updated translation keys, tested and removed ha-types file translations

* Removed setting an if to true for testing

* Use template literal instead of string concatenation
2019-11-01 16:12:49 +01:00
springstan
0fa8db1682 Translated unused entities, device registry page and domain toggler dialog (#4141)
* Translated device registry and domain toggler dialog

* Translated unused entities page

* Extracted unused_entities keys and created new section for domain_toggler

* Moved unknow_error and area_picker_label into devices section
2019-11-01 16:07:18 +01:00
Bram Kragten
46f5224e70 Migrate voice command dialog (#4150)
* Migrate voice command dialog

* Cleanup

* Correct types

* Added animation when listening and we should talk back right? :'-)

* Set recognition to english

* Comments

* Update on change of hass
2019-10-29 14:59:35 -07:00
Bram Kragten
12be2a9775 Merge branch 'master' into dev 2019-10-29 15:14:04 +01:00
springstan
6196bbdc5e Translated hui-editor and hui-root pages (#4142) 2019-10-29 13:15:47 +01:00
Marianne Hval
b41f4777d4 More descriptive delete/move buttons + red delete (#4145) 2019-10-29 13:13:30 +01:00
springstan
f2812bc706 Translated automation and script editor (#4146) 2019-10-29 13:12:35 +01:00
Bram Kragten
04500bc237 Merge pull request #4148 from springstan/addTitleAttributeConfigUIMode
Added title attributes and translated them in Configure UI Mode
2019-10-29 13:07:43 +01:00
Bram Kragten
2a6b877cf1 Bumped version to 20191025.1 2019-10-29 12:58:20 +01:00
Bram Kragten
c3896a4613 Only do haptic on entity interactions 2019-10-29 12:56:52 +01:00
Bram Kragten
c6fb896fe4 Merge pull request #4152 from home-assistant/haptics
Only do haptic on entity interactions
2019-10-29 12:53:44 +01:00
Bram Kragten
669fbb7e77 Only do haptic on entity interactions 2019-10-29 12:37:25 +01:00
springstan
971865e4f9 Added title attributes and translated them in Configure UI Mode 2019-10-28 20:13:24 +01:00
Paulus Schoutsen
9078e41855 Clarify tslint disable 2019-10-28 10:45:58 -07:00
Bram Kragten
466c48a7d0 Merge pull request #4128 from Twanislas/netatmo-states-translation
Add Netatmo specific states
2019-10-28 15:51:42 +01:00
Bram Kragten
31a047ce9e Merge pull request #4136 from home-assistant/dev
20191025.0
2019-10-25 17:44:34 +02:00
Bram Kragten
bd24ffa5d0 Bumped version to 20191025.0 2019-10-25 17:12:30 +02:00
Bram Kragten
99f4bd7398 Updated translations 2019-10-25 17:12:22 +02:00
Bram Kragten
417177b097 Merge pull request #4135 from home-assistant/entity-reg-1-col
Change entity reg table to full width
2019-10-25 17:08:43 +02:00
Bram Kragten
c407cab501 Change entity reg table to full width 2019-10-25 16:54:42 +02:00
Bram Kragten
044cf22f47 Fix variable column widths (#4127) 2019-10-24 09:43:28 -07:00
Bram Kragten
75aa940d44 Merge pull request #4129 from home-assistant/delete-view-btn
Fix styling delete view btn
2019-10-24 17:32:04 +02:00
Bram Kragten
7be8080726 Fix styling delete view btn 2019-10-24 12:32:59 +02:00
Antoine Rahier
13fbc813cd Add Netatmo thermostat states 2019-10-24 11:20:06 +02:00
Antoine Rahier
44d1458229 Revert "Add Netatmo specific states"
Wrong file !

This reverts commit f06f3ee2e5.
2019-10-24 11:16:41 +02:00
Antoine Rahier
f06f3ee2e5 Add Netatmo specific states 2019-10-24 11:09:42 +02:00
Bram Kragten
a889a02e15 Merge pull request #4123 from iantrich/aria-ts-translations
translated most aria-labels in ts files
2019-10-24 10:34:46 +02:00
Ian Richardson
6bf3d6a689 lint 2019-10-23 23:20:54 -05:00
Ian Richardson
1d7dcca495 translated most aria-labels in ts files 2019-10-23 23:02:58 -05:00
Bram Kragten
ad8f049570 Merge pull request #4117 from home-assistant/dev
20191023.0
2019-10-23 21:36:21 +02:00
Bram Kragten
73c56a68b6 Bumped version to 20191023.0 2019-10-23 21:16:25 +02:00
Bram Kragten
b34b52f305 Update translations 2019-10-23 21:16:03 +02:00
Ian Richardson
39d052273d support actions on rows (#4023)
* support actions on rows

* address comments

* add type

* pointer events

* move action area to row name to avoid handler competition

* add action to state-badge as well

* correct type

* address comments

* handle 'enter' in long-press and make entities state-badge selecatable
2019-10-23 10:30:09 -07:00
Bram Kragten
e435b9153b Clean up device card (#4108) 2019-10-22 16:11:56 -07:00
Bram Kragten
0792278927 Fix more integrations message (#4106)
* Fix more integrations message

* Period outside link
2019-10-22 16:10:42 -07:00
Josh McCarty
06d59b3cde Adds a white background to the QR code for MFA (#4107) 2019-10-23 00:04:09 +02:00
Josh McCarty
1e7497ad33 Revise hassio update card design (#4041)
* Revise hassio update card design

See https://github.com/home-assistant/home-assistant-polymer/issues/3916

* Remove available text; use secondary text color instead of yellow/orange

* Less bold text for update header
2019-10-22 23:06:18 +02:00
Ian Richardson
49d0f2359b add sensor card to first demo (#4035)
* add sensor card to first demo

* Change to temperature sensor
2019-10-22 14:43:34 -05:00
Timmo
bb73039205 Confirmation Dialog (#4053)
*  Add confirmation dialog

*  Add confirmation dialog to service calls

* 🔨 Change returned value

*  Add confirmation dialog to integration remove

*  Add to entity registry

*  Add to delete card and add translation

*  Add to views deletion

* 🔨 Remove async

* 🔨 Fix min-width for smaller screens

* 🔨 Remove async

* Fix wrong merge

* Update ha-config-entry-page.ts
2019-10-22 21:20:41 +02:00
Bram Kragten
d4d6b7e2ce Fix delete button styling in hui-edit-view (#4102) 2019-10-22 21:17:55 +02:00
Drake Loud
7b5201599d added title to view configuation (#4037)
* added title to view configuation

* replacing let with const

* added localization change

* updated view config title

* linter updates

* Prettier updates
2019-10-22 10:27:36 +02:00
springstan
11c08e9a69 Added view title to heading of Lovelace Add Card picker (#4083)
* Added view title to heading of Lovelace Add Card picker

* Fixed building error, cleaned up code

* Changed _view to _viewConfig and added check for undefined _cardConfig

* Added else if for undefined _cardConfig, added else for manual cards

* Used template literal instead of string concatenation
2019-10-22 10:24:25 +02:00
aquarium
731bb176f7 Fix link to translation documentation (#4098) 2019-10-22 08:43:14 +02:00
Phi Dong
b0fce93de8 Fix issue where long states do not wrap (#4101) 2019-10-22 08:39:37 +02:00
Paulus Schoutsen
fdbe89e87e Fix develop build 2019-10-21 16:51:54 -07:00
Ian Richardson
a8d0a2293f revert lovelace selectable text (#4095) 2019-10-21 16:45:15 -07:00
Paulus Schoutsen
8ac278bc59 Remove unused deps 2019-10-21 16:33:03 -07:00
Paulus Schoutsen
70d6c6b902 Refactor Webpack build scripts (#4093)
* Refactor Webpack build scripts

* Add Gallery too

* Fix icons

* Update travis
2019-10-21 15:02:54 -07:00
Drake Loud
0621218e16 Changed button to text (#4067)
* Changed button to text

* Moved styles to hui-edit-view

* prettier updates
2019-10-21 23:32:15 +02:00
Bram Kragten
2424376fba Change ha-device-picker to combo box + improve name handling + show area (#4089)
* Change ha-device-picker to combo box + improve name handling + show area

* unused import
2019-10-21 12:40:16 -07:00
Bram Kragten
3973374f3f Add positive_time_period_dict to ha-form (#4090)
* Add positive_time_period_dict to ha-form

* select input on focus

* Update ha-form-positive_time_period_dict.ts
2019-10-21 12:36:26 -07:00
Ian Richardson
c25a38b82f add actions to state-badge element and state-label-badge (default Lovelace badge type) (#4028)
* add actions to state-badge element

* address comments

state-label-badge no longer handles clicks
added actions to hui-state-label-badge
moved ha-badges-card to Lit
2019-10-21 14:03:01 -05:00
Ian Richardson
3c0ba1d7eb Convert more-info-sun to Lit (#4075)
* Convert more-info-sun to Lit

* address comments
2019-10-21 13:04:22 -05:00
Ian Richardson
be678b02c5 Convert more-info-weather to LitElement (#4073)
* Convert more-info-weather to LitElement

* address comments

* add shouldUpdate
2019-10-21 13:04:08 -05:00
Ian Richardson
0078b48e3c card level themes (#4057)
* card level themes

weather-forecast
shopping-list
plant-status
markdown
alarm-panel

* fix markdown

* address comments

also added picture cards

* update updated

* address comments

* address comments
2019-10-21 12:38:06 -05:00
springstan
540f1d9bce Fix for Zone icon visibility on Map panel and Lovelace Map card (#4085)
* Set icon color to black in ha-panel-map.js

* Changed icon color depending on dark mode in lovelace map card

* Fixed build error by swapping var for let and const

* Replaced hardcoded style with the light and dark classes
2019-10-21 17:45:40 +02:00
Bram Kragten
5e3cb812ec Fix yaml name collision in card editor (#4079)
* Fix `yaml` name collision

* Add types for js-yaml change to named imports
2019-10-21 17:36:09 +02:00
aquarium
6d10a5dd4c Split data table background color into separate theme variable (#4031)
* Split data table background color into separate theme variable

* Remove reference to MDC in data table theme variable name
2019-10-21 09:27:06 +02:00
Sean Mooney
96d14b7ab7 Changed outdated "GUI editor" reference to "visual editor" (#4068)
Changed warning text from "GUI editor" to "visual editor" to match the updated naming being used in card editors.
2019-10-20 15:10:07 +02:00
Phi Dong
b96b026905 Add hovers (title attrs) to buttons on integrations config entry (#4059)
* Add integration specific titles to config buttons

* Rename tranlation var to integration, move button labels inline
2019-10-20 15:02:11 +02:00
Paulus Schoutsen
c25f2d3941 Move compression from Webpack to Gulp (#4074) 2019-10-20 14:42:20 +02:00
Paulus Schoutsen
785453aa79 Clean up mixins (#4076)
* Clean up mixins

* Lint
2019-10-20 14:31:58 +02:00
Phi Dong
4dbf5327bd Update snapshot actions to be a list (#4045)
* Update snapshot actions to be a list

* Update text of delete button to warning color
2019-10-19 18:03:22 +02:00
springstan
603240c467 Translated developer tools info page (#4054)
* Translated developer tools info page

* Added set or remove as a variable in translation of key default_ui
2019-10-19 17:59:57 +02:00
Paulus Schoutsen
bbc3e7d93f Make updateHass not async (#4060) 2019-10-19 17:18:56 +02:00
Ian Richardson
fbee4937a0 add icon option to entities card name (#4024)
* add icon option to entities card name

* account for only icon

* address comments
2019-10-19 14:25:14 +02:00
Phi Dong
0a77728652 Move Home Assistant and version on info page into h2 (#4061) 2019-10-19 14:24:28 +02:00
Phi Dong
e3ed0cf436 Hide save on card configuration if no card is picked (#4062) 2019-10-19 14:18:59 +02:00
Bram Kragten
d05dc2e4dc Bring back babel (#3974)
* Bring back babel

* bump preset env

* Remove empty TS properties in mixins
2019-10-19 14:06:04 +02:00
Paulus Schoutsen
c437cd3865 Use only decorators (#4058)
* Use only decorators

* Remove unused imports
2019-10-18 22:50:27 -07:00
springstan
442171169b Translated developer tools template page (#4050) 2019-10-18 23:05:07 +02:00
springstan
cc12dbb6ee Translated developer tools services page (#4049)
* Translated developer tools services page

* Changed translation parameter from error to data
2019-10-18 23:04:43 +02:00
springstan
60b3a960ae Translated developer tools mqtt page (#4052) 2019-10-18 23:04:26 +02:00
shbatm
5a957c3c9e Add width property to light card to fix #3964 (#3972)
* Add width property to light card to fix #3964

* Update Light Card Name CSS

* Update css per review
2019-10-18 22:58:25 +02:00
Ian Richardson
be4d431dc3 more-info dialog for counter (#4038)
* more-info dialog for counter

* address comments
2019-10-18 14:55:53 -05:00
Ian Richardson
0005c75091 fix double tap on glance entity (#4051) 2019-10-18 21:08:12 +02:00
springstan
880b382a16 Translated developer tools logs page (#4046)
* Translated developer tools logs page

* Changed translation key from load_log to load_full_log
2019-10-18 17:43:04 +02:00
Ian Richardson
d012512a79 add feature request template (#4026) 2019-10-18 17:01:41 +02:00
springstan
e2ac842690 Translated developer tools state page (#4039)
* Translated developer tools state page

* Fixed merge error by updating used icon in developer-tools-state.js
2019-10-18 16:33:11 +02:00
Ian Richardson
67d8d48855 translate view editor and explain panel mode (#4043) 2019-10-18 16:28:31 +02:00
Bram Kragten
00f2d36cb5 Migrate ha-form to lit (#4000)
* Migrate ha-form to lit

* Fix import path

* Update

* add default, change suffix, fix import

* Fix select
2019-10-18 16:08:34 +02:00
springstan
035057b185 Made thermostat history graph localizable (#4004)
* Made thermostat history graph localizable

* Changed string concatenation with white spaces from .join() to template literal

* Used variables in translations instead of template literals

* Update src/translations/en.json

Removed unnecessary space in heating value

Co-Authored-By: Bram Kragten <mail@bramkragten.nl>
2019-10-17 23:28:12 +02:00
springstan
982966c8d9 Translated developer tools events page (#4033)
* Translated developer tools events page

* Implemented translations (alert on empty event type, notification on firing an event), Removed unnecessary translation key-value pairs

* Added falsely removed key-value pairs
2019-10-17 23:17:39 +02:00
Ian Richardson
f5e3a9ad40 Convert thermostat to round-slider (#3734)
* Convert to round-slider

Closes https://github.com/home-assistant/home-assistant-polymer/issues/3622
Closes https://github.com/home-assistant/home-assistant-polymer/issues/2756

* scaling

* address review comments

* css tweaks

* remove jquery

* address comments

* simplify set-temperature

* handle long name

* remove increased handleSize

* address comments

* address comments

* address comments

* address comment

* need coffee
2019-10-17 21:00:39 +02:00
Ian Richardson
141c3f1ea4 add confirmation option to actions (#4006)
* add confirmation option to actions

* address comments
2019-10-16 11:03:36 -05:00
Ian Richardson
4ea483e3de Switch theme (#4017)
* introduce ha-switch theming

* fix typo

* remove comment
2019-10-16 17:08:28 +02:00
Josh McCarty
8eca956cd1 Uses information-outline icon for entity more info link (#4016) 2019-10-16 16:53:25 +02:00
Ian Richardson
c9242a5075 Custom badges (#3867)
* custom badges

* incremental

* functional

* cleanup

* cleanup

* address review comments

* address more comments

* address review comments

* address review comments

* cleanup

* address review comments

* address comments

* address comments

* fix entity-filter

* set hass once

* hass
2019-10-16 16:09:13 +02:00
Ian Richardson
df29a5becb Add double tap action (#3879)
* add dbltap_action

* apply to picture-glance

* types and boolean

* fix typo

* simplify double tap logic

* extract hasDoubleClick functionality

* address comments

* address comments

* double_tap_action
2019-10-16 08:57:05 -05:00
Ian Richardson
fb589337f8 display card errors with yaml config (#4018) 2019-10-16 09:22:57 +02:00
Ian Richardson
ea5ee6189d filter system users from Person editor (#3958)
* filter system users from Person editor

* address review comments

* address comments

* address comments
2019-10-15 19:19:46 -05:00
Josh McCarty
a39e47cced Updates alarm more-info dialog button styles to match lovelace card (#3955)
* Updates alarm more-info dialog button styles to match lovelace card

* Use flex for controlling button size

* Limit max-width of alarm buttons

This matches the width of the code input field
2019-10-15 21:04:29 +02:00
Ian Richardson
49d69f65ad add show_state option for picture-glance entities (#3937)
* add states option below picture-glance entities

* address review comments
2019-10-15 09:49:03 +02:00
Marcus Fredlund
424d677bcb Alarm Panel card: number buttons outline and adjusted padding and font-size fixing #2812 (#3991)
* outline on alarm panel buttons

* Fix for font-size before mwc-button is working.

* suggested edits from review

* changed to just outline

* Remove base-unit
2019-10-14 21:29:37 +02:00
springstan
59e4cdc62a Added integration name to System Options dialog (#3996)
* Added integration name to System Options dialog

* Passed integration name into translation string

* Translated integration name

* Added option for no translation
2019-10-14 21:28:44 +02:00
Bram Kragten
9d3dfad98c Update integration-badge.ts (#4001) 2019-10-14 21:28:13 +02:00
Erik Montnemery
555b746f4b Allow device actions to specify extra fields (#4002)
* Allow device actions to specify extra fields

* Typing etc.

* Use deviceAutomationsEqual to compare automations
2019-10-14 21:27:51 +02:00
Bram Kragten
ce6a97d065 Merge pull request #3995 from scop/customize-yaml-message
Improve customize.yaml non-inclusion warning message
2019-10-14 21:27:07 +02:00
Ville Skyttä
88567df36d Prettier 2019-10-14 22:03:28 +03:00
Bram Kragten
f55cbd9e9a Merge pull request #3999 from home-assistant/dev
20191014.0
2019-10-14 11:01:07 +02:00
Bram Kragten
7d00cc1eff Merge branch 'master' into dev 2019-10-14 10:28:17 +02:00
Bram Kragten
29301ddee7 Bumped version to 20191014.0 2019-10-14 10:22:13 +02:00
Bram Kragten
978b773968 Update translations 2019-10-14 10:21:56 +02:00
springstan
4f30cae6aa Translated Advanced mode toggle (#3997) 2019-10-13 22:39:08 +02:00
Ville Skyttä
5f29b66a8d Further tweak the message 2019-10-13 21:15:06 +03:00
springstan
b94da1bd19 Added note about more integrations to bottom of Set up new integration dialog (#3992)
* Added note about more integrations to bottom of Set up new integration dialog

* Fixed note about more integration by encapsulating it in one paragraph
2019-10-13 15:57:24 +02:00
Franck Nijhof
f9b0a0fc13 Fixes documentation URL for HTML5 push notifications (#3993) 2019-10-13 13:34:34 +02:00
Sven
300ffdae04 Add no devices available hint to person editor (#3906)
* Add no devices available hint to person editor (#3731)

* Fix device_tracker check and adjust messages

* fix lint

* Add location, fix memoize usage, hide entity picker and update text

* Move links to a list
2019-10-13 13:22:37 +02:00
Tom Raithel
476525e0d4 Remove 'Pick script to edit' headline from script editor (#3873)
Fixes #3829
2019-10-13 13:18:43 +02:00
Ville Skyttä
edecf9d58f Improve customize.yaml non-inclusion warning message
Link to relevant docs, note that changes won't be applied after a
restart if the include isn't in place (changes _do_ actually affect the
current runtime configuration and are persisted in customize.yaml).
2019-10-13 14:07:02 +03:00
Sean Mooney
38bf2e116b Update ISSUE_TEMPLATE.md (#3987)
Now that ui-schema repo is merged here and there's a feature request label, I think this line is no longer necessary.

 `- This is for bugs only. Feature and enhancement requests should go in our community forum: https://community.home-assistant.io/c/feature-requests`
2019-10-12 23:14:51 -07:00
Paulus Schoutsen
0719c4d1ae Add cache headers to demo and cast (#3982)
* Add cache headers to demo and cast

* Demo only to copy static from frontend public folder

* Update headers

* Update demo headers

* Update cast headers

* Update _headers

* Update _headers
2019-10-12 23:12:01 -07:00
Bram Kragten
12840231be Add code mirror editors to the dev-tools data fields (#3981)
* Add yaml code mirror editor to the dev-tools yaml fields

* Add jinja2 editor on dev template

* Migrate to UpdatingElement, review comments

* update cm, add types

* types

* dev tools mqtt
2019-10-12 12:33:51 -07:00
Bram Kragten
4728c12225 Update google cloud card (#3978)
* Update google cloud card

* Oops
2019-10-11 08:48:56 -07:00
Bram Kragten
90526ac563 Migrate entity registry to data-table (#3965)
* Migrate entity registry to data-table

* icons

* Styling

* Review comments

* fix not selector

* typos + move columns out of class

* Localize + comments

* Fucked up the rebase
2019-10-11 14:55:45 +02:00
Hayley McIldoon
6f7ea03e35 Remove redundant text in Cloud panel #3824 (#3967) 2019-10-11 14:55:12 +02:00
Erik Montnemery
78900e05ad Allow device conditions to specify extra fields (#3973) 2019-10-11 13:16:54 +02:00
Ian Richardson
495f4aa19c ability to hide tabs (#3811)
* ability to hide tabs

* address review comments

* address review comments

* address review comments

* review comments
2019-10-11 13:14:34 +02:00
Bram Kragten
88c480759f Convert YAMLTextArea to code mirror editor (#3980)
* Convert YAMLTextArea to code mirror editor

* Review comments

* Clean up
2019-10-10 21:58:21 +02:00
Josh McCarty
ab75365636 Move alexa state reporting switch position for consistency (#3953) 2019-10-10 10:51:58 +02:00
Tom Raithel
0266617c71 Remove overflow attribute from view column (#3971)
Fixes #3253
2019-10-10 10:50:46 +02:00
Mauricio Bonani
aef45c5043 Upgrade MDI icons to 4.5.95 (#3977)
* Upgrade MDI icons to 4.5.95

* Upgrade MDI icons to 4.5.95
2019-10-10 09:44:10 +02:00
Hayley McIldoon
deeb0146c7 Renamed Toggle Editor button #3617 (#3968) 2019-10-09 23:03:46 +02:00
Bram Kragten
f1fabd09a6 Merge pull request #3970 from emontnemery/automation_device_action_form
Allow device triggers to specify extra fields
2019-10-09 21:48:57 +02:00
Bram Kragten
0d77bdaf32 Merge pull request #3949 from iantrich/fix-glance-state_image
picture-glance: check for entity state change
2019-10-09 21:48:05 +02:00
Bram Kragten
320be2e5d9 Convert integration entry page to data table (#3963)
* Convert integration entry page to data table

* Simplify device-card

In a future PR this has to be changed further

* Center no devices text

* Review comments
2019-10-09 08:48:41 -07:00
Bram Kragten
6a098ad0b5 Save orig value + some cleanup 2019-10-09 13:34:34 +02:00
Erik
e895e91a11 Allow device triggers to specify extra fields 2019-10-09 07:41:25 +02:00
Paulus Schoutsen
fc3f7ca4b2 Mark Persian as right to left. 2019-10-08 10:24:21 -07:00
Bram Kragten
9ad7f0dbac Hide empty blocks on device page (#3950)
* Hide empty blocks on device page

* lint

* Rename entities on device rename

* check if entity_id is valid

* clarify var name

* Review comments

* Use regex to replace not allowed chars

* Align with backend
2019-10-08 08:53:31 -07:00
Bram Kragten
12d8a04c15 Update translations 2019-10-08 16:13:06 +02:00
Bram Kragten
0f7a3887a7 Fix updating entity id in entity-registry-dialog (#3962) 2019-10-08 12:27:29 +02:00
Bram Kragten
ef51f29e28 Add report state toggle for Google (#3855) 2019-10-08 10:25:57 +02:00
Ian Richardson
b61bbee35a fix media row secondary info (#3957) 2019-10-08 10:10:37 +02:00
Markus Nigbur
64dd8c463d Changed the states-ui toggle button to unobstrusive text link. (#3959) 2019-10-08 10:01:51 +02:00
akargl
d2a95e9f06 Add default text to Markdown card editor (#3960) 2019-10-08 09:58:37 +02:00
Timmo
0cb0525516 Add localized names to card editor (#3941)
* 🔨 Add localized names to card editor

* ✏️ Rename alarm_panel to alarm-panel

* ✏️ Proper case

* 🔨 Move to else

* 🔨 Remove name and object from card picker
2019-10-07 20:17:12 +02:00
Ian Richardson
dcaf4fdfe2 add title option to stack cards (#3839) 2019-10-07 10:17:52 +02:00
ottersen
0c13757910 Correct usage of Zigbee vs ZigBee #1 (#3938)
ref clarification by Zigbee Alliance of correct usage of "Zigbee" vs "ZigBee"

https://twitter.com/Frenck/status/1178239254713061377
2019-10-07 09:55:31 +02:00
ottersen
0cdcd74c9d Correct usage of Zigbee vs ZigBee #2 (#3939)
ref clarification by Zigbee Alliance of correct usage of "Zigbee" vs "ZigBee"

https://twitter.com/Frenck/status/1178239254713061377
2019-10-07 09:55:18 +02:00
Phi Dong
db3968399f Add intro to OZW log z-wave config section (#3935) 2019-10-07 09:53:37 +02:00
Malte Franken
7494a49238 Normalize longitude to the range between -180 to +180 degrees (#3872)
* normalize longitude to the range between -180 to +180 degrees

* only normalize longitude if out of valid range
2019-10-07 09:49:00 +02:00
Ian Richardson
55d2a3c8b1 simplify 2019-10-06 23:16:14 -05:00
Ian Richardson
be4e45c22c picture-glance: check for entity state change 2019-10-06 23:08:14 -05:00
Matthew Donoughe
efb28d337a editor support for scene script action (#3942)
Script support added in home-assistant/home-assistant#27223
2019-10-05 15:14:37 -07:00
Phi Dong
edd77e1f32 Exclude persistent_notifications domain when computing unused entities (#3936) 2019-10-05 14:01:00 +02:00
Bram Kragten
848dd7e071 Fixes to script localization (#3930)
* Fixes

* fix toast
2019-10-04 21:46:32 +02:00
Jens Vanhooydonck
ba79633758 HA Cloud overflow wrap (#3905)
* Overflow of Remote Control URL

* Overflow wrap of certificate information

Closes #2982

* Changed styling to class break-word

* Changed break-work to class

* Update src/panels/config/cloud/account/cloud-remote-pref.ts

Co-Authored-By: Bram Kragten <mail@bramkragten.nl>
2019-10-04 16:46:01 +02:00
Charles Garwood
860973bdbd Add localization to ZHA panel (#3881)
* Add localization for ZHA config panel

* Additional ZHA panel localization
2019-10-04 14:07:49 +02:00
Sven
d4d897e79e Make cloud account panel translatable (#3875)
* Add localization to cloud login & register (#3844)

* Add localization to cloud account (#3844)

* Add localization to cloud forgot password (#3844)

* fix lint errors

* Update src/panels/config/cloud/account/cloud-account.js

Co-Authored-By: Bram Kragten <mail@bramkragten.nl>

* Update src/panels/config/cloud/register/cloud-register.js

Co-Authored-By: Bram Kragten <mail@bramkragten.nl>

* fix issues pointed out in review

* fix code style issue

* Add localization for dialogs, alexa and google

* fix exposed entities name
2019-10-04 13:14:53 +02:00
Bram Kragten
4850f3d588 Fix whitespace error in services (#3880) 2019-10-04 12:35:49 +02:00
Bram Kragten
8bc53c235f Fix styling system options (#3877) 2019-10-04 12:04:27 +02:00
Charles Garwood
c74793b1d5 Add link to Z-Wave panel docs (#3874) 2019-10-03 20:51:37 +02:00
Marcel Brückner
56bac8a8c1 Make script editor translatable (#3866)
* Make script editor's script picker translatable (home-assistant/home-assistant-polymer#3848)

* Make script editor translatable (home-assistant/home-assistant-polymer#3848)

* Fix linting errors (home-assistant/home-assistant-polymer#3866)

* Fix linting errors (home-assistant/home-assistant-polymer#3866)

* Move unsaved_confirm translation key to common section (home-assistant/home-assistant-polymer#3866)

Instead of adding the same text multiple times for every section, add a common section to indicate reusable translations.

* Add variable to localization text

* Use JavaScript instead of Polymer data binding
2019-10-03 20:31:53 +02:00
Sven
184575fd54 Add localization to persons config page (#3846) (#3871) 2019-10-03 14:38:34 +02:00
Sven
e148559d3e Add localization to user config page (#3845) (#3869) 2019-10-03 12:22:45 +02:00
Phi Dong
95b76dbb85 Fix issue where help icon overlapped with header text on mobile devices (#3868) 2019-10-03 11:50:36 +02:00
378 changed files with 76975 additions and 57978 deletions

View File

@@ -1,11 +1,24 @@
---
name: Bug report
about: Create a report to help us improve
title: ""
labels: bug
assignees: ""
---
<!-- READ THIS FIRST:
- If you need additional help with this template please refer to https://www.home-assistant.io/help/reporting_issues/
- Make sure you are running the latest version of Home Assistant before reporting an issue: https://github.com/home-assistant/home-assistant/releases
- This is for bugs only. Feature and enhancement requests should go in our community forum: https://community.home-assistant.io/c/feature-requests
- Provide as many details as possible. Do not delete any text from this template!
-->
**Checklist:**
- [ ] I updated to the latest version available
- [ ] I cleared the cache of my browser
**Home Assistant release with the issue:**
<!--
- Frontend -> Developer tools -> Info
- Or use this command: hass --version
@@ -14,22 +27,25 @@
**Last working Home Assistant release (if known):**
**UI (States or Lovelace UI?):**
<!--
- Frontend -> Developer tools -> Info
-->
**Browser and Operating System:**
<!--
Provide details about what browser (and version) you are seeing the issue in. And also which operating system this is on. If possible try to replicate the issue in other browsers and include your findings here.
-->
**Description of problem:**
<!--
Explain what the issue is, and how things should look/behave. If possible provide a screenshot with a description.
-->
**Javascript errors shown in the web inspector (if applicable):**
```
```

View File

@@ -0,0 +1,19 @@
---
name: Feature request
about: Suggest an idea for this project
title: ""
labels: feature request
assignees: ""
---
**Is your feature request related to a problem? Please describe.**
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
**Describe the solution you'd like**
A clear and concise description of what you want to happen.
**Describe alternatives you've considered**
A clear and concise description of any alternative solutions or features you've considered.
**Additional context**
Add any other context or screenshots about the feature request here.

View File

@@ -8,19 +8,20 @@ install: yarn install
script:
- npm run build
- hassio/script/build_hassio
# Because else eslint fails because hassio has cleaned that build
- ./node_modules/.bin/gulp gen-icons-app
- npm run test
# - xvfb-run wct --module-resolution=node --npm
# - 'if [ "${TRAVIS_PULL_REQUEST}" = "false" ]; then wct --module-resolution=node --npm --plugin sauce; fi'
services:
- docker
before_deploy:
- 'docker pull lokalise/lokalise-cli@sha256:2198814ebddfda56ee041a4b427521757dd57f75415ea9693696a64c550cef21'
- "docker pull lokalise/lokalise-cli@sha256:2198814ebddfda56ee041a4b427521757dd57f75415ea9693696a64c550cef21"
deploy:
provider: script
script: script/travis_deploy
'on':
"on":
branch: master
dist: trusty
addons:
sauce_connect: true

View File

@@ -3,7 +3,7 @@ module.exports.babelLoaderConfig = ({ latestBuild }) => {
throw Error("latestBuild not defined for babel loader config");
}
return {
test: /\.m?js$/,
test: /\.m?js$|\.tsx?$/,
use: {
loader: "babel-loader",
options: {
@@ -12,6 +12,12 @@ module.exports.babelLoaderConfig = ({ latestBuild }) => {
require("@babel/preset-env").default,
{ modules: false },
],
[
require("@babel/preset-typescript").default,
{
jsxPragma: "h",
},
],
].filter(Boolean),
plugins: [
// Part of ES2018. Converts {...a, b: 2} to Object.assign({}, a, {b: 2})
@@ -21,6 +27,12 @@ module.exports.babelLoaderConfig = ({ latestBuild }) => {
],
// Only support the syntax, Webpack will handle it.
"@babel/syntax-dynamic-import",
[
"@babel/transform-react-jsx",
{
pragma: "h",
},
],
[
require("@babel/plugin-proposal-decorators").default,
{ decoratorsBeforeExport: true },

6
build-scripts/env.js Normal file
View File

@@ -0,0 +1,6 @@
module.exports = {
isProdBuild: process.env.NODE_ENV === "production",
isStatsBuild: process.env.STATS === "1",
isTravis: process.env.TRAVIS === "true",
isNetlify: process.env.NETLIFY === "true",
};

View File

@@ -1,10 +1,13 @@
// Run HA develop mode
const gulp = require("gulp");
const envVars = require("../env");
require("./clean.js");
require("./translations.js");
require("./gen-icons.js");
require("./gather-static.js");
require("./compress.js");
require("./webpack.js");
require("./service-worker.js");
require("./entry-html.js");
@@ -18,7 +21,7 @@ gulp.task(
"clean",
gulp.parallel(
"gen-service-worker-dev",
"gen-icons",
gulp.parallel("gen-icons-app", "gen-icons-mdi"),
"gen-pages-dev",
"gen-index-app-dev",
gulp.series("create-test-translation", "build-translations")
@@ -35,13 +38,11 @@ gulp.task(
process.env.NODE_ENV = "production";
},
"clean",
gulp.parallel("gen-icons", "build-translations"),
gulp.parallel("gen-icons-app", "gen-icons-mdi", "build-translations"),
"copy-static",
gulp.parallel(
"webpack-prod-app",
// Do not compress static files in CI, it's SLOW.
...(process.env.CI === "true" ? [] : ["compress-static"])
),
"webpack-prod-app",
...// Don't compress running tests
(envVars.isTravis ? [] : ["compress-app"]),
gulp.parallel(
"gen-pages-prod",
"gen-index-app-prod",

View File

@@ -1,4 +1,3 @@
// Run cast develop mode
const gulp = require("gulp");
require("./clean.js");
@@ -16,7 +15,12 @@ gulp.task(
process.env.NODE_ENV = "development";
},
"clean-cast",
gulp.parallel("gen-icons", "gen-index-cast-dev", "build-translations"),
gulp.parallel(
"gen-icons-app",
"gen-icons-mdi",
"gen-index-cast-dev",
"build-translations"
),
"copy-static-cast",
"webpack-dev-server-cast"
)
@@ -29,7 +33,7 @@ gulp.task(
process.env.NODE_ENV = "production";
},
"clean-cast",
gulp.parallel("gen-icons", "build-translations"),
gulp.parallel("gen-icons-app", "gen-icons-mdi", "build-translations"),
"copy-static-cast",
"webpack-prod-cast",
"gen-index-cast-prod"

View File

@@ -9,15 +9,31 @@ gulp.task(
return del([config.root, config.build_dir]);
})
);
gulp.task(
"clean-demo",
gulp.parallel("clean-translations", function cleanOutputAndBuildDir() {
return del([config.demo_root, config.build_dir]);
})
);
gulp.task(
"clean-cast",
gulp.parallel("clean-translations", function cleanOutputAndBuildDir() {
return del([config.cast_root, config.build_dir]);
})
);
gulp.task(
"clean-hassio",
gulp.parallel("clean-translations", function cleanOutputAndBuildDir() {
return del([config.hassio_root, config.build_dir]);
})
);
gulp.task(
"clean-gallery",
gulp.parallel("clean-translations", function cleanOutputAndBuildDir() {
return del([config.gallery_root, config.build_dir]);
})
);

View File

@@ -0,0 +1,38 @@
// Tasks to compress
const gulp = require("gulp");
const zopfli = require("gulp-zopfli-green");
const merge = require("merge-stream");
const path = require("path");
const paths = require("../paths");
gulp.task("compress-app", function compressApp() {
const jsLatest = gulp
.src(path.resolve(paths.output, "**/*.js"))
.pipe(zopfli())
.pipe(gulp.dest(paths.output));
const jsEs5 = gulp
.src(path.resolve(paths.output_es5, "**/*.js"))
.pipe(zopfli())
.pipe(gulp.dest(paths.output_es5));
const polyfills = gulp
.src(path.resolve(paths.static, "polyfills/*.js"))
.pipe(zopfli())
.pipe(gulp.dest(path.resolve(paths.static, "polyfills")));
const translations = gulp
.src(path.resolve(paths.static, "translations/*.json"))
.pipe(zopfli())
.pipe(gulp.dest(path.resolve(paths.static, "translations")));
return merge(jsLatest, jsEs5, polyfills, translations);
});
gulp.task("compress-hassio", function compressApp() {
return gulp
.src(path.resolve(paths.hassio_root, "**/*.js"))
.pipe(zopfli())
.pipe(gulp.dest(paths.hassio_root));
});

View File

@@ -17,7 +17,8 @@ gulp.task(
},
"clean-demo",
gulp.parallel(
"gen-icons",
"gen-icons-app",
"gen-icons-mdi",
"gen-icons-demo",
"gen-index-demo-dev",
"build-translations"
@@ -34,7 +35,12 @@ gulp.task(
process.env.NODE_ENV = "production";
},
"clean-demo",
gulp.parallel("gen-icons", "gen-icons-demo", "build-translations"),
gulp.parallel(
"gen-icons-app",
"gen-icons-mdi",
"gen-icons-demo",
"build-translations"
),
"copy-static-demo",
"webpack-prod-demo",
"gen-index-demo-prod"

View File

@@ -14,7 +14,7 @@ function hasHtml(data) {
function recursiveCheckHasHtml(file, data, errors, recKey) {
Object.keys(data).forEach(function(key) {
if (typeof data[key] === "object") {
nextRecKey = recKey ? `${recKey}.${key}` : key;
const nextRecKey = recKey ? `${recKey}.${key}` : key;
recursiveCheckHasHtml(file, data[key], errors, nextRecKey);
} else if (hasHtml(data[key])) {
errors.push(`HTML found in ${file.path} at key ${recKey}.${key}`);
@@ -23,7 +23,7 @@ function recursiveCheckHasHtml(file, data, errors, recKey) {
}
function checkHtml() {
let errors = [];
const errors = [];
return mapStream(function(file, cb) {
const content = file.contents;

View File

@@ -11,12 +11,6 @@ const config = require("../paths.js");
const templatePath = (tpl) =>
path.resolve(config.polymer_dir, "src/html/", `${tpl}.html.template`);
const demoTemplatePath = (tpl) =>
path.resolve(config.demo_dir, "src/html/", `${tpl}.html.template`);
const castTemplatePath = (tpl) =>
path.resolve(config.cast_dir, "src/html/", `${tpl}.html.template`);
const readFile = (pth) => fs.readFileSync(pth).toString();
const renderTemplate = (pth, data = {}, pathFunc = templatePath) => {
@@ -25,10 +19,19 @@ const renderTemplate = (pth, data = {}, pathFunc = templatePath) => {
};
const renderDemoTemplate = (pth, data = {}) =>
renderTemplate(pth, data, demoTemplatePath);
renderTemplate(pth, data, (tpl) =>
path.resolve(config.demo_dir, "src/html/", `${tpl}.html.template`)
);
const renderCastTemplate = (pth, data = {}) =>
renderTemplate(pth, data, castTemplatePath);
renderTemplate(pth, data, (tpl) =>
path.resolve(config.cast_dir, "src/html/", `${tpl}.html.template`)
);
const renderGalleryTemplate = (pth, data = {}) =>
renderTemplate(pth, data, (tpl) =>
path.resolve(config.gallery_dir, "src/html/", `${tpl}.html.template`)
);
const minifyHtml = (content) =>
minify(content, {
@@ -209,8 +212,33 @@ gulp.task("gen-index-demo-prod", (done) => {
es5Compatibility: es5Manifest["compatibility.js"],
es5DemoJS: es5Manifest["main.js"],
});
const minified = minifyHtml(content).replace(/#THEMEC/g, "{{ theme_color }}");
const minified = minifyHtml(content);
fs.outputFileSync(path.resolve(config.demo_root, "index.html"), minified);
done();
});
gulp.task("gen-index-gallery-dev", (done) => {
// In dev mode we don't mangle names, so we hardcode urls. That way we can
// run webpack as last in watch mode, which blocks output.
const content = renderGalleryTemplate("index", {
latestGalleryJS: "./entrypoint.js",
});
fs.outputFileSync(path.resolve(config.gallery_root, "index.html"), content);
done();
});
gulp.task("gen-index-gallery-prod", (done) => {
const latestManifest = require(path.resolve(
config.gallery_output,
"manifest.json"
));
const content = renderGalleryTemplate("index", {
latestGalleryJS: latestManifest["entrypoint.js"],
});
const minified = minifyHtml(content);
fs.outputFileSync(path.resolve(config.gallery_root, "index.html"), minified);
done();
});

View File

@@ -0,0 +1,38 @@
// Run demo develop mode
const gulp = require("gulp");
require("./clean.js");
require("./translations.js");
require("./gen-icons.js");
require("./gather-static.js");
require("./webpack.js");
require("./service-worker.js");
require("./entry-html.js");
gulp.task(
"develop-gallery",
gulp.series(
async function setEnv() {
process.env.NODE_ENV = "development";
},
"clean-gallery",
gulp.parallel("gen-icons-app", "gen-icons-app", "build-translations"),
"copy-static-gallery",
"gen-index-gallery-dev",
"webpack-dev-server-gallery"
)
);
gulp.task(
"build-gallery",
gulp.series(
async function setEnv() {
process.env.NODE_ENV = "production";
},
"clean-gallery",
gulp.parallel("gen-icons-app", "gen-icons-mdi", "build-translations"),
"copy-static-gallery",
"webpack-prod-gallery",
"gen-index-gallery-prod"
)
);

View File

@@ -4,8 +4,6 @@ const gulp = require("gulp");
const path = require("path");
const cpx = require("cpx");
const fs = require("fs-extra");
const zopfli = require("gulp-zopfli-green");
const merge = require("merge-stream");
const paths = require("../paths");
const npmPath = (...parts) =>
@@ -67,20 +65,6 @@ function copyMapPanel(staticDir) {
);
}
function compressStatic(staticDir) {
const staticPath = genStaticPath(staticDir);
const polyfills = gulp
.src(staticPath("polyfills/*.js"))
.pipe(zopfli())
.pipe(gulp.dest(staticPath("polyfills")));
const translations = gulp
.src(staticPath("translations/*.json"))
.pipe(zopfli())
.pipe(gulp.dest(staticPath("translations")));
return merge(polyfills, translations);
}
gulp.task("copy-static", (done) => {
const staticDir = paths.static;
const staticPath = genStaticPath(paths.static);
@@ -100,11 +84,12 @@ gulp.task("copy-static", (done) => {
done();
});
gulp.task("compress-static", () => compressStatic(paths.static));
gulp.task("copy-static-demo", (done) => {
// Copy app static files
fs.copySync(polyPath("public"), paths.demo_root);
fs.copySync(
polyPath("public/static"),
path.resolve(paths.demo_root, "static")
);
// Copy demo static files
fs.copySync(path.resolve(paths.demo_dir, "public"), paths.demo_root);
@@ -126,3 +111,15 @@ gulp.task("copy-static-cast", (done) => {
copyTranslations(paths.cast_static);
done();
});
gulp.task("copy-static-gallery", (done) => {
// Copy app static files
fs.copySync(polyPath("public/static"), paths.gallery_static);
// Copy gallery static files
fs.copySync(path.resolve(paths.gallery_dir, "public"), paths.gallery_root);
copyMapPanel(paths.gallery_static);
copyFonts(paths.gallery_static);
copyTranslations(paths.gallery_static);
done();
});

View File

@@ -57,18 +57,6 @@ function generateIconset(iconsetName, iconNames) {
return `<ha-iconset-svg name="${iconsetName}" size="24"><svg><defs>${iconDefs}</defs></svg></ha-iconset-svg>`;
}
// Generate the full MDI iconset
function genMDIIcons() {
const meta = JSON.parse(
fs.readFileSync(path.resolve(ICON_PACKAGE_PATH, META_PATH), "UTF-8")
);
const iconNames = meta.map((iconInfo) => iconInfo.name);
if (!fs.existsSync(OUTPUT_DIR)) {
fs.mkdirSync(OUTPUT_DIR);
}
fs.writeFileSync(MDI_OUTPUT_PATH, generateIconset("mdi", iconNames));
}
// Helper function to map recursively over files in a folder and it's subfolders
function mapFiles(startPath, filter, mapFunc) {
const files = fs.readdirSync(startPath);
@@ -101,24 +89,27 @@ function findIcons(searchPath, iconsetName) {
return icons;
}
function genHassIcons() {
gulp.task("gen-icons-mdi", (done) => {
const meta = JSON.parse(
fs.readFileSync(path.resolve(ICON_PACKAGE_PATH, META_PATH), "UTF-8")
);
const iconNames = meta.map((iconInfo) => iconInfo.name);
if (!fs.existsSync(OUTPUT_DIR)) {
fs.mkdirSync(OUTPUT_DIR);
}
fs.writeFileSync(MDI_OUTPUT_PATH, generateIconset("mdi", iconNames));
done();
});
gulp.task("gen-icons-app", (done) => {
const iconNames = findIcons("./src", "hass");
BUILT_IN_PANEL_ICONS.forEach((name) => iconNames.add(name));
if (!fs.existsSync(OUTPUT_DIR)) {
fs.mkdirSync(OUTPUT_DIR);
}
fs.writeFileSync(HASS_OUTPUT_PATH, generateIconset("hass", iconNames));
}
gulp.task("gen-icons-mdi", (done) => {
genMDIIcons();
done();
});
gulp.task("gen-icons-hass", (done) => {
genHassIcons();
done();
});
gulp.task("gen-icons", gulp.series("gen-icons-hass", "gen-icons-mdi"));
gulp.task("gen-icons-demo", (done) => {
const iconNames = findIcons(path.resolve(paths.demo_dir, "./src"), "hademo");
@@ -129,8 +120,21 @@ gulp.task("gen-icons-demo", (done) => {
done();
});
module.exports = {
findIcons,
generateIconset,
genMDIIcons,
};
gulp.task("gen-icons-hassio", (done) => {
const iconNames = findIcons(
path.resolve(paths.hassio_dir, "./src"),
"hassio"
);
// Find hassio icons inside HA main repo.
for (const item of findIcons(
path.resolve(paths.polymer_dir, "./src"),
"hassio"
)) {
iconNames.add(item);
}
fs.writeFileSync(
path.resolve(paths.hassio_dir, "hassio-icons.html"),
generateIconset("hassio", iconNames)
);
done();
});

View File

@@ -0,0 +1,34 @@
const gulp = require("gulp");
const envVars = require("../env");
require("./clean.js");
require("./gen-icons.js");
require("./webpack.js");
require("./compress.js");
gulp.task(
"develop-hassio",
gulp.series(
async function setEnv() {
process.env.NODE_ENV = "development";
},
"clean-hassio",
gulp.parallel("gen-icons-hassio", "gen-icons-mdi"),
"webpack-watch-hassio"
)
);
gulp.task(
"build-hassio",
gulp.series(
async function setEnv() {
process.env.NODE_ENV = "production";
},
"clean-hassio",
gulp.parallel("gen-icons-hassio", "gen-icons-mdi"),
"webpack-prod-hassio",
...// Don't compress running tests
(envVars.isTravis ? [] : ["compress-hassio"])
)
);

View File

@@ -45,11 +45,10 @@ function recursiveFlatten(prefix, data) {
let output = {};
Object.keys(data).forEach(function(key) {
if (typeof data[key] === "object") {
output = Object.assign(
{},
output,
recursiveFlatten(prefix + key + ".", data[key])
);
output = {
...output,
...recursiveFlatten(prefix + key + ".", data[key]),
};
} else {
output[prefix + key] = data[key];
}
@@ -99,18 +98,16 @@ function recursiveEmpty(data) {
* @link https://docs.lokalise.co/article/KO5SZWLLsy-key-referencing
*/
const re_key_reference = /\[%key:([^%]+)%\]/;
function lokalise_transform(data, original) {
function lokaliseTransform(data, original, file) {
const output = {};
Object.entries(data).forEach(([key, value]) => {
if (value instanceof Object) {
output[key] = lokalise_transform(value, original);
output[key] = lokaliseTransform(value, original, file);
} else {
output[key] = value.replace(re_key_reference, (match, key) => {
const replace = key.split("::").reduce((tr, k) => tr[k], original);
if (typeof replace !== "string") {
throw Error(
`Invalid key placeholder ${key} in src/translations/en.json`
);
throw Error(`Invalid key placeholder ${key} in ${file.path}`);
}
return replace;
});
@@ -183,7 +180,7 @@ gulp.task(
.src("src/translations/en.json")
.pipe(
transform(function(data, file) {
return lokalise_transform(data, data);
return lokaliseTransform(data, data, file);
})
)
.pipe(rename("translationMaster.json"))
@@ -198,6 +195,11 @@ gulp.task(
gulp.series("build-master-translation", function() {
return gulp
.src([inDir + "/*.json", workDir + "/test.json"], { allowEmpty: true })
.pipe(
transform(function(data, file) {
return lokaliseTransform(data, data, file);
})
)
.pipe(
foreach(function(stream, file) {
// For each language generate a merged json file. It begins with the master

View File

@@ -1,6 +1,5 @@
// Tasks to run webpack.
const gulp = require("gulp");
const path = require("path");
const webpack = require("webpack");
const WebpackDevServer = require("webpack-dev-server");
const log = require("fancy-log");
@@ -9,8 +8,33 @@ const {
createAppConfig,
createDemoConfig,
createCastConfig,
createHassioConfig,
createGalleryConfig,
} = require("../webpack");
const bothBuilds = (createConfigFunc, params) => [
createConfigFunc({ ...params, latestBuild: true }),
createConfigFunc({ ...params, latestBuild: false }),
];
const runDevServer = ({
compiler,
contentBase,
port,
listenHost = "localhost",
}) =>
new WebpackDevServer(compiler, {
open: true,
watchContentBase: true,
contentBase,
}).listen(port, listenHost, function(err) {
if (err) {
throw err;
}
// Server listening
log("[webpack-dev-server]", `http://localhost:${port}`);
});
const handler = (done) => (err, stats) => {
if (err) {
console.log(err.stack || err);
@@ -32,20 +56,11 @@ const handler = (done) => (err, stats) => {
};
gulp.task("webpack-watch-app", () => {
const compiler = webpack([
createAppConfig({
isProdBuild: false,
latestBuild: true,
isStatsBuild: false,
}),
createAppConfig({
isProdBuild: false,
latestBuild: false,
isStatsBuild: false,
}),
]);
compiler.watch({}, handler());
// we are not calling done, so this command will run forever
webpack(bothBuilds(createAppConfig, { isProdBuild: false })).watch(
{},
handler()
);
});
gulp.task(
@@ -53,47 +68,17 @@ gulp.task(
() =>
new Promise((resolve) =>
webpack(
[
createAppConfig({
isProdBuild: true,
latestBuild: true,
isStatsBuild: false,
}),
createAppConfig({
isProdBuild: true,
latestBuild: false,
isStatsBuild: false,
}),
],
bothBuilds(createAppConfig, { isProdBuild: true }),
handler(resolve)
)
)
);
gulp.task("webpack-dev-server-demo", () => {
const compiler = webpack([
createDemoConfig({
isProdBuild: false,
latestBuild: false,
isStatsBuild: false,
}),
createDemoConfig({
isProdBuild: false,
latestBuild: true,
isStatsBuild: false,
}),
]);
new WebpackDevServer(compiler, {
open: true,
watchContentBase: true,
contentBase: path.resolve(paths.demo_dir, "dist"),
}).listen(8090, "localhost", function(err) {
if (err) {
throw err;
}
// Server listening
log("[webpack-dev-server]", "http://localhost:8090");
runDevServer({
compiler: webpack(bothBuilds(createDemoConfig, { isProdBuild: false })),
contentBase: paths.demo_root,
port: 8090,
});
});
@@ -102,51 +87,22 @@ gulp.task(
() =>
new Promise((resolve) =>
webpack(
[
createDemoConfig({
isProdBuild: true,
latestBuild: false,
isStatsBuild: false,
}),
createDemoConfig({
isProdBuild: true,
latestBuild: true,
isStatsBuild: false,
}),
],
bothBuilds(createDemoConfig, {
isProdBuild: true,
}),
handler(resolve)
)
)
);
gulp.task("webpack-dev-server-cast", () => {
const compiler = webpack([
createCastConfig({
isProdBuild: false,
latestBuild: false,
}),
createCastConfig({
isProdBuild: false,
latestBuild: true,
}),
]);
new WebpackDevServer(compiler, {
open: true,
watchContentBase: true,
contentBase: path.resolve(paths.cast_dir, "dist"),
}).listen(
8080,
runDevServer({
compiler: webpack(bothBuilds(createCastConfig, { isProdBuild: false })),
contentBase: paths.cast_root,
port: 8080,
// Accessible from the network, because that's how Cast hits it.
"0.0.0.0",
function(err) {
if (err) {
throw err;
}
// Server listening
log("[webpack-dev-server]", "http://localhost:8080");
}
);
listenHost: "0.0.0.0",
});
});
gulp.task(
@@ -154,16 +110,59 @@ gulp.task(
() =>
new Promise((resolve) =>
webpack(
[
createCastConfig({
isProdBuild: true,
latestBuild: false,
}),
createCastConfig({
isProdBuild: true,
latestBuild: true,
}),
],
bothBuilds(createCastConfig, {
isProdBuild: true,
}),
handler(resolve)
)
)
);
gulp.task("webpack-watch-hassio", () => {
// we are not calling done, so this command will run forever
webpack(
createHassioConfig({
isProdBuild: false,
latestBuild: false,
})
).watch({}, handler());
});
gulp.task(
"webpack-prod-hassio",
() =>
new Promise((resolve) =>
webpack(
createHassioConfig({
isProdBuild: true,
latestBuild: false,
}),
handler(resolve)
)
)
);
gulp.task("webpack-dev-server-gallery", () => {
runDevServer({
compiler: webpack(
createGalleryConfig({ latestBuild: true, isProdBuild: false })
),
contentBase: paths.gallery_root,
port: 8100,
});
});
gulp.task(
"webpack-prod-gallery",
() =>
new Promise((resolve) =>
webpack(
createGalleryConfig({
isProdBuild: true,
latestBuild: true,
}),
handler(resolve)
)
)

View File

@@ -20,4 +20,13 @@ module.exports = {
cast_static: path.resolve(__dirname, "../cast/dist/static"),
cast_output: path.resolve(__dirname, "../cast/dist/frontend_latest"),
cast_output_es5: path.resolve(__dirname, "../cast/dist/frontend_es5"),
gallery_dir: path.resolve(__dirname, "../gallery"),
gallery_root: path.resolve(__dirname, "../gallery/dist"),
gallery_output: path.resolve(__dirname, "../gallery/dist/frontend_latest"),
gallery_static: path.resolve(__dirname, "../gallery/dist/static"),
hassio_dir: path.resolve(__dirname, "../hassio"),
hassio_root: path.resolve(__dirname, "../hassio/build"),
hassio_publicPath: "/api/hassio/app/",
};

View File

@@ -3,8 +3,6 @@ const fs = require("fs");
const path = require("path");
const TerserPlugin = require("terser-webpack-plugin");
const WorkboxPlugin = require("workbox-webpack-plugin");
const CompressionPlugin = require("compression-webpack-plugin");
const zopfli = require("@gfx/zopfli");
const ManifestPlugin = require("webpack-manifest-plugin");
const paths = require("./paths.js");
const { babelLoaderConfig } = require("./babel.js");
@@ -17,288 +15,246 @@ if (!version) {
}
version = version[0];
const genMode = (isProdBuild) => (isProdBuild ? "production" : "development");
const genDevTool = (isProdBuild) =>
isProdBuild ? "source-map" : "inline-cheap-module-source-map";
const genFilename = (isProdBuild, dontHash = new Set()) => ({ chunk }) => {
if (!isProdBuild || dontHash.has(chunk.name)) {
return `${chunk.name}.js`;
}
return `${chunk.name}.${chunk.hash.substr(0, 8)}.js`;
};
const genChunkFilename = (isProdBuild, isStatsBuild) =>
isProdBuild && !isStatsBuild ? "chunk.[chunkhash].js" : "[name].chunk.js";
const resolve = {
extensions: [".ts", ".js", ".json", ".tsx"],
alias: {
react: "preact-compat",
"react-dom": "preact-compat",
// Not necessary unless you consume a module using `createClass`
"create-react-class": "preact-compat/lib/create-react-class",
// Not necessary unless you consume a module requiring `react-dom-factories`
"react-dom-factories": "preact-compat/lib/react-dom-factories",
},
};
const tsLoader = (latestBuild) => ({
test: /\.ts|tsx$/,
exclude: [path.resolve(paths.polymer_dir, "node_modules")],
use: [
{
loader: "ts-loader",
options: {
compilerOptions: latestBuild
? { noEmit: false }
: { target: "es5", noEmit: false },
},
},
],
});
const cssLoader = {
test: /\.css$/,
use: "raw-loader",
};
const htmlLoader = {
test: /\.(html)$/,
use: {
loader: "html-loader",
options: {
exportAsEs6Default: true,
},
},
};
const plugins = [
// Ignore moment.js locales
new webpack.IgnorePlugin(/^\.\/locale$/, /moment$/),
// Color.js is bloated, it contains all color definitions for all material color sets.
new webpack.NormalModuleReplacementPlugin(
/@polymer\/paper-styles\/color\.js$/,
path.resolve(paths.polymer_dir, "src/util/empty.js")
),
// Ignore roboto pointing at CDN. We use local font-roboto-local.
new webpack.NormalModuleReplacementPlugin(
/@polymer\/font-roboto\/roboto\.js$/,
path.resolve(paths.polymer_dir, "src/util/empty.js")
),
// Ignore mwc icons pointing at CDN.
new webpack.NormalModuleReplacementPlugin(
/@material\/mwc-icon\/mwc-icon-font\.js$/,
path.resolve(paths.polymer_dir, "src/util/empty.js")
),
];
const optimization = (latestBuild) => ({
minimizer: [
new TerserPlugin({
cache: true,
parallel: true,
extractComments: true,
sourceMap: true,
terserOptions: {
safari10: true,
ecma: latestBuild ? undefined : 5,
},
}),
],
});
const createAppConfig = ({ isProdBuild, latestBuild, isStatsBuild }) => {
const isCI = process.env.CI === "true";
// Create an object mapping browser urls to their paths during build
const translationMetadata = require("../build-translations/translationMetadata.json");
const workBoxTranslationsTemplatedURLs = {};
const englishFP = translationMetadata.translations.en.fingerprints;
Object.keys(englishFP).forEach((key) => {
workBoxTranslationsTemplatedURLs[
`/static/translations/${englishFP[key]}`
] = `build-translations/output/${key}.json`;
});
const entry = {
app: "./src/entrypoints/app.ts",
authorize: "./src/entrypoints/authorize.ts",
onboarding: "./src/entrypoints/onboarding.ts",
core: "./src/entrypoints/core.ts",
compatibility: "./src/entrypoints/compatibility.ts",
"custom-panel": "./src/entrypoints/custom-panel.ts",
"hass-icons": "./src/entrypoints/hass-icons.ts",
};
const rules = [tsLoader(latestBuild), cssLoader, htmlLoader];
if (!latestBuild) {
rules.push(babelLoaderConfig({ latestBuild }));
}
const createWebpackConfig = ({
entry,
outputRoot,
defineOverlay,
isProdBuild,
latestBuild,
isStatsBuild,
}) => {
return {
mode: genMode(isProdBuild),
devtool: genDevTool(isProdBuild),
mode: isProdBuild ? "production" : "development",
devtool: isProdBuild ? "source-map" : "inline-cheap-module-source-map",
entry,
module: {
rules,
rules: [
babelLoaderConfig({ latestBuild }),
{
test: /\.css$/,
use: "raw-loader",
},
{
test: /\.(html)$/,
use: {
loader: "html-loader",
options: {
exportAsEs6Default: true,
},
},
},
],
},
optimization: {
minimizer: [
new TerserPlugin({
cache: true,
parallel: true,
extractComments: true,
sourceMap: true,
terserOptions: {
safari10: true,
ecma: latestBuild ? undefined : 5,
},
}),
],
},
optimization: optimization(latestBuild),
plugins: [
new ManifestPlugin(),
new webpack.DefinePlugin({
__DEV__: JSON.stringify(!isProdBuild),
__DEMO__: false,
__DEV__: !isProdBuild,
__BUILD__: JSON.stringify(latestBuild ? "latest" : "es5"),
__VERSION__: JSON.stringify(version),
__DEMO__: false,
__STATIC_PATH__: "/static/",
"process.env.NODE_ENV": JSON.stringify(
isProdBuild ? "production" : "development"
),
...defineOverlay,
}),
...plugins,
isProdBuild &&
!isCI &&
!isStatsBuild &&
new CompressionPlugin({
cache: true,
exclude: [/\.js\.map$/, /\.LICENSE$/, /\.py$/, /\.txt$/],
algorithm(input, compressionOptions, callback) {
return zopfli.gzip(input, compressionOptions, callback);
},
}),
latestBuild &&
new WorkboxPlugin.InjectManifest({
swSrc: "./src/entrypoints/service-worker-hass.js",
swDest: "service_worker.js",
importWorkboxFrom: "local",
include: [/\.js$/],
templatedURLs: {
...workBoxTranslationsTemplatedURLs,
"/static/icons/favicon-192x192.png":
"public/icons/favicon-192x192.png",
"/static/fonts/roboto/Roboto-Light.woff2":
"node_modules/roboto-fontface/fonts/roboto/Roboto-Light.woff2",
"/static/fonts/roboto/Roboto-Medium.woff2":
"node_modules/roboto-fontface/fonts/roboto/Roboto-Medium.woff2",
"/static/fonts/roboto/Roboto-Regular.woff2":
"node_modules/roboto-fontface/fonts/roboto/Roboto-Regular.woff2",
"/static/fonts/roboto/Roboto-Bold.woff2":
"node_modules/roboto-fontface/fonts/roboto/Roboto-Bold.woff2",
},
}),
// Ignore moment.js locales
new webpack.IgnorePlugin(/^\.\/locale$/, /moment$/),
// Color.js is bloated, it contains all color definitions for all material color sets.
new webpack.NormalModuleReplacementPlugin(
/@polymer\/paper-styles\/color\.js$/,
path.resolve(paths.polymer_dir, "src/util/empty.js")
),
// Ignore roboto pointing at CDN. We use local font-roboto-local.
new webpack.NormalModuleReplacementPlugin(
/@polymer\/font-roboto\/roboto\.js$/,
path.resolve(paths.polymer_dir, "src/util/empty.js")
),
// Ignore mwc icons pointing at CDN.
new webpack.NormalModuleReplacementPlugin(
/@material\/mwc-icon\/mwc-icon-font\.js$/,
path.resolve(paths.polymer_dir, "src/util/empty.js")
),
].filter(Boolean),
resolve: {
extensions: [".ts", ".js", ".json", ".tsx"],
alias: {
react: "preact-compat",
"react-dom": "preact-compat",
// Not necessary unless you consume a module using `createClass`
"create-react-class": "preact-compat/lib/create-react-class",
// Not necessary unless you consume a module requiring `react-dom-factories`
"react-dom-factories": "preact-compat/lib/react-dom-factories",
},
},
output: {
filename: genFilename(isProdBuild),
chunkFilename: genChunkFilename(isProdBuild, isStatsBuild),
path: latestBuild ? paths.output : paths.output_es5,
filename: ({ chunk }) => {
const dontHash = new Set();
if (!isProdBuild || dontHash.has(chunk.name)) {
return `${chunk.name}.js`;
}
return `${chunk.name}.${chunk.hash.substr(0, 8)}.js`;
},
chunkFilename:
isProdBuild && !isStatsBuild
? "chunk.[chunkhash].js"
: "[name].chunk.js",
path: path.resolve(
outputRoot,
latestBuild ? "frontend_latest" : "frontend_es5"
),
publicPath: latestBuild ? "/frontend_latest/" : "/frontend_es5/",
// For workerize loader
globalObject: "self",
},
resolve,
};
};
const createAppConfig = ({ isProdBuild, latestBuild, isStatsBuild }) => {
const config = createWebpackConfig({
entry: {
app: "./src/entrypoints/app.ts",
authorize: "./src/entrypoints/authorize.ts",
onboarding: "./src/entrypoints/onboarding.ts",
core: "./src/entrypoints/core.ts",
compatibility: "./src/entrypoints/compatibility.ts",
"custom-panel": "./src/entrypoints/custom-panel.ts",
"hass-icons": "./src/entrypoints/hass-icons.ts",
},
outputRoot: paths.root,
isProdBuild,
latestBuild,
isStatsBuild,
});
if (latestBuild) {
// Create an object mapping browser urls to their paths during build
const translationMetadata = require("../build-translations/translationMetadata.json");
const workBoxTranslationsTemplatedURLs = {};
const englishFP = translationMetadata.translations.en.fingerprints;
Object.keys(englishFP).forEach((key) => {
workBoxTranslationsTemplatedURLs[
`/static/translations/${englishFP[key]}`
] = `build-translations/output/${key}.json`;
});
config.plugins.push(
new WorkboxPlugin.InjectManifest({
swSrc: "./src/entrypoints/service-worker-hass.js",
swDest: "service_worker.js",
importWorkboxFrom: "local",
include: [/\.js$/],
templatedURLs: {
...workBoxTranslationsTemplatedURLs,
"/static/icons/favicon-192x192.png":
"public/icons/favicon-192x192.png",
"/static/fonts/roboto/Roboto-Light.woff2":
"node_modules/roboto-fontface/fonts/roboto/Roboto-Light.woff2",
"/static/fonts/roboto/Roboto-Medium.woff2":
"node_modules/roboto-fontface/fonts/roboto/Roboto-Medium.woff2",
"/static/fonts/roboto/Roboto-Regular.woff2":
"node_modules/roboto-fontface/fonts/roboto/Roboto-Regular.woff2",
"/static/fonts/roboto/Roboto-Bold.woff2":
"node_modules/roboto-fontface/fonts/roboto/Roboto-Bold.woff2",
},
})
);
}
return config;
};
const createDemoConfig = ({ isProdBuild, latestBuild, isStatsBuild }) => {
const rules = [tsLoader(latestBuild), cssLoader, htmlLoader];
if (!latestBuild) {
rules.push(babelLoaderConfig({ latestBuild }));
}
return {
mode: genMode(isProdBuild),
devtool: genDevTool(isProdBuild),
return createWebpackConfig({
entry: {
main: "./demo/src/entrypoint.ts",
compatibility: "./src/entrypoints/compatibility.ts",
},
module: {
rules,
},
optimization: optimization(latestBuild),
plugins: [
new ManifestPlugin(),
new webpack.DefinePlugin({
__DEV__: !isProdBuild,
__BUILD__: JSON.stringify(latestBuild ? "latest" : "es5"),
__VERSION__: JSON.stringify(`DEMO-${version}`),
__DEMO__: true,
__STATIC_PATH__: "/static/",
"process.env.NODE_ENV": JSON.stringify(
isProdBuild ? "production" : "development"
),
}),
...plugins,
].filter(Boolean),
resolve,
output: {
filename: genFilename(isProdBuild),
chunkFilename: genChunkFilename(isProdBuild, isStatsBuild),
path: path.resolve(
paths.demo_root,
latestBuild ? "frontend_latest" : "frontend_es5"
main: path.resolve(paths.demo_dir, "src/entrypoint.ts"),
compatibility: path.resolve(
paths.polymer_dir,
"src/entrypoints/compatibility.ts"
),
publicPath: latestBuild ? "/frontend_latest/" : "/frontend_es5/",
// For workerize loader
globalObject: "self",
},
};
outputRoot: paths.demo_root,
defineOverlay: {
__VERSION__: JSON.stringify(`DEMO-${version}`),
__DEMO__: true,
},
isProdBuild,
latestBuild,
isStatsBuild,
});
};
const createCastConfig = ({ isProdBuild, latestBuild }) => {
const isStatsBuild = false;
const entry = {
launcher: "./cast/src/launcher/entrypoint.ts",
launcher: path.resolve(paths.cast_dir, "src/launcher/entrypoint.ts"),
};
if (latestBuild) {
entry.receiver = "./cast/src/receiver/entrypoint.ts";
entry.receiver = path.resolve(paths.cast_dir, "src/receiver/entrypoint.ts");
}
const rules = [tsLoader(latestBuild), cssLoader, htmlLoader];
if (!latestBuild) {
rules.push(babelLoaderConfig({ latestBuild }));
}
return {
mode: genMode(isProdBuild),
devtool: genDevTool(isProdBuild),
return createWebpackConfig({
entry,
module: {
rules,
outputRoot: paths.cast_root,
isProdBuild,
latestBuild,
});
};
const createHassioConfig = ({ isProdBuild, latestBuild }) => {
if (latestBuild) {
throw new Error("Hass.io does not support latest build!");
}
const config = createWebpackConfig({
entry: {
entrypoint: path.resolve(paths.hassio_dir, "src/entrypoint.js"),
},
optimization: optimization(latestBuild),
plugins: [
new ManifestPlugin(),
new webpack.DefinePlugin({
__DEV__: !isProdBuild,
__BUILD__: JSON.stringify(latestBuild ? "latest" : "es5"),
__VERSION__: JSON.stringify(version),
__DEMO__: false,
__STATIC_PATH__: "/static/",
"process.env.NODE_ENV": JSON.stringify(
isProdBuild ? "production" : "development"
),
}),
...plugins,
].filter(Boolean),
resolve,
output: {
filename: genFilename(isProdBuild),
chunkFilename: genChunkFilename(isProdBuild, isStatsBuild),
path: path.resolve(
paths.cast_root,
latestBuild ? "frontend_latest" : "frontend_es5"
),
publicPath: latestBuild ? "/frontend_latest/" : "/frontend_es5/",
// For workerize loader
globalObject: "self",
outputRoot: "",
isProdBuild,
latestBuild,
});
config.output.path = paths.hassio_root;
config.output.publicPath = paths.hassio_publicPath;
return config;
};
const createGalleryConfig = ({ isProdBuild, latestBuild }) => {
if (!latestBuild) {
throw new Error("Gallery only supports latest build!");
}
const config = createWebpackConfig({
entry: {
entrypoint: path.resolve(paths.gallery_dir, "src/entrypoint.js"),
},
};
outputRoot: paths.gallery_root,
isProdBuild,
latestBuild,
});
return config;
};
module.exports = {
resolve,
plugins,
optimization,
createAppConfig,
createDemoConfig,
createCastConfig,
createHassioConfig,
createGalleryConfig,
};

20
cast/public/_headers Normal file
View File

@@ -0,0 +1,20 @@
/*
Cache-Control: public, max-age: 0, s-maxage=3600, must-revalidate
Content-Security-Policy: form-action https:
Feature-Policy: vibrate 'none'; geolocation 'none'; midi 'none'; microphone 'none'; camera 'none'; magnetometer 'none'; gyroscope 'none'; speaker 'none'; vibrate 'none'; payment 'none'
Referrer-Policy: no-referrer-when-downgrade
X-Content-Type-Options: nosniff
X-Frame-Options: DENY
X-XSS-Protection: 1; mode=block
/images/*
Cache-Control: public, max-age: 604800, s-maxage=604800
/manifest.json
Cache-Control: public, max-age: 3600, s-maxage=3600
/frontend_es5/*
Cache-Control: public, max-age: 604800, s-maxage=604800
/frontend_latest/*
Cache-Control: public, max-age: 604800, s-maxage=604800

11
cast/webpack.config.js Normal file
View File

@@ -0,0 +1,11 @@
const { createCastConfig } = require("../build-scripts/webpack.js");
const { isProdBuild } = require("../build-scripts/env.js");
// File just used for stats builds
const latestBuild = true;
module.exports = createCastConfig({
isProdBuild,
latestBuild,
});

18
demo/public/_headers Normal file
View File

@@ -0,0 +1,18 @@
/*
Cache-Control: public, max-age: 0, s-maxage=3600, must-revalidate
Content-Security-Policy: form-action https:
Referrer-Policy: no-referrer-when-downgrade
X-Content-Type-Options: nosniff
X-XSS-Protection: 1; mode=block
/api/*
Cache-Control: public, max-age: 604800, s-maxage=604800
/assets/*
Cache-Control: public, max-age: 604800, s-maxage=604800
/frontend_es5/*
Cache-Control: public, max-age: 604800, s-maxage=604800
/frontend_latest/*
Cache-Control: public, max-age: 604800, s-maxage=604800

View File

@@ -217,6 +217,18 @@ export const demoEntitiesArsaboo: DemoConfig["entities"] = (localize) =>
icon: "hademo:currency-usd",
},
},
"sensor.study_temp": {
entity_id: "sensor.study_temp",
state: "20.9",
attributes: {
unit_of_measurement: "°C",
device_class: "temperature",
friendly_name: localize(
"ui.panel.page-demo.config.arsaboo.names.temperature_study"
),
icon: "hademo:thermometer",
},
},
"cover.garagedoor": {
entity_id: "cover.garagedoor",
state: "closed",

View File

@@ -446,6 +446,11 @@ export const demoLovelaceArsaboo: DemoConfig["lovelace"] = (localize) => ({
"script.tv_off",
],
},
{
type: "sensor",
entity: "sensor.study_temp",
graph: "line",
},
{
type: "entities",
title: "Doorbell",

View File

@@ -23,27 +23,24 @@ export const demoThemeJimpower = () => ({
"paper-listbox-background-color": "#2E333A",
"table-row-background-color": "#353840",
"paper-grey-50": "var(--primary-text-color)",
"paper-toggle-button-checked-button-color": "var(--accent-color)",
"switch-checked-color": "var(--accent-color)",
"paper-dialog-background-color": "#434954",
"secondary-text-color": "#5294E2",
"google-red-500": "#E45E65",
"divider-color": "rgba(0, 0, 0, .12)",
"paper-toggle-button-unchecked-ink-color": "var(--disabled-text-color)",
"google-green-500": "#39E949",
"paper-toggle-button-unchecked-button-color": "var(--disabled-text-color)",
"switch-unchecked-button-color": "var(--disabled-text-color)",
"label-badge-border-color": "green",
"paper-listbox-color": "var(--primary-color)",
"paper-slider-disabled-secondary-color": "var(--disabled-text-color)",
"paper-toggle-button-checked-ink-color": "var(--accent-color)",
"paper-card-background-color": "#434954",
"label-badge-text-color": "var(--primary-text-color)",
"paper-slider-knob-start-color": "var(--accent-color)",
"paper-toggle-button-unchecked-bar-color": "var(--disabled-text-color)",
"switch-unchecked-track-color": "var(--disabled-text-color)",
"dark-primary-color": "var(--accent-color)",
"paper-slider-secondary-color": "var(--secondary-background-color)",
"paper-slider-pin-color": "var(--accent-color)",
"paper-item-icon-active-color": "#F9C536",
"accent-color": "#E45E65",
"paper-toggle-button-checked-bar-color": "var(--accent-color)",
"table-row-alternative-background-color": "#3E424B",
});

View File

@@ -24,27 +24,24 @@ export const demoThemeKernehed = () => ({
"paper-listbox-background-color": "#141414",
"table-row-background-color": "#292929",
"paper-grey-50": "var(--primary-text-color)",
"paper-toggle-button-checked-button-color": "var(--accent-color)",
"switch-checked-color": "var(--accent-color)",
"paper-dialog-background-color": "#292929",
"secondary-text-color": "#b58e31",
"google-red-500": "#b58e31",
"divider-color": "rgba(0, 0, 0, .12)",
"paper-toggle-button-unchecked-ink-color": "var(--disabled-text-color)",
"google-green-500": "#2980b9",
"paper-toggle-button-unchecked-button-color": "var(--disabled-text-color)",
"switch-unchecked-button-color": "var(--disabled-text-color)",
"label-badge-border-color": "green",
"paper-listbox-color": "#777777",
"paper-slider-disabled-secondary-color": "var(--disabled-text-color)",
"paper-toggle-button-checked-ink-color": "var(--accent-color)",
"paper-card-background-color": "#292929",
"label-badge-text-color": "var(--primary-text-color)",
"paper-slider-knob-start-color": "var(--accent-color)",
"paper-toggle-button-unchecked-bar-color": "var(--disabled-text-color)",
"switch-unchecked-track-color": "var(--disabled-text-color)",
"dark-primary-color": "var(--accent-color)",
"paper-slider-secondary-color": "var(--secondary-background-color)",
"paper-slider-pin-color": "var(--accent-color)",
"paper-item-icon-active-color": "#b58e31",
"accent-color": "#2980b9",
"paper-toggle-button-checked-bar-color": "var(--accent-color)",
"table-row-alternative-background-color": "#292929",
});

View File

@@ -12,8 +12,7 @@ export const demoThemeTeachingbirds = () => ({
"paper-slider-knob-color": "var(--primary-color)",
"paper-listbox-color": "#FFFFFF",
"paper-toggle-button-checked-bar-color": "var(--light-primary-color)",
"paper-toggle-button-checked-ink-color": "var(--dark-primary-color)",
"paper-toggle-button-unchecked-bar-color": "var(--primary-text-color)",
"switch-unchecked-track-color": "var(--primary-text-color)",
"paper-card-background-color": "#4e4e4e",
"label-badge-text-color": "var(--text-primary-color)",
"primary-background-color": "#303030",
@@ -22,7 +21,7 @@ export const demoThemeTeachingbirds = () => ({
"secondary-background-color": "#2b2b2b",
"paper-slider-knob-start-color": "var(--primary-color)",
"paper-item-icon-active-color": "#d8bf50",
"paper-toggle-button-checked-button-color": "var(--primary-color)",
"switch-checked-color": "var(--primary-color)",
"secondary-text-color": "#389638",
"disabled-text-color": "#545454",
"paper-item-icon_-_color": "var(--primary-text-color)",

View File

@@ -161,8 +161,8 @@ if (!window.cardTools) {
};
cardTools.longpress = (element) => {
customElements.whenDefined("long-press").then(() => {
const longpress = document.body.querySelector("long-press");
customElements.whenDefined("action-handler").then(() => {
const longpress = document.body.querySelector("action-handler");
longpress.bind(element);
});
return element;

View File

@@ -1,10 +1,4 @@
import {
LitElement,
html,
CSSResult,
css,
PropertyDeclarations,
} from "lit-element";
import { LitElement, html, CSSResult, css, property } from "lit-element";
import { until } from "lit-html/directives/until";
import "@material/mwc-button";
import "@polymer/paper-spinner/paper-spinner-lite";
@@ -20,19 +14,11 @@ import {
} from "../configs/demo-configs";
export class HADemoCard extends LitElement implements LovelaceCard {
public lovelace?: Lovelace;
public hass!: MockHomeAssistant;
private _switching?: boolean;
@property() public lovelace?: Lovelace;
@property() public hass!: MockHomeAssistant;
@property() private _switching?: boolean;
private _hidden = localStorage.hide_demo_card;
static get properties(): PropertyDeclarations {
return {
lovelace: {},
hass: {},
_switching: {},
};
}
public getCardSize() {
return this._hidden ? 0 : 2;
}

View File

@@ -1,10 +1,9 @@
const { createDemoConfig } = require("../build-scripts/webpack.js");
const { isProdBuild, isStatsBuild } = require("../build-scripts/env.js");
// This file exists because we haven't migrated the stats script yet
// File just used for stats builds
const isProdBuild = process.env.NODE_ENV === "production";
const isStatsBuild = process.env.STATS === "1";
const latestBuild = false;
const latestBuild = true;
module.exports = createDemoConfig({
isProdBuild,

View File

@@ -1,18 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<meta name="theme-color" content="#2157BC">
<title>HAGallery</title>
<script src='./main.js' async></script>
<style>
body {
font-family: Roboto, Noto, sans-serif;
margin: 0;
padding: 0;
}
</style>
</head>
<body></body>
</html>

View File

@@ -4,14 +4,6 @@
# Stop on errors
set -e
cd "$(dirname "$0")/.."
cd "$(dirname "$0")/../.."
OUTPUT_DIR=dist
rm -rf $OUTPUT_DIR
cd ..
./node_modules/.bin/gulp build-translations gen-icons
cd gallery
NODE_ENV=production ../node_modules/.bin/webpack -p --config webpack.config.js
./node_modules/.bin/gulp build-gallery

View File

@@ -4,10 +4,6 @@
# Stop on errors
set -e
cd "$(dirname "$0")/.."
cd "$(dirname "$0")/../.."
cd ..
./node_modules/.bin/gulp build-translations gen-icons
cd gallery
../node_modules/.bin/webpack-dev-server
./node_modules/.bin/gulp develop-gallery

View File

@@ -1,6 +1,6 @@
import { html } from "@polymer/polymer/lib/utils/html-tag";
import { PolymerElement } from "@polymer/polymer/polymer-element";
import JsYaml from "js-yaml";
import { safeLoad } from "js-yaml";
import { createCardElement } from "../../../src/panels/lovelace/common/create-card-element";
@@ -62,7 +62,7 @@ class DemoCard extends PolymerElement {
card.removeChild(card.lastChild);
}
const el = createCardElement(JsYaml.safeLoad(config.config)[0]);
const el = createCardElement(safeLoad(config.config)[0]);
el.hass = this.hass;
card.appendChild(el);
}

View File

@@ -26,7 +26,9 @@ class DemoCards extends PolymerElement {
</style>
<app-toolbar>
<div class="filters">
<ha-switch checked="{{_showConfig}}">Show config</ha-switch>
<ha-switch checked="[[_showConfig]]" on-change="_showConfigToggled">
Show config
</ha-switch>
</div>
</app-toolbar>
<div class="cards">
@@ -51,6 +53,10 @@ class DemoCards extends PolymerElement {
},
};
}
_showConfigToggled(ev) {
this._showConfig = ev.target.checked;
}
}
customElements.define("demo-cards", DemoCards);

View File

@@ -2,7 +2,8 @@ import { html, LitElement, TemplateResult } from "lit-element";
import "@material/mwc-button";
import "../../../src/components/ha-card";
import { longPress } from "../../../src/panels/lovelace/common/directives/long-press-directive";
import { actionHandler } from "../../../src/panels/lovelace/common/directives/action-handler-directive";
import { ActionHandlerEvent } from "../../../src/data/lovelace";
export class DemoUtilLongPress extends LitElement {
protected render(): TemplateResult | void {
@@ -12,9 +13,8 @@ export class DemoUtilLongPress extends LitElement {
() => html`
<ha-card>
<mwc-button
@ha-click="${this._handleTap}"
@ha-hold="${this._handleHold}"
.longPress="${longPress()}"
@action=${this._handleAction}
.actionHandler=${actionHandler({})}
>
(long) press me!
</mwc-button>
@@ -28,12 +28,8 @@ export class DemoUtilLongPress extends LitElement {
`;
}
private _handleTap(ev: Event) {
this._addValue(ev, "tap");
}
private _handleHold(ev: Event) {
this._addValue(ev, "hold");
private _handleAction(ev: ActionHandlerEvent) {
this._addValue(ev, ev.detail.action!);
}
private _addValue(ev: Event, value: string) {

View File

@@ -0,0 +1,22 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta
name="viewport"
content="width=device-width, initial-scale=1, shrink-to-fit=no"
/>
<meta name="theme-color" content="#2157BC" />
<title>HAGallery</title>
<script type="module" src="<%= latestGalleryJS %>"></script>
<style>
body {
font-family: Roboto, Noto, sans-serif;
margin: 0;
padding: 0;
}
</style>
</head>
<body></body>
</html>

View File

@@ -1,6 +1,6 @@
const path = require("path");
const CopyWebpackPlugin = require("copy-webpack-plugin");
const webpackBase = require("../build-scripts/webpack.js");
const { createGalleryConfig } = require("../build-scripts/webpack.js");
const { babelLoaderConfig } = require("../build-scripts/babel.js");
const isProd = process.env.NODE_ENV === "production";
@@ -9,80 +9,64 @@ const buildPath = path.resolve(__dirname, "dist");
const publicPath = isProd ? "./" : "http://localhost:8080/";
const latestBuild = true;
const rules = [
{
exclude: [path.resolve(__dirname, "../node_modules")],
test: /\.ts$/,
use: [
{
loader: "ts-loader",
options: {
compilerOptions: latestBuild
? { noEmit: false }
: {
target: "es5",
noEmit: false,
},
module.exports = createGalleryConfig({
latestBuild: true,
});
const bla = () => {
const oldExports = {
mode: isProd ? "production" : "development",
// Disabled in prod while we make Home Assistant able to serve the right files.
// Was source-map
devtool: isProd ? "none" : "inline-source-map",
entry: "./src/entrypoint.js",
module: {
rules: [
babelLoaderConfig({ latestBuild }),
{
test: /\.css$/,
use: "raw-loader",
},
},
],
},
{
test: /\.css$/,
use: "raw-loader",
},
{
test: /\.(html)$/,
use: {
loader: "html-loader",
options: {
exportAsEs6Default: true,
},
{
test: /\.(html)$/,
use: {
loader: "html-loader",
options: {
exportAsEs6Default: true,
},
},
},
],
},
},
];
if (!latestBuild) {
rules.push(babelLoaderConfig({ latestBuild }));
}
module.exports = {
mode: isProd ? "production" : "development",
// Disabled in prod while we make Home Assistant able to serve the right files.
// Was source-map
devtool: isProd ? "none" : "inline-source-map",
entry: "./src/entrypoint.js",
module: {
rules,
},
optimization: webpackBase.optimization(latestBuild),
plugins: [
new CopyWebpackPlugin([
"public",
{ from: "../public", to: "static" },
{ from: "../build-translations/output", to: "static/translations" },
{
from: "../node_modules/leaflet/dist/leaflet.css",
to: "static/images/leaflet/",
},
{
from: "../node_modules/roboto-fontface/fonts/roboto/*.woff2",
to: "static/fonts/roboto/",
},
{
from: "../node_modules/leaflet/dist/images",
to: "static/images/leaflet/",
},
]),
].filter(Boolean),
resolve: webpackBase.resolve,
output: {
filename: "[name].js",
chunkFilename: chunkFilename,
path: buildPath,
publicPath,
},
devServer: {
contentBase: "./public",
},
optimization: webpackBase.optimization(latestBuild),
plugins: [
new CopyWebpackPlugin([
"public",
{ from: "../public", to: "static" },
{ from: "../build-translations/output", to: "static/translations" },
{
from: "../node_modules/leaflet/dist/leaflet.css",
to: "static/images/leaflet/",
},
{
from: "../node_modules/roboto-fontface/fonts/roboto/*.woff2",
to: "static/fonts/roboto/",
},
{
from: "../node_modules/leaflet/dist/images",
to: "static/images/leaflet/",
},
]),
].filter(Boolean),
resolve: webpackBase.resolve,
output: {
filename: "[name].js",
chunkFilename: chunkFilename,
path: buildPath,
publicPath,
},
devServer: {
contentBase: "./public",
},
};
};

View File

@@ -4,11 +4,6 @@
# Stop on errors
set -e
cd "$(dirname "$0")/.."
cd "$(dirname "$0")/../.."
OUTPUT_DIR=build
rm -rf $OUTPUT_DIR
node script/gen-icons.js
NODE_ENV=production CI=false ../node_modules/.bin/webpack -p --config webpack.config.js
./node_modules/.bin/gulp build-hassio

View File

@@ -4,11 +4,6 @@
# Stop on errors
set -e
cd "$(dirname "$0")/.."
cd "$(dirname "$0")/../.."
OUTPUT_DIR=build
rm -rf $OUTPUT_DIR
mkdir $OUTPUT_DIR
node script/gen-icons.js
../node_modules/.bin/webpack --watch --progress
./node_modules/.bin/gulp develop-hassio

View File

@@ -1,20 +0,0 @@
#!/usr/bin/env node
const fs = require("fs");
const {
findIcons,
generateIconset,
genMDIIcons,
} = require("../../build-scripts/gulp/gen-icons.js");
function genHassioIcons() {
const iconNames = findIcons("./src", "hassio");
for (const item of findIcons("../src", "hassio")) {
iconNames.add(item);
}
fs.writeFileSync("./hassio-icons.html", generateIconset("hassio", iconNames));
}
genMDIIcons();
genHassioIcons();

View File

@@ -7,6 +7,7 @@ import {
property,
customElement,
} from "lit-element";
import "@polymer/iron-icon/iron-icon";
import { HomeAssistant } from "../../../src/types";
import {
@@ -33,12 +34,15 @@ export class HassioUpdate extends LitElement {
@property() public error?: string;
protected render(): TemplateResult | void {
if (
this.hassInfo.version === this.hassInfo.last_version &&
this.supervisorInfo.version === this.supervisorInfo.last_version &&
(!this.hassOsInfo ||
this.hassOsInfo.version === this.hassOsInfo.version_latest)
) {
const updatesAvailable: number = [
this.hassInfo,
this.supervisorInfo,
this.hassOsInfo,
].filter((value) => {
return !!value && value.version !== value.last_version;
}).length;
if (!updatesAvailable) {
return html``;
}
@@ -50,6 +54,11 @@ export class HassioUpdate extends LitElement {
`
: ""}
<div class="card-group">
<div class="title">
${updatesAvailable > 1
? "Updates Available 🎉"
: "Update Available 🎉"}
</div>
${this._renderUpdateCard(
"Home Assistant",
this.hassInfo.version,
@@ -57,7 +66,8 @@ export class HassioUpdate extends LitElement {
"hassio/homeassistant/update",
`https://${
this.hassInfo.last_version.includes("b") ? "rc" : "www"
}.home-assistant.io/latest-release-notes/`
}.home-assistant.io/latest-release-notes/`,
"hassio:home-assistant"
)}
${this._renderUpdateCard(
"Hass.io Supervisor",
@@ -89,18 +99,31 @@ export class HassioUpdate extends LitElement {
curVersion: string,
lastVersion: string,
apiPath: string,
releaseNotesUrl: string
releaseNotesUrl: string,
icon?: string
): TemplateResult {
if (lastVersion === curVersion) {
return html``;
}
return html`
<paper-card heading="${name} update available! 🎉">
<paper-card>
<div class="card-content">
${name} ${lastVersion} is available and you are currently running
${name} ${curVersion}.
${icon
? html`
<div class="icon">
<iron-icon .icon="${icon}" />
</div>
`
: ""}
<div class="update-heading">${name} ${lastVersion}</div>
<div class="warning">
You are currently running version ${curVersion}
</div>
</div>
<div class="card-actions">
<a href="${releaseNotesUrl}" target="_blank">
<mwc-button>Release notes</mwc-button>
</a>
<ha-call-api-button
.hass=${this.hass}
.path=${apiPath}
@@ -108,9 +131,6 @@ export class HassioUpdate extends LitElement {
>
Update
</ha-call-api-button>
<a href="${releaseNotesUrl}" target="_blank">
<mwc-button>Release notes</mwc-button>
</a>
</div>
</paper-card>
`;
@@ -140,6 +160,23 @@ export class HassioUpdate extends LitElement {
display: inline-block;
margin-bottom: 32px;
}
.icon {
--iron-icon-height: 48px;
--iron-icon-width: 48px;
float: right;
margin: 0 0 2px 10px;
}
.update-heading {
font-size: var(--paper-font-subhead_-_font-size);
font-weight: 500;
margin-bottom: 0.5em;
}
.warning {
color: var(--secondary-text-color);
}
.card-actions {
text-align: right;
}
.errors {
color: var(--google-red-500);
padding: 16px;

66
hassio/src/dialogs/snapshot/dialog-hassio-snapshot.ts Normal file → Executable file
View File

@@ -3,6 +3,7 @@ import "@material/mwc-button";
import "@polymer/paper-checkbox/paper-checkbox";
import "@polymer/paper-dialog-scrollable/paper-dialog-scrollable";
import "@polymer/paper-icon-button/paper-icon-button";
import "@polymer/iron-icon/iron-icon";
import "@polymer/paper-input/paper-input";
import { html } from "@polymer/polymer/lib/utils/html-tag";
import { PolymerElement } from "@polymer/polymer/polymer-element";
@@ -94,13 +95,23 @@ class HassioSnapshotDialog extends PolymerElement {
.details {
color: var(--secondary-text-color);
}
.download {
color: var(--primary-color);
}
.warning,
.error {
color: var(--google-red-500);
}
.buttons {
display: flex;
flex-direction: column;
}
.buttons li {
list-style-type: none;
}
.buttons .icon {
margin-right: 16px;
}
.no-margin-top {
margin-top: 0;
}
</style>
<ha-paper-dialog
id="dialog"
@@ -132,7 +143,7 @@ class HassioSnapshotDialog extends PolymerElement {
</template>
<template is="dom-if" if="[[_addons.length]]">
<div>Add-ons:</div>
<paper-dialog-scrollable>
<paper-dialog-scrollable class="no-margin-top">
<template is="dom-repeat" items="[[_addons]]" sort="_sortAddons">
<paper-checkbox checked="{{item.checked}}">
[[item.name]] <span class="details">([[item.version]])</span>
@@ -151,28 +162,35 @@ class HassioSnapshotDialog extends PolymerElement {
<template is="dom-if" if="[[error]]">
<p class="error">Error: [[error]]</p>
</template>
<div class="buttons">
<paper-icon-button
icon="hassio:delete"
on-click="_deleteClicked"
class="warning"
title="Delete snapshot"
></paper-icon-button>
<paper-icon-button
on-click="_downloadClicked"
icon="hassio:download"
class="download"
title="Download snapshot"
></paper-icon-button>
<mwc-button on-click="_partialRestoreClicked"
>Restore selected</mwc-button
>
<div>Actions:</div>
<ul class="buttons">
<li>
<mwc-button on-click="_downloadClicked">
<iron-icon icon="hassio:download" class="icon"></iron-icon>
Download Snapshot
</mwc-button>
</li>
<li>
<mwc-button on-click="_partialRestoreClicked">
<iron-icon icon="hassio:history" class="icon"> </iron-icon>
Restore Selected
</mwc-button>
</li>
<template is="dom-if" if="[[_isFullSnapshot(snapshot.type)]]">
<mwc-button on-click="_fullRestoreClicked"
>Wipe &amp; restore</mwc-button
>
<li>
<mwc-button on-click="_fullRestoreClicked">
<iron-icon icon="hassio:history" class="icon"> </iron-icon>
Wipe &amp; restore
</mwc-button>
</li>
</template>
</div>
<li>
<mwc-button on-click="_deleteClicked">
<iron-icon icon="hassio:delete" class="icon warning"> </iron-icon>
<span class="warning">Delete Snapshot</span>
</mwc-button>
</li>
</ul>
</ha-paper-dialog>
`;
}

View File

@@ -3,7 +3,7 @@ import { PolymerElement } from "@polymer/polymer";
import "@polymer/paper-icon-button";
import "../../src/resources/ha-style";
import applyThemesOnElement from "../../src/common/dom/apply_themes_on_element";
import { applyThemesOnElement } from "../../src/common/dom/apply_themes_on_element";
import { fireEvent } from "../../src/common/dom/fire_event";
import {
HassRouterPage,

View File

@@ -1,85 +1,11 @@
const webpack = require("webpack");
const CompressionPlugin = require("compression-webpack-plugin");
const zopfli = require("@gfx/zopfli");
const { createHassioConfig } = require("../build-scripts/webpack.js");
const { isProdBuild } = require("../build-scripts/env.js");
const config = require("./config.js");
const webpackBase = require("../build-scripts/webpack.js");
const { babelLoaderConfig } = require("../build-scripts/babel.js");
// File just used for stats builds
const isProdBuild = process.env.NODE_ENV === "production";
const isCI = process.env.CI === "true";
const chunkFilename = isProdBuild ? "chunk.[chunkhash].js" : "[name].chunk.js";
const latestBuild = false;
const rules = [
{
exclude: [config.nodeDir],
test: /\.ts$/,
use: [
{
loader: "ts-loader",
options: {
compilerOptions: latestBuild
? { noEmit: false }
: {
target: "es5",
noEmit: false,
},
},
},
],
},
{
test: /\.(html)$/,
use: {
loader: "html-loader",
options: {
exportAsEs6Default: true,
},
},
},
];
if (!latestBuild) {
rules.push(babelLoaderConfig({ latestBuild }));
}
module.exports = {
mode: isProdBuild ? "production" : "development",
devtool: isProdBuild ? "source-map" : "inline-source-map",
entry: {
entrypoint: "./src/entrypoint.js",
},
module: {
rules,
},
optimization: webpackBase.optimization(latestBuild),
plugins: [
new webpack.DefinePlugin({
__DEV__: JSON.stringify(!isProdBuild),
__DEMO__: false,
__BUILD__: JSON.stringify(latestBuild ? "latest" : "es5"),
"process.env.NODE_ENV": JSON.stringify(
isProdBuild ? "production" : "development"
),
}),
isProdBuild &&
!isCI &&
new CompressionPlugin({
cache: true,
exclude: [/\.js\.map$/, /\.LICENSE$/, /\.py$/, /\.txt$/],
algorithm(input, compressionOptions, callback) {
return zopfli.gzip(input, compressionOptions, callback);
},
}),
].filter(Boolean),
resolve: {
extensions: [".ts", ".js", ".json"],
},
output: {
filename: "[name].js",
chunkFilename,
path: config.buildDir,
publicPath: `${config.publicPath}/`,
},
};
module.exports = createHassioConfig({
isProdBuild,
latestBuild,
});

View File

@@ -25,7 +25,7 @@
"@material/mwc-fab": "^0.8.0",
"@material/mwc-ripple": "^0.8.0",
"@material/mwc-switch": "^0.8.0",
"@mdi/svg": "4.4.95",
"@mdi/svg": "4.5.95",
"@polymer/app-layout": "^3.0.2",
"@polymer/app-localize-behavior": "^3.0.1",
"@polymer/app-route": "^3.0.2",
@@ -74,7 +74,7 @@
"@webcomponents/webcomponentsjs": "^2.2.7",
"chart.js": "~2.8.0",
"chartjs-chart-timeline": "^0.3.0",
"codemirror": "^5.45.0",
"codemirror": "^5.49.0",
"cpx": "^1.5.0",
"deep-clone-simple": "^1.1.1",
"es6-object-assign": "^1.1.0",
@@ -84,7 +84,6 @@
"hls.js": "^0.12.4",
"home-assistant-js-websocket": "^4.4.0",
"intl-messageformat": "^2.2.0",
"jquery": "^3.4.0",
"js-yaml": "^3.13.1",
"leaflet": "^1.4.0",
"lit-element": "^2.2.1",
@@ -98,7 +97,6 @@
"react-big-calendar": "^0.20.4",
"regenerator-runtime": "^0.13.2",
"roboto-fontface": "^0.10.0",
"round-slider": "^1.3.3",
"superstruct": "^0.6.1",
"tslib": "^1.10.0",
"unfetch": "^4.1.0",
@@ -112,18 +110,21 @@
"@babel/plugin-proposal-decorators": "^7.4.0",
"@babel/plugin-proposal-object-rest-spread": "^7.4.0",
"@babel/plugin-syntax-dynamic-import": "^7.2.0",
"@babel/preset-env": "^7.4.0",
"@gfx/zopfli": "^1.0.11",
"@babel/plugin-transform-react-jsx": "^7.3.0",
"@babel/preset-env": "^7.4.2",
"@babel/preset-typescript": "^7.4.0",
"@types/chai": "^4.1.7",
"@types/chromecast-caf-receiver": "^3.0.12",
"@types/chromecast-caf-sender": "^1.0.1",
"@types/codemirror": "^0.0.78",
"@types/hls.js": "^0.12.3",
"@types/js-yaml": "^3.12.1",
"@types/leaflet": "^1.4.3",
"@types/memoize-one": "4.1.0",
"@types/mocha": "^5.2.6",
"@types/webspeechapi": "^0.0.29",
"babel-loader": "^8.0.5",
"chai": "^4.2.0",
"compression-webpack-plugin": "^2.0.0",
"copy-webpack-plugin": "^5.0.2",
"del": "^4.0.0",
"eslint": "^6.3.0",
@@ -159,7 +160,6 @@
"require-dir": "^1.2.0",
"sinon": "^7.3.1",
"terser-webpack-plugin": "^1.2.3",
"ts-loader": "^6.1.1",
"ts-mocha": "^6.0.0",
"tslint": "^5.14.0",
"tslint-config-prettier": "^1.18.0",

View File

@@ -29,11 +29,13 @@ mkdir -p ${LOCAL_DIR}
docker run \
-v ${LOCAL_DIR}:/opt/dest/locale \
--rm \
lokalise/lokalise-cli@sha256:b8329d20280263cad04f65b843e54b9e8e6909a348a678eac959550b5ef5c75f lokalise \
lokalise/lokalise-cli-2@sha256:f1860b26be22fa73b8c93bc5f8690f2afc867610a42de6fc27adc790e5d4425d lokalise2 \
--token ${LOKALISE_TOKEN} \
export ${PROJECT_ID} \
--export_empty skip \
--type json \
--unzip_to /opt/dest
--project-id ${PROJECT_ID} \
file download \
--export-empty-as skip \
--format json \
--original-filenames=false \
--unzip-to /opt/dest
./node_modules/.bin/gulp check-downloaded-translations

View File

@@ -33,10 +33,11 @@ fi
docker run \
-v ${LOCAL_FILE}:/opt/src/${LOCAL_FILE} \
lokalise/lokalise-cli@sha256:2198814ebddfda56ee041a4b427521757dd57f75415ea9693696a64c550cef21 lokalise \
lokalise/lokalise-cli-2@sha256:f1860b26be22fa73b8c93bc5f8690f2afc867610a42de6fc27adc790e5d4425d lokalise2 \
--token ${LOKALISE_TOKEN} \
import ${PROJECT_ID} \
--project-id ${PROJECT_ID} \
file upload \
--file /opt/src/${LOCAL_FILE} \
--lang_iso ${LANG_ISO} \
--convert_placeholders 0 \
--replace 1
--lang-iso ${LANG_ISO} \
--convert-placeholders=false \
--replace-modified=true

View File

@@ -2,7 +2,7 @@ from setuptools import setup, find_packages
setup(
name="home-assistant-frontend",
version="20191002.2",
version="20191114.0",
description="The Home Assistant frontend",
url="https://github.com/home-assistant/home-assistant-polymer",
author="The Home Assistant Authors",

View File

@@ -7,7 +7,7 @@ import {
css,
} from "lit-element";
import "@material/mwc-button";
import "../components/ha-form";
import "../components/ha-form/ha-form";
import "../components/ha-markdown";
import { litLocalizeLiteMixin } from "../mixins/lit-localize-lite-mixin";
import { AuthProvider } from "../data/auth";

View File

@@ -2,10 +2,10 @@ import { litLocalizeLiteMixin } from "../mixins/lit-localize-lite-mixin";
import {
LitElement,
html,
PropertyDeclarations,
PropertyValues,
CSSResult,
css,
property,
} from "lit-element";
import "./ha-auth-flow";
import { AuthProvider, fetchAuthProviders } from "../data/auth";
@@ -20,11 +20,11 @@ interface QueryParams {
}
class HaAuthorize extends litLocalizeLiteMixin(LitElement) {
public clientId?: string;
public redirectUri?: string;
public oauth2State?: string;
private _authProvider?: AuthProvider;
private _authProviders?: AuthProvider[];
@property() public clientId?: string;
@property() public redirectUri?: string;
@property() public oauth2State?: string;
@property() private _authProvider?: AuthProvider;
@property() private _authProviders?: AuthProvider[];
constructor() {
super();
@@ -48,16 +48,6 @@ class HaAuthorize extends litLocalizeLiteMixin(LitElement) {
}
}
static get properties(): PropertyDeclarations {
return {
_authProvider: {},
_authProviders: {},
clientId: {},
redirectUri: {},
oauth2State: {},
};
}
protected render() {
if (!this._authProviders) {
return html`

View File

@@ -1,31 +0,0 @@
import { html } from "@polymer/polymer/lib/utils/html-tag";
import { PolymerElement } from "@polymer/polymer/polymer-element";
import "../components/entity/ha-state-label-badge";
class HaBadgesCard extends PolymerElement {
static get template() {
return html`
<style>
ha-state-label-badge {
display: inline-block;
margin-bottom: var(--ha-state-label-badge-margin-bottom, 16px);
}
</style>
<template is="dom-repeat" items="[[states]]">
<ha-state-label-badge
hass="[[hass]]"
state="[[item]]"
></ha-state-label-badge>
</template>
`;
}
static get properties() {
return {
hass: Object,
states: Array,
};
}
}
customElements.define("ha-badges-card", HaBadgesCard);

View File

@@ -0,0 +1,45 @@
import { TemplateResult, html } from "lit-html";
import { customElement, LitElement, property } from "lit-element";
import { HassEntity } from "home-assistant-js-websocket";
import "../components/entity/ha-state-label-badge";
import { HomeAssistant } from "../types";
import { fireEvent } from "../common/dom/fire_event";
@customElement("ha-badges-card")
class HaBadgesCard extends LitElement {
@property() public hass?: HomeAssistant;
@property() public states?: HassEntity[];
protected render(): TemplateResult | void {
if (!this.hass || !this.states) {
return html``;
}
return html`
${this.states.map(
(state) => html`
<ha-state-label-badge
.hass=${this.hass}
.state=${state}
@click=${this._handleClick}
></ha-state-label-badge>
`
)}
`;
}
private _handleClick(ev: Event) {
const entityId = ((ev.target as any).state as HassEntity).entity_id;
fireEvent(this, "hass-more-info", {
entityId,
});
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-badges-card": HaBadgesCard;
}
}

View File

@@ -1,9 +1,7 @@
import { HomeAssistant } from "../../types";
/** Return if a component is loaded. */
export default function isComponentLoaded(
export const isComponentLoaded = (
hass: HomeAssistant,
component: string
): boolean {
return hass && hass.config.components.indexOf(component) !== -1;
}
): boolean => hass && hass.config.components.indexOf(component) !== -1;

View File

@@ -35,6 +35,7 @@ export const DOMAINS_WITH_MORE_INFO = [
"camera",
"climate",
"configurator",
"counter",
"cover",
"fan",
"group",

View File

@@ -21,12 +21,12 @@ const hexToRgb = (hex: string): string | null => {
* localTheme: selected theme.
* updateMeta: boolean if we should update the theme-color meta element.
*/
export default function applyThemesOnElement(
export const applyThemesOnElement = (
element,
themes,
localTheme,
updateMeta = false
) {
) => {
if (!element._themes) {
element._themes = {};
}
@@ -76,4 +76,4 @@ export default function applyThemesOnElement(
styles["--primary-color"] || meta.getAttribute("default-content");
meta.setAttribute("content", themeColor);
}
}
};

View File

@@ -0,0 +1,14 @@
/* tslint:disable */
// @ts-ignore
export const SpeechRecognition =
// @ts-ignore
window.SpeechRecognition || window.webkitSpeechRecognition;
// @ts-ignore
export const SpeechGrammarList =
// @ts-ignore
window.SpeechGrammarList || window.webkitSpeechGrammarList;
// @ts-ignore
export const SpeechRecognitionEvent =
// @ts-ignore
window.SpeechRecognitionEvent || window.webkitSpeechRecognitionEvent;
/* tslint:enable */

View File

@@ -14,6 +14,7 @@ const fixedIcons = {
climate: "hass:thermostat",
configurator: "hass:settings",
conversation: "hass:text-to-speech",
counter: "hass:counter",
device_tracker: "hass:account",
fan: "hass:fan",
google_assistant: "hass:google-assistant",

21
src/common/util/patch.ts Normal file
View File

@@ -0,0 +1,21 @@
export const applyPatch = (data, path, value) => {
if (path.length === 1) {
data[path[0]] = value;
} else {
if (!data[path[0]]) {
data[path[0]] = {};
}
return applyPatch(data[path[0]], path.slice(1), value);
}
};
export const getPath = (data, path) => {
if (path.length === 1) {
return data[path[0]];
} else {
if (data[path[0]] === undefined) {
return undefined;
}
return getPath(data[path[0]], path.slice(1));
}
};

View File

@@ -3,6 +3,7 @@ import { PolymerElement } from "@polymer/polymer/polymer-element";
import "./ha-progress-button";
import { EventsMixin } from "../../mixins/events-mixin";
import { showConfirmationDialog } from "../../dialogs/confirmation/show-dialog-confirmation";
/*
* @appliesMixin EventsMixin
@@ -49,10 +50,7 @@ class HaCallServiceButton extends EventsMixin(PolymerElement) {
};
}
buttonTapped() {
if (this.confirmation && !window.confirm(this.confirmation)) {
return;
}
callService() {
this.progress = true;
var el = this;
var eventData = {
@@ -79,6 +77,17 @@ class HaCallServiceButton extends EventsMixin(PolymerElement) {
el.fire("hass-service-called", eventData);
});
}
buttonTapped() {
if (this.confirmation) {
showConfirmationDialog(this, {
text: this.confirmation,
confirm: () => this.callService(),
});
} else {
this.callService();
}
}
}
customElements.define("ha-call-service-button", HaCallServiceButton);

View File

@@ -59,31 +59,31 @@ export interface SortingChangedEvent {
export type SortingDirection = "desc" | "asc" | null;
export interface DataTabelColumnContainer {
[key: string]: DataTabelColumnData;
export interface DataTableColumnContainer {
[key: string]: DataTableColumnData;
}
export interface DataTabelSortColumnData {
export interface DataTableSortColumnData {
sortable?: boolean;
filterable?: boolean;
filterKey?: string;
direction?: SortingDirection;
}
export interface DataTabelColumnData extends DataTabelSortColumnData {
export interface DataTableColumnData extends DataTableSortColumnData {
title: string;
type?: "numeric";
template?: (data: any) => TemplateResult;
type?: "numeric" | "icon";
template?: <T>(data: any, row: T) => TemplateResult;
}
export interface DataTabelRowData {
export interface DataTableRowData {
[key: string]: any;
}
@customElement("ha-data-table")
export class HaDataTable extends BaseElement {
@property({ type: Object }) public columns: DataTabelColumnContainer = {};
@property({ type: Array }) public data: DataTabelRowData[] = [];
@property({ type: Object }) public columns: DataTableColumnContainer = {};
@property({ type: Array }) public data: DataTableRowData[] = [];
@property({ type: Boolean }) public selectable = false;
@property({ type: String }) public id = "id";
protected mdcFoundation!: MDCDataTableFoundation;
@@ -98,9 +98,9 @@ export class HaDataTable extends BaseElement {
@property({ type: String }) private _filter = "";
@property({ type: String }) private _sortColumn?: string;
@property({ type: String }) private _sortDirection: SortingDirection = null;
@property({ type: Array }) private _filteredData: DataTabelRowData[] = [];
@property({ type: Array }) private _filteredData: DataTableRowData[] = [];
private _sortColumns: {
[key: string]: DataTabelSortColumnData;
[key: string]: DataTableSortColumnData;
} = {};
private curRequest = 0;
private _worker: any | undefined;
@@ -134,8 +134,8 @@ export class HaDataTable extends BaseElement {
}
}
const clonedColumns: DataTabelColumnContainer = deepClone(this.columns);
Object.values(clonedColumns).forEach((column: DataTabelColumnData) => {
const clonedColumns: DataTableColumnContainer = deepClone(this.columns);
Object.values(clonedColumns).forEach((column: DataTableColumnData) => {
delete column.title;
delete column.type;
delete column.template;
@@ -190,9 +190,12 @@ export class HaDataTable extends BaseElement {
const [key, column] = columnEntry;
const sorted = key === this._sortColumn;
const classes = {
"mdc-data-table__cell--numeric": Boolean(
"mdc-data-table__header-cell--numeric": Boolean(
column.type && column.type === "numeric"
),
"mdc-data-table__header-cell--icon": Boolean(
column.type && column.type === "icon"
),
sortable: Boolean(column.sortable),
"not-sorted": Boolean(column.sortable && !sorted),
};
@@ -222,8 +225,8 @@ export class HaDataTable extends BaseElement {
<tbody class="mdc-data-table__content">
${repeat(
this._filteredData!,
(row: DataTabelRowData) => row[this.id],
(row: DataTabelRowData) => html`
(row: DataTableRowData) => row[this.id],
(row: DataTableRowData) => html`
<tr
data-row-id="${row[this.id]}"
@click=${this._handleRowClick}
@@ -251,10 +254,13 @@ export class HaDataTable extends BaseElement {
"mdc-data-table__cell--numeric": Boolean(
column.type && column.type === "numeric"
),
"mdc-data-table__cell--icon": Boolean(
column.type && column.type === "icon"
),
})}"
>
${column.template
? column.template(row[key])
? column.template(row[key], row)
: row[key]}
</td>
`;
@@ -421,7 +427,7 @@ export class HaDataTable extends BaseElement {
}
.mdc-data-table {
background-color: var(--card-background-color);
background-color: var(--data-table-background-color);
border-radius: 4px;
border-width: 1px;
border-style: solid;
@@ -516,6 +522,11 @@ export class HaDataTable extends BaseElement {
text-align: left;
}
.mdc-data-table__cell--icon {
color: var(--secondary-text-color);
text-align: center;
}
.mdc-data-table__header-cell {
font-family: Roboto, sans-serif;
-moz-osx-font-smoothing: grayscale;
@@ -543,6 +554,10 @@ export class HaDataTable extends BaseElement {
text-align: left;
}
.mdc-data-table__header-cell--icon {
text-align: center;
}
/* custom from here */
.mdc-data-table {
@@ -554,7 +569,7 @@ export class HaDataTable extends BaseElement {
.mdc-data-table__header-cell.sortable {
cursor: pointer;
}
.mdc-data-table__header-cell.not-sorted:not(.mdc-data-table__cell--numeric)
.mdc-data-table__header-cell.not-sorted:not(.mdc-data-table__header-cell--numeric):not(.mdc-data-table__header-cell--icon)
span {
position: relative;
left: -24px;
@@ -565,7 +580,7 @@ export class HaDataTable extends BaseElement {
.mdc-data-table__header-cell.not-sorted ha-icon {
left: -36px;
}
.mdc-data-table__header-cell.not-sorted:not(.mdc-data-table__cell--numeric):hover
.mdc-data-table__header-cell.not-sorted:not(.mdc-data-table__header-cell--numeric):not(.mdc-data-table__header-cell--icon):hover
span {
left: 0px;
}

View File

@@ -1,7 +1,7 @@
import {
DataTabelColumnContainer,
DataTabelColumnData,
DataTabelRowData,
DataTableColumnContainer,
DataTableColumnData,
DataTableRowData,
SortingDirection,
} from "./ha-data-table";
@@ -9,8 +9,8 @@ import memoizeOne from "memoize-one";
export const filterSortData = memoizeOne(
async (
data: DataTabelRowData[],
columns: DataTabelColumnContainer,
data: DataTableRowData[],
columns: DataTableColumnContainer,
filter: string,
direction: SortingDirection,
sortColumn?: string
@@ -27,8 +27,8 @@ export const filterSortData = memoizeOne(
const _memFilterData = memoizeOne(
async (
data: DataTabelRowData[],
columns: DataTabelColumnContainer,
data: DataTableRowData[],
columns: DataTableColumnContainer,
filter: string
) => {
if (!filter) {
@@ -40,8 +40,8 @@ const _memFilterData = memoizeOne(
const _memSortData = memoizeOne(
(
data: DataTabelRowData[],
columns: DataTabelColumnContainer,
data: DataTableRowData[],
columns: DataTableColumnContainer,
direction: SortingDirection,
sortColumn: string
) => {
@@ -50,8 +50,8 @@ const _memSortData = memoizeOne(
);
export const filterData = (
data: DataTabelRowData[],
columns: DataTabelColumnContainer,
data: DataTableRowData[],
columns: DataTableColumnContainer,
filter: string
) =>
data.filter((row) => {
@@ -71,8 +71,8 @@ export const filterData = (
});
export const sortData = (
data: DataTabelRowData[],
column: DataTabelColumnData,
data: DataTableRowData[],
column: DataTableColumnData,
direction: SortingDirection,
sortColumn: string
) =>

View File

@@ -1,7 +1,7 @@
import "@polymer/paper-input/paper-input";
import "@polymer/paper-item/paper-item";
import "@polymer/paper-item/paper-item-body";
import "@polymer/paper-dropdown-menu/paper-dropdown-menu-light";
import "@vaadin/vaadin-combo-box/vaadin-combo-box-light";
import "@polymer/paper-listbox/paper-listbox";
import memoizeOne from "memoize-one";
import {
@@ -21,83 +21,295 @@ import { fireEvent } from "../../common/dom/fire_event";
import {
DeviceRegistryEntry,
subscribeDeviceRegistry,
computeDeviceName,
} from "../../data/device_registry";
import { compare } from "../../common/string/compare";
import { PolymerChangedEvent } from "../../polymer-types";
import {
AreaRegistryEntry,
subscribeAreaRegistry,
} from "../../data/area_registry";
import { DeviceEntityLookup } from "../../panels/config/devices/ha-devices-data-table";
import {
EntityRegistryEntry,
subscribeEntityRegistry,
} from "../../data/entity_registry";
import { computeDomain } from "../../common/entity/compute_domain";
interface Device {
name: string;
area: string;
id: string;
}
const rowRenderer = (root: HTMLElement, _owner, model: { item: Device }) => {
if (!root.firstElementChild) {
root.innerHTML = `
<style>
paper-item {
margin: -10px 0;
padding: 0;
}
</style>
<paper-item>
<paper-item-body two-line="">
<div class='name'>[[item.name]]</div>
<div secondary>[[item.area]]</div>
</paper-item-body>
</paper-item>
`;
}
root.querySelector(".name")!.textContent = model.item.name!;
root.querySelector("[secondary]")!.textContent = model.item.area!;
};
@customElement("ha-device-picker")
class HaDevicePicker extends SubscribeMixin(LitElement) {
@property() public hass?: HomeAssistant;
export class HaDevicePicker extends SubscribeMixin(LitElement) {
@property() public hass!: HomeAssistant;
@property() public label?: string;
@property() public value?: string;
@property() public devices?: DeviceRegistryEntry[];
@property() public areas?: AreaRegistryEntry[];
@property() public entities?: EntityRegistryEntry[];
/**
* Show only devices with entities from specific domains.
* @type {Array}
* @attr include-domains
*/
@property({ type: Array, attribute: "include-domains" })
public includeDomains?: string[];
/**
* Show no devices with entities of these domains.
* @type {Array}
* @attr exclude-domains
*/
@property({ type: Array, attribute: "exclude-domains" })
public excludeDomains?: string[];
/**
* Show only deviced with entities of these device classes.
* @type {Array}
* @attr include-device-classes
*/
@property({ type: Array, attribute: "include-device-classes" })
public includeDeviceClasses?: string[];
@property({ type: Boolean })
private _opened?: boolean;
private _sortedDevices = memoizeOne((devices?: DeviceRegistryEntry[]) => {
if (!devices || devices.length === 1) {
return devices || [];
private _getDevices = memoizeOne(
(
devices: DeviceRegistryEntry[],
areas: AreaRegistryEntry[],
entities: EntityRegistryEntry[],
includeDomains: this["includeDomains"],
excludeDomains: this["excludeDomains"],
includeDeviceClasses: this["includeDeviceClasses"]
): Device[] => {
if (!devices.length) {
return [];
}
const deviceEntityLookup: DeviceEntityLookup = {};
for (const entity of entities) {
if (!entity.device_id) {
continue;
}
if (!(entity.device_id in deviceEntityLookup)) {
deviceEntityLookup[entity.device_id] = [];
}
deviceEntityLookup[entity.device_id].push(entity);
}
const areaLookup: { [areaId: string]: AreaRegistryEntry } = {};
for (const area of areas) {
areaLookup[area.area_id] = area;
}
let inputDevices = [...devices];
if (includeDomains) {
inputDevices = inputDevices.filter((device) => {
const devEntities = deviceEntityLookup[device.id];
if (!devEntities || !devEntities.length) {
return false;
}
return deviceEntityLookup[device.id].some((entity) =>
includeDomains.includes(computeDomain(entity.entity_id))
);
});
}
if (excludeDomains) {
inputDevices = inputDevices.filter((device) => {
const devEntities = deviceEntityLookup[device.id];
if (!devEntities || !devEntities.length) {
return true;
}
return entities.every(
(entity) =>
!excludeDomains.includes(computeDomain(entity.entity_id))
);
});
}
if (includeDeviceClasses) {
inputDevices = inputDevices.filter((device) => {
const devEntities = deviceEntityLookup[device.id];
if (!devEntities || !devEntities.length) {
return false;
}
return deviceEntityLookup[device.id].some((entity) => {
const stateObj = this.hass.states[entity.entity_id];
if (!stateObj) {
return false;
}
return (
stateObj.attributes.device_class &&
includeDeviceClasses.includes(stateObj.attributes.device_class)
);
});
});
}
const outputDevices = inputDevices.map((device) => {
return {
id: device.id,
name: computeDeviceName(
device,
this.hass,
deviceEntityLookup[device.id]
),
area: device.area_id ? areaLookup[device.area_id].name : "No area",
};
});
if (outputDevices.length === 1) {
return outputDevices;
}
return outputDevices.sort((a, b) => compare(a.name || "", b.name || ""));
}
const sorted = [...devices];
sorted.sort((a, b) => compare(a.name || "", b.name || ""));
return sorted;
});
);
public hassSubscribe(): UnsubscribeFunc[] {
return [
subscribeDeviceRegistry(this.hass!.connection!, (devices) => {
subscribeDeviceRegistry(this.hass.connection!, (devices) => {
this.devices = devices;
}),
subscribeAreaRegistry(this.hass.connection!, (areas) => {
this.areas = areas;
}),
subscribeEntityRegistry(this.hass.connection!, (entities) => {
this.entities = entities;
}),
];
}
protected render(): TemplateResult | void {
if (!this.devices || !this.areas || !this.entities) {
return;
}
const devices = this._getDevices(
this.devices,
this.areas,
this.entities,
this.includeDomains,
this.excludeDomains,
this.includeDeviceClasses
);
return html`
<paper-dropdown-menu-light .label=${this.label}>
<paper-listbox
slot="dropdown-content"
.selected=${this._value}
attr-for-selected="data-device-id"
@iron-select=${this._deviceChanged}
<vaadin-combo-box-light
item-value-path="id"
item-id-path="id"
item-label-path="name"
.items=${devices}
.value=${this._value}
.renderer=${rowRenderer}
@opened-changed=${this._openedChanged}
@value-changed=${this._deviceChanged}
>
<paper-input
.label=${this.label === undefined && this.hass
? this.hass.localize("ui.components.device-picker.device")
: this.label}
class="input"
autocapitalize="none"
autocomplete="off"
autocorrect="off"
spellcheck="false"
>
<paper-item data-device-id="">
No device
</paper-item>
${this._sortedDevices(this.devices).map(
(device) => html`
<paper-item data-device-id=${device.id}>
${device.name_by_user || device.name}
</paper-item>
`
)}
</paper-listbox>
</paper-dropdown-menu-light>
${this.value
? html`
<paper-icon-button
aria-label=${this.hass.localize(
"ui.components.device-picker.clear"
)}
slot="suffix"
class="clear-button"
icon="hass:close"
@click=${this._clearValue}
no-ripple
>
Clear
</paper-icon-button>
`
: ""}
${devices.length > 0
? html`
<paper-icon-button
aria-label=${this.hass.localize(
"ui.components.device-picker.show_devices"
)}
slot="suffix"
class="toggle-button"
.icon=${this._opened ? "hass:menu-up" : "hass:menu-down"}
>
Toggle
</paper-icon-button>
`
: ""}
</paper-input>
</vaadin-combo-box-light>
`;
}
private _clearValue(ev: Event) {
ev.stopPropagation();
this._setValue("");
}
private get _value() {
return this.value || "";
}
private _deviceChanged(ev) {
const newValue = ev.detail.item.dataset.deviceId;
private _openedChanged(ev: PolymerChangedEvent<boolean>) {
this._opened = ev.detail.value;
}
private _deviceChanged(ev: PolymerChangedEvent<string>) {
const newValue = ev.detail.value;
if (newValue !== this._value) {
this.value = newValue;
setTimeout(() => {
fireEvent(this, "value-changed", { value: newValue });
fireEvent(this, "change");
}, 0);
this._setValue(newValue);
}
}
private _setValue(value: string) {
this.value = value;
setTimeout(() => {
fireEvent(this, "value-changed", { value });
fireEvent(this, "change");
}, 0);
}
static get styles(): CSSResult {
return css`
paper-dropdown-menu-light {
width: 100%;
paper-input > paper-icon-button {
width: 24px;
height: 24px;
padding: 2px;
color: var(--secondary-text-color);
}
paper-listbox {
min-width: 200px;
}
paper-item {
cursor: pointer;
[hidden] {
display: none;
}
`;
}

View File

@@ -22,7 +22,20 @@ import { HassEntity } from "home-assistant-js-websocket";
class HaEntitiesPickerLight extends LitElement {
@property() public hass?: HomeAssistant;
@property() public value?: string[];
@property({ attribute: "domain-filter" }) public domainFilter?: string;
/**
* Show entities from specific domains.
* @type {string}
* @attr include-domains
*/
@property({ type: Array, attribute: "include-domains" })
public includeDomains?: string[];
/**
* Show no entities of these domains.
* @type {Array}
* @attr exclude-domains
*/
@property({ type: Array, attribute: "exclude-domains" })
public excludeDomains?: string[];
@property({ attribute: "picked-entity-label" })
public pickedEntityLabel?: string;
@property({ attribute: "pick-entity-label" }) public pickEntityLabel?: string;
@@ -31,6 +44,7 @@ class HaEntitiesPickerLight extends LitElement {
if (!this.hass) {
return;
}
const currentEntities = this._currentEntities;
return html`
${currentEntities.map(
@@ -40,7 +54,8 @@ class HaEntitiesPickerLight extends LitElement {
allow-custom-entity
.curValue=${entityId}
.hass=${this.hass}
.domainFilter=${this.domainFilter}
.includeDomains=${this.includeDomains}
.excludeDomains=${this.excludeDomains}
.entityFilter=${this._entityFilter}
.value=${entityId}
.label=${this.pickedEntityLabel}
@@ -52,7 +67,8 @@ class HaEntitiesPickerLight extends LitElement {
<div>
<ha-entity-picker
.hass=${this.hass}
.domainFilter=${this.domainFilter}
.includeDomains=${this.includeDomains}
.excludeDomains=${this.excludeDomains}
.entityFilter=${this._entityFilter}
.label=${this.pickEntityLabel}
@value-changed=${this._addEntity}

View File

@@ -21,6 +21,7 @@ import { HomeAssistant } from "../../types";
import { HassEntity } from "home-assistant-js-websocket";
import { PolymerChangedEvent } from "../../polymer-types";
import { fireEvent } from "../../common/dom/fire_event";
import { computeDomain } from "../../common/entity/compute_domain";
export type HaEntityPickerEntityFilterFunc = (entityId: HassEntity) => boolean;
@@ -60,7 +61,27 @@ class HaEntityPicker extends LitElement {
@property() public hass?: HomeAssistant;
@property() public label?: string;
@property() public value?: string;
@property({ attribute: "domain-filter" }) public domainFilter?: string;
/**
* Show entities from specific domains.
* @type {Array}
* @attr include-domains
*/
@property({ type: Array, attribute: "include-domains" })
public includeDomains?: string[];
/**
* Show no entities of these domains.
* @type {Array}
* @attr exclude-domains
*/
@property({ type: Array, attribute: "exclude-domains" })
public excludeDomains?: string[];
/**
* Show only entities of these device classes.
* @type {Array}
* @attr include-device-classes
*/
@property({ type: Array, attribute: "include-device-classes" })
public includeDeviceClasses?: string[];
@property() public entityFilter?: HaEntityPickerEntityFilterFunc;
@property({ type: Boolean }) private _opened?: boolean;
@property() private _hass?: HomeAssistant;
@@ -68,8 +89,10 @@ class HaEntityPicker extends LitElement {
private _getStates = memoizeOne(
(
hass: this["hass"],
domainFilter: this["domainFilter"],
entityFilter: this["entityFilter"]
includeDomains: this["includeDomains"],
excludeDomains: this["excludeDomains"],
entityFilter: this["entityFilter"],
includeDeviceClasses: this["includeDeviceClasses"]
) => {
let states: HassEntity[] = [];
@@ -78,14 +101,30 @@ class HaEntityPicker extends LitElement {
}
let entityIds = Object.keys(hass.states);
if (domainFilter) {
if (includeDomains) {
entityIds = entityIds.filter((eid) =>
includeDomains.includes(computeDomain(eid))
);
}
if (excludeDomains) {
entityIds = entityIds.filter(
(eid) => eid.substr(0, eid.indexOf(".")) === domainFilter
(eid) => !excludeDomains.includes(computeDomain(eid))
);
}
states = entityIds.sort().map((key) => hass!.states[key]);
if (includeDeviceClasses) {
states = states.filter(
(stateObj) =>
// We always want to include the entity of the current value
stateObj.entity_id === this.value ||
(stateObj.attributes.device_class &&
includeDeviceClasses.includes(stateObj.attributes.device_class))
);
}
if (entityFilter) {
states = states.filter(
(stateObj) =>
@@ -93,6 +132,7 @@ class HaEntityPicker extends LitElement {
stateObj.entity_id === this.value || entityFilter!(stateObj)
);
}
return states;
}
);
@@ -108,8 +148,10 @@ class HaEntityPicker extends LitElement {
protected render(): TemplateResult | void {
const states = this._getStates(
this._hass,
this.domainFilter,
this.entityFilter
this.includeDomains,
this.excludeDomains,
this.entityFilter,
this.includeDeviceClasses
);
return html`
@@ -139,10 +181,13 @@ class HaEntityPicker extends LitElement {
${this.value
? html`
<paper-icon-button
aria-label="Clear"
aria-label=${this.hass!.localize(
"ui.components.entity.entity-picker.clear"
)}
slot="suffix"
class="clear-button"
icon="hass:close"
@click=${this._clearValue}
no-ripple
>
Clear
@@ -152,7 +197,9 @@ class HaEntityPicker extends LitElement {
${states.length > 0
? html`
<paper-icon-button
aria-label="Show entities"
aria-label=${this.hass!.localize(
"ui.components.entity.entity-picker.show_entities"
)}
slot="suffix"
class="toggle-button"
.icon=${this._opened ? "hass:menu-up" : "hass:menu-down"}
@@ -166,6 +213,11 @@ class HaEntityPicker extends LitElement {
`;
}
private _clearValue(ev: Event) {
ev.stopPropagation();
this._setValue("");
}
private get _value() {
return this.value || "";
}
@@ -177,14 +229,18 @@ class HaEntityPicker extends LitElement {
private _valueChanged(ev: PolymerChangedEvent<string>) {
const newValue = ev.detail.value;
if (newValue !== this._value) {
this.value = ev.detail.value;
setTimeout(() => {
fireEvent(this, "value-changed", { value: this.value });
fireEvent(this, "change");
}, 0);
this._setValue(newValue);
}
}
private _setValue(value: string) {
this.value = value;
setTimeout(() => {
fireEvent(this, "value-changed", { value });
fireEvent(this, "change");
}, 0);
}
static get styles(): CSSResult {
return css`
paper-input > paper-icon-button {

View File

@@ -11,7 +11,6 @@ import {
import { HassEntity } from "home-assistant-js-websocket";
import { classMap } from "lit-html/directives/class-map";
import { fireEvent } from "../../common/dom/fire_event";
import { HomeAssistant } from "../../types";
import { computeStateDomain } from "../../common/entity/compute_state_domain";
@@ -90,16 +89,6 @@ export class HaStateLabelBadge extends LitElement {
`;
}
protected firstUpdated(changedProperties: PropertyValues): void {
super.firstUpdated(changedProperties);
this.addEventListener("click", (ev) => {
ev.stopPropagation();
if (this.state) {
fireEvent(this, "hass-more-info", { entityId: this.state.entity_id });
}
});
}
protected updated(changedProperties: PropertyValues): void {
super.updated(changedProperties);

View File

@@ -1,8 +1,9 @@
import { Constructor, customElement, CSSResult, css } from "lit-element";
import { customElement, CSSResult, css } from "lit-element";
import "@material/mwc-checkbox";
// tslint:disable-next-line
import { Checkbox } from "@material/mwc-checkbox";
import { style } from "@material/mwc-checkbox/mwc-checkbox-css";
import { Constructor } from "../types";
// tslint:disable-next-line
const MwcCheckbox = customElements.get("mwc-checkbox") as Constructor<Checkbox>;

View File

@@ -0,0 +1,160 @@
import { loadCodeMirror } from "../resources/codemirror.ondemand";
import { fireEvent } from "../common/dom/fire_event";
import {
UpdatingElement,
property,
customElement,
PropertyValues,
} from "lit-element";
import { Editor } from "codemirror";
declare global {
interface HASSDomEvents {
"editor-save": undefined;
}
}
@customElement("ha-code-editor")
export class HaCodeEditor extends UpdatingElement {
public codemirror?: Editor;
@property() public mode?: string;
@property() public autofocus = false;
@property() public rtl = false;
@property() public error = false;
@property() private _value = "";
public set value(value: string) {
this._value = value;
}
public get value(): string {
return this.codemirror ? this.codemirror.getValue() : this._value;
}
public get hasComments(): boolean {
return this.shadowRoot!.querySelector("span.cm-comment") ? true : false;
}
public connectedCallback() {
super.connectedCallback();
if (!this.codemirror) {
return;
}
this.codemirror.refresh();
if (this.autofocus !== false) {
this.codemirror.focus();
}
}
protected update(changedProps: PropertyValues): void {
super.update(changedProps);
if (!this.codemirror) {
return;
}
if (changedProps.has("mode")) {
this.codemirror.setOption("mode", this.mode);
}
if (changedProps.has("autofocus")) {
this.codemirror.setOption("autofocus", this.autofocus !== false);
}
if (changedProps.has("_value") && this._value !== this.value) {
this.codemirror.setValue(this._value);
}
if (changedProps.has("rtl")) {
this.codemirror.setOption("gutters", this._calcGutters());
this._setScrollBarDirection();
}
if (changedProps.has("error")) {
this.classList.toggle("error-state", this.error);
}
}
protected firstUpdated(changedProps: PropertyValues): void {
super.firstUpdated(changedProps);
this._load();
}
private async _load(): Promise<void> {
const loaded = await loadCodeMirror();
const codeMirror = loaded.codeMirror;
const shadowRoot = this.attachShadow({ mode: "open" });
shadowRoot!.innerHTML = `
<style>
${loaded.codeMirrorCss}
.CodeMirror {
height: var(--code-mirror-height, auto);
direction: var(--code-mirror-direction, ltr);
}
.CodeMirror-scroll {
max-height: var(--code-mirror-max-height, --code-mirror-height);
}
.CodeMirror-gutters {
border-right: 1px solid var(--paper-input-container-color, var(--secondary-text-color));
background-color: var(--paper-dialog-background-color, var(--primary-background-color));
transition: 0.2s ease border-right;
}
:host(.error-state) .CodeMirror-gutters {
border-color: var(--error-state-color, red);
}
.CodeMirror-focused .CodeMirror-gutters {
border-right: 2px solid var(--paper-input-container-focus-color, var(--primary-color));
}
.CodeMirror-linenumber {
color: var(--paper-dialog-color, var(--primary-text-color));
}
.rtl .CodeMirror-vscrollbar {
right: auto;
left: 0px;
}
.rtl-gutter {
width: 20px;
}
</style>`;
this.codemirror = codeMirror(shadowRoot, {
value: this._value,
lineNumbers: true,
tabSize: 2,
mode: this.mode,
autofocus: this.autofocus !== false,
viewportMargin: Infinity,
extraKeys: {
Tab: "indentMore",
"Shift-Tab": "indentLess",
},
gutters: this._calcGutters(),
});
this._setScrollBarDirection();
this.codemirror!.on("changes", () => this._onChange());
}
private _onChange(): void {
const newValue = this.value;
if (newValue === this._value) {
return;
}
this._value = newValue;
fireEvent(this, "value-changed", { value: this._value });
}
private _calcGutters(): string[] {
return this.rtl ? ["rtl-gutter", "CodeMirror-linenumbers"] : [];
}
private _setScrollBarDirection(): void {
if (this.codemirror) {
this.codemirror.getWrapperElement().classList.toggle("rtl", this.rtl);
}
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-code-editor": HaCodeEditor;
}
}

View File

@@ -1,12 +1,8 @@
import {
classMap,
html,
customElement,
Constructor,
} from "@material/mwc-base/base-element";
import { classMap, html, customElement } from "@material/mwc-base/base-element";
import { ripple } from "@material/mwc-ripple/ripple-directive.js";
import "@material/mwc-fab";
import { Constructor } from "../types";
// tslint:disable-next-line
import { Fab } from "@material/mwc-fab";
// tslint:disable-next-line

View File

@@ -1,265 +0,0 @@
import "@polymer/paper-checkbox/paper-checkbox";
import "@polymer/paper-dropdown-menu/paper-dropdown-menu";
import "@polymer/paper-icon-button/paper-icon-button";
import "@polymer/paper-input/paper-input";
import "@polymer/paper-item/paper-item";
import "@polymer/paper-listbox/paper-listbox";
import { html } from "@polymer/polymer/lib/utils/html-tag";
import { PolymerElement } from "@polymer/polymer/polymer-element";
import "./ha-paper-slider";
import { EventsMixin } from "../mixins/events-mixin";
/*
* @appliesMixin EventsMixin
*/
class HaForm extends EventsMixin(PolymerElement) {
static get template() {
return html`
<style>
.error {
color: red;
}
paper-checkbox {
display: inline-block;
padding: 22px 0;
}
</style>
<template is="dom-if" if="[[_isArray(schema)]]" restamp="">
<template is="dom-if" if="[[error.base]]">
<div class="error">[[computeError(error.base, schema)]]</div>
</template>
<template is="dom-repeat" items="[[schema]]">
<ha-form
data="[[_getValue(data, item)]]"
schema="[[item]]"
error="[[_getValue(error, item)]]"
on-data-changed="_valueChanged"
compute-error="[[computeError]]"
compute-label="[[computeLabel]]"
compute-suffix="[[computeSuffix]]"
></ha-form>
</template>
</template>
<template is="dom-if" if="[[!_isArray(schema)]]" restamp="">
<template is="dom-if" if="[[error]]">
<div class="error">[[computeError(error, schema)]]</div>
</template>
<template
is="dom-if"
if='[[_equals(schema.type, "string")]]'
restamp=""
>
<template
is="dom-if"
if='[[_includes(schema.name, "password")]]'
restamp=""
>
<paper-input
type="[[_passwordFieldType(unmaskedPassword)]]"
label="[[computeLabel(schema)]]"
value="{{data}}"
required="[[schema.required]]"
auto-validate="[[schema.required]]"
error-message="Required"
>
<paper-icon-button
toggles
active="{{unmaskedPassword}}"
slot="suffix"
icon="[[_passwordFieldIcon(unmaskedPassword)]]"
id="iconButton"
title="Click to toggle between masked and clear password"
>
</paper-icon-button>
</paper-input>
</template>
<template
is="dom-if"
if='[[!_includes(schema.name, "password")]]'
restamp=""
>
<paper-input
label="[[computeLabel(schema)]]"
value="{{data}}"
required="[[schema.required]]"
auto-validate="[[schema.required]]"
error-message="Required"
></paper-input>
</template>
</template>
<template
is="dom-if"
if='[[_equals(schema.type, "integer")]]'
restamp=""
>
<template is="dom-if" if="[[_isRange(schema)]]" restamp="">
<div>
[[computeLabel(schema)]]
<ha-paper-slider
pin=""
value="{{data}}"
min="[[schema.valueMin]]"
max="[[schema.valueMax]]"
></ha-paper-slider>
</div>
</template>
<template is="dom-if" if="[[!_isRange(schema)]]" restamp="">
<paper-input
label="[[computeLabel(schema)]]"
value="{{data}}"
type="number"
required="[[schema.required]]"
auto-validate="[[schema.required]]"
error-message="Required"
></paper-input>
</template>
</template>
<template is="dom-if" if='[[_equals(schema.type, "float")]]' restamp="">
<!-- TODO -->
<paper-input
label="[[computeLabel(schema)]]"
value="{{data}}"
required="[[schema.required]]"
auto-validate="[[schema.required]]"
error-message="Required"
>
<span suffix="" slot="suffix">[[computeSuffix(schema)]]</span>
</paper-input>
</template>
<template
is="dom-if"
if='[[_equals(schema.type, "boolean")]]'
restamp=""
>
<div>
<paper-checkbox checked="{{data}}"
>[[computeLabel(schema)]]</paper-checkbox
>
</div>
</template>
<template
is="dom-if"
if='[[_equals(schema.type, "select")]]'
restamp=""
>
<paper-dropdown-menu label="[[computeLabel(schema)]]">
<paper-listbox
slot="dropdown-content"
attr-for-selected="item-name"
selected="{{data}}"
>
<template is="dom-repeat" items="[[schema.options]]">
<paper-item item-name$="[[_optionValue(item)]]"
>[[_optionLabel(item)]]</paper-item
>
</template>
</paper-listbox>
</paper-dropdown-menu>
</template>
</template>
`;
}
static get properties() {
return {
data: {
type: Object,
notify: true,
},
schema: Object,
error: Object,
// A function that computes the label to be displayed for a given
// schema object.
computeLabel: {
type: Function,
value: () => (schema) => schema && schema.name,
},
// A function that computes the suffix to be displayed for a given
// schema object.
computeSuffix: {
type: Function,
value: () => (schema) =>
schema &&
schema.description &&
schema.description.unit_of_measurement,
},
// A function that computes an error message to be displayed for a
// given error ID, and relevant schema object
computeError: {
type: Function,
value: () => (error, schema) => error, // eslint-disable-line no-unused-vars
},
};
}
focus() {
const input = this.shadowRoot.querySelector(
"ha-form, paper-input, ha-paper-slider, paper-checkbox, paper-dropdown-menu"
);
if (!input) {
return;
}
input.focus();
}
_isArray(val) {
return Array.isArray(val);
}
_isRange(schema) {
return "valueMin" in schema && "valueMax" in schema;
}
_equals(a, b) {
return a === b;
}
_includes(a, b) {
return a.indexOf(b) >= 0;
}
_getValue(obj, item) {
if (obj) {
return obj[item.name];
}
return null;
}
_valueChanged(ev) {
let value = ev.detail.value;
if (ev.model.item.type === "integer") {
value = Number(ev.detail.value);
}
this.set(["data", ev.model.item.name], value);
}
_passwordFieldType(unmaskedPassword) {
return unmaskedPassword ? "text" : "password";
}
_passwordFieldIcon(unmaskedPassword) {
return unmaskedPassword ? "hass:eye-off" : "hass:eye";
}
_optionValue(item) {
return Array.isArray(item) ? item[0] : item;
}
_optionLabel(item) {
return Array.isArray(item) ? item[1] : item;
}
}
customElements.define("ha-form", HaForm);

View File

@@ -0,0 +1,70 @@
import {
customElement,
LitElement,
html,
property,
TemplateResult,
CSSResult,
css,
query,
} from "lit-element";
import {
HaFormElement,
HaFormBooleanData,
HaFormBooleanSchema,
} from "./ha-form";
import { fireEvent } from "../../common/dom/fire_event";
import "@polymer/paper-checkbox/paper-checkbox";
// Not duplicate, is for typing
// tslint:disable-next-line
import { PaperCheckboxElement } from "@polymer/paper-checkbox/paper-checkbox";
@customElement("ha-form-boolean")
export class HaFormBoolean extends LitElement implements HaFormElement {
@property() public schema!: HaFormBooleanSchema;
@property() public data!: HaFormBooleanData;
@property() public label!: string;
@property() public suffix!: string;
@query("paper-checkbox") private _input?: HTMLElement;
public focus() {
if (this._input) {
this._input.focus();
}
}
protected render(): TemplateResult {
return html`
<paper-checkbox .checked=${this.data} @change=${this._valueChanged}>
${this.label}
</paper-checkbox>
`;
}
private _valueChanged(ev: Event) {
fireEvent(
this,
"value-changed",
{
value: (ev.target as PaperCheckboxElement).checked,
},
{ bubbles: false }
);
}
static get styles(): CSSResult {
return css`
paper-checkbox {
display: inline-block;
padding: 22px 0;
}
`;
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-form-boolean": HaFormBoolean;
}
}

View File

@@ -0,0 +1,69 @@
import {
customElement,
LitElement,
html,
property,
TemplateResult,
query,
} from "lit-element";
import { HaFormElement, HaFormFloatData, HaFormFloatSchema } from "./ha-form";
import { fireEvent } from "../../common/dom/fire_event";
import "@polymer/paper-input/paper-input";
// Not duplicate, is for typing
// tslint:disable-next-line
import { PaperInputElement } from "@polymer/paper-input/paper-input";
@customElement("ha-form-float")
export class HaFormFloat extends LitElement implements HaFormElement {
@property() public schema!: HaFormFloatSchema;
@property() public data!: HaFormFloatData;
@property() public label!: string;
@property() public suffix!: string;
@query("paper-input") private _input?: HTMLElement;
public focus() {
if (this._input) {
this._input.focus();
}
}
protected render(): TemplateResult {
return html`
<paper-input
.label=${this.label}
.value=${this._value}
.required=${this.schema.required}
.autoValidate=${this.schema.required}
@value-changed=${this._valueChanged}
>
<span suffix="" slot="suffix">${this.suffix}</span>
</paper-input>
`;
}
private get _value() {
return this.data || 0;
}
private _valueChanged(ev: Event) {
const value = Number((ev.target as PaperInputElement).value);
if (this._value === value) {
return;
}
fireEvent(
this,
"value-changed",
{
value,
},
{ bubbles: false }
);
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-form-float": HaFormFloat;
}
}

View File

@@ -0,0 +1,89 @@
import {
customElement,
LitElement,
html,
property,
TemplateResult,
query,
} from "lit-element";
import {
HaFormElement,
HaFormIntegerData,
HaFormIntegerSchema,
} from "./ha-form";
import { fireEvent } from "../../common/dom/fire_event";
import "../ha-paper-slider";
import "@polymer/paper-input/paper-input";
// Not duplicate, is for typing
// tslint:disable-next-line
import { PaperInputElement } from "@polymer/paper-input/paper-input";
import { PaperSliderElement } from "@polymer/paper-slider/paper-slider";
@customElement("ha-form-integer")
export class HaFormInteger extends LitElement implements HaFormElement {
@property() public schema!: HaFormIntegerSchema;
@property() public data!: HaFormIntegerData;
@property() public label!: string;
@property() public suffix!: string;
@query("paper-input ha-paper-slider") private _input?: HTMLElement;
public focus() {
if (this._input) {
this._input.focus();
}
}
protected render(): TemplateResult {
return "valueMin" in this.schema && "valueMax" in this.schema
? html`
<div>
${this.label}
<ha-paper-slider
pin=""
.value=${this._value}
.min=${this.schema.valueMin}
.max=${this.schema.valueMax}
@value-changed=${this._valueChanged}
></ha-paper-slider>
</div>
`
: html`
<paper-input
type="number"
.label=${this.label}
.value=${this.data}
.required=${this.schema.required}
.autoValidate=${this.schema.required}
@value-changed=${this._valueChanged}
></paper-input>
`;
}
private get _value() {
return this.data || 0;
}
private _valueChanged(ev: Event) {
const value = Number(
(ev.target as PaperInputElement | PaperSliderElement).value
);
if (this._value === value) {
return;
}
fireEvent(
this,
"value-changed",
{
value,
},
{ bubbles: false }
);
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-form-integer": HaFormInteger;
}
}

View File

@@ -0,0 +1,119 @@
import {
customElement,
LitElement,
html,
property,
TemplateResult,
query,
} from "lit-element";
import { HaFormElement, HaFormTimeData, HaFormTimeSchema } from "./ha-form";
import { fireEvent } from "../../common/dom/fire_event";
@customElement("ha-form-positive_time_period_dict")
export class HaFormTimePeriod extends LitElement implements HaFormElement {
@property() public schema!: HaFormTimeSchema;
@property() public data!: HaFormTimeData;
@property() public label!: string;
@property() public suffix!: string;
@query("paper-time-input") private _input?: HTMLElement;
public focus() {
if (this._input) {
this._input.focus();
}
}
protected render(): TemplateResult | void {
return html`
<paper-time-input
.label=${this.label}
.required=${this.schema.required}
.autoValidate=${this.schema.required}
error-message="Required"
enable-second
format="24"
.hour=${this._parseDuration(this._hours)}
.min=${this._parseDuration(this._minutes)}
.sec=${this._parseDuration(this._seconds)}
@hour-changed=${this._hourChanged}
@min-changed=${this._minChanged}
@sec-changed=${this._secChanged}
float-input-labels
no-hours-limit
always-float-input-labels
hour-label="hh"
min-label="mm"
sec-label="ss"
></paper-time-input>
`;
}
private get _hours() {
return this.data && this.data.hours ? Number(this.data.hours) : 0;
}
private get _minutes() {
return this.data && this.data.minutes ? Number(this.data.minutes) : 0;
}
private get _seconds() {
return this.data && this.data.seconds ? Number(this.data.seconds) : 0;
}
private _parseDuration(value) {
return value.toString().padStart(2, "0");
}
private _hourChanged(ev) {
this._durationChanged(ev, "hours");
}
private _minChanged(ev) {
this._durationChanged(ev, "minutes");
}
private _secChanged(ev) {
this._durationChanged(ev, "seconds");
}
private _durationChanged(ev, unit) {
let value = Number(ev.detail.value);
if (value === this[`_${unit}`]) {
return;
}
let hours = this._hours;
let minutes = this._minutes;
if (unit === "seconds" && value > 59) {
minutes = minutes + Math.floor(value / 60);
value %= 60;
}
if (unit === "minutes" && value > 59) {
hours = hours + Math.floor(value / 60);
value %= 60;
}
fireEvent(
this,
"value-changed",
{
value: {
hours,
minutes,
seconds: this._seconds,
...{ [unit]: value },
},
},
{ bubbles: false }
);
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-form-positive_time_period_dict": HaFormTimePeriod;
}
}

View File

@@ -0,0 +1,78 @@
import {
customElement,
LitElement,
html,
property,
TemplateResult,
query,
} from "lit-element";
import { HaFormElement, HaFormSelectData, HaFormSelectSchema } from "./ha-form";
import { fireEvent } from "../../common/dom/fire_event";
import "@polymer/paper-dropdown-menu/paper-dropdown-menu";
import "@polymer/paper-listbox/paper-listbox";
import "@polymer/paper-item/paper-item";
@customElement("ha-form-select")
export class HaFormSelect extends LitElement implements HaFormElement {
@property() public schema!: HaFormSelectSchema;
@property() public data!: HaFormSelectData;
@property() public label!: string;
@property() public suffix!: string;
@query("paper-dropdown-menu") private _input?: HTMLElement;
public focus() {
if (this._input) {
this._input.focus();
}
}
protected render(): TemplateResult {
return html`
<paper-dropdown-menu .label=${this.label}>
<paper-listbox
slot="dropdown-content"
attr-for-selected="item-value"
.selected=${this.data}
@selected-item-changed=${this._valueChanged}
>
${this.schema.options!.map(
(item) => html`
<paper-item .itemValue=${this._optionValue(item)}>
${this._optionLabel(item)}
</paper-item>
`
)}
</paper-listbox>
</paper-dropdown-menu>
`;
}
private _optionValue(item) {
return Array.isArray(item) ? item[0] : item;
}
private _optionLabel(item) {
return Array.isArray(item) ? item[1] : item;
}
private _valueChanged(ev: CustomEvent) {
if (!ev.detail.value) {
return;
}
fireEvent(
this,
"value-changed",
{
value: ev.detail.value.itemValue,
},
{ bubbles: false }
);
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-form-select": HaFormSelect;
}
}

View File

@@ -0,0 +1,93 @@
import {
customElement,
LitElement,
html,
property,
TemplateResult,
query,
} from "lit-element";
import { HaFormElement, HaFormStringData, HaFormStringSchema } from "./ha-form";
import { fireEvent } from "../../common/dom/fire_event";
import "@polymer/paper-input/paper-input";
import "@polymer/paper-icon-button/paper-icon-button";
// Not duplicate, is for typing
// tslint:disable-next-line
import { PaperInputElement } from "@polymer/paper-input/paper-input";
@customElement("ha-form-string")
export class HaFormString extends LitElement implements HaFormElement {
@property() public schema!: HaFormStringSchema;
@property() public data!: HaFormStringData;
@property() public label!: string;
@property() public suffix!: string;
@property() private _unmaskedPassword = false;
@query("paper-input") private _input?: HTMLElement;
public focus() {
if (this._input) {
this._input.focus();
}
}
protected render(): TemplateResult {
return this.schema.name.includes("password")
? html`
<paper-input
.type=${this._unmaskedPassword ? "text" : "password"}
.label=${this.label}
.value=${this.data}
.required=${this.schema.required}
.autoValidate=${this.schema.required}
@value-changed=${this._valueChanged}
>
<paper-icon-button
toggles
.active=${this._unmaskedPassword}
slot="suffix"
.icon=${this._unmaskedPassword ? "hass:eye-off" : "hass:eye"}
id="iconButton"
title="Click to toggle between masked and clear password"
@click=${this._toggleUnmaskedPassword}
>
</paper-icon-button>
</paper-input>
`
: html`
<paper-input
.label=${this.label}
.value=${this.data}
.required=${this.schema.required}
.autoValidate=${this.schema.required}
error-message="Required"
@value-changed=${this._valueChanged}
></paper-input>
`;
}
private _toggleUnmaskedPassword(ev: Event) {
this._unmaskedPassword = (ev.target as any).active;
}
private _valueChanged(ev: Event) {
const value = (ev.target as PaperInputElement).value;
if (this.data === value) {
return;
}
fireEvent(
this,
"value-changed",
{
value,
},
{ bubbles: false }
);
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-form-string": HaFormString;
}
}

View File

@@ -0,0 +1,237 @@
import {
customElement,
LitElement,
html,
property,
query,
CSSResult,
css,
PropertyValues,
} from "lit-element";
import "./ha-form-string";
import "./ha-form-integer";
import "./ha-form-float";
import "./ha-form-boolean";
import "./ha-form-select";
import "./ha-form-positive_time_period_dict";
import { fireEvent } from "../../common/dom/fire_event";
export type HaFormSchema =
| HaFormStringSchema
| HaFormIntegerSchema
| HaFormFloatSchema
| HaFormBooleanSchema
| HaFormSelectSchema
| HaFormTimeSchema;
export interface HaFormBaseSchema {
name: string;
default?: HaFormData;
required?: boolean;
optional?: boolean;
description?: { suffix?: string };
}
export interface HaFormIntegerSchema extends HaFormBaseSchema {
type: "integer";
default?: HaFormIntegerData;
valueMin?: number;
valueMax?: number;
}
export interface HaFormSelectSchema extends HaFormBaseSchema {
type: "select";
options?: string[];
}
export interface HaFormFloatSchema extends HaFormBaseSchema {
type: "float";
}
export interface HaFormStringSchema extends HaFormBaseSchema {
type: "string";
}
export interface HaFormBooleanSchema extends HaFormBaseSchema {
type: "boolean";
}
export interface HaFormTimeSchema extends HaFormBaseSchema {
type: "time";
}
export interface HaFormDataContainer {
[key: string]: HaFormData;
}
export type HaFormData =
| HaFormStringData
| HaFormIntegerData
| HaFormFloatData
| HaFormBooleanData
| HaFormSelectData
| HaFormTimeData;
export type HaFormStringData = string;
export type HaFormIntegerData = number;
export type HaFormFloatData = number;
export type HaFormBooleanData = boolean;
export type HaFormSelectData = string;
export interface HaFormTimeData {
hours?: number;
minutes?: number;
seconds?: number;
}
export interface HaFormElement extends LitElement {
schema: HaFormSchema;
data: HaFormDataContainer | HaFormData;
label?: string;
suffix?: string;
}
@customElement("ha-form")
export class HaForm extends LitElement implements HaFormElement {
@property() public data!: HaFormDataContainer | HaFormData;
@property() public schema!: HaFormSchema;
@property() public error;
@property() public computeError?: (schema: HaFormSchema, error) => string;
@property() public computeLabel?: (schema: HaFormSchema) => string;
@property() public computeSuffix?: (schema: HaFormSchema) => string;
@query("ha-form") private _childForm?: HaForm;
@query("#element") private _elementContainer?: HTMLDivElement;
public focus() {
const input = this._childForm
? this._childForm
: this._elementContainer
? this._elementContainer.lastChild
: undefined;
if (!input) {
return;
}
(input as HTMLElement).focus();
}
protected render() {
if (Array.isArray(this.schema)) {
return html`
${this.error && this.error.base
? html`
<div class="error">
${this._computeError(this.error.base, this.schema)}
</div>
`
: ""}
${this.schema.map(
(item) => html`
<ha-form
.data=${this._getValue(this.data, item)}
.schema=${item}
.error=${this._getValue(this.error, item)}
@value-changed=${this._valueChanged}
.computeError=${this.computeError}
.computeLabel=${this.computeLabel}
.computeSuffix=${this.computeSuffix}
></ha-form>
`
)}
`;
}
return html`
${this.error
? html`
<div class="error">
${this._computeError(this.error, this.schema)}
</div>
`
: ""}
<div id="element" @value-changed=${this._valueChanged}></div>
`;
}
protected updated(changedProperties: PropertyValues) {
const schemaChanged = changedProperties.has("schema");
const oldSchema = schemaChanged
? changedProperties.get("schema")
: undefined;
if (
!Array.isArray(this.schema) &&
schemaChanged &&
(!oldSchema || (oldSchema as HaFormSchema).type !== this.schema.type)
) {
const element = document.createElement(
`ha-form-${this.schema.type}`
) as HaFormElement;
element.schema = this.schema;
element.data = this.data;
element.label = this._computeLabel(this.schema);
element.suffix = this._computeSuffix(this.schema);
if (this._elementContainer!.lastChild) {
this._elementContainer!.removeChild(this._elementContainer!.lastChild);
}
this._elementContainer!.append(element);
} else if (this._elementContainer && this._elementContainer.lastChild) {
const element = this._elementContainer!.lastChild as HaFormElement;
element.schema = this.schema;
element.data = this.data;
element.label = this._computeLabel(this.schema);
element.suffix = this._computeSuffix(this.schema);
}
}
private _computeLabel(schema: HaFormSchema) {
return this.computeLabel
? this.computeLabel(schema)
: schema
? schema.name
: "";
}
private _computeSuffix(schema: HaFormSchema) {
return this.computeSuffix
? this.computeSuffix(schema)
: schema && schema.description
? schema.description.suffix
: "";
}
private _computeError(error, schema: HaFormSchema) {
return this.computeError ? this.computeError(error, schema) : error;
}
private _getValue(obj, item) {
if (obj) {
return obj[item.name];
}
return null;
}
private _valueChanged(ev: CustomEvent) {
ev.stopPropagation();
const schema = (ev.target as HaFormElement).schema;
const data = this.data as HaFormDataContainer;
data[schema.name] = ev.detail.value;
fireEvent(this, "value-changed", {
value: { ...data },
});
}
static get styles(): CSSResult {
return css`
.error {
color: var(--error-color);
}
`;
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-form": HaForm;
}
}

View File

@@ -1,4 +1,5 @@
import { Constructor } from "lit-element";
import { Constructor } from "../types";
import "@polymer/iron-icon/iron-icon";
// Not duplicate, this is for typing.
// tslint:disable-next-line

View File

@@ -1,31 +1,21 @@
import {
html,
LitElement,
PropertyDeclarations,
PropertyValues,
TemplateResult,
CSSResult,
css,
property,
} from "lit-element";
import { classMap } from "lit-html/directives/class-map";
import "./ha-icon";
class HaLabelBadge extends LitElement {
public value?: string;
public icon?: string;
public label?: string;
public description?: string;
public image?: string;
static get properties(): PropertyDeclarations {
return {
value: {},
icon: {},
label: {},
description: {},
image: {},
};
}
@property() public value?: string;
@property() public icon?: string;
@property() public label?: string;
@property() public description?: string;
@property() public image?: string;
protected render(): TemplateResult | void {
return html`

View File

@@ -51,7 +51,7 @@ class HaMenuButton extends LitElement {
));
return html`
<paper-icon-button
aria-label="Sidebar Toggle"
aria-label=${this.hass.localize("ui.sidebar.sidebar_toggle")}
.icon=${this.hassio ? "hassio:menu" : "hass:menu"}
@click=${this._toggleMenu}
></paper-icon-button>

View File

@@ -1,6 +1,6 @@
import "@polymer/paper-dropdown-menu/paper-dropdown-menu";
import { Constructor } from "lit-element";
import { PolymerElement } from "@polymer/polymer";
import { Constructor } from "../types";
const paperDropdownClass = customElements.get(
"paper-dropdown-menu"

View File

@@ -1,5 +1,5 @@
import { Constructor } from "lit-element";
import "@polymer/paper-icon-button/paper-icon-button";
import { Constructor } from "../types";
// Not duplicate, this is for typing.
// tslint:disable-next-line
import { PaperIconButtonElement } from "@polymer/paper-icon-button/paper-icon-button";

View File

@@ -1,5 +1,5 @@
import { Constructor } from "lit-element";
import "@polymer/paper-icon-button/paper-icon-button";
import { Constructor } from "../types";
// Not duplicate, this is for typing.
// tslint:disable-next-line
import { PaperIconButtonElement } from "@polymer/paper-icon-button/paper-icon-button";

View File

@@ -1,5 +1,5 @@
import { Constructor } from "lit-element";
import "@polymer/paper-icon-button/paper-icon-button";
import { Constructor } from "../types";
// Not duplicate, this is for typing.
// tslint:disable-next-line
import { PaperIconButtonElement } from "@polymer/paper-icon-button/paper-icon-button";

View File

@@ -1,5 +1,5 @@
import { Constructor } from "lit-element";
import "@polymer/paper-icon-button/paper-icon-button";
import { Constructor } from "../types";
// Not duplicate, this is for typing.
// tslint:disable-next-line
import { PaperIconButtonElement } from "@polymer/paper-icon-button/paper-icon-button";

View File

@@ -138,7 +138,7 @@ class HaSidebar extends LitElement {
${!this.narrow
? html`
<paper-icon-button
aria-label="Sidebar Toggle"
aria-label=${hass.localize("ui.sidebar.sidebar_toggle")}
.icon=${hass.dockedSidebar === "docked"
? "hass:menu-open"
: "hass:menu"}
@@ -181,7 +181,9 @@ class HaSidebar extends LitElement {
? html`
<a
aria-role="option"
aria-label="App Configuration"
aria-label=${hass.localize(
"ui.sidebar.external_app_configuration"
)}
href="#external-app-configuration"
tabindex="-1"
@click=${this._handleExternalAppConfiguration}

View File

@@ -1,56 +0,0 @@
import "@polymer/paper-icon-button/paper-icon-button";
import { html } from "@polymer/polymer/lib/utils/html-tag";
import { PolymerElement } from "@polymer/polymer/polymer-element";
import { EventsMixin } from "../mixins/events-mixin";
import isComponentLoaded from "../common/config/is_component_loaded";
import { fireEvent } from "../common/dom/fire_event";
/*
* @appliesMixin EventsMixin
*/
class HaStartVoiceButton extends EventsMixin(PolymerElement) {
static get template() {
return html`
<paper-icon-button
aria-label="Start conversation"
icon="hass:microphone"
hidden$="[[!canListen]]"
on-click="handleListenClick"
></paper-icon-button>
`;
}
static get properties() {
return {
hass: {
type: Object,
value: null,
},
canListen: {
type: Boolean,
computed: "computeCanListen(hass)",
notify: true,
},
};
}
computeCanListen(hass) {
return (
"webkitSpeechRecognition" in window &&
isComponentLoaded(hass, "conversation")
);
}
handleListenClick() {
fireEvent(this, "show-dialog", {
dialogImport: () =>
import(/* webpackChunkName: "voice-command-dialog" */ "../dialogs/ha-voice-command-dialog"),
dialogTag: "ha-voice-command-dialog",
});
}
}
customElements.define("ha-start-voice-button", HaStartVoiceButton);

View File

@@ -1,8 +1,9 @@
import { Constructor, customElement, CSSResult, css, query } from "lit-element";
import { customElement, CSSResult, css, query } from "lit-element";
import "@material/mwc-switch";
import { style } from "@material/mwc-switch/mwc-switch-css";
// tslint:disable-next-line
import { Switch } from "@material/mwc-switch";
import { Constructor } from "../types";
// tslint:disable-next-line
const MwcSwitch = customElements.get("mwc-switch") as Constructor<Switch>;
@@ -12,7 +13,10 @@ export class HaSwitch extends MwcSwitch {
protected firstUpdated() {
super.firstUpdated();
this.style.setProperty("--mdc-theme-secondary", "var(--primary-color)");
this.style.setProperty(
"--mdc-theme-secondary",
"var(--switch-checked-color)"
);
this.classList.toggle(
"slotted",
Boolean(this._slot.assignedNodes().length)
@@ -28,13 +32,21 @@ export class HaSwitch extends MwcSwitch {
flex-direction: row;
align-items: center;
}
.mdc-switch.mdc-switch--checked .mdc-switch__thumb {
background-color: var(--switch-checked-button-color);
border-color: var(--switch-checked-button-color);
}
.mdc-switch.mdc-switch--checked .mdc-switch__track {
background-color: var(--switch-checked-track-color);
border-color: var(--switch-checked-track-color);
}
.mdc-switch:not(.mdc-switch--checked) .mdc-switch__thumb {
background-color: var(--paper-toggle-button-unchecked-button-color);
border-color: var(--paper-toggle-button-unchecked-button-color);
background-color: var(--switch-unchecked-button-color);
border-color: var(--switch-unchecked-button-color);
}
.mdc-switch:not(.mdc-switch--checked) .mdc-switch__track {
background-color: var(--paper-toggle-button-unchecked-bar-color);
border-color: var(--paper-toggle-button-unchecked-bar-color);
background-color: var(--switch-unchecked-track-color);
border-color: var(--switch-unchecked-track-color);
}
:host(.slotted) .mdc-switch {
margin-right: 24px;

View File

@@ -77,7 +77,12 @@ class LocationEditor extends LitElement {
}
private _updateLocation(latlng: LatLng) {
this.location = this._ignoreFitToMap = [latlng.lat, latlng.lng];
let longitude = latlng.lng;
if (Math.abs(longitude) > 180.0) {
// Normalize longitude if map provides values beyond -180 to +180 degrees.
longitude = (((longitude % 360.0) + 540.0) % 360.0) - 180.0;
}
this.location = this._ignoreFitToMap = [latlng.lat, longitude];
fireEvent(this, "change", undefined, { bubbles: false });
}

View File

@@ -87,6 +87,10 @@ export class PaperTimeInput extends PolymerElement {
label {
@apply --paper-font-caption;
color: var(
--paper-input-container-color,
var(--secondary-text-color)
);
}
.time-input-wrap {
@@ -106,14 +110,17 @@ export class PaperTimeInput extends PolymerElement {
id="hour"
type="number"
value="{{hour}}"
label="[[hourLabel]]"
on-change="_shouldFormatHour"
required=""
on-focus="_onFocus"
required
prevent-invalid-input
auto-validate="[[autoValidate]]"
prevent-invalid-input=""
maxlength="2"
max="[[_computeHourMax(format)]]"
min="0"
no-label-float=""
no-label-float$="[[!floatInputLabels]]"
always-float-label$="[[alwaysFloatInputLabels]]"
disabled="[[disabled]]"
>
<span suffix="" slot="suffix">:</span>
@@ -124,15 +131,40 @@ export class PaperTimeInput extends PolymerElement {
id="min"
type="number"
value="{{min}}"
label="[[minLabel]]"
on-change="_formatMin"
required=""
on-focus="_onFocus"
required
auto-validate="[[autoValidate]]"
prevent-invalid-input=""
prevent-invalid-input
maxlength="2"
max="59"
min="0"
no-label-float=""
no-label-float$="[[!floatInputLabels]]"
always-float-label$="[[alwaysFloatInputLabels]]"
disabled="[[disabled]]"
>
<span hidden$="[[!enableSecond]]" suffix slot="suffix">:</span>
</paper-input>
<!-- Sec Input -->
<paper-input
id="sec"
type="number"
value="{{sec}}"
label="[[secLabel]]"
on-change="_formatSec"
on-focus="_onFocus"
required
auto-validate="[[autoValidate]]"
prevent-invalid-input
maxlength="2"
max="59"
min="0"
no-label-float$="[[!floatInputLabels]]"
always-float-label$="[[alwaysFloatInputLabels]]"
disabled="[[disabled]]"
hidden$="[[!enableSecond]]"
>
</paper-input>
@@ -180,6 +212,20 @@ export class PaperTimeInput extends PolymerElement {
type: Boolean,
value: false,
},
/**
* float the input labels
*/
floatInputLabels: {
type: Boolean,
value: false,
},
/**
* always float the input labels
*/
alwaysFloatInputLabels: {
type: Boolean,
value: false,
},
/**
* 12 or 24 hr format
*/
@@ -208,6 +254,48 @@ export class PaperTimeInput extends PolymerElement {
type: String,
notify: true,
},
/**
* second
*/
sec: {
type: String,
notify: true,
},
/**
* Suffix for the hour input
*/
hourLabel: {
type: String,
value: "",
},
/**
* Suffix for the min input
*/
minLabel: {
type: String,
value: ":",
},
/**
* Suffix for the sec input
*/
secLabel: {
type: String,
value: "",
},
/**
* show the sec field
*/
enableSecond: {
type: Boolean,
value: false,
},
/**
* limit hours input
*/
noHoursLimit: {
type: Boolean,
value: false,
},
/**
* AM or PM
*/
@@ -223,7 +311,7 @@ export class PaperTimeInput extends PolymerElement {
type: String,
notify: true,
readOnly: true,
computed: "_computeTime(min, hour, amPm)",
computed: "_computeTime(min, hour, sec, amPm)",
},
};
}
@@ -238,6 +326,10 @@ export class PaperTimeInput extends PolymerElement {
if (!this.$.hour.validate() | !this.$.min.validate()) {
valid = false;
}
// Validate second field
if (this.enableSecond && !this.$.sec.validate()) {
valid = false;
}
// Validate AM PM if 12 hour time
if (this.format === 12 && !this.$.dropdown.validate()) {
valid = false;
@@ -248,15 +340,37 @@ export class PaperTimeInput extends PolymerElement {
/**
* Create time string
*/
_computeTime(min, hour, amPm) {
if (hour && min) {
// No ampm on 24 hr time
if (this.format === 24) {
amPm = "";
_computeTime(min, hour, sec, amPm) {
let str;
if (hour || min || (sec && this.enableSecond)) {
hour = hour || "00";
min = min || "00";
sec = sec || "00";
str = hour + ":" + min;
// add sec field
if (this.enableSecond && sec) {
str = str + ":" + sec;
}
// No ampm on 24 hr time
if (this.format === 12) {
str = str + " " + amPm;
}
return hour + ":" + min + " " + amPm;
}
return undefined;
return str;
}
_onFocus(ev) {
ev.target.inputElement.inputElement.select();
}
/**
* Format sec
*/
_formatSec() {
if (this.sec.toString().length === 1) {
this.sec = this.sec.toString().padStart(2, "0");
}
}
/**
@@ -264,16 +378,16 @@ export class PaperTimeInput extends PolymerElement {
*/
_formatMin() {
if (this.min.toString().length === 1) {
this.min = this.min < 10 ? "0" + this.min : this.min;
this.min = this.min.toString().padStart(2, "0");
}
}
/**
* Hour needs a leading zero in 24hr format
* Format hour
*/
_shouldFormatHour() {
if (this.format === 24 && this.hour.toString().length === 1) {
this.hour = this.hour < 10 ? "0" + this.hour : this.hour;
this.hour = this.hour.toString().padStart(2, "0");
}
}
@@ -281,6 +395,9 @@ export class PaperTimeInput extends PolymerElement {
* 24 hour format has a max hr of 23
*/
_computeHourMax(format) {
if (this.noHoursLimit) {
return null;
}
if (format === 12) {
return format;
}

View File

@@ -181,23 +181,63 @@ class StateHistoryChartLine extends LocalizeMixin(PolymerElement) {
state.attributes.target_temp_low
);
addColumn(name + " current temperature", true);
addColumn(
`${this.hass.localize(
"ui.card.climate.current_temperature",
"name",
name
)}`,
true
);
if (hasHeat) {
addColumn(name + " heating", true, true);
addColumn(
`${this.hass.localize("ui.card.climate.heating", "name", name)}`,
true,
true
);
// The "heating" series uses steppedArea to shade the area below the current
// temperature when the thermostat is calling for heat.
}
if (hasCool) {
addColumn(name + " cooling", true, true);
addColumn(
`${this.hass.localize("ui.card.climate.cooling", "name", name)}`,
true,
true
);
// The "cooling" series uses steppedArea to shade the area below the current
// temperature when the thermostat is calling for heat.
}
if (hasTargetRange) {
addColumn(name + " target temperature high", true);
addColumn(name + " target temperature low", true);
addColumn(
`${this.hass.localize(
"ui.card.climate.target_temperature_mode",
"name",
name,
"mode",
this.hass.localize("ui.card.climate.high")
)}`,
true
);
addColumn(
`${this.hass.localize(
"ui.card.climate.target_temperature_mode",
"name",
name,
"mode",
this.hass.localize("ui.card.climate.low")
)}`,
true
);
} else {
addColumn(name + " target temperature", true);
addColumn(
`${this.hass.localize(
"ui.card.climate.target_temperature_entity",
"name",
name
)}`,
true
);
}
states.states.forEach((state) => {

View File

@@ -18,19 +18,20 @@ import { fireEvent } from "../../common/dom/fire_event";
import { User, fetchUsers } from "../../data/user";
import { compare } from "../../common/string/compare";
class HaEntityPicker extends LitElement {
class HaUserPicker extends LitElement {
public hass?: HomeAssistant;
@property() public label?: string;
@property() public value?: string;
@property() public users?: User[];
private _sortedUsers = memoizeOne((users?: User[]) => {
if (!users || users.length === 1) {
return users || [];
if (!users) {
return [];
}
const sorted = [...users];
sorted.sort((a, b) => compare(a.name, b.name));
return sorted;
return users
.filter((user) => !user.system_generated)
.sort((a, b) => compare(a.name, b.name));
});
protected render(): TemplateResult | void {
@@ -101,4 +102,4 @@ class HaEntityPicker extends LitElement {
}
}
customElements.define("ha-user-picker", HaEntityPicker);
customElements.define("ha-user-picker", HaUserPicker);

View File

@@ -1,5 +1,7 @@
import { HomeAssistant } from "../types";
import { EntityFilter } from "../common/entity/entity_filter";
import { AutomationConfig } from "./automation";
import { PlaceholderContainer } from "../panels/config/automation/thingtalk/dialog-thingtalk";
interface CloudStatusBase {
logged_in: boolean;
@@ -63,6 +65,11 @@ export interface CloudWebhook {
managed?: boolean;
}
export interface ThingTalkConversion {
config: Partial<AutomationConfig>;
placeholders: PlaceholderContainer;
}
export const fetchCloudStatus = (hass: HomeAssistant) =>
hass.callWS<CloudStatus>({ type: "cloud/status" });
@@ -91,6 +98,9 @@ export const disconnectCloudRemote = (hass: HomeAssistant) =>
export const fetchCloudSubscriptionInfo = (hass: HomeAssistant) =>
hass.callWS<SubscriptionInfo>({ type: "cloud/subscription" });
export const convertThingTalk = (hass: HomeAssistant, query: string) =>
hass.callWS<ThingTalkConversion>({ type: "cloud/thingtalk/convert", query });
export const updateCloudPref = (
hass: HomeAssistant,
prefs: {

39
src/data/conversation.ts Normal file
View File

@@ -0,0 +1,39 @@
import { HomeAssistant } from "../types";
interface ProcessResults {
card: { [key: string]: { [key: string]: string } };
speech: {
[SpeechType in "plain" | "ssml"]: { extra_data: any; speech: string }
};
}
export interface AgentInfo {
attribution?: { name: string; url: string };
onboarding?: { text: string; url: string };
}
export const processText = (
hass: HomeAssistant,
text: string,
// tslint:disable-next-line: variable-name
conversation_id: string
): Promise<ProcessResults> =>
hass.callWS({
type: "conversation/process",
text,
conversation_id,
});
export const getAgentInfo = (hass: HomeAssistant): Promise<AgentInfo> =>
hass.callWS({
type: "conversation/agent/info",
});
export const setConversationOnboarding = (
hass: HomeAssistant,
value: boolean
): Promise<boolean> =>
hass.callWS({
type: "conversation/onboarding/set",
shown: value,
});

View File

@@ -39,6 +39,24 @@ export const fetchDeviceTriggers = (hass: HomeAssistant, deviceId: string) =>
device_id: deviceId,
});
export const fetchDeviceActionCapabilities = (
hass: HomeAssistant,
action: DeviceAction
) =>
hass.callWS<DeviceAction[]>({
type: "device_automation/action/capabilities",
action,
});
export const fetchDeviceConditionCapabilities = (
hass: HomeAssistant,
condition: DeviceCondition
) =>
hass.callWS<DeviceCondition[]>({
type: "device_automation/condition/capabilities",
condition,
});
export const fetchDeviceTriggerCapabilities = (
hass: HomeAssistant,
trigger: DeviceTrigger
@@ -48,7 +66,7 @@ export const fetchDeviceTriggerCapabilities = (
trigger,
});
const whitelist = ["above", "below", "for"];
const whitelist = ["above", "below", "code", "for"];
export const deviceAutomationsEqual = (
a: DeviceAutomation,

View File

@@ -1,6 +1,8 @@
import { HomeAssistant } from "../types";
import { createCollection, Connection } from "home-assistant-js-websocket";
import { debounce } from "../common/util/debounce";
import { EntityRegistryEntry } from "./entity_registry";
import { computeStateName } from "../common/entity/compute_state_name";
export interface DeviceRegistryEntry {
id: string;
@@ -20,6 +22,33 @@ export interface DeviceRegistryEntryMutableParams {
name_by_user?: string | null;
}
export const computeDeviceName = (
device: DeviceRegistryEntry,
hass: HomeAssistant,
entities?: EntityRegistryEntry[] | string[]
) => {
return (
device.name_by_user ||
device.name ||
(entities && fallbackDeviceName(hass, entities)) ||
hass.localize("ui.panel.config.devices.unnamed_device")
);
};
export const fallbackDeviceName = (
hass: HomeAssistant,
entities: EntityRegistryEntry[] | string[]
) => {
for (const entity of entities || []) {
const entityId = typeof entity === "string" ? entity : entity.entity_id;
const stateObj = hass.states[entityId];
if (stateObj) {
return computeStateName(stateObj);
}
}
return undefined;
};
export const updateDeviceRegistryEntry = (
hass: HomeAssistant,
deviceId: string,

View File

@@ -1,5 +1,6 @@
import { HomeAssistant } from "../types";
import { Connection, getCollection } from "home-assistant-js-websocket";
import { HASSDomEvent } from "../common/dom/fire_event";
export interface LovelaceConfig {
title?: string;
@@ -11,13 +12,23 @@ export interface LovelaceConfig {
export interface LovelaceViewConfig {
index?: number;
title?: string;
badges?: string[];
badges?: Array<string | LovelaceBadgeConfig>;
cards?: LovelaceCardConfig[];
path?: string;
icon?: string;
theme?: string;
panel?: boolean;
background?: string;
visible?: boolean | ShowViewConfig[];
}
export interface ShowViewConfig {
user?: string;
}
export interface LovelaceBadgeConfig {
type?: string;
[key: string]: any;
}
export interface LovelaceCardConfig {
@@ -27,11 +38,11 @@ export interface LovelaceCardConfig {
[key: string]: any;
}
export interface ToggleActionConfig {
export interface ToggleActionConfig extends BaseActionConfig {
action: "toggle";
}
export interface CallServiceActionConfig {
export interface CallServiceActionConfig extends BaseActionConfig {
action: "call-service";
service: string;
service_data?: {
@@ -40,24 +51,37 @@ export interface CallServiceActionConfig {
};
}
export interface NavigateActionConfig {
export interface NavigateActionConfig extends BaseActionConfig {
action: "navigate";
navigation_path: string;
}
export interface UrlActionConfig {
export interface UrlActionConfig extends BaseActionConfig {
action: "url";
url_path: string;
}
export interface MoreInfoActionConfig {
export interface MoreInfoActionConfig extends BaseActionConfig {
action: "more-info";
}
export interface NoActionConfig {
export interface NoActionConfig extends BaseActionConfig {
action: "none";
}
export interface BaseActionConfig {
confirmation?: ConfirmationRestrictionConfig;
}
export interface ConfirmationRestrictionConfig {
text?: string;
exemptions?: RestrictionConfig[];
}
export interface RestrictionConfig {
user: string;
}
export type ActionConfig =
| ToggleActionConfig
| CallServiceActionConfig
@@ -103,3 +127,14 @@ export const getLovelaceCollection = (conn: Connection) =>
export interface WindowWithLovelaceProm extends Window {
llConfProm?: Promise<LovelaceConfig>;
}
export interface ActionHandlerOptions {
hasHold?: boolean;
hasDoubleClick?: boolean;
}
export interface ActionHandlerDetail {
action: string;
}
export type ActionHandlerEvent = HASSDomEvent<ActionHandlerDetail>;

91
src/data/scene.ts Normal file
View File

@@ -0,0 +1,91 @@
import {
HassEntityBase,
HassEntityAttributeBase,
} from "home-assistant-js-websocket";
import { HomeAssistant, ServiceCallResponse } from "../types";
export const SCENE_IGNORED_DOMAINS = [
"sensor",
"binary_sensor",
"device_tracker",
"person",
"persistent_notification",
"configuration",
"image_processing",
"sun",
"weather",
"zone",
];
export const SCENE_SAVED_ATTRIBUTES = {
light: [
"brightness",
"color_temp",
"effect",
"rgb_color",
"xy_color",
"hs_color",
],
media_player: [
"is_volume_muted",
"volume_level",
"sound_mode",
"source",
"media_content_id",
"media_content_type",
],
climate: [
"target_temperature",
"target_temperature_high",
"target_temperature_low",
"target_humidity",
"fan_mode",
"swing_mode",
"hvac_mode",
"preset_mode",
],
vacuum: ["cleaning_mode"],
fan: ["speed", "current_direction"],
water_heather: ["temperature", "operation_mode"],
};
export interface SceneEntity extends HassEntityBase {
attributes: HassEntityAttributeBase & { id?: string };
}
export interface SceneConfig {
name: string;
entities: SceneEntities;
}
export interface SceneEntities {
[entityId: string]: string | { state: string; [key: string]: any };
}
export const activateScene = (
hass: HomeAssistant,
entityId: string
): Promise<ServiceCallResponse> =>
hass.callService("scene", "turn_on", { entity_id: entityId });
export const applyScene = (
hass: HomeAssistant,
entities: SceneEntities
): Promise<ServiceCallResponse> =>
hass.callService("scene", "apply", { entities });
export const getSceneConfig = (
hass: HomeAssistant,
sceneId: string
): Promise<SceneConfig> =>
hass.callApi<SceneConfig>("GET", `config/scene/config/${sceneId}`);
export const saveScene = (
hass: HomeAssistant,
sceneId: string,
config: SceneConfig
) => hass.callApi("POST", `config/scene/config/${sceneId}`, config);
export const deleteScene = (hass: HomeAssistant, id: string) =>
hass.callApi("DELETE", `config/scene/config/${id}`);

View File

@@ -60,7 +60,13 @@ class DialogConfigEntrySystemOptions extends LitElement {
@opened-changed="${this._openedChanged}"
>
<h2>
${this.hass.localize("ui.dialogs.config_entry_system_options.title")}
${this.hass.localize(
"ui.dialogs.config_entry_system_options.title",
"integration",
this.hass.localize(
`component.${this._params.entry.domain}.config.title`
) || this._params.entry.domain
)}
</h2>
<paper-dialog-scrollable>
${this._loading
@@ -82,14 +88,22 @@ class DialogConfigEntrySystemOptions extends LitElement {
.disabled=${this._submitting}
>
<div>
${this.hass.localize(
"ui.dialogs.config_entry_system_options.enable_new_entities_label"
)}
</div>
<div class="secondary">
${this.hass.localize(
"ui.dialogs.config_entry_system_options.enable_new_entities_description"
)}
<p>
${this.hass.localize(
"ui.dialogs.config_entry_system_options.enable_new_entities_label"
)}
</p>
<p class="secondary">
${this.hass.localize(
"ui.dialogs.config_entry_system_options.enable_new_entities_description",
"integration",
this.hass.localize(
`component.${
this._params.entry.domain
}.config.title`
) || this._params.entry.domain
)}
</p>
</div>
</ha-switch>
</div>
@@ -160,7 +174,9 @@ class DialogConfigEntrySystemOptions extends LitElement {
padding-bottom: 24px;
color: var(--primary-text-color);
}
p {
margin: 0;
}
.secondary {
color: var(--secondary-text-color);
}

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