mirror of
				https://github.com/home-assistant/frontend.git
				synced 2025-10-31 14:39:38 +00:00 
			
		
		
		
	Compare commits
	
		
			293 Commits
		
	
	
		
			20220405.0
			...
			fix-button
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|   | 026d027386 | ||
|   | d0ead1fdb8 | ||
|   | b0e6c41238 | ||
|   | 2c1550b10f | ||
|   | ffc4ca5b56 | ||
|   | 85ad6619b7 | ||
|   | 7358faf88e | ||
|   | 19d014307a | ||
|   | 5217f5c50c | ||
|   | c4624faa71 | ||
|   | b35ba4d673 | ||
|   | f8303bff76 | ||
|   | e61aa266a6 | ||
|   | d7971c69ad | ||
|   | 966a624ef6 | ||
|   | 7cc576a616 | ||
|   | 2dec8e70ec | ||
|   | 97663aef42 | ||
|   | 3f1a2526b3 | ||
|   | e7517a8b61 | ||
|   | e3d394eb32 | ||
|   | 536ea822b3 | ||
|   | 8e4e22b6f8 | ||
|   | 2eaa246a03 | ||
|   | e841bf89be | ||
|   | 36e1203fb1 | ||
|   | 3acab5a39c | ||
|   | 49cfde1fe7 | ||
|   | 49c018c000 | ||
|   | b71b230bfd | ||
|   | e1fd7244a5 | ||
|   | 067c2fdfa8 | ||
|   | a02b817d7f | ||
|   | 7db6e0b779 | ||
|   | 1d5cc91a2d | ||
|   | 0623e7dce4 | ||
|   | da106d278c | ||
|   | 51c5ab33f0 | ||
|   | 8ac4a6d900 | ||
|   | fae1bcf0e0 | ||
|   | 9a9eec40b2 | ||
|   | 6ab19d66d5 | ||
|   | a0a7ce014f | ||
|   | bfeb90780f | ||
|   | 1f105b6c15 | ||
|   | 5b7b0ea326 | ||
|   | 32a991989f | ||
|   | 788f76ab9c | ||
|   | f6411dce66 | ||
|   | 6f19ea1d84 | ||
|   | 448609533f | ||
|   | 6c48ace41e | ||
|   | c41e100c1c | ||
|   | 8216b522c2 | ||
|   | 82035d587a | ||
|   | 2796c3570a | ||
|   | f4f51e1de5 | ||
|   | af6b0d3266 | ||
|   | 7d1c77a38f | ||
|   | f807618f75 | ||
|   | 4cfb6713cb | ||
|   | d32f84f28d | ||
|   | 5fb1504211 | ||
|   | c37e1f0c9d | ||
|   | 90c234ffad | ||
|   | dd3a3ec586 | ||
|   | 6f67da09c0 | ||
|   | ba27c184f6 | ||
|   | b37f97128a | ||
|   | ee0de942f7 | ||
|   | ae2d48f2f4 | ||
|   | 1bd760b455 | ||
|   | 3d66a68791 | ||
|   | 01a53439c4 | ||
|   | 09ee8dbeb6 | ||
|   | f36c91550d | ||
|   | 6be6c711d0 | ||
|   | 72a36fb1cd | ||
|   | 4c982b3323 | ||
|   | c9c3be71cc | ||
|   | f1b965dcc5 | ||
|   | a08a23a93d | ||
|   | 2040a49458 | ||
|   | df94f4f907 | ||
|   | 96d375cb84 | ||
|   | 7a9c2f56c5 | ||
|   | 5ec7193e5c | ||
|   | d89e4337f2 | ||
|   | 2e192d5021 | ||
|   | 7db28c0156 | ||
|   | f09c842981 | ||
|   | b295bbd706 | ||
|   | 8d3132fefc | ||
|   | 00c5d3dbbb | ||
|   | ca37aff47d | ||
|   | 9ed069ef6a | ||
|   | 6faa3eb848 | ||
|   | 6c73ae5bf7 | ||
|   | ce77ddf365 | ||
|   | cf05fbaa9d | ||
|   | 552c474feb | ||
|   | a4f8e886bc | ||
|   | cc0c96b8b4 | ||
|   | 445f0e23fe | ||
|   | 6f240297d1 | ||
|   | 6da4981b70 | ||
|   | cfadf4d700 | ||
|   | 7e60de0531 | ||
|   | aaef6d7b91 | ||
|   | 58c5ce2638 | ||
|   | a9d01c7b55 | ||
|   | c5de8a4361 | ||
|   | b53645ce92 | ||
|   | de34a5a597 | ||
|   | bd8e15bdd1 | ||
|   | 45c7e0eeeb | ||
|   | a35a380ec7 | ||
|   | 02e67d1146 | ||
|   | a5411f7ac4 | ||
|   | e8da203fe1 | ||
|   | 10aa0a8829 | ||
|   | 85a37e2d2f | ||
|   | ba8621fa2c | ||
|   | 43e80f1a2e | ||
|   | 3a305a44b6 | ||
|   | e99143139e | ||
|   | f0c7232704 | ||
|   | b2186592df | ||
|   | e51e3e79d5 | ||
|   | 3b6b4d7664 | ||
|   | 239e71b414 | ||
|   | 080cad0ccd | ||
|   | dd49fd2788 | ||
|   | a571fb5528 | ||
|   | 1369c1ae8c | ||
|   | f5864181af | ||
|   | a4a0d7cf19 | ||
|   | 092dfd1e87 | ||
|   | a29ac33810 | ||
|   | 1421df2a5a | ||
|   | 591b8cc503 | ||
|   | 011467ece0 | ||
|   | f52e8c3392 | ||
|   | c8b87b65bd | ||
|   | 98cc82db44 | ||
|   | f510e2a8e0 | ||
|   | 3438912ba5 | ||
|   | 671c8e387f | ||
|   | 0108ec65cf | ||
|   | 39f7034578 | ||
|   | bf8affaf2b | ||
|   | e16a61eb53 | ||
|   | cadbe45bab | ||
|   | 51f971337d | ||
|   | 1f3c23de29 | ||
|   | bdfb17d957 | ||
|   | 8c97aee1fe | ||
|   | 38b4090daa | ||
|   | b8c55f2f65 | ||
|   | 7ca379e0a1 | ||
|   | 1617a9dfed | ||
|   | 2c9411c6c3 | ||
|   | 67626d4a06 | ||
|   | 8135611688 | ||
|   | 3ccbf6983e | ||
|   | e4f91195d8 | ||
|   | 2751f8f33b | ||
|   | 57f2df3b3e | ||
|   | 6822f0d067 | ||
|   | cfba957313 | ||
|   | 3149ffbf19 | ||
|   | 4cd8b76d7e | ||
|   | 4b644d8bc5 | ||
|   | 307cd5ad8c | ||
|   | ebc807a6a4 | ||
|   | 66adecdfc9 | ||
|   | 2cc6432a0f | ||
|   | a2c0c0474a | ||
|   | 27884b9a54 | ||
|   | 293df61872 | ||
|   | f82dada3e5 | ||
|   | e5824c4794 | ||
|   | 186550229c | ||
|   | 7877dd8e6b | ||
|   | b03abc249b | ||
|   | fda03918b9 | ||
|   | 6747375a1b | ||
|   | 53b6e31881 | ||
|   | fa004de2d1 | ||
|   | 3605f7b70f | ||
|   | 5348c54c91 | ||
|   | 684e4421bc | ||
|   | 28f5611df5 | ||
|   | 8da73d49d7 | ||
|   | 049ddd5f84 | ||
|   | 8ae2d4e93a | ||
|   | 824bb9ba35 | ||
|   | d550b1a18e | ||
|   | dea6c0e761 | ||
|   | 9caee357c0 | ||
|   | 35d892c418 | ||
|   | 9572a2a46b | ||
|   | 8996361b26 | ||
|   | 02ee731602 | ||
|   | bb1e6bf35b | ||
|   | c1b65285c1 | ||
|   | 8b8d6e5fa3 | ||
|   | c34fe184e8 | ||
|   | 7363838f86 | ||
|   | 3081425ccd | ||
|   | 95d494a54c | ||
|   | 145e5d7bc6 | ||
|   | 876fd9e85a | ||
|   | e8c30cabca | ||
|   | 490f84a7b1 | ||
|   | ca28178b86 | ||
|   | 2fceb0aeee | ||
|   | 86f39d1d43 | ||
|   | 1faf60444d | ||
|   | e927091d21 | ||
|   | cff2f856b3 | ||
|   | a743e3bbba | ||
|   | f8a52d250e | ||
|   | b70a523bdf | ||
|   | 8f2ed747e6 | ||
|   | 5deccefb15 | ||
|   | 3f04abfa9d | ||
|   | 8e55c83996 | ||
|   | dee59486ba | ||
|   | 77ef509aea | ||
|   | bfa7bccfa6 | ||
|   | a8c365edc8 | ||
|   | 94953ddf6c | ||
|   | 6b67546daf | ||
|   | 3e188d1f87 | ||
|   | f69eb15a90 | ||
|   | dfe348187f | ||
|   | 9706c56c5c | ||
|   | 3677c5be2c | ||
|   | bd339fa963 | ||
|   | 28f1b6bdf4 | ||
|   | c5aac3b81d | ||
|   | 70836597e9 | ||
|   | 958a1de2fd | ||
|   | 36d30266e3 | ||
|   | 558ab9761d | ||
|   | 269ef370e4 | ||
|   | ba2958ecd2 | ||
|   | 3b8b6eb315 | ||
|   | 4f13db3178 | ||
|   | ee7aa54ab4 | ||
|   | c305dd4cd5 | ||
|   | 6865791596 | ||
|   | 2099259393 | ||
|   | 27ca45dc70 | ||
|   | d290c11219 | ||
|   | cabe10ffdb | ||
|   | aa562c21a8 | ||
|   | 22175a7271 | ||
|   | 1e0647c0d1 | ||
|   | 58d94da8b3 | ||
|   | d97763a3e8 | ||
|   | aa129aa123 | ||
|   | f648317206 | ||
|   | 0685fdf7c6 | ||
|   | 6fd4cda534 | ||
|   | 511368da13 | ||
|   | 76e1721c58 | ||
|   | bad5a389b5 | ||
|   | 85d1f49763 | ||
|   | 7723d47ac1 | ||
|   | 30b130ca74 | ||
|   | a124ec0717 | ||
|   | 323d98ecf7 | ||
|   | 125a601ae3 | ||
|   | 3c549c6b31 | ||
|   | 9c1494c74d | ||
|   | e751abd775 | ||
|   | 714f2447b7 | ||
|   | d900e40d04 | ||
|   | 8b82383790 | ||
|   | 5a2cc2646c | ||
|   | 16a0902989 | ||
|   | 8f67aa38af | ||
|   | 34184cf2ab | ||
|   | 611cd2818e | ||
|   | 0a4e8fd5d0 | ||
|   | 11f0361f48 | ||
|   | cfa048ea4e | ||
|   | bbca7b762b | ||
|   | 1dba849567 | ||
|   | aff1ec10bf | ||
|   | 351ec08a71 | 
							
								
								
									
										2
									
								
								.github/ISSUE_TEMPLATE.md
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								.github/ISSUE_TEMPLATE.md
									
									
									
									
										vendored
									
									
								
							| @@ -51,7 +51,7 @@ DO NOT DELETE ANY TEXT from this template! Otherwise, your issue may be closed w | |||||||
| <!-- | <!-- | ||||||
|   Provide details about the versions you are using, which helps us reproducing |   Provide details about the versions you are using, which helps us reproducing | ||||||
|   and finding the issue quicker. Version information is found in the |   and finding the issue quicker. Version information is found in the | ||||||
|   Home Assistant frontend: Configuration -> Info. |   Home Assistant frontend: Settings -> About. | ||||||
|  |  | ||||||
|   Browser version and operating system is important! Please try to replicate |   Browser version and operating system is important! Please try to replicate | ||||||
|   your issue in a different browser and be sure to include your findings. |   your issue in a different browser and be sure to include your findings. | ||||||
|   | |||||||
							
								
								
									
										6
									
								
								.github/ISSUE_TEMPLATE/bug_report.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										6
									
								
								.github/ISSUE_TEMPLATE/bug_report.yml
									
									
									
									
										vendored
									
									
								
							| @@ -1,4 +1,4 @@ | |||||||
| name: Report a bug with the UI, Frontend or Lovelace | name: Report a bug with the UI / Dashboards | ||||||
| description: Report an issue related to the Home Assistant frontend. | description: Report an issue related to the Home Assistant frontend. | ||||||
| labels: bug | labels: bug | ||||||
| body: | body: | ||||||
| @@ -9,7 +9,7 @@ body: | |||||||
|  |  | ||||||
|         If you have a feature or enhancement request for the frontend, please [start an discussion][fr] instead of creating an issue. |         If you have a feature or enhancement request for the frontend, please [start an discussion][fr] instead of creating an issue. | ||||||
|  |  | ||||||
|         **Please not not report issues for custom Lovelace cards.** |         **Please not not report issues for custom cards.** | ||||||
|  |  | ||||||
|         [fr]: https://github.com/home-assistant/frontend/discussions |         [fr]: https://github.com/home-assistant/frontend/discussions | ||||||
|         [releases]: https://github.com/home-assistant/home-assistant/releases |         [releases]: https://github.com/home-assistant/home-assistant/releases | ||||||
| @@ -64,7 +64,7 @@ body: | |||||||
|       label: What version of Home Assistant Core has the issue? |       label: What version of Home Assistant Core has the issue? | ||||||
|       placeholder: core- |       placeholder: core- | ||||||
|       description: > |       description: > | ||||||
|         Can be found in the Configuration panel -> Info. |         Can be found in: [Settings -> About](https://my.home-assistant.io/redirect/info/). | ||||||
|   - type: input |   - type: input | ||||||
|     attributes: |     attributes: | ||||||
|       label: What was the last working version of Home Assistant Core? |       label: What was the last working version of Home Assistant Core? | ||||||
|   | |||||||
							
								
								
									
										8
									
								
								.github/ISSUE_TEMPLATE/config.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										8
									
								
								.github/ISSUE_TEMPLATE/config.yml
									
									
									
									
										vendored
									
									
								
							| @@ -1,17 +1,17 @@ | |||||||
| blank_issues_enabled: false | blank_issues_enabled: false | ||||||
| contact_links: | contact_links: | ||||||
|   - name: Request a feature for the UI, Frontend or Lovelace |   - name: Request a feature for the UI / Dashboards | ||||||
|     url: https://github.com/home-assistant/frontend/discussions/category_choices |     url: https://github.com/home-assistant/frontend/discussions/category_choices | ||||||
|     about: Request an new feature for the Home Assistant frontend. |     about: Request an new feature for the Home Assistant frontend. | ||||||
|   - name: Report a bug that is NOT related to the UI, Frontend or Lovelace |   - name: Report a bug that is NOT related to the UI / Dashboards | ||||||
|     url: https://github.com/home-assistant/core/issues |     url: https://github.com/home-assistant/core/issues | ||||||
|     about: This is the issue tracker for our frontend. Please report other issues with the backend repository. |     about: This is the issue tracker for our frontend. Please report other issues in the backend ("core") repository. | ||||||
|   - name: Report incorrect or missing information on our website |   - name: Report incorrect or missing information on our website | ||||||
|     url: https://github.com/home-assistant/home-assistant.io/issues |     url: https://github.com/home-assistant/home-assistant.io/issues | ||||||
|     about: Our documentation has its own issue tracker. Please report issues with the website there. |     about: Our documentation has its own issue tracker. Please report issues with the website there. | ||||||
|   - name: I have a question or need support |   - name: I have a question or need support | ||||||
|     url: https://www.home-assistant.io/help |     url: https://www.home-assistant.io/help | ||||||
|     about: We use GitHub for tracking bugs, check our website for resources on getting help. |     about: We use GitHub for tracking bugs. Check our website for resources on getting help. | ||||||
|   - name: I'm unsure where to go |   - name: I'm unsure where to go | ||||||
|     url: https://www.home-assistant.io/join-chat |     url: https://www.home-assistant.io/join-chat | ||||||
|     about: If you are unsure where to go, then joining our chat is recommended; Just ask! |     about: If you are unsure where to go, then joining our chat is recommended; Just ask! | ||||||
|   | |||||||
| @@ -26,8 +26,8 @@ module.exports = { | |||||||
|   }, |   }, | ||||||
|   version() { |   version() { | ||||||
|     const version = fs |     const version = fs | ||||||
|       .readFileSync(path.resolve(paths.polymer_dir, "setup.cfg"), "utf8") |       .readFileSync(path.resolve(paths.polymer_dir, "pyproject.toml"), "utf8") | ||||||
|       .match(/version\W+=\W(\d{8}\.\d)/); |       .match(/version\W+=\W"(\d{8}\.\d)"/); | ||||||
|     if (!version) { |     if (!version) { | ||||||
|       throw Error("Version not found"); |       throw Error("Version not found"); | ||||||
|     } |     } | ||||||
|   | |||||||
| @@ -3,10 +3,10 @@ const webpack = require("webpack"); | |||||||
| const path = require("path"); | const path = require("path"); | ||||||
| const TerserPlugin = require("terser-webpack-plugin"); | const TerserPlugin = require("terser-webpack-plugin"); | ||||||
| const { WebpackManifestPlugin } = require("webpack-manifest-plugin"); | const { WebpackManifestPlugin } = require("webpack-manifest-plugin"); | ||||||
| const paths = require("./paths.js"); |  | ||||||
| const bundle = require("./bundle.js"); |  | ||||||
| const log = require("fancy-log"); | const log = require("fancy-log"); | ||||||
| const WebpackBar = require("webpackbar"); | const WebpackBar = require("webpackbar"); | ||||||
|  | const paths = require("./paths.js"); | ||||||
|  | const bundle = require("./bundle.js"); | ||||||
|  |  | ||||||
| class LogStartCompilePlugin { | class LogStartCompilePlugin { | ||||||
|   ignoredFirst = false; |   ignoredFirst = false; | ||||||
| @@ -138,6 +138,8 @@ const createWebpackConfig = ({ | |||||||
|         "lit/directives/cache$": "lit/directives/cache.js", |         "lit/directives/cache$": "lit/directives/cache.js", | ||||||
|         "lit/directives/repeat$": "lit/directives/repeat.js", |         "lit/directives/repeat$": "lit/directives/repeat.js", | ||||||
|         "lit/polyfill-support$": "lit/polyfill-support.js", |         "lit/polyfill-support$": "lit/polyfill-support.js", | ||||||
|  |         "@lit-labs/virtualizer/layouts/grid": | ||||||
|  |           "@lit-labs/virtualizer/layouts/grid.js", | ||||||
|       }, |       }, | ||||||
|     }, |     }, | ||||||
|     output: { |     output: { | ||||||
|   | |||||||
| @@ -194,7 +194,7 @@ export const demoLovelaceJimpower: DemoConfig["lovelace"] = () => ({ | |||||||
|                     type: "state-icon", |                     type: "state-icon", | ||||||
|                     tap_action: { |                     tap_action: { | ||||||
|                       action: "call-service", |                       action: "call-service", | ||||||
|                       service_data: { |                       data: { | ||||||
|                         entity_id: "group.downstairs_lights", |                         entity_id: "group.downstairs_lights", | ||||||
|                       }, |                       }, | ||||||
|                       service: "homeassistant.toggle", |                       service: "homeassistant.toggle", | ||||||
|   | |||||||
| @@ -377,7 +377,7 @@ export const demoLovelaceTeachingbirds: DemoConfig["lovelace"] = () => ({ | |||||||
|               name: "AC bed", |               name: "AC bed", | ||||||
|               tap_action: { |               tap_action: { | ||||||
|                 action: "call-service", |                 action: "call-service", | ||||||
|                 service_data: { |                 data: { | ||||||
|                   entity_id: "script.air_cleaner_quiet", |                   entity_id: "script.air_cleaner_quiet", | ||||||
|                 }, |                 }, | ||||||
|                 service: "script.turn_on", |                 service: "script.turn_on", | ||||||
| @@ -390,7 +390,7 @@ export const demoLovelaceTeachingbirds: DemoConfig["lovelace"] = () => ({ | |||||||
|               name: "AC bed", |               name: "AC bed", | ||||||
|               tap_action: { |               tap_action: { | ||||||
|                 action: "call-service", |                 action: "call-service", | ||||||
|                 service_data: { |                 data: { | ||||||
|                   entity_id: "script.air_cleaner_auto", |                   entity_id: "script.air_cleaner_auto", | ||||||
|                 }, |                 }, | ||||||
|                 service: "script.turn_on", |                 service: "script.turn_on", | ||||||
| @@ -403,7 +403,7 @@ export const demoLovelaceTeachingbirds: DemoConfig["lovelace"] = () => ({ | |||||||
|               name: "AC bed", |               name: "AC bed", | ||||||
|               tap_action: { |               tap_action: { | ||||||
|                 action: "call-service", |                 action: "call-service", | ||||||
|                 service_data: { |                 data: { | ||||||
|                   entity_id: "script.air_cleaner_turbo", |                   entity_id: "script.air_cleaner_turbo", | ||||||
|                 }, |                 }, | ||||||
|                 service: "script.turn_on", |                 service: "script.turn_on", | ||||||
| @@ -416,7 +416,7 @@ export const demoLovelaceTeachingbirds: DemoConfig["lovelace"] = () => ({ | |||||||
|               name: "AC", |               name: "AC", | ||||||
|               tap_action: { |               tap_action: { | ||||||
|                 action: "call-service", |                 action: "call-service", | ||||||
|                 service_data: { |                 data: { | ||||||
|                   entity_id: "script.ac_off", |                   entity_id: "script.ac_off", | ||||||
|                 }, |                 }, | ||||||
|                 service: "script.turn_on", |                 service: "script.turn_on", | ||||||
| @@ -429,7 +429,7 @@ export const demoLovelaceTeachingbirds: DemoConfig["lovelace"] = () => ({ | |||||||
|               name: "AC", |               name: "AC", | ||||||
|               tap_action: { |               tap_action: { | ||||||
|                 action: "call-service", |                 action: "call-service", | ||||||
|                 service_data: { |                 data: { | ||||||
|                   entity_id: "script.ac_on", |                   entity_id: "script.ac_on", | ||||||
|                 }, |                 }, | ||||||
|                 service: "script.turn_on", |                 service: "script.turn_on", | ||||||
| @@ -629,7 +629,7 @@ export const demoLovelaceTeachingbirds: DemoConfig["lovelace"] = () => ({ | |||||||
|                   entity: "scene.morning_lights", |                   entity: "scene.morning_lights", | ||||||
|                   tap_action: { |                   tap_action: { | ||||||
|                     action: "call-service", |                     action: "call-service", | ||||||
|                     service_data: { |                     data: { | ||||||
|                       entity_id: "scene.morning_lights", |                       entity_id: "scene.morning_lights", | ||||||
|                     }, |                     }, | ||||||
|                     service: "scene.turn_on", |                     service: "scene.turn_on", | ||||||
| @@ -641,7 +641,7 @@ export const demoLovelaceTeachingbirds: DemoConfig["lovelace"] = () => ({ | |||||||
|                   entity: "scene.movie_time", |                   entity: "scene.movie_time", | ||||||
|                   tap_action: { |                   tap_action: { | ||||||
|                     action: "call-service", |                     action: "call-service", | ||||||
|                     service_data: { |                     data: { | ||||||
|                       entity_id: "scene.movie_time", |                       entity_id: "scene.movie_time", | ||||||
|                     }, |                     }, | ||||||
|                     service: "scene.turn_on", |                     service: "scene.turn_on", | ||||||
| @@ -702,7 +702,7 @@ export const demoLovelaceTeachingbirds: DemoConfig["lovelace"] = () => ({ | |||||||
|                   entity: "light.downstairs_lights", |                   entity: "light.downstairs_lights", | ||||||
|                   tap_action: { |                   tap_action: { | ||||||
|                     action: "call-service", |                     action: "call-service", | ||||||
|                     service_data: { |                     data: { | ||||||
|                       entity_id: "light.downstairs_lights", |                       entity_id: "light.downstairs_lights", | ||||||
|                     }, |                     }, | ||||||
|                     service: "light.toggle", |                     service: "light.toggle", | ||||||
| @@ -714,7 +714,7 @@ export const demoLovelaceTeachingbirds: DemoConfig["lovelace"] = () => ({ | |||||||
|                   entity: "light.upstairs_lights", |                   entity: "light.upstairs_lights", | ||||||
|                   tap_action: { |                   tap_action: { | ||||||
|                     action: "call-service", |                     action: "call-service", | ||||||
|                     service_data: { |                     data: { | ||||||
|                       entity_id: "light.upstairs_lights", |                       entity_id: "light.upstairs_lights", | ||||||
|                     }, |                     }, | ||||||
|                     service: "light.toggle", |                     service: "light.toggle", | ||||||
|   | |||||||
| @@ -1,4 +1,4 @@ | |||||||
| import { format, startOfToday, startOfTomorrow } from "date-fns"; | import { format, startOfToday, startOfTomorrow } from "date-fns/esm"; | ||||||
| import { EnergySolarForecasts } from "../../../src/data/energy"; | import { EnergySolarForecasts } from "../../../src/data/energy"; | ||||||
| import { MockHomeAssistant } from "../../../src/fake_data/provide_hass"; | import { MockHomeAssistant } from "../../../src/fake_data/provide_hass"; | ||||||
|  |  | ||||||
|   | |||||||
| @@ -4,7 +4,7 @@ import { | |||||||
|   addMonths, |   addMonths, | ||||||
|   differenceInHours, |   differenceInHours, | ||||||
|   endOfDay, |   endOfDay, | ||||||
| } from "date-fns"; | } from "date-fns/esm"; | ||||||
| import { HassEntity } from "home-assistant-js-websocket"; | import { HassEntity } from "home-assistant-js-websocket"; | ||||||
| import { StatisticValue } from "../../../src/data/history"; | import { StatisticValue } from "../../../src/data/history"; | ||||||
| import { MockHomeAssistant } from "../../../src/fake_data/provide_hass"; | import { MockHomeAssistant } from "../../../src/fake_data/provide_hass"; | ||||||
|   | |||||||
| @@ -119,7 +119,7 @@ export const basicTrace: DemoTrace = { | |||||||
|             params: { |             params: { | ||||||
|               domain: "input_boolean", |               domain: "input_boolean", | ||||||
|               service: "toggle", |               service: "toggle", | ||||||
|               service_data: {}, |               data: {}, | ||||||
|               target: { |               target: { | ||||||
|                 entity_id: ["input_boolean.toggle_4"], |                 entity_id: ["input_boolean.toggle_4"], | ||||||
|               }, |               }, | ||||||
| @@ -164,7 +164,7 @@ export const basicTrace: DemoTrace = { | |||||||
|             params: { |             params: { | ||||||
|               domain: "input_boolean", |               domain: "input_boolean", | ||||||
|               service: "toggle", |               service: "toggle", | ||||||
|               service_data: {}, |               data: {}, | ||||||
|               target: { |               target: { | ||||||
|                 entity_id: ["input_boolean.toggle_2"], |                 entity_id: ["input_boolean.toggle_2"], | ||||||
|               }, |               }, | ||||||
| @@ -182,7 +182,7 @@ export const basicTrace: DemoTrace = { | |||||||
|             params: { |             params: { | ||||||
|               domain: "input_boolean", |               domain: "input_boolean", | ||||||
|               service: "toggle", |               service: "toggle", | ||||||
|               service_data: {}, |               data: {}, | ||||||
|               target: { |               target: { | ||||||
|                 entity_id: ["input_boolean.toggle_3"], |                 entity_id: ["input_boolean.toggle_3"], | ||||||
|               }, |               }, | ||||||
| @@ -200,7 +200,7 @@ export const basicTrace: DemoTrace = { | |||||||
|             params: { |             params: { | ||||||
|               domain: "input_boolean", |               domain: "input_boolean", | ||||||
|               service: "toggle", |               service: "toggle", | ||||||
|               service_data: {}, |               data: {}, | ||||||
|               target: { |               target: { | ||||||
|                 entity_id: ["input_boolean.toggle_4"], |                 entity_id: ["input_boolean.toggle_4"], | ||||||
|               }, |               }, | ||||||
| @@ -298,11 +298,11 @@ export const basicTrace: DemoTrace = { | |||||||
|       source: "state of input_boolean.toggle_1", |       source: "state of input_boolean.toggle_1", | ||||||
|       entity_id: "automation.toggle_toggles", |       entity_id: "automation.toggle_toggles", | ||||||
|       context_id: "6cfcae368e7b3686fad6c59e83ae76c9", |       context_id: "6cfcae368e7b3686fad6c59e83ae76c9", | ||||||
|       when: "2021-03-25T04:36:51.240832+00:00", |       when: 1616647011.240832, | ||||||
|       domain: "automation", |       domain: "automation", | ||||||
|     }, |     }, | ||||||
|     { |     { | ||||||
|       when: "2021-03-25T04:36:51.249828+00:00", |       when: 1616647011.249828, | ||||||
|       name: "Toggle 4", |       name: "Toggle 4", | ||||||
|       state: "on", |       state: "on", | ||||||
|       entity_id: "input_boolean.toggle_4", |       entity_id: "input_boolean.toggle_4", | ||||||
| @@ -313,7 +313,7 @@ export const basicTrace: DemoTrace = { | |||||||
|       context_name: "Ensure Party mode", |       context_name: "Ensure Party mode", | ||||||
|     }, |     }, | ||||||
|     { |     { | ||||||
|       when: "2021-03-25T04:36:51.258947+00:00", |       when: 1616647011.258947, | ||||||
|       name: "Toggle 2", |       name: "Toggle 2", | ||||||
|       state: "on", |       state: "on", | ||||||
|       entity_id: "input_boolean.toggle_2", |       entity_id: "input_boolean.toggle_2", | ||||||
| @@ -324,7 +324,7 @@ export const basicTrace: DemoTrace = { | |||||||
|       context_name: "Ensure Party mode", |       context_name: "Ensure Party mode", | ||||||
|     }, |     }, | ||||||
|     { |     { | ||||||
|       when: "2021-03-25T04:36:51.261806+00:00", |       when: 1616647011.261806, | ||||||
|       name: "Toggle 3", |       name: "Toggle 3", | ||||||
|       state: "off", |       state: "off", | ||||||
|       entity_id: "input_boolean.toggle_3", |       entity_id: "input_boolean.toggle_3", | ||||||
| @@ -335,7 +335,7 @@ export const basicTrace: DemoTrace = { | |||||||
|       context_name: "Ensure Party mode", |       context_name: "Ensure Party mode", | ||||||
|     }, |     }, | ||||||
|     { |     { | ||||||
|       when: "2021-03-25T04:36:51.265246+00:00", |       when: 1616647011.265246, | ||||||
|       name: "Toggle 4", |       name: "Toggle 4", | ||||||
|       state: "off", |       state: "off", | ||||||
|       entity_id: "input_boolean.toggle_4", |       entity_id: "input_boolean.toggle_4", | ||||||
|   | |||||||
| @@ -185,11 +185,11 @@ export const motionLightTrace: DemoTrace = { | |||||||
|         "has been triggered by state of binary_sensor.pauluss_macbook_pro_camera_in_use", |         "has been triggered by state of binary_sensor.pauluss_macbook_pro_camera_in_use", | ||||||
|       source: "state of binary_sensor.pauluss_macbook_pro_camera_in_use", |       source: "state of binary_sensor.pauluss_macbook_pro_camera_in_use", | ||||||
|       entity_id: "automation.auto_elgato", |       entity_id: "automation.auto_elgato", | ||||||
|       when: "2021-03-14T06:07:01.768492+00:00", |       when: 1615702021.768492, | ||||||
|       domain: "automation", |       domain: "automation", | ||||||
|     }, |     }, | ||||||
|     { |     { | ||||||
|       when: "2021-03-14T06:07:01.872187+00:00", |       when: 1615702021.872187, | ||||||
|       name: "Elgato Key Light Air", |       name: "Elgato Key Light Air", | ||||||
|       state: "on", |       state: "on", | ||||||
|       entity_id: "light.elgato_key_light_air", |       entity_id: "light.elgato_key_light_air", | ||||||
| @@ -200,7 +200,7 @@ export const motionLightTrace: DemoTrace = { | |||||||
|       context_name: "Auto Elgato", |       context_name: "Auto Elgato", | ||||||
|     }, |     }, | ||||||
|     { |     { | ||||||
|       when: "2021-03-14T06:07:53.284505+00:00", |       when: 1615702073.284505, | ||||||
|       name: "Elgato Key Light Air", |       name: "Elgato Key Light Air", | ||||||
|       state: "off", |       state: "off", | ||||||
|       entity_id: "light.elgato_key_light_air", |       entity_id: "light.elgato_key_light_air", | ||||||
|   | |||||||
| @@ -62,6 +62,45 @@ const ACTIONS = [ | |||||||
|       entity_id: "input_boolean.toggle_4", |       entity_id: "input_boolean.toggle_4", | ||||||
|     }, |     }, | ||||||
|   }, |   }, | ||||||
|  |   { | ||||||
|  |     parallel: [ | ||||||
|  |       { scene: "scene.kitchen_morning" }, | ||||||
|  |       { | ||||||
|  |         service: "media_player.play_media", | ||||||
|  |         target: { entity_id: "media_player.living_room" }, | ||||||
|  |         data: { media_content_id: "", media_content_type: "" }, | ||||||
|  |         metadata: { title: "Happy Song" }, | ||||||
|  |       }, | ||||||
|  |     ], | ||||||
|  |   }, | ||||||
|  |   { | ||||||
|  |     stop: "No one is home!", | ||||||
|  |   }, | ||||||
|  |   { repeat: { count: 3, sequence: [{ delay: "00:00:01" }] } }, | ||||||
|  |   { | ||||||
|  |     repeat: { | ||||||
|  |       for_each: ["bread", "butter", "cheese"], | ||||||
|  |       sequence: [{ delay: "00:00:01" }], | ||||||
|  |     }, | ||||||
|  |   }, | ||||||
|  |   { | ||||||
|  |     if: [{ condition: "state" }], | ||||||
|  |     then: [{ delay: "00:00:01" }], | ||||||
|  |     else: [{ delay: "00:00:05" }], | ||||||
|  |   }, | ||||||
|  |   { | ||||||
|  |     choose: [ | ||||||
|  |       { | ||||||
|  |         conditions: [{ condition: "state" }], | ||||||
|  |         sequence: [{ delay: "00:00:01" }], | ||||||
|  |       }, | ||||||
|  |       { | ||||||
|  |         conditions: [{ condition: "sun" }], | ||||||
|  |         sequence: [{ delay: "00:00:05" }], | ||||||
|  |       }, | ||||||
|  |     ], | ||||||
|  |     default: [{ delay: "00:00:03" }], | ||||||
|  |   }, | ||||||
| ]; | ]; | ||||||
|  |  | ||||||
| @customElement("demo-automation-describe-action") | @customElement("demo-automation-describe-action") | ||||||
|   | |||||||
| @@ -20,6 +20,10 @@ import { HaWaitForTriggerAction } from "../../../../src/panels/config/automation | |||||||
| import { HaWaitAction } from "../../../../src/panels/config/automation/action/types/ha-automation-action-wait_template"; | import { HaWaitAction } from "../../../../src/panels/config/automation/action/types/ha-automation-action-wait_template"; | ||||||
| import { Action } from "../../../../src/data/script"; | import { Action } from "../../../../src/data/script"; | ||||||
| import { HaConditionAction } from "../../../../src/panels/config/automation/action/types/ha-automation-action-condition"; | import { HaConditionAction } from "../../../../src/panels/config/automation/action/types/ha-automation-action-condition"; | ||||||
|  | import { HaParallelAction } from "../../../../src/panels/config/automation/action/types/ha-automation-action-parallel"; | ||||||
|  | import { HaIfAction } from "../../../../src/panels/config/automation/action/types/ha-automation-action-if"; | ||||||
|  | import { HaStopAction } from "../../../../src/panels/config/automation/action/types/ha-automation-action-stop"; | ||||||
|  | import { HaPlayMediaAction } from "../../../../src/panels/config/automation/action/types/ha-automation-action-play_media"; | ||||||
|  |  | ||||||
| const SCHEMAS: { name: string; actions: Action[] }[] = [ | const SCHEMAS: { name: string; actions: Action[] }[] = [ | ||||||
|   { name: "Event", actions: [HaEventAction.defaultConfig] }, |   { name: "Event", actions: [HaEventAction.defaultConfig] }, | ||||||
| @@ -28,11 +32,15 @@ const SCHEMAS: { name: string; actions: Action[] }[] = [ | |||||||
|   { name: "Condition", actions: [HaConditionAction.defaultConfig] }, |   { name: "Condition", actions: [HaConditionAction.defaultConfig] }, | ||||||
|   { name: "Delay", actions: [HaDelayAction.defaultConfig] }, |   { name: "Delay", actions: [HaDelayAction.defaultConfig] }, | ||||||
|   { name: "Scene", actions: [HaSceneAction.defaultConfig] }, |   { name: "Scene", actions: [HaSceneAction.defaultConfig] }, | ||||||
|  |   { name: "Play media", actions: [HaPlayMediaAction.defaultConfig] }, | ||||||
|   { name: "Wait", actions: [HaWaitAction.defaultConfig] }, |   { name: "Wait", actions: [HaWaitAction.defaultConfig] }, | ||||||
|   { name: "WaitForTrigger", actions: [HaWaitForTriggerAction.defaultConfig] }, |   { name: "WaitForTrigger", actions: [HaWaitForTriggerAction.defaultConfig] }, | ||||||
|   { name: "Repeat", actions: [HaRepeatAction.defaultConfig] }, |   { name: "Repeat", actions: [HaRepeatAction.defaultConfig] }, | ||||||
|  |   { name: "If-Then", actions: [HaIfAction.defaultConfig] }, | ||||||
|   { name: "Choose", actions: [HaChooseAction.defaultConfig] }, |   { name: "Choose", actions: [HaChooseAction.defaultConfig] }, | ||||||
|   { name: "Variables", actions: [{ variables: { hello: "1" } }] }, |   { name: "Variables", actions: [{ variables: { hello: "1" } }] }, | ||||||
|  |   { name: "Parallel", actions: [HaParallelAction.defaultConfig] }, | ||||||
|  |   { name: "Stop", actions: [HaStopAction.defaultConfig] }, | ||||||
| ]; | ]; | ||||||
|  |  | ||||||
| @customElement("demo-automation-editor-action") | @customElement("demo-automation-editor-action") | ||||||
| @@ -86,6 +94,6 @@ class DemoHaAutomationEditorAction extends LitElement { | |||||||
|  |  | ||||||
| declare global { | declare global { | ||||||
|   interface HTMLElementTagNameMap { |   interface HTMLElementTagNameMap { | ||||||
|     "demo-ha-automation-editor-action": DemoHaAutomationEditorAction; |     "demo-automation-editor-action": DemoHaAutomationEditorAction; | ||||||
|   } |   } | ||||||
| } | } | ||||||
|   | |||||||
| @@ -8,7 +8,7 @@ import { mockEntityRegistry } from "../../../../demo/src/stubs/entity_registry"; | |||||||
| import { mockDeviceRegistry } from "../../../../demo/src/stubs/device_registry"; | import { mockDeviceRegistry } from "../../../../demo/src/stubs/device_registry"; | ||||||
| import { mockAreaRegistry } from "../../../../demo/src/stubs/area_registry"; | import { mockAreaRegistry } from "../../../../demo/src/stubs/area_registry"; | ||||||
| import { mockHassioSupervisor } from "../../../../demo/src/stubs/hassio_supervisor"; | import { mockHassioSupervisor } from "../../../../demo/src/stubs/hassio_supervisor"; | ||||||
| import type { Condition } from "../../../../src/data/automation"; | import type { ConditionWithShorthand } from "../../../../src/data/automation"; | ||||||
| import "../../../../src/panels/config/automation/condition/ha-automation-condition"; | import "../../../../src/panels/config/automation/condition/ha-automation-condition"; | ||||||
| import { HaDeviceCondition } from "../../../../src/panels/config/automation/condition/types/ha-automation-condition-device"; | import { HaDeviceCondition } from "../../../../src/panels/config/automation/condition/types/ha-automation-condition-device"; | ||||||
| import { HaLogicalCondition } from "../../../../src/panels/config/automation/condition/types/ha-automation-condition-logical"; | import { HaLogicalCondition } from "../../../../src/panels/config/automation/condition/types/ha-automation-condition-logical"; | ||||||
| @@ -20,7 +20,7 @@ import { HaTimeCondition } from "../../../../src/panels/config/automation/condit | |||||||
| import { HaTriggerCondition } from "../../../../src/panels/config/automation/condition/types/ha-automation-condition-trigger"; | import { HaTriggerCondition } from "../../../../src/panels/config/automation/condition/types/ha-automation-condition-trigger"; | ||||||
| import { HaZoneCondition } from "../../../../src/panels/config/automation/condition/types/ha-automation-condition-zone"; | import { HaZoneCondition } from "../../../../src/panels/config/automation/condition/types/ha-automation-condition-zone"; | ||||||
|  |  | ||||||
| const SCHEMAS: { name: string; conditions: Condition[] }[] = [ | const SCHEMAS: { name: string; conditions: ConditionWithShorthand[] }[] = [ | ||||||
|   { |   { | ||||||
|     name: "State", |     name: "State", | ||||||
|     conditions: [{ condition: "state", ...HaStateCondition.defaultConfig }], |     conditions: [{ condition: "state", ...HaStateCondition.defaultConfig }], | ||||||
| @@ -69,6 +69,14 @@ const SCHEMAS: { name: string; conditions: Condition[] }[] = [ | |||||||
|     name: "Trigger", |     name: "Trigger", | ||||||
|     conditions: [{ condition: "trigger", ...HaTriggerCondition.defaultConfig }], |     conditions: [{ condition: "trigger", ...HaTriggerCondition.defaultConfig }], | ||||||
|   }, |   }, | ||||||
|  |   { | ||||||
|  |     name: "Shorthand", | ||||||
|  |     conditions: [ | ||||||
|  |       { and: HaLogicalCondition.defaultConfig.conditions }, | ||||||
|  |       { or: HaLogicalCondition.defaultConfig.conditions }, | ||||||
|  |       { not: HaLogicalCondition.defaultConfig.conditions }, | ||||||
|  |     ], | ||||||
|  |   }, | ||||||
| ]; | ]; | ||||||
|  |  | ||||||
| @customElement("demo-automation-editor-condition") | @customElement("demo-automation-editor-condition") | ||||||
|   | |||||||
| @@ -159,13 +159,19 @@ export class DemoHaAlert extends LitElement { | |||||||
|  |  | ||||||
|   firstUpdated(changedProps) { |   firstUpdated(changedProps) { | ||||||
|     super.firstUpdated(changedProps); |     super.firstUpdated(changedProps); | ||||||
|     applyThemesOnElement(this.shadowRoot!.querySelector(".dark"), { |     applyThemesOnElement( | ||||||
|  |       this.shadowRoot!.querySelector(".dark"), | ||||||
|  |       { | ||||||
|         default_theme: "default", |         default_theme: "default", | ||||||
|         default_dark_theme: "default", |         default_dark_theme: "default", | ||||||
|         themes: {}, |         themes: {}, | ||||||
|         darkMode: true, |         darkMode: true, | ||||||
|         theme: "default", |         theme: "default", | ||||||
|     }); |       }, | ||||||
|  |       undefined, | ||||||
|  |       undefined, | ||||||
|  |       true | ||||||
|  |     ); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   static get styles() { |   static get styles() { | ||||||
|   | |||||||
| @@ -170,6 +170,7 @@ const SCHEMAS: { | |||||||
|           select: { options: ["Option 1", "Option 2"], mode: "list" }, |           select: { options: ["Option 1", "Option 2"], mode: "list" }, | ||||||
|         }, |         }, | ||||||
|       }, |       }, | ||||||
|  |       template: { name: "Template", selector: { template: {} } }, | ||||||
|       select: { |       select: { | ||||||
|         name: "Select", |         name: "Select", | ||||||
|         selector: { |         selector: { | ||||||
|   | |||||||
							
								
								
									
										3
									
								
								gallery/src/pages/components/ha-tip.markdown
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								gallery/src/pages/components/ha-tip.markdown
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,3 @@ | |||||||
|  | --- | ||||||
|  | title: Tips | ||||||
|  | --- | ||||||
							
								
								
									
										73
									
								
								gallery/src/pages/components/ha-tip.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										73
									
								
								gallery/src/pages/components/ha-tip.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,73 @@ | |||||||
|  | import { html, css, LitElement, TemplateResult } from "lit"; | ||||||
|  | import { customElement } from "lit/decorators"; | ||||||
|  | import "../../../../src/components/ha-tip"; | ||||||
|  | import "../../../../src/components/ha-card"; | ||||||
|  | import { applyThemesOnElement } from "../../../../src/common/dom/apply_themes_on_element"; | ||||||
|  |  | ||||||
|  | const tips: (string | TemplateResult)[] = [ | ||||||
|  |   "Test tip", | ||||||
|  |   "Bigger test tip, with some random text just to fill up as much space as possible without it looking like I'm really trying to to that", | ||||||
|  |   html`<i>Tip</i> <b>with</b> <sub>HTML</sub>`, | ||||||
|  | ]; | ||||||
|  |  | ||||||
|  | @customElement("demo-components-ha-tip") | ||||||
|  | export class DemoHaTip extends LitElement { | ||||||
|  |   protected render(): TemplateResult { | ||||||
|  |     return html` ${["light", "dark"].map( | ||||||
|  |       (mode) => html` | ||||||
|  |         <div class=${mode}> | ||||||
|  |           <ha-card header="ha-tip ${mode} demo"> | ||||||
|  |             <div class="card-content"> | ||||||
|  |               ${tips.map((tip) => html`<ha-tip>${tip}</ha-tip>`)} | ||||||
|  |             </div> | ||||||
|  |           </ha-card> | ||||||
|  |         </div> | ||||||
|  |       ` | ||||||
|  |     )}`; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   firstUpdated(changedProps) { | ||||||
|  |     super.firstUpdated(changedProps); | ||||||
|  |     applyThemesOnElement( | ||||||
|  |       this.shadowRoot!.querySelector(".dark"), | ||||||
|  |       { | ||||||
|  |         default_theme: "default", | ||||||
|  |         default_dark_theme: "default", | ||||||
|  |         themes: {}, | ||||||
|  |         darkMode: true, | ||||||
|  |         theme: "default", | ||||||
|  |       }, | ||||||
|  |       undefined, | ||||||
|  |       undefined, | ||||||
|  |       true | ||||||
|  |     ); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   static get styles() { | ||||||
|  |     return css` | ||||||
|  |       :host { | ||||||
|  |         display: flex; | ||||||
|  |         flex-direction: row; | ||||||
|  |         justify-content: space-between; | ||||||
|  |       } | ||||||
|  |       .dark, | ||||||
|  |       .light { | ||||||
|  |         display: block; | ||||||
|  |         background-color: var(--primary-background-color); | ||||||
|  |         padding: 0 50px; | ||||||
|  |       } | ||||||
|  |       ha-tip { | ||||||
|  |         margin-bottom: 14px; | ||||||
|  |       } | ||||||
|  |       ha-card { | ||||||
|  |         margin: 24px auto; | ||||||
|  |       } | ||||||
|  |     `; | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | declare global { | ||||||
|  |   interface HTMLElementTagNameMap { | ||||||
|  |     "demo-components-ha-tip": DemoHaTip; | ||||||
|  |   } | ||||||
|  | } | ||||||
| @@ -249,7 +249,7 @@ const CONFIGS = [ | |||||||
|       name: Bed light |       name: Bed light | ||||||
|       action_name: Toggle light |       action_name: Toggle light | ||||||
|       service: light.toggle |       service: light.toggle | ||||||
|       service_data: |       data: | ||||||
|         entity_id: light.bed_light |         entity_id: light.bed_light | ||||||
|     - type: section |     - type: section | ||||||
|       label: Links |       label: Links | ||||||
|   | |||||||
| @@ -199,7 +199,7 @@ const CONFIGS = [ | |||||||
|       tap_action: |       tap_action: | ||||||
|         action: call-service |         action: call-service | ||||||
|         service: light.turn_on |         service: light.turn_on | ||||||
|         service_data: |         data: | ||||||
|           entity_id: light.ceiling_lights |           entity_id: light.ceiling_lights | ||||||
|     - entity: sun.sun |     - entity: sun.sun | ||||||
|       name: Regular |       name: Regular | ||||||
|   | |||||||
| @@ -40,7 +40,7 @@ const CONFIGS = [ | |||||||
|         left: 90% |         left: 90% | ||||||
|         padding: 0px |         padding: 0px | ||||||
|       service: light.turn_off |       service: light.turn_off | ||||||
|       service_data: |       data: | ||||||
|         entity_id: group.all_lights |         entity_id: group.all_lights | ||||||
|     - type: icon |     - type: icon | ||||||
|       icon: mdi:cctv |       icon: mdi:cctv | ||||||
| @@ -88,7 +88,7 @@ const CONFIGS = [ | |||||||
|         left: 90% |         left: 90% | ||||||
|         padding: 0px |         padding: 0px | ||||||
|       service: light.turn_off |       service: light.turn_off | ||||||
|       service_data: |       data: | ||||||
|         entity_id: group.all_lights |         entity_id: group.all_lights | ||||||
|     - type: icon |     - type: icon | ||||||
|       icon: mdi:cctv |       icon: mdi:cctv | ||||||
|   | |||||||
| @@ -133,6 +133,12 @@ const ENTITIES = [ | |||||||
|     friendly_name: "Update with auto update", |     friendly_name: "Update with auto update", | ||||||
|     auto_update: true, |     auto_update: true, | ||||||
|   }), |   }), | ||||||
|  |   getEntity("update", "update20", "on", { | ||||||
|  |     ...base_attributes, | ||||||
|  |     in_progress: true, | ||||||
|  |     title: undefined, | ||||||
|  |     friendly_name: "Installing without title", | ||||||
|  |   }), | ||||||
| ]; | ]; | ||||||
|  |  | ||||||
| @customElement("demo-more-info-update") | @customElement("demo-more-info-update") | ||||||
|   | |||||||
| @@ -68,6 +68,7 @@ class HassioAddonRepositoryEl extends LitElement { | |||||||
|           ${addons.map( |           ${addons.map( | ||||||
|             (addon) => html` |             (addon) => html` | ||||||
|               <ha-card |               <ha-card | ||||||
|  |                 outlined | ||||||
|                 .addon=${addon} |                 .addon=${addon} | ||||||
|                 class=${addon.available ? "" : "not_available"} |                 class=${addon.available ? "" : "not_available"} | ||||||
|                 @click=${this._addonTapped} |                 @click=${this._addonTapped} | ||||||
|   | |||||||
| @@ -50,6 +50,7 @@ class HassioAddonAudio extends LitElement { | |||||||
|   protected render(): TemplateResult { |   protected render(): TemplateResult { | ||||||
|     return html` |     return html` | ||||||
|       <ha-card |       <ha-card | ||||||
|  |         outlined | ||||||
|         .header=${this.supervisor.localize("addon.configuration.audio.header")} |         .header=${this.supervisor.localize("addon.configuration.audio.header")} | ||||||
|       > |       > | ||||||
|         <div class="card-content"> |         <div class="card-content"> | ||||||
|   | |||||||
| @@ -39,7 +39,14 @@ import type { HomeAssistant } from "../../../../src/types"; | |||||||
| import { suggestAddonRestart } from "../../dialogs/suggestAddonRestart"; | import { suggestAddonRestart } from "../../dialogs/suggestAddonRestart"; | ||||||
| import { hassioStyle } from "../../resources/hassio-style"; | import { hassioStyle } from "../../resources/hassio-style"; | ||||||
|  |  | ||||||
| const SUPPORTED_UI_TYPES = ["string", "select", "boolean", "integer", "float"]; | const SUPPORTED_UI_TYPES = [ | ||||||
|  |   "string", | ||||||
|  |   "select", | ||||||
|  |   "boolean", | ||||||
|  |   "integer", | ||||||
|  |   "float", | ||||||
|  |   "schema", | ||||||
|  | ]; | ||||||
|  |  | ||||||
| const ADDON_YAML_SCHEMA = DEFAULT_SCHEMA.extend([ | const ADDON_YAML_SCHEMA = DEFAULT_SCHEMA.extend([ | ||||||
|   new Type("!secret", { |   new Type("!secret", { | ||||||
| @@ -48,6 +55,8 @@ const ADDON_YAML_SCHEMA = DEFAULT_SCHEMA.extend([ | |||||||
|   }), |   }), | ||||||
| ]); | ]); | ||||||
|  |  | ||||||
|  | const MASKED_FIELDS = ["password", "secret", "token"]; | ||||||
|  |  | ||||||
| @customElement("hassio-addon-config") | @customElement("hassio-addon-config") | ||||||
| class HassioAddonConfig extends LitElement { | class HassioAddonConfig extends LitElement { | ||||||
|   @property({ attribute: false }) public addon!: HassioAddonDetails; |   @property({ attribute: false }) public addon!: HassioAddonDetails; | ||||||
| @@ -75,16 +84,63 @@ class HassioAddonConfig extends LitElement { | |||||||
|   public computeLabel = (entry: HaFormSchema): string => |   public computeLabel = (entry: HaFormSchema): string => | ||||||
|     this.addon.translations[this.hass.language]?.configuration?.[entry.name] |     this.addon.translations[this.hass.language]?.configuration?.[entry.name] | ||||||
|       ?.name || |       ?.name || | ||||||
|     this.addon.translations.en?.configuration?.[entry.name].name || |     this.addon.translations.en?.configuration?.[entry.name]?.name || | ||||||
|     entry.name; |     entry.name; | ||||||
|  |  | ||||||
|   private _schema = memoizeOne((schema: HaFormSchema[]): HaFormSchema[] => |   public computeHelper = (entry: HaFormSchema): string => | ||||||
|     // @ts-expect-error supervisor does not implement [string, string] for select.options[] |     this.addon.translations[this.hass.language]?.configuration?.[entry.name] | ||||||
|  |       ?.description || | ||||||
|  |     this.addon.translations.en?.configuration?.[entry.name]?.description || | ||||||
|  |     ""; | ||||||
|  |  | ||||||
|  |   private _convertSchema = memoizeOne( | ||||||
|  |     // Convert supervisor schema to selectors | ||||||
|  |     (schema: Record<string, any>): HaFormSchema[] => | ||||||
|       schema.map((entry) => |       schema.map((entry) => | ||||||
|         entry.type === "select" |         entry.type === "select" | ||||||
|           ? { |           ? { | ||||||
|             ...entry, |               name: entry.name, | ||||||
|             options: entry.options.map((option) => [option, option]), |               required: entry.required, | ||||||
|  |               selector: { select: { options: entry.options } }, | ||||||
|  |             } | ||||||
|  |           : entry.type === "string" | ||||||
|  |           ? entry.multiple | ||||||
|  |             ? { | ||||||
|  |                 name: entry.name, | ||||||
|  |                 required: entry.required, | ||||||
|  |                 selector: { | ||||||
|  |                   select: { options: [], multiple: true, custom_value: true }, | ||||||
|  |                 }, | ||||||
|  |               } | ||||||
|  |             : { | ||||||
|  |                 name: entry.name, | ||||||
|  |                 required: entry.required, | ||||||
|  |                 selector: { | ||||||
|  |                   text: { | ||||||
|  |                     type: | ||||||
|  |                       entry.format || MASKED_FIELDS.includes(entry.name) | ||||||
|  |                         ? "password" | ||||||
|  |                         : "text", | ||||||
|  |                   }, | ||||||
|  |                 }, | ||||||
|  |               } | ||||||
|  |           : entry.type === "boolean" | ||||||
|  |           ? { | ||||||
|  |               name: entry.name, | ||||||
|  |               required: entry.required, | ||||||
|  |               selector: { boolean: {} }, | ||||||
|  |             } | ||||||
|  |           : entry.type === "schema" | ||||||
|  |           ? { | ||||||
|  |               name: entry.name, | ||||||
|  |               required: entry.required, | ||||||
|  |               selector: { object: {} }, | ||||||
|  |             } | ||||||
|  |           : entry.type === "float" || entry.type === "integer" | ||||||
|  |           ? { | ||||||
|  |               name: entry.name, | ||||||
|  |               required: entry.required, | ||||||
|  |               selector: { number: { mode: "box" } }, | ||||||
|             } |             } | ||||||
|           : entry |           : entry | ||||||
|       ) |       ) | ||||||
| @@ -106,7 +162,7 @@ class HassioAddonConfig extends LitElement { | |||||||
|         ); |         ); | ||||||
|     return html` |     return html` | ||||||
|       <h1>${this.addon.name}</h1> |       <h1>${this.addon.name}</h1> | ||||||
|       <ha-card> |       <ha-card outlined> | ||||||
|         <div class="header"> |         <div class="header"> | ||||||
|           <h2> |           <h2> | ||||||
|             ${this.supervisor.localize("addon.configuration.options.header")} |             ${this.supervisor.localize("addon.configuration.options.header")} | ||||||
| @@ -140,7 +196,8 @@ class HassioAddonConfig extends LitElement { | |||||||
|                 .data=${this._options!} |                 .data=${this._options!} | ||||||
|                 @value-changed=${this._configChanged} |                 @value-changed=${this._configChanged} | ||||||
|                 .computeLabel=${this.computeLabel} |                 .computeLabel=${this.computeLabel} | ||||||
|                 .schema=${this._schema( |                 .computeHelper=${this.computeHelper} | ||||||
|  |                 .schema=${this._convertSchema( | ||||||
|                   this._showOptional |                   this._showOptional | ||||||
|                     ? this.addon.schema! |                     ? this.addon.schema! | ||||||
|                     : this._filteredShchema( |                     : this._filteredShchema( | ||||||
| @@ -197,8 +254,9 @@ class HassioAddonConfig extends LitElement { | |||||||
|   protected firstUpdated(changedProps) { |   protected firstUpdated(changedProps) { | ||||||
|     super.firstUpdated(changedProps); |     super.firstUpdated(changedProps); | ||||||
|     this._canShowSchema = !this.addon.schema!.find( |     this._canShowSchema = !this.addon.schema!.find( | ||||||
|  |       (entry) => | ||||||
|         // @ts-ignore |         // @ts-ignore | ||||||
|       (entry) => !SUPPORTED_UI_TYPES.includes(entry.type) || entry.multiple |         !SUPPORTED_UI_TYPES.includes(entry.type) | ||||||
|     ); |     ); | ||||||
|     this._yamlMode = !this._canShowSchema; |     this._yamlMode = !this._canShowSchema; | ||||||
|   } |   } | ||||||
|   | |||||||
| @@ -1,4 +1,3 @@ | |||||||
| import { PaperInputElement } from "@polymer/paper-input/paper-input"; |  | ||||||
| import { | import { | ||||||
|   css, |   css, | ||||||
|   CSSResultGroup, |   CSSResultGroup, | ||||||
| @@ -8,10 +7,13 @@ import { | |||||||
|   TemplateResult, |   TemplateResult, | ||||||
| } from "lit"; | } from "lit"; | ||||||
| import { customElement, property, state } from "lit/decorators"; | import { customElement, property, state } from "lit/decorators"; | ||||||
|  | import memoizeOne from "memoize-one"; | ||||||
| import { fireEvent } from "../../../../src/common/dom/fire_event"; | import { fireEvent } from "../../../../src/common/dom/fire_event"; | ||||||
| import "../../../../src/components/buttons/ha-progress-button"; | import "../../../../src/components/buttons/ha-progress-button"; | ||||||
| import "../../../../src/components/ha-alert"; | import "../../../../src/components/ha-alert"; | ||||||
| import "../../../../src/components/ha-card"; | import "../../../../src/components/ha-card"; | ||||||
|  | import "../../../../src/components/ha-form/ha-form"; | ||||||
|  | import type { HaFormSchema } from "../../../../src/components/ha-form/types"; | ||||||
| import { | import { | ||||||
|   HassioAddonDetails, |   HassioAddonDetails, | ||||||
|   HassioAddonSetOptionParams, |   HassioAddonSetOptionParams, | ||||||
| @@ -24,16 +26,6 @@ import { HomeAssistant } from "../../../../src/types"; | |||||||
| import { suggestAddonRestart } from "../../dialogs/suggestAddonRestart"; | import { suggestAddonRestart } from "../../dialogs/suggestAddonRestart"; | ||||||
| import { hassioStyle } from "../../resources/hassio-style"; | import { hassioStyle } from "../../resources/hassio-style"; | ||||||
|  |  | ||||||
| interface NetworkItem { |  | ||||||
|   description: string; |  | ||||||
|   container: string; |  | ||||||
|   host: number | null; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| interface NetworkItemInput extends PaperInputElement { |  | ||||||
|   container: string; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| @customElement("hassio-addon-network") | @customElement("hassio-addon-network") | ||||||
| class HassioAddonNetwork extends LitElement { | class HassioAddonNetwork extends LitElement { | ||||||
|   @property({ attribute: false }) public hass!: HomeAssistant; |   @property({ attribute: false }) public hass!: HomeAssistant; | ||||||
| @@ -42,9 +34,13 @@ class HassioAddonNetwork extends LitElement { | |||||||
|  |  | ||||||
|   @property({ attribute: false }) public addon!: HassioAddonDetails; |   @property({ attribute: false }) public addon!: HassioAddonDetails; | ||||||
|  |  | ||||||
|  |   @state() private _showOptional = false; | ||||||
|  |  | ||||||
|  |   @state() private _configHasChanged = false; | ||||||
|  |  | ||||||
|   @state() private _error?: string; |   @state() private _error?: string; | ||||||
|  |  | ||||||
|   @state() private _config?: NetworkItem[]; |   @state() private _config?: Record<string, any>; | ||||||
|  |  | ||||||
|   public connectedCallback(): void { |   public connectedCallback(): void { | ||||||
|     super.connectedCallback(); |     super.connectedCallback(); | ||||||
| @@ -56,59 +52,61 @@ class HassioAddonNetwork extends LitElement { | |||||||
|       return html``; |       return html``; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |     const hasHiddenOptions = Object.keys(this._config).find( | ||||||
|  |       (entry) => this._config![entry] === null | ||||||
|  |     ); | ||||||
|  |  | ||||||
|     return html` |     return html` | ||||||
|       <ha-card |       <ha-card | ||||||
|  |         outlined | ||||||
|         .header=${this.supervisor.localize( |         .header=${this.supervisor.localize( | ||||||
|           "addon.configuration.network.header" |           "addon.configuration.network.header" | ||||||
|         )} |         )} | ||||||
|       > |       > | ||||||
|         <div class="card-content"> |         <div class="card-content"> | ||||||
|  |           <p> | ||||||
|  |             ${this.supervisor.localize( | ||||||
|  |               "addon.configuration.network.introduction" | ||||||
|  |             )} | ||||||
|  |           </p> | ||||||
|           ${this._error |           ${this._error | ||||||
|             ? html`<ha-alert alert-type="error">${this._error}</ha-alert>` |             ? html`<ha-alert alert-type="error">${this._error}</ha-alert>` | ||||||
|             : ""} |             : ""} | ||||||
|  |  | ||||||
|           <table> |           <ha-form | ||||||
|             <tbody> |             .data=${this._config} | ||||||
|               <tr> |  | ||||||
|                 <th> |  | ||||||
|                   ${this.supervisor.localize( |  | ||||||
|                     "addon.configuration.network.container" |  | ||||||
|                   )} |  | ||||||
|                 </th> |  | ||||||
|                 <th> |  | ||||||
|                   ${this.supervisor.localize( |  | ||||||
|                     "addon.configuration.network.host" |  | ||||||
|                   )} |  | ||||||
|                 </th> |  | ||||||
|                 <th>${this.supervisor.localize("common.description")}</th> |  | ||||||
|               </tr> |  | ||||||
|               ${this._config!.map( |  | ||||||
|                 (item) => html` |  | ||||||
|                   <tr> |  | ||||||
|                     <td>${item.container}</td> |  | ||||||
|                     <td> |  | ||||||
|                       <paper-input |  | ||||||
|             @value-changed=${this._configChanged} |             @value-changed=${this._configChanged} | ||||||
|                         placeholder=${this.supervisor.localize( |             .computeLabel=${this._computeLabel} | ||||||
|                           "addon.configuration.network.disabled" |             .computeHelper=${this._computeHelper} | ||||||
|  |             .schema=${this._createSchema( | ||||||
|  |               this._config, | ||||||
|  |               this._showOptional, | ||||||
|  |               this.hass.userData?.showAdvanced || false | ||||||
|             )} |             )} | ||||||
|                         .value=${item.host ? String(item.host) : ""} |           ></ha-form> | ||||||
|                         .container=${item.container} |  | ||||||
|                         no-label-float |  | ||||||
|                       ></paper-input> |  | ||||||
|                     </td> |  | ||||||
|                     <td>${this._computeDescription(item)}</td> |  | ||||||
|                   </tr> |  | ||||||
|                 ` |  | ||||||
|               )} |  | ||||||
|             </tbody> |  | ||||||
|           </table> |  | ||||||
|         </div> |         </div> | ||||||
|  |         ${hasHiddenOptions | ||||||
|  |           ? html`<ha-formfield | ||||||
|  |               class="show-optional" | ||||||
|  |               .label=${this.supervisor.localize( | ||||||
|  |                 "addon.configuration.network.show_disabled" | ||||||
|  |               )} | ||||||
|  |             > | ||||||
|  |               <ha-switch | ||||||
|  |                 @change=${this._toggleOptional} | ||||||
|  |                 .checked=${this._showOptional} | ||||||
|  |               > | ||||||
|  |               </ha-switch> | ||||||
|  |             </ha-formfield>` | ||||||
|  |           : ""} | ||||||
|         <div class="card-actions"> |         <div class="card-actions"> | ||||||
|           <ha-progress-button class="warning" @click=${this._resetTapped}> |           <ha-progress-button class="warning" @click=${this._resetTapped}> | ||||||
|             ${this.supervisor.localize("common.reset_defaults")} |             ${this.supervisor.localize("common.reset_defaults")} | ||||||
|           </ha-progress-button> |           </ha-progress-button> | ||||||
|           <ha-progress-button @click=${this._saveTapped}> |           <ha-progress-button | ||||||
|  |             @click=${this._saveTapped} | ||||||
|  |             .disabled=${!this._configHasChanged} | ||||||
|  |           > | ||||||
|             ${this.supervisor.localize("common.save")} |             ${this.supervisor.localize("common.save")} | ||||||
|           </ha-progress-button> |           </ha-progress-button> | ||||||
|         </div> |         </div> | ||||||
| @@ -123,50 +121,60 @@ class HassioAddonNetwork extends LitElement { | |||||||
|     } |     } | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   private _computeDescription = (item: NetworkItem): string => |   private _createSchema = memoizeOne( | ||||||
|     this.addon.translations[this.hass.language]?.network?.[item.container] |     ( | ||||||
|       ?.description || |       config: Record<string, number>, | ||||||
|     this.addon.translations.en?.network?.[item.container]?.description || |       showOptional: boolean, | ||||||
|     item.description; |       advanced: boolean | ||||||
|  |     ): HaFormSchema[] => | ||||||
|  |       (showOptional | ||||||
|  |         ? Object.keys(config) | ||||||
|  |         : Object.keys(config).filter((entry) => config[entry] !== null) | ||||||
|  |       ).map((entry) => ({ | ||||||
|  |         name: entry, | ||||||
|  |         selector: { | ||||||
|  |           number: { | ||||||
|  |             mode: "box", | ||||||
|  |             min: 0, | ||||||
|  |             max: 65535, | ||||||
|  |             unit_of_measurement: advanced ? entry : undefined, | ||||||
|  |           }, | ||||||
|  |         }, | ||||||
|  |       })) | ||||||
|  |   ); | ||||||
|  |  | ||||||
|  |   private _computeLabel = (_: HaFormSchema): string => ""; | ||||||
|  |  | ||||||
|  |   private _computeHelper = (item: HaFormSchema): string => | ||||||
|  |     this.addon.translations[this.hass.language]?.network?.[item.name] || | ||||||
|  |     this.addon.translations.en?.network?.[item.name] || | ||||||
|  |     this.addon.network_description?.[item.name] || | ||||||
|  |     item.name; | ||||||
|  |  | ||||||
|   private _setNetworkConfig(): void { |   private _setNetworkConfig(): void { | ||||||
|     const network = this.addon.network || {}; |     this._config = this.addon.network || {}; | ||||||
|     const description = this.addon.network_description || {}; |  | ||||||
|     const items: NetworkItem[] = Object.keys(network).map((key) => ({ |  | ||||||
|       container: key, |  | ||||||
|       host: network[key], |  | ||||||
|       description: description[key], |  | ||||||
|     })); |  | ||||||
|     this._config = items.sort((a, b) => (a.container > b.container ? 1 : -1)); |  | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   private async _configChanged(ev: Event): Promise<void> { |   private async _configChanged(ev: CustomEvent): Promise<void> { | ||||||
|     const target = ev.target as NetworkItemInput; |     this._configHasChanged = true; | ||||||
|     this._config!.forEach((item) => { |     this._config! = ev.detail.value; | ||||||
|       if ( |  | ||||||
|         item.container === target.container && |  | ||||||
|         item.host !== parseInt(String(target.value), 10) |  | ||||||
|       ) { |  | ||||||
|         item.host = target.value ? parseInt(String(target.value), 10) : null; |  | ||||||
|       } |  | ||||||
|     }); |  | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   private async _resetTapped(ev: CustomEvent): Promise<void> { |   private async _resetTapped(ev: CustomEvent): Promise<void> { | ||||||
|     const button = ev.currentTarget as any; |     const button = ev.currentTarget as any; | ||||||
|     button.progress = true; |  | ||||||
|  |  | ||||||
|     const data: HassioAddonSetOptionParams = { |     const data: HassioAddonSetOptionParams = { | ||||||
|       network: null, |       network: null, | ||||||
|     }; |     }; | ||||||
|  |  | ||||||
|     try { |     try { | ||||||
|       await setHassioAddonOption(this.hass, this.addon.slug, data); |       await setHassioAddonOption(this.hass, this.addon.slug, data); | ||||||
|  |       this._configHasChanged = false; | ||||||
|       const eventdata = { |       const eventdata = { | ||||||
|         success: true, |         success: true, | ||||||
|         response: undefined, |         response: undefined, | ||||||
|         path: "option", |         path: "option", | ||||||
|       }; |       }; | ||||||
|  |       button.actionSuccess(); | ||||||
|       fireEvent(this, "hass-api-called", eventdata); |       fireEvent(this, "hass-api-called", eventdata); | ||||||
|       if (this.addon?.state === "started") { |       if (this.addon?.state === "started") { | ||||||
|         await suggestAddonRestart(this, this.hass, this.supervisor, this.addon); |         await suggestAddonRestart(this, this.hass, this.supervisor, this.addon); | ||||||
| @@ -177,19 +185,21 @@ class HassioAddonNetwork extends LitElement { | |||||||
|         "error", |         "error", | ||||||
|         extractApiErrorMessage(err) |         extractApiErrorMessage(err) | ||||||
|       ); |       ); | ||||||
|  |       button.actionError(); | ||||||
|  |     } | ||||||
|   } |   } | ||||||
|  |  | ||||||
|     button.progress = false; |   private _toggleOptional() { | ||||||
|  |     this._showOptional = !this._showOptional; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   private async _saveTapped(ev: CustomEvent): Promise<void> { |   private async _saveTapped(ev: CustomEvent): Promise<void> { | ||||||
|     const button = ev.currentTarget as any; |     const button = ev.currentTarget as any; | ||||||
|     button.progress = true; |  | ||||||
|  |  | ||||||
|     this._error = undefined; |     this._error = undefined; | ||||||
|     const networkconfiguration = {}; |     const networkconfiguration = {}; | ||||||
|     this._config!.forEach((item) => { |     Object.entries(this._config!).forEach(([key, value]) => { | ||||||
|       networkconfiguration[item.container] = parseInt(String(item.host), 10); |       networkconfiguration[key] = value ?? null; | ||||||
|     }); |     }); | ||||||
|  |  | ||||||
|     const data: HassioAddonSetOptionParams = { |     const data: HassioAddonSetOptionParams = { | ||||||
| @@ -198,11 +208,13 @@ class HassioAddonNetwork extends LitElement { | |||||||
|  |  | ||||||
|     try { |     try { | ||||||
|       await setHassioAddonOption(this.hass, this.addon.slug, data); |       await setHassioAddonOption(this.hass, this.addon.slug, data); | ||||||
|  |       this._configHasChanged = false; | ||||||
|       const eventdata = { |       const eventdata = { | ||||||
|         success: true, |         success: true, | ||||||
|         response: undefined, |         response: undefined, | ||||||
|         path: "option", |         path: "option", | ||||||
|       }; |       }; | ||||||
|  |       button.actionSuccess(); | ||||||
|       fireEvent(this, "hass-api-called", eventdata); |       fireEvent(this, "hass-api-called", eventdata); | ||||||
|       if (this.addon?.state === "started") { |       if (this.addon?.state === "started") { | ||||||
|         await suggestAddonRestart(this, this.hass, this.supervisor, this.addon); |         await suggestAddonRestart(this, this.hass, this.supervisor, this.addon); | ||||||
| @@ -213,8 +225,8 @@ class HassioAddonNetwork extends LitElement { | |||||||
|         "error", |         "error", | ||||||
|         extractApiErrorMessage(err) |         extractApiErrorMessage(err) | ||||||
|       ); |       ); | ||||||
|  |       button.actionError(); | ||||||
|     } |     } | ||||||
|     button.progress = false; |  | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   static get styles(): CSSResultGroup { |   static get styles(): CSSResultGroup { | ||||||
| @@ -232,6 +244,9 @@ class HassioAddonNetwork extends LitElement { | |||||||
|           display: flex; |           display: flex; | ||||||
|           justify-content: space-between; |           justify-content: space-between; | ||||||
|         } |         } | ||||||
|  |         .show-optional { | ||||||
|  |           padding: 16px; | ||||||
|  |         } | ||||||
|       `, |       `, | ||||||
|     ]; |     ]; | ||||||
|   } |   } | ||||||
|   | |||||||
| @@ -38,7 +38,7 @@ class HassioAddonDocumentationDashboard extends LitElement { | |||||||
|     } |     } | ||||||
|     return html` |     return html` | ||||||
|       <div class="content"> |       <div class="content"> | ||||||
|         <ha-card> |         <ha-card outlined> | ||||||
|           ${this._error |           ${this._error | ||||||
|             ? html`<ha-alert alert-type="error">${this._error}</ha-alert>` |             ? html`<ha-alert alert-type="error">${this._error}</ha-alert>` | ||||||
|             : ""} |             : ""} | ||||||
|   | |||||||
| @@ -17,7 +17,12 @@ import { | |||||||
|   HassioAddonDetails, |   HassioAddonDetails, | ||||||
| } from "../../../src/data/hassio/addon"; | } from "../../../src/data/hassio/addon"; | ||||||
| import { extractApiErrorMessage } from "../../../src/data/hassio/common"; | import { extractApiErrorMessage } from "../../../src/data/hassio/common"; | ||||||
|  | import { | ||||||
|  |   fetchHassioSupervisorInfo, | ||||||
|  |   setSupervisorOption, | ||||||
|  | } from "../../../src/data/hassio/supervisor"; | ||||||
| import { Supervisor } from "../../../src/data/supervisor/supervisor"; | import { Supervisor } from "../../../src/data/supervisor/supervisor"; | ||||||
|  | import { showConfirmationDialog } from "../../../src/dialogs/generic/show-dialog-box"; | ||||||
| import "../../../src/layouts/hass-error-screen"; | import "../../../src/layouts/hass-error-screen"; | ||||||
| import "../../../src/layouts/hass-loading-screen"; | import "../../../src/layouts/hass-loading-screen"; | ||||||
| import "../../../src/layouts/hass-tabs-subpage"; | import "../../../src/layouts/hass-tabs-subpage"; | ||||||
| @@ -166,6 +171,44 @@ class HassioAddonDashboard extends LitElement { | |||||||
|   protected async firstUpdated(): Promise<void> { |   protected async firstUpdated(): Promise<void> { | ||||||
|     if (this.route.path === "") { |     if (this.route.path === "") { | ||||||
|       const requestedAddon = extractSearchParam("addon"); |       const requestedAddon = extractSearchParam("addon"); | ||||||
|  |       const requestedAddonRepository = extractSearchParam("repository_url"); | ||||||
|  |       if (requestedAddonRepository) { | ||||||
|  |         const supervisorInfo = await fetchHassioSupervisorInfo(this.hass); | ||||||
|  |         if ( | ||||||
|  |           !supervisorInfo.addons_repositories.find( | ||||||
|  |             (repo) => repo === requestedAddonRepository | ||||||
|  |           ) | ||||||
|  |         ) { | ||||||
|  |           if ( | ||||||
|  |             !(await showConfirmationDialog(this, { | ||||||
|  |               title: this.supervisor.localize("my.add_addon_repository_title"), | ||||||
|  |               text: this.supervisor.localize( | ||||||
|  |                 "my.add_addon_repository_description", | ||||||
|  |                 { addon: requestedAddon, repository: requestedAddonRepository } | ||||||
|  |               ), | ||||||
|  |               confirmText: this.supervisor.localize("common.add"), | ||||||
|  |               dismissText: this.supervisor.localize("common.cancel"), | ||||||
|  |             })) | ||||||
|  |           ) { | ||||||
|  |             this._error = this.supervisor.localize( | ||||||
|  |               "my.error_repository_not_found" | ||||||
|  |             ); | ||||||
|  |             return; | ||||||
|  |           } | ||||||
|  |  | ||||||
|  |           try { | ||||||
|  |             await setSupervisorOption(this.hass, { | ||||||
|  |               addons_repositories: [ | ||||||
|  |                 ...supervisorInfo.addons_repositories, | ||||||
|  |                 requestedAddonRepository, | ||||||
|  |               ], | ||||||
|  |             }); | ||||||
|  |           } catch (err: any) { | ||||||
|  |             this._error = extractApiErrorMessage(err); | ||||||
|  |           } | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |  | ||||||
|       if (requestedAddon) { |       if (requestedAddon) { | ||||||
|         const addonsInfo = await fetchHassioAddonsInfo(this.hass); |         const addonsInfo = await fetchHassioAddonsInfo(this.hass); | ||||||
|         const validAddon = addonsInfo.addons.some( |         const validAddon = addonsInfo.addons.some( | ||||||
|   | |||||||
| @@ -166,7 +166,7 @@ class HassioAddonInfo extends LitElement { | |||||||
|           ` |           ` | ||||||
|         : ""} |         : ""} | ||||||
|  |  | ||||||
|       <ha-card> |       <ha-card outlined> | ||||||
|         <div class="card-content"> |         <div class="card-content"> | ||||||
|           <div class="addon-header"> |           <div class="addon-header"> | ||||||
|             ${!this.narrow ? this.addon.name : ""} |             ${!this.narrow ? this.addon.name : ""} | ||||||
| @@ -649,7 +649,7 @@ class HassioAddonInfo extends LitElement { | |||||||
|  |  | ||||||
|       ${this.addon.long_description |       ${this.addon.long_description | ||||||
|         ? html` |         ? html` | ||||||
|             <ha-card> |             <ha-card outlined> | ||||||
|               <div class="card-content"> |               <div class="card-content"> | ||||||
|                 <ha-markdown |                 <ha-markdown | ||||||
|                   .content=${this.addon.long_description} |                   .content=${this.addon.long_description} | ||||||
|   | |||||||
| @@ -2,6 +2,7 @@ import "@material/mwc-button"; | |||||||
| import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; | import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; | ||||||
| import { customElement, property, state } from "lit/decorators"; | import { customElement, property, state } from "lit/decorators"; | ||||||
| import "../../../../src/components/ha-alert"; | import "../../../../src/components/ha-alert"; | ||||||
|  | import "../../../../src/components/ha-ansi-to-html"; | ||||||
| import "../../../../src/components/ha-card"; | import "../../../../src/components/ha-card"; | ||||||
| import { | import { | ||||||
|   fetchHassioAddonLogs, |   fetchHassioAddonLogs, | ||||||
| @@ -11,7 +12,6 @@ import { extractApiErrorMessage } from "../../../../src/data/hassio/common"; | |||||||
| import { Supervisor } from "../../../../src/data/supervisor/supervisor"; | import { Supervisor } from "../../../../src/data/supervisor/supervisor"; | ||||||
| import { haStyle } from "../../../../src/resources/styles"; | import { haStyle } from "../../../../src/resources/styles"; | ||||||
| import { HomeAssistant } from "../../../../src/types"; | import { HomeAssistant } from "../../../../src/types"; | ||||||
| import "../../components/hassio-ansi-to-html"; |  | ||||||
| import { hassioStyle } from "../../resources/hassio-style"; | import { hassioStyle } from "../../resources/hassio-style"; | ||||||
|  |  | ||||||
| @customElement("hassio-addon-logs") | @customElement("hassio-addon-logs") | ||||||
| @@ -34,15 +34,15 @@ class HassioAddonLogs extends LitElement { | |||||||
|   protected render(): TemplateResult { |   protected render(): TemplateResult { | ||||||
|     return html` |     return html` | ||||||
|       <h1>${this.addon.name}</h1> |       <h1>${this.addon.name}</h1> | ||||||
|       <ha-card> |       <ha-card outlined> | ||||||
|         ${this._error |         ${this._error | ||||||
|           ? html`<ha-alert alert-type="error">${this._error}</ha-alert>` |           ? html`<ha-alert alert-type="error">${this._error}</ha-alert>` | ||||||
|           : ""} |           : ""} | ||||||
|         <div class="card-content"> |         <div class="card-content"> | ||||||
|           ${this._content |           ${this._content | ||||||
|             ? html`<hassio-ansi-to-html |             ? html`<ha-ansi-to-html | ||||||
|                 .content=${this._content} |                 .content=${this._content} | ||||||
|               ></hassio-ansi-to-html>` |               ></ha-ansi-to-html>` | ||||||
|             : ""} |             : ""} | ||||||
|         </div> |         </div> | ||||||
|         <div class="card-actions"> |         <div class="card-actions"> | ||||||
|   | |||||||
| @@ -1,7 +1,7 @@ | |||||||
| import "@material/mwc-button"; | import "@material/mwc-button"; | ||||||
| import { ActionDetail } from "@material/mwc-list"; | import { ActionDetail } from "@material/mwc-list"; | ||||||
| import "@material/mwc-list/mwc-list-item"; | import "@material/mwc-list/mwc-list-item"; | ||||||
| import { mdiDelete, mdiDotsVertical, mdiPlus } from "@mdi/js"; | import { mdiBackupRestore, mdiDelete, mdiDotsVertical, mdiPlus } from "@mdi/js"; | ||||||
| import { | import { | ||||||
|   css, |   css, | ||||||
|   CSSResultGroup, |   CSSResultGroup, | ||||||
| @@ -166,7 +166,15 @@ export class HassioBackups extends LitElement { | |||||||
|     } |     } | ||||||
|     return html` |     return html` | ||||||
|       <hass-tabs-subpage-data-table |       <hass-tabs-subpage-data-table | ||||||
|         .tabs=${supervisorTabs(this.hass)} |         .tabs=${atLeastVersion(this.hass.config.version, 2022, 5) | ||||||
|  |           ? [ | ||||||
|  |               { | ||||||
|  |                 translationKey: "panel.backups", | ||||||
|  |                 path: `/hassio/backups`, | ||||||
|  |                 iconPath: mdiBackupRestore, | ||||||
|  |               }, | ||||||
|  |             ] | ||||||
|  |           : supervisorTabs(this.hass)} | ||||||
|         .hass=${this.hass} |         .hass=${this.hass} | ||||||
|         .localizeFunc=${this.supervisor.localize} |         .localizeFunc=${this.supervisor.localize} | ||||||
|         .searchLabel=${this.supervisor.localize("search")} |         .searchLabel=${this.supervisor.localize("search")} | ||||||
| @@ -182,7 +190,9 @@ export class HassioBackups extends LitElement { | |||||||
|         selectable |         selectable | ||||||
|         hasFab |         hasFab | ||||||
|         .mainPage=${!atLeastVersion(this.hass.config.version, 2021, 12)} |         .mainPage=${!atLeastVersion(this.hass.config.version, 2021, 12)} | ||||||
|         back-path="/config" |         back-path=${atLeastVersion(this.hass.config.version, 2022, 5) | ||||||
|  |           ? "/config/system" | ||||||
|  |           : "/config"} | ||||||
|         supervisor |         supervisor | ||||||
|       > |       > | ||||||
|         <ha-button-menu |         <ha-button-menu | ||||||
|   | |||||||
| @@ -26,7 +26,7 @@ class HassioAddons extends LitElement { | |||||||
|         <div class="card-group"> |         <div class="card-group"> | ||||||
|           ${!this.supervisor.supervisor.addons?.length |           ${!this.supervisor.supervisor.addons?.length | ||||||
|             ? html` |             ? html` | ||||||
|                 <ha-card> |                 <ha-card outlined> | ||||||
|                   <div class="card-content"> |                   <div class="card-content"> | ||||||
|                     <button class="link" @click=${this._openStore}> |                     <button class="link" @click=${this._openStore}> | ||||||
|                       ${this.supervisor.localize("dashboard.no_addons")} |                       ${this.supervisor.localize("dashboard.no_addons")} | ||||||
| @@ -38,7 +38,11 @@ class HassioAddons extends LitElement { | |||||||
|                 .sort((a, b) => caseInsensitiveStringCompare(a.name, b.name)) |                 .sort((a, b) => caseInsensitiveStringCompare(a.name, b.name)) | ||||||
|                 .map( |                 .map( | ||||||
|                   (addon) => html` |                   (addon) => html` | ||||||
|                     <ha-card .addon=${addon} @click=${this._addonTapped}> |                     <ha-card | ||||||
|  |                       outlined | ||||||
|  |                       .addon=${addon} | ||||||
|  |                       @click=${this._addonTapped} | ||||||
|  |                     > | ||||||
|                       <div class="card-content"> |                       <div class="card-content"> | ||||||
|                         <hassio-card-content |                         <hassio-card-content | ||||||
|                           .hass=${this.hass} |                           .hass=${this.hass} | ||||||
|   | |||||||
| @@ -10,6 +10,7 @@ import { HomeAssistant, Route } from "../../../src/types"; | |||||||
| import { supervisorTabs } from "../hassio-tabs"; | import { supervisorTabs } from "../hassio-tabs"; | ||||||
| import "./hassio-addons"; | import "./hassio-addons"; | ||||||
| import "./hassio-update"; | import "./hassio-update"; | ||||||
|  | import "../../../src/layouts/hass-subpage"; | ||||||
|  |  | ||||||
| @customElement("hassio-dashboard") | @customElement("hassio-dashboard") | ||||||
| class HassioDashboard extends LitElement { | class HassioDashboard extends LitElement { | ||||||
| @@ -22,6 +23,31 @@ class HassioDashboard extends LitElement { | |||||||
|   @property({ attribute: false }) public route!: Route; |   @property({ attribute: false }) public route!: Route; | ||||||
|  |  | ||||||
|   protected render(): TemplateResult { |   protected render(): TemplateResult { | ||||||
|  |     if (atLeastVersion(this.hass.config.version, 2022, 5)) { | ||||||
|  |       return html`<hass-subpage | ||||||
|  |         .hass=${this.hass} | ||||||
|  |         .narrow=${this.narrow} | ||||||
|  |         .route=${this.route} | ||||||
|  |         .header=${this.supervisor.localize("panel.addons")} | ||||||
|  |       > | ||||||
|  |         <hassio-addons | ||||||
|  |           .hass=${this.hass} | ||||||
|  |           .supervisor=${this.supervisor} | ||||||
|  |         ></hassio-addons> | ||||||
|  |         <a href="/hassio/store"> | ||||||
|  |           <ha-fab | ||||||
|  |             .label=${this.supervisor.localize("panel.store")} | ||||||
|  |             extended | ||||||
|  |             class="non-tabs" | ||||||
|  |           > | ||||||
|  |             <ha-svg-icon | ||||||
|  |               slot="icon" | ||||||
|  |               .path=${mdiStorePlus} | ||||||
|  |             ></ha-svg-icon> </ha-fab | ||||||
|  |         ></a> | ||||||
|  |       </hass-subpage>`; | ||||||
|  |     } | ||||||
|  |  | ||||||
|     return html` |     return html` | ||||||
|       <hass-tabs-subpage |       <hass-tabs-subpage | ||||||
|         .hass=${this.hass} |         .hass=${this.hass} | ||||||
| @@ -74,6 +100,12 @@ class HassioDashboard extends LitElement { | |||||||
|         .content { |         .content { | ||||||
|           margin: 0 auto; |           margin: 0 auto; | ||||||
|         } |         } | ||||||
|  |         ha-fab.non-tabs { | ||||||
|  |           position: fixed; | ||||||
|  |           right: calc(16px + env(safe-area-inset-right)); | ||||||
|  |           bottom: calc(16px + env(safe-area-inset-bottom)); | ||||||
|  |           z-index: 1; | ||||||
|  |         } | ||||||
|       `, |       `, | ||||||
|     ]; |     ]; | ||||||
|   } |   } | ||||||
|   | |||||||
| @@ -85,7 +85,7 @@ export class HassioUpdate extends LitElement { | |||||||
|       return html``; |       return html``; | ||||||
|     } |     } | ||||||
|     return html` |     return html` | ||||||
|       <ha-card> |       <ha-card outlined> | ||||||
|         <div class="card-content"> |         <div class="card-content"> | ||||||
|           <div class="icon"> |           <div class="icon"> | ||||||
|             <ha-svg-icon .path=${mdiHomeAssistant}></ha-svg-icon> |             <ha-svg-icon .path=${mdiHomeAssistant}></ha-svg-icon> | ||||||
|   | |||||||
| @@ -3,8 +3,8 @@ import { customElement, property } from "lit/decorators"; | |||||||
| import { atLeastVersion } from "../../src/common/config/version"; | import { atLeastVersion } from "../../src/common/config/version"; | ||||||
| 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 { fireEvent } from "../../src/common/dom/fire_event"; | ||||||
| import { isNavigationClick } from "../../src/common/dom/is-navigation-click"; |  | ||||||
| import { mainWindow } from "../../src/common/dom/get_main_window"; | import { mainWindow } from "../../src/common/dom/get_main_window"; | ||||||
|  | import { isNavigationClick } from "../../src/common/dom/is-navigation-click"; | ||||||
| import { navigate } from "../../src/common/navigate"; | import { navigate } from "../../src/common/navigate"; | ||||||
| import { HassioPanelInfo } from "../../src/data/hassio/supervisor"; | import { HassioPanelInfo } from "../../src/data/hassio/supervisor"; | ||||||
| import { Supervisor } from "../../src/data/supervisor/supervisor"; | import { Supervisor } from "../../src/data/supervisor/supervisor"; | ||||||
| @@ -73,6 +73,18 @@ export class HassioMain extends SupervisorBaseElement { | |||||||
|       }); |       }); | ||||||
|     }); |     }); | ||||||
|  |  | ||||||
|  |     // Forward keydown events to the main window for quickbar access | ||||||
|  |     document.body.addEventListener("keydown", (ev: KeyboardEvent) => { | ||||||
|  |       if (ev.altKey || ev.ctrlKey || ev.shiftKey || ev.metaKey) { | ||||||
|  |         // Ignore if modifier keys are pressed | ||||||
|  |         return; | ||||||
|  |       } | ||||||
|  |       // @ts-ignore | ||||||
|  |       fireEvent(mainWindow, "hass-quick-bar-trigger", ev, { | ||||||
|  |         bubbles: false, | ||||||
|  |       }); | ||||||
|  |     }); | ||||||
|  |  | ||||||
|     makeDialogManager(this, this.shadowRoot!); |     makeDialogManager(this, this.shadowRoot!); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   | |||||||
| @@ -15,7 +15,7 @@ import { | |||||||
| } from "../../src/panels/my/ha-panel-my"; | } from "../../src/panels/my/ha-panel-my"; | ||||||
| import { HomeAssistant, Route } from "../../src/types"; | import { HomeAssistant, Route } from "../../src/types"; | ||||||
|  |  | ||||||
| const REDIRECTS: Redirects = { | export const REDIRECTS: Redirects = { | ||||||
|   supervisor: { |   supervisor: { | ||||||
|     redirect: "/hassio/dashboard", |     redirect: "/hassio/dashboard", | ||||||
|   }, |   }, | ||||||
| @@ -42,6 +42,9 @@ const REDIRECTS: Redirects = { | |||||||
|     params: { |     params: { | ||||||
|       addon: "string", |       addon: "string", | ||||||
|     }, |     }, | ||||||
|  |     optional_params: { | ||||||
|  |       repository_url: "url", | ||||||
|  |     }, | ||||||
|   }, |   }, | ||||||
|   supervisor_ingress: { |   supervisor_ingress: { | ||||||
|     redirect: "/hassio/ingress", |     redirect: "/hassio/ingress", | ||||||
| @@ -124,6 +127,14 @@ class HassioMyRedirect extends LitElement { | |||||||
|       } |       } | ||||||
|       resultParams[key] = params[key]; |       resultParams[key] = params[key]; | ||||||
|     }); |     }); | ||||||
|  |     Object.entries(redirect.optional_params || {}).forEach(([key, type]) => { | ||||||
|  |       if (params[key]) { | ||||||
|  |         if (!this._checkParamType(type, params[key])) { | ||||||
|  |           throw Error(); | ||||||
|  |         } | ||||||
|  |         resultParams[key] = params[key]; | ||||||
|  |       } | ||||||
|  |     }); | ||||||
|     return `?${createSearchParam(resultParams)}`; |     return `?${createSearchParam(resultParams)}`; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   | |||||||
| @@ -8,7 +8,10 @@ import { atLeastVersion } from "../../src/common/config/version"; | |||||||
| import type { PageNavigation } from "../../src/layouts/hass-tabs-subpage"; | import type { PageNavigation } from "../../src/layouts/hass-tabs-subpage"; | ||||||
| import { HomeAssistant } from "../../src/types"; | import { HomeAssistant } from "../../src/types"; | ||||||
|  |  | ||||||
| export const supervisorTabs = (hass: HomeAssistant): PageNavigation[] => [ | export const supervisorTabs = (hass: HomeAssistant): PageNavigation[] => | ||||||
|  |   atLeastVersion(hass.config.version, 2022, 5) | ||||||
|  |     ? [] | ||||||
|  |     : [ | ||||||
|         { |         { | ||||||
|           translationKey: atLeastVersion(hass.config.version, 2021, 12) |           translationKey: atLeastVersion(hass.config.version, 2021, 12) | ||||||
|             ? "panel.addons" |             ? "panel.addons" | ||||||
| @@ -28,4 +31,4 @@ export const supervisorTabs = (hass: HomeAssistant): PageNavigation[] => [ | |||||||
|           path: `/hassio/system`, |           path: `/hassio/system`, | ||||||
|           iconPath: mdiCogs, |           iconPath: mdiCogs, | ||||||
|         }, |         }, | ||||||
| ]; |       ]; | ||||||
|   | |||||||
| @@ -48,7 +48,7 @@ class HassioCoreInfo extends LitElement { | |||||||
|     ]; |     ]; | ||||||
|  |  | ||||||
|     return html` |     return html` | ||||||
|       <ha-card header="Core"> |       <ha-card header="Core" outlined> | ||||||
|         <div class="card-content"> |         <div class="card-content"> | ||||||
|           <div> |           <div> | ||||||
|             <ha-settings-row> |             <ha-settings-row> | ||||||
|   | |||||||
| @@ -66,7 +66,7 @@ class HassioHostInfo extends LitElement { | |||||||
|       }, |       }, | ||||||
|     ]; |     ]; | ||||||
|     return html` |     return html` | ||||||
|       <ha-card header="Host"> |       <ha-card header="Host" outlined> | ||||||
|         <div class="card-content"> |         <div class="card-content"> | ||||||
|           <div> |           <div> | ||||||
|             ${this.supervisor.host.features.includes("hostname") |             ${this.supervisor.host.features.includes("hostname") | ||||||
|   | |||||||
| @@ -23,6 +23,10 @@ import { | |||||||
|   showAlertDialog, |   showAlertDialog, | ||||||
|   showConfirmationDialog, |   showConfirmationDialog, | ||||||
| } from "../../../src/dialogs/generic/show-dialog-box"; | } from "../../../src/dialogs/generic/show-dialog-box"; | ||||||
|  | import { | ||||||
|  |   UNHEALTHY_REASON_URL, | ||||||
|  |   UNSUPPORTED_REASON_URL, | ||||||
|  | } from "../../../src/panels/config/system-health/ha-config-system-health"; | ||||||
| import { haStyle } from "../../../src/resources/styles"; | import { haStyle } from "../../../src/resources/styles"; | ||||||
| import { HomeAssistant } from "../../../src/types"; | import { HomeAssistant } from "../../../src/types"; | ||||||
| import { bytesToString } from "../../../src/util/bytes-to-string"; | import { bytesToString } from "../../../src/util/bytes-to-string"; | ||||||
| @@ -30,11 +34,6 @@ import { documentationUrl } from "../../../src/util/documentation-url"; | |||||||
| import "../components/supervisor-metric"; | import "../components/supervisor-metric"; | ||||||
| import { hassioStyle } from "../resources/hassio-style"; | import { hassioStyle } from "../resources/hassio-style"; | ||||||
|  |  | ||||||
| const UNSUPPORTED_REASON_URL = {}; |  | ||||||
| const UNHEALTHY_REASON_URL = { |  | ||||||
|   privileged: "/more-info/unsupported/privileged", |  | ||||||
| }; |  | ||||||
|  |  | ||||||
| @customElement("hassio-supervisor-info") | @customElement("hassio-supervisor-info") | ||||||
| class HassioSupervisorInfo extends LitElement { | class HassioSupervisorInfo extends LitElement { | ||||||
|   @property({ attribute: false }) public hass!: HomeAssistant; |   @property({ attribute: false }) public hass!: HomeAssistant; | ||||||
| @@ -58,7 +57,7 @@ class HassioSupervisorInfo extends LitElement { | |||||||
|       }, |       }, | ||||||
|     ]; |     ]; | ||||||
|     return html` |     return html` | ||||||
|       <ha-card header="Supervisor"> |       <ha-card header="Supervisor" outlined> | ||||||
|         <div class="card-content"> |         <div class="card-content"> | ||||||
|           <div> |           <div> | ||||||
|             <ha-settings-row> |             <ha-settings-row> | ||||||
|   | |||||||
| @@ -1,3 +1,4 @@ | |||||||
|  | import "../../../src/components/ha-ansi-to-html"; | ||||||
| import "@material/mwc-button"; | import "@material/mwc-button"; | ||||||
| import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; | import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; | ||||||
| import { customElement, property, state } from "lit/decorators"; | import { customElement, property, state } from "lit/decorators"; | ||||||
| @@ -11,7 +12,6 @@ import { Supervisor } from "../../../src/data/supervisor/supervisor"; | |||||||
| import "../../../src/layouts/hass-loading-screen"; | import "../../../src/layouts/hass-loading-screen"; | ||||||
| import { haStyle } from "../../../src/resources/styles"; | import { haStyle } from "../../../src/resources/styles"; | ||||||
| import { HomeAssistant } from "../../../src/types"; | import { HomeAssistant } from "../../../src/types"; | ||||||
| import "../components/hassio-ansi-to-html"; |  | ||||||
| import { hassioStyle } from "../resources/hassio-style"; | import { hassioStyle } from "../resources/hassio-style"; | ||||||
|  |  | ||||||
| interface LogProvider { | interface LogProvider { | ||||||
| @@ -65,7 +65,7 @@ class HassioSupervisorLog extends LitElement { | |||||||
|  |  | ||||||
|   protected render(): TemplateResult | void { |   protected render(): TemplateResult | void { | ||||||
|     return html` |     return html` | ||||||
|       <ha-card> |       <ha-card outlined> | ||||||
|         ${this._error |         ${this._error | ||||||
|           ? html`<ha-alert alert-type="error">${this._error}</ha-alert>` |           ? html`<ha-alert alert-type="error">${this._error}</ha-alert>` | ||||||
|           : ""} |           : ""} | ||||||
| @@ -89,8 +89,8 @@ class HassioSupervisorLog extends LitElement { | |||||||
|  |  | ||||||
|         <div class="card-content" id="content"> |         <div class="card-content" id="content"> | ||||||
|           ${this._content |           ${this._content | ||||||
|             ? html`<hassio-ansi-to-html .content=${this._content}> |             ? html`<ha-ansi-to-html .content=${this._content}> | ||||||
|               </hassio-ansi-to-html>` |               </ha-ansi-to-html>` | ||||||
|             : html`<hass-loading-screen no-toolbar></hass-loading-screen>`} |             : html`<hass-loading-screen no-toolbar></hass-loading-screen>`} | ||||||
|         </div> |         </div> | ||||||
|         <div class="card-actions"> |         <div class="card-actions"> | ||||||
|   | |||||||
| @@ -128,6 +128,7 @@ class UpdateAvailableCard extends LitElement { | |||||||
|  |  | ||||||
|     return html` |     return html` | ||||||
|       <ha-card |       <ha-card | ||||||
|  |         outlined | ||||||
|         .header=${this.supervisor.localize("update_available.update_name", { |         .header=${this.supervisor.localize("update_available.update_name", { | ||||||
|           name: this._name, |           name: this._name, | ||||||
|         })} |         })} | ||||||
|   | |||||||
| @@ -72,8 +72,8 @@ | |||||||
|     "@material/mwc-textfield": "0.25.3", |     "@material/mwc-textfield": "0.25.3", | ||||||
|     "@material/mwc-top-app-bar-fixed": "^0.25.3", |     "@material/mwc-top-app-bar-fixed": "^0.25.3", | ||||||
|     "@material/top-app-bar": "14.0.0-canary.261f2db59.0", |     "@material/top-app-bar": "14.0.0-canary.261f2db59.0", | ||||||
|     "@mdi/js": "6.6.95", |     "@mdi/js": "6.7.96", | ||||||
|     "@mdi/svg": "6.6.95", |     "@mdi/svg": "6.7.96", | ||||||
|     "@polymer/app-layout": "^3.1.0", |     "@polymer/app-layout": "^3.1.0", | ||||||
|     "@polymer/iron-flex-layout": "^3.0.1", |     "@polymer/iron-flex-layout": "^3.0.1", | ||||||
|     "@polymer/iron-icon": "^3.0.1", |     "@polymer/iron-icon": "^3.0.1", | ||||||
| @@ -108,7 +108,7 @@ | |||||||
|     "fuse.js": "^6.0.0", |     "fuse.js": "^6.0.0", | ||||||
|     "google-timezones-json": "^1.0.2", |     "google-timezones-json": "^1.0.2", | ||||||
|     "hls.js": "^1.1.5", |     "hls.js": "^1.1.5", | ||||||
|     "home-assistant-js-websocket": "^7.0.1", |     "home-assistant-js-websocket": "^7.0.3", | ||||||
|     "idb-keyval": "^5.1.3", |     "idb-keyval": "^5.1.3", | ||||||
|     "intl-messageformat": "^9.9.1", |     "intl-messageformat": "^9.9.1", | ||||||
|     "js-yaml": "^4.1.0", |     "js-yaml": "^4.1.0", | ||||||
|   | |||||||
| @@ -1,3 +1,30 @@ | |||||||
| [build-system] | [build-system] | ||||||
| requires = ["setuptools~=60.5", "wheel~=0.37.1"] | requires = ["setuptools~=62.3", "wheel~=0.37.1"] | ||||||
| build-backend = "setuptools.build_meta" | build-backend = "setuptools.build_meta" | ||||||
|  |  | ||||||
|  | [project] | ||||||
|  | name         = "home-assistant-frontend" | ||||||
|  | version      = "20220525.0" | ||||||
|  | license      = {text = "Apache-2.0"} | ||||||
|  | description  = "The Home Assistant frontend" | ||||||
|  | readme       = "README.md" | ||||||
|  | authors      = [ | ||||||
|  |     {name = "The Home Assistant Authors", email = "hello@home-assistant.io"} | ||||||
|  | ] | ||||||
|  | requires-python = ">=3.4.0" | ||||||
|  |  | ||||||
|  | [project.urls] | ||||||
|  | "Homepage" = "https://github.com/home-assistant/frontend" | ||||||
|  |  | ||||||
|  | [tool.setuptools] | ||||||
|  | platforms = ["any"] | ||||||
|  | zip-safe  = false | ||||||
|  | include-package-data = true | ||||||
|  |  | ||||||
|  | [tool.setuptools.packages.find] | ||||||
|  | include = ["hass_frontend*"] | ||||||
|  |  | ||||||
|  | [tool.mypy] | ||||||
|  | python_version = 3.4 | ||||||
|  | show_error_codes = true | ||||||
|  | strict = true | ||||||
|   | |||||||
| @@ -15,7 +15,7 @@ if [ -z $(which hass) ]; then | |||||||
|   echo "Installing Home Asstant core from dev." |   echo "Installing Home Asstant core from dev." | ||||||
|   python3 -m pip install --upgrade \ |   python3 -m pip install --upgrade \ | ||||||
|     colorlog \ |     colorlog \ | ||||||
|     git+git://github.com/home-assistant/home-assistant.git@dev |     git+https://github.com/home-assistant/home-assistant.git@dev | ||||||
| fi | fi | ||||||
|  |  | ||||||
| if [ ! -d "${WD}/config" ]; then | if [ ! -d "${WD}/config" ]; then | ||||||
|   | |||||||
| @@ -50,14 +50,14 @@ async function main(args) { | |||||||
|     return; |     return; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   const setup = fs.readFileSync("setup.cfg", "utf8"); |   const setup = fs.readFileSync("pyproject.toml", "utf8"); | ||||||
|   const version = setup.match(/\d{8}\.\d+/)[0]; |   const version = setup.match(/version\W+=\W"(\d{8}\.\d)"/)[1]; | ||||||
|   const newVersion = method(version); |   const newVersion = method(version); | ||||||
|  |  | ||||||
|   console.log("Current version:", version); |   console.log("Current version:", version); | ||||||
|   console.log("New version:", newVersion); |   console.log("New version:", newVersion); | ||||||
|  |  | ||||||
|   fs.writeFileSync("setup.cfg", setup.replace(version, newVersion), "utf-8"); |   fs.writeFileSync("pyproject.toml", setup.replace(version, newVersion), "utf-8"); | ||||||
|  |  | ||||||
|   if (!commit) { |   if (!commit) { | ||||||
|     return; |     return; | ||||||
|   | |||||||
							
								
								
									
										26
									
								
								setup.cfg
									
									
									
									
									
								
							
							
						
						
									
										26
									
								
								setup.cfg
									
									
									
									
									
								
							| @@ -1,26 +0,0 @@ | |||||||
| [metadata] |  | ||||||
| name         = home-assistant-frontend |  | ||||||
| version      = 20220405.0 |  | ||||||
| author       = The Home Assistant Authors |  | ||||||
| author_email = hello@home-assistant.io |  | ||||||
| license      = Apache-2.0 |  | ||||||
| platforms    = any |  | ||||||
| description  = The Home Assistant frontend |  | ||||||
| long_description = file: README.md |  | ||||||
| long_description_content_type = text/markdown |  | ||||||
| url          = https://github.com/home-assistant/frontend |  | ||||||
|  |  | ||||||
| [options] |  | ||||||
| packages = find: |  | ||||||
| zip_safe = False |  | ||||||
| include_package_data = True |  | ||||||
| python_requires = >= 3.4.0 |  | ||||||
|  |  | ||||||
| [options.packages.find] |  | ||||||
| include = |  | ||||||
|     hass_frontend* |  | ||||||
|  |  | ||||||
| [mypy] |  | ||||||
| python_version = 3.4 |  | ||||||
| show_error_codes = True |  | ||||||
| strict = True |  | ||||||
							
								
								
									
										16
									
								
								src/common/datetime/duration.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										16
									
								
								src/common/datetime/duration.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,16 @@ | |||||||
|  | import secondsToDuration from "./seconds_to_duration"; | ||||||
|  |  | ||||||
|  | const DAY_IN_SECONDS = 86400; | ||||||
|  | const HOUR_IN_SECONDS = 3600; | ||||||
|  | const MINUTE_IN_SECONDS = 60; | ||||||
|  |  | ||||||
|  | export const UNIT_TO_SECOND_CONVERT = { | ||||||
|  |   s: 1, | ||||||
|  |   min: MINUTE_IN_SECONDS, | ||||||
|  |   h: HOUR_IN_SECONDS, | ||||||
|  |   d: DAY_IN_SECONDS, | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | export const formatDuration = (duration: string, units: string): string => | ||||||
|  |   secondsToDuration(parseFloat(duration) * UNIT_TO_SECOND_CONVERT[units]) || | ||||||
|  |   "0"; | ||||||
							
								
								
									
										41
									
								
								src/common/dom/ancestors-with-property.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										41
									
								
								src/common/dom/ancestors-with-property.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,41 @@ | |||||||
|  | const DEFAULT_OWN = true; | ||||||
|  |  | ||||||
|  | // Finds the closest ancestor of an element that has a specific optionally owned property, | ||||||
|  | // traversing slot and shadow root boundaries until the body element is reached | ||||||
|  | export const closestWithProperty = ( | ||||||
|  |   element: Element | null, | ||||||
|  |   property: string | symbol, | ||||||
|  |   own = DEFAULT_OWN | ||||||
|  | ) => { | ||||||
|  |   if (!element || element === document.body) return null; | ||||||
|  |  | ||||||
|  |   element = element.assignedSlot ?? element; | ||||||
|  |   if (element.parentElement) { | ||||||
|  |     element = element.parentElement; | ||||||
|  |   } else { | ||||||
|  |     const root = element.getRootNode(); | ||||||
|  |     element = root instanceof ShadowRoot ? root.host : null; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   if ( | ||||||
|  |     own | ||||||
|  |       ? Object.prototype.hasOwnProperty.call(element, property) | ||||||
|  |       : element && property in element | ||||||
|  |   ) | ||||||
|  |     return element; | ||||||
|  |   return closestWithProperty(element, property, own); | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | // Finds the set of all such ancestors and includes starting element as first in the set | ||||||
|  | export const ancestorsWithProperty = ( | ||||||
|  |   element: Element | null, | ||||||
|  |   property: string | symbol, | ||||||
|  |   own = DEFAULT_OWN | ||||||
|  | ) => { | ||||||
|  |   const ancestors: Set<Element> = new Set(); | ||||||
|  |   while (element) { | ||||||
|  |     ancestors.add(element); | ||||||
|  |     element = closestWithProperty(element, property, own); | ||||||
|  |   } | ||||||
|  |   return ancestors; | ||||||
|  | }; | ||||||
| @@ -12,7 +12,7 @@ export const isNavigationClick = (e: MouseEvent) => { | |||||||
|  |  | ||||||
|   const anchor = e |   const anchor = e | ||||||
|     .composedPath() |     .composedPath() | ||||||
|     .filter((n) => (n as HTMLElement).tagName === "A")[0] as |     .find((n) => (n as HTMLElement).tagName === "A") as | ||||||
|     | HTMLAnchorElement |     | HTMLAnchorElement | ||||||
|     | undefined; |     | undefined; | ||||||
|   if ( |   if ( | ||||||
|   | |||||||
| @@ -29,8 +29,11 @@ import { | |||||||
|   mdiPowerPlug, |   mdiPowerPlug, | ||||||
|   mdiPowerPlugOff, |   mdiPowerPlugOff, | ||||||
|   mdiRadioboxBlank, |   mdiRadioboxBlank, | ||||||
|   mdiSmoke, |  | ||||||
|   mdiSnowflake, |   mdiSnowflake, | ||||||
|  |   mdiSmokeDetector, | ||||||
|  |   mdiSmokeDetectorAlert, | ||||||
|  |   mdiSmokeDetectorVariant, | ||||||
|  |   mdiSmokeDetectorVariantAlert, | ||||||
|   mdiSquare, |   mdiSquare, | ||||||
|   mdiSquareOutline, |   mdiSquareOutline, | ||||||
|   mdiStop, |   mdiStop, | ||||||
| @@ -52,6 +55,8 @@ export const binarySensorIcon = (state?: string, stateObj?: HassEntity) => { | |||||||
|       return is_off ? mdiBattery : mdiBatteryOutline; |       return is_off ? mdiBattery : mdiBatteryOutline; | ||||||
|     case "battery_charging": |     case "battery_charging": | ||||||
|       return is_off ? mdiBattery : mdiBatteryCharging; |       return is_off ? mdiBattery : mdiBatteryCharging; | ||||||
|  |     case "carbon_monoxide": | ||||||
|  |       return is_off ? mdiSmokeDetector : mdiSmokeDetectorAlert; | ||||||
|     case "cold": |     case "cold": | ||||||
|       return is_off ? mdiThermometer : mdiSnowflake; |       return is_off ? mdiThermometer : mdiSnowflake; | ||||||
|     case "connectivity": |     case "connectivity": | ||||||
| @@ -68,7 +73,7 @@ export const binarySensorIcon = (state?: string, stateObj?: HassEntity) => { | |||||||
|     case "tamper": |     case "tamper": | ||||||
|       return is_off ? mdiCheckCircle : mdiAlertCircle; |       return is_off ? mdiCheckCircle : mdiAlertCircle; | ||||||
|     case "smoke": |     case "smoke": | ||||||
|       return is_off ? mdiCheckCircle : mdiSmoke; |       return is_off ? mdiSmokeDetectorVariant : mdiSmokeDetectorVariantAlert; | ||||||
|     case "heat": |     case "heat": | ||||||
|       return is_off ? mdiThermometer : mdiFire; |       return is_off ? mdiThermometer : mdiFire; | ||||||
|     case "light": |     case "light": | ||||||
|   | |||||||
| @@ -1,6 +1,11 @@ | |||||||
| import { HassEntity } from "home-assistant-js-websocket"; | import { HassEntity } from "home-assistant-js-websocket"; | ||||||
|  | import { UNAVAILABLE_STATES } from "../../data/entity"; | ||||||
|  |  | ||||||
| export const computeActiveState = (stateObj: HassEntity): string => { | export const computeActiveState = (stateObj: HassEntity): string => { | ||||||
|  |   if (UNAVAILABLE_STATES.includes(stateObj.state)) { | ||||||
|  |     return stateObj.state; | ||||||
|  |   } | ||||||
|  |  | ||||||
|   const domain = stateObj.entity_id.split(".")[0]; |   const domain = stateObj.entity_id.split(".")[0]; | ||||||
|   let state = stateObj.state; |   let state = stateObj.state; | ||||||
|  |  | ||||||
|   | |||||||
| @@ -2,50 +2,74 @@ import { HassEntity } from "home-assistant-js-websocket"; | |||||||
| import { UNAVAILABLE, UNKNOWN } from "../../data/entity"; | import { UNAVAILABLE, UNKNOWN } from "../../data/entity"; | ||||||
| import { FrontendLocaleData } from "../../data/translation"; | import { FrontendLocaleData } from "../../data/translation"; | ||||||
| import { | import { | ||||||
|   updateIsInstalling, |  | ||||||
|   UpdateEntity, |  | ||||||
|   UPDATE_SUPPORT_PROGRESS, |   UPDATE_SUPPORT_PROGRESS, | ||||||
|  |   updateIsInstallingFromAttributes, | ||||||
| } from "../../data/update"; | } from "../../data/update"; | ||||||
| import { formatDate } from "../datetime/format_date"; | import { formatDate } from "../datetime/format_date"; | ||||||
| import { formatDateTime } from "../datetime/format_date_time"; | import { formatDateTime } from "../datetime/format_date_time"; | ||||||
| import { formatTime } from "../datetime/format_time"; | import { formatTime } from "../datetime/format_time"; | ||||||
| import { formatNumber, isNumericState } from "../number/format_number"; | import { formatNumber, isNumericFromAttributes } from "../number/format_number"; | ||||||
| import { LocalizeFunc } from "../translations/localize"; | import { LocalizeFunc } from "../translations/localize"; | ||||||
| import { computeStateDomain } from "./compute_state_domain"; | import { supportsFeatureFromAttributes } from "./supports-feature"; | ||||||
| import { supportsFeature } from "./supports-feature"; | import { formatDuration, UNIT_TO_SECOND_CONVERT } from "../datetime/duration"; | ||||||
|  | import { computeDomain } from "./compute_domain"; | ||||||
|  |  | ||||||
| export const computeStateDisplay = ( | export const computeStateDisplay = ( | ||||||
|   localize: LocalizeFunc, |   localize: LocalizeFunc, | ||||||
|   stateObj: HassEntity, |   stateObj: HassEntity, | ||||||
|   locale: FrontendLocaleData, |   locale: FrontendLocaleData, | ||||||
|   state?: string |   state?: string | ||||||
| ): string => { | ): string => | ||||||
|   const compareState = state !== undefined ? state : stateObj.state; |   computeStateDisplayFromEntityAttributes( | ||||||
|  |     localize, | ||||||
|  |     locale, | ||||||
|  |     stateObj.entity_id, | ||||||
|  |     stateObj.attributes, | ||||||
|  |     state !== undefined ? state : stateObj.state | ||||||
|  |   ); | ||||||
|  |  | ||||||
|   if (compareState === UNKNOWN || compareState === UNAVAILABLE) { | export const computeStateDisplayFromEntityAttributes = ( | ||||||
|     return localize(`state.default.${compareState}`); |   localize: LocalizeFunc, | ||||||
|  |   locale: FrontendLocaleData, | ||||||
|  |   entityId: string, | ||||||
|  |   attributes: any, | ||||||
|  |   state: string | ||||||
|  | ): string => { | ||||||
|  |   if (state === UNKNOWN || state === UNAVAILABLE) { | ||||||
|  |     return localize(`state.default.${state}`); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   // Entities with a `unit_of_measurement` or `state_class` are numeric values and should use `formatNumber` |   // Entities with a `unit_of_measurement` or `state_class` are numeric values and should use `formatNumber` | ||||||
|   if (isNumericState(stateObj)) { |   if (isNumericFromAttributes(attributes)) { | ||||||
|     if (stateObj.attributes.device_class === "monetary") { |     // state is duration | ||||||
|  |     if ( | ||||||
|  |       attributes.device_class === "duration" && | ||||||
|  |       attributes.unit_of_measurement && | ||||||
|  |       UNIT_TO_SECOND_CONVERT[attributes.unit_of_measurement] | ||||||
|  |     ) { | ||||||
|       try { |       try { | ||||||
|         return formatNumber(compareState, locale, { |         return formatDuration(state, attributes.unit_of_measurement); | ||||||
|  |       } catch (_err) { | ||||||
|  |         // fallback to default | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |     if (attributes.device_class === "monetary") { | ||||||
|  |       try { | ||||||
|  |         return formatNumber(state, locale, { | ||||||
|           style: "currency", |           style: "currency", | ||||||
|           currency: stateObj.attributes.unit_of_measurement, |           currency: attributes.unit_of_measurement, | ||||||
|  |           minimumFractionDigits: 2, | ||||||
|         }); |         }); | ||||||
|       } catch (_err) { |       } catch (_err) { | ||||||
|         // fallback to default |         // fallback to default | ||||||
|       } |       } | ||||||
|     } |     } | ||||||
|     return `${formatNumber(compareState, locale)}${ |     return `${formatNumber(state, locale)}${ | ||||||
|       stateObj.attributes.unit_of_measurement |       attributes.unit_of_measurement ? " " + attributes.unit_of_measurement : "" | ||||||
|         ? " " + stateObj.attributes.unit_of_measurement |  | ||||||
|         : "" |  | ||||||
|     }`; |     }`; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   const domain = computeStateDomain(stateObj); |   const domain = computeDomain(entityId); | ||||||
|  |  | ||||||
|   if (domain === "input_datetime") { |   if (domain === "input_datetime") { | ||||||
|     if (state !== undefined) { |     if (state !== undefined) { | ||||||
| @@ -80,36 +104,32 @@ export const computeStateDisplay = ( | |||||||
|     } else { |     } else { | ||||||
|       // If not trying to display an explicit state, create `Date` object from `stateObj`'s attributes then format. |       // If not trying to display an explicit state, create `Date` object from `stateObj`'s attributes then format. | ||||||
|       let date: Date; |       let date: Date; | ||||||
|       if (stateObj.attributes.has_date && stateObj.attributes.has_time) { |       if (attributes.has_date && attributes.has_time) { | ||||||
|         date = new Date( |         date = new Date( | ||||||
|           stateObj.attributes.year, |           attributes.year, | ||||||
|           stateObj.attributes.month - 1, |           attributes.month - 1, | ||||||
|           stateObj.attributes.day, |           attributes.day, | ||||||
|           stateObj.attributes.hour, |           attributes.hour, | ||||||
|           stateObj.attributes.minute |           attributes.minute | ||||||
|         ); |         ); | ||||||
|         return formatDateTime(date, locale); |         return formatDateTime(date, locale); | ||||||
|       } |       } | ||||||
|       if (stateObj.attributes.has_date) { |       if (attributes.has_date) { | ||||||
|         date = new Date( |         date = new Date(attributes.year, attributes.month - 1, attributes.day); | ||||||
|           stateObj.attributes.year, |  | ||||||
|           stateObj.attributes.month - 1, |  | ||||||
|           stateObj.attributes.day |  | ||||||
|         ); |  | ||||||
|         return formatDate(date, locale); |         return formatDate(date, locale); | ||||||
|       } |       } | ||||||
|       if (stateObj.attributes.has_time) { |       if (attributes.has_time) { | ||||||
|         date = new Date(); |         date = new Date(); | ||||||
|         date.setHours(stateObj.attributes.hour, stateObj.attributes.minute); |         date.setHours(attributes.hour, attributes.minute); | ||||||
|         return formatTime(date, locale); |         return formatTime(date, locale); | ||||||
|       } |       } | ||||||
|       return stateObj.state; |       return state; | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   if (domain === "humidifier") { |   if (domain === "humidifier") { | ||||||
|     if (compareState === "on" && stateObj.attributes.humidity) { |     if (state === "on" && attributes.humidity) { | ||||||
|       return `${stateObj.attributes.humidity} %`; |       return `${attributes.humidity} %`; | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
|  |  | ||||||
| @@ -119,7 +139,7 @@ export const computeStateDisplay = ( | |||||||
|     domain === "number" || |     domain === "number" || | ||||||
|     domain === "input_number" |     domain === "input_number" | ||||||
|   ) { |   ) { | ||||||
|     return formatNumber(compareState, locale); |     return formatNumber(state, locale); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   // state of button is a timestamp |   // state of button is a timestamp | ||||||
| @@ -127,12 +147,12 @@ export const computeStateDisplay = ( | |||||||
|     domain === "button" || |     domain === "button" || | ||||||
|     domain === "input_button" || |     domain === "input_button" || | ||||||
|     domain === "scene" || |     domain === "scene" || | ||||||
|     (domain === "sensor" && stateObj.attributes.device_class === "timestamp") |     (domain === "sensor" && attributes.device_class === "timestamp") | ||||||
|   ) { |   ) { | ||||||
|     try { |     try { | ||||||
|       return formatDateTime(new Date(compareState), locale); |       return formatDateTime(new Date(state), locale); | ||||||
|     } catch (_err) { |     } catch (_err) { | ||||||
|       return compareState; |       return state; | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
|  |  | ||||||
| @@ -143,30 +163,28 @@ export const computeStateDisplay = ( | |||||||
|     // When the latest version is skipped, show the latest version |     // When the latest version is skipped, show the latest version | ||||||
|     // When update is not available, show "Up-to-date" |     // When update is not available, show "Up-to-date" | ||||||
|     // When update is not available and there is no latest_version show "Unavailable" |     // When update is not available and there is no latest_version show "Unavailable" | ||||||
|     return compareState === "on" |     return state === "on" | ||||||
|       ? updateIsInstalling(stateObj as UpdateEntity) |       ? updateIsInstallingFromAttributes(attributes) | ||||||
|         ? supportsFeature(stateObj, UPDATE_SUPPORT_PROGRESS) |         ? supportsFeatureFromAttributes(attributes, UPDATE_SUPPORT_PROGRESS) | ||||||
|           ? localize("ui.card.update.installing_with_progress", { |           ? localize("ui.card.update.installing_with_progress", { | ||||||
|               progress: stateObj.attributes.in_progress, |               progress: attributes.in_progress, | ||||||
|             }) |             }) | ||||||
|           : localize("ui.card.update.installing") |           : localize("ui.card.update.installing") | ||||||
|         : stateObj.attributes.latest_version |         : attributes.latest_version | ||||||
|       : stateObj.attributes.skipped_version === |       : attributes.skipped_version === attributes.latest_version | ||||||
|         stateObj.attributes.latest_version |       ? attributes.latest_version ?? localize("state.default.unavailable") | ||||||
|       ? stateObj.attributes.latest_version ?? |  | ||||||
|         localize("state.default.unavailable") |  | ||||||
|       : localize("ui.card.update.up_to_date"); |       : localize("ui.card.update.up_to_date"); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   return ( |   return ( | ||||||
|     // Return device class translation |     // Return device class translation | ||||||
|     (stateObj.attributes.device_class && |     (attributes.device_class && | ||||||
|       localize( |       localize( | ||||||
|         `component.${domain}.state.${stateObj.attributes.device_class}.${compareState}` |         `component.${domain}.state.${attributes.device_class}.${state}` | ||||||
|       )) || |       )) || | ||||||
|     // Return default translation |     // Return default translation | ||||||
|     localize(`component.${domain}.state._.${compareState}`) || |     localize(`component.${domain}.state._.${state}`) || | ||||||
|     // We don't know! Return the raw state. |     // We don't know! Return the raw state. | ||||||
|     compareState |     state | ||||||
|   ); |   ); | ||||||
| }; | }; | ||||||
|   | |||||||
| @@ -1,7 +1,13 @@ | |||||||
| import { HassEntity } from "home-assistant-js-websocket"; | import { HassEntity } from "home-assistant-js-websocket"; | ||||||
| import { computeObjectId } from "./compute_object_id"; | import { computeObjectId } from "./compute_object_id"; | ||||||
|  |  | ||||||
|  | export const computeStateNameFromEntityAttributes = ( | ||||||
|  |   entityId: string, | ||||||
|  |   attributes: { [key: string]: any } | ||||||
|  | ): string => | ||||||
|  |   attributes.friendly_name === undefined | ||||||
|  |     ? computeObjectId(entityId).replace(/_/g, " ") | ||||||
|  |     : attributes.friendly_name || ""; | ||||||
|  |  | ||||||
| export const computeStateName = (stateObj: HassEntity): string => | export const computeStateName = (stateObj: HassEntity): string => | ||||||
|   stateObj.attributes.friendly_name === undefined |   computeStateNameFromEntityAttributes(stateObj.entity_id, stateObj.attributes); | ||||||
|     ? computeObjectId(stateObj.entity_id).replace(/_/g, " ") |  | ||||||
|     : stateObj.attributes.friendly_name || ""; |  | ||||||
|   | |||||||
| @@ -8,29 +8,29 @@ import { | |||||||
|   mdiCalendar, |   mdiCalendar, | ||||||
|   mdiCast, |   mdiCast, | ||||||
|   mdiCastConnected, |   mdiCastConnected, | ||||||
|  |   mdiCheckCircleOutline, | ||||||
|   mdiClock, |   mdiClock, | ||||||
|  |   mdiCloseCircleOutline, | ||||||
|   mdiGestureTapButton, |   mdiGestureTapButton, | ||||||
|   mdiLanConnect, |   mdiLanConnect, | ||||||
|   mdiLanDisconnect, |   mdiLanDisconnect, | ||||||
|   mdiLightSwitch, |  | ||||||
|   mdiLock, |   mdiLock, | ||||||
|   mdiLockAlert, |   mdiLockAlert, | ||||||
|   mdiLockClock, |   mdiLockClock, | ||||||
|   mdiLockOpen, |   mdiLockOpen, | ||||||
|  |   mdiPackage, | ||||||
|  |   mdiPackageDown, | ||||||
|   mdiPackageUp, |   mdiPackageUp, | ||||||
|   mdiPowerPlug, |   mdiPowerPlug, | ||||||
|   mdiPowerPlugOff, |   mdiPowerPlugOff, | ||||||
|   mdiRestart, |   mdiRestart, | ||||||
|   mdiToggleSwitch, |   mdiToggleSwitchVariant, | ||||||
|   mdiToggleSwitchOff, |   mdiToggleSwitchVariantOff, | ||||||
|   mdiCheckCircleOutline, |  | ||||||
|   mdiCloseCircleOutline, |  | ||||||
|   mdiWeatherNight, |   mdiWeatherNight, | ||||||
|   mdiPackage, |  | ||||||
|   mdiPackageDown, |  | ||||||
| } from "@mdi/js"; | } from "@mdi/js"; | ||||||
| import { HassEntity } from "home-assistant-js-websocket"; | import { HassEntity } from "home-assistant-js-websocket"; | ||||||
| import { updateIsInstalling, UpdateEntity } from "../../data/update"; | import { UpdateEntity, updateIsInstalling } from "../../data/update"; | ||||||
|  | import { weatherIcon } from "../../data/weather"; | ||||||
| /** | /** | ||||||
|  * Return the icon to be used for a domain. |  * Return the icon to be used for a domain. | ||||||
|  * |  * | ||||||
| @@ -47,6 +47,20 @@ export const domainIcon = ( | |||||||
|   stateObj?: HassEntity, |   stateObj?: HassEntity, | ||||||
|   state?: string |   state?: string | ||||||
| ): string => { | ): string => { | ||||||
|  |   const icon = domainIconWithoutDefault(domain, stateObj, state); | ||||||
|  |   if (icon) { | ||||||
|  |     return icon; | ||||||
|  |   } | ||||||
|  |   // eslint-disable-next-line | ||||||
|  |   console.warn(`Unable to find icon for domain ${domain}`); | ||||||
|  |   return DEFAULT_DOMAIN_ICON; | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | export const domainIconWithoutDefault = ( | ||||||
|  |   domain: string, | ||||||
|  |   stateObj?: HassEntity, | ||||||
|  |   state?: string | ||||||
|  | ): string | undefined => { | ||||||
|   const compareState = state !== undefined ? state : stateObj?.state; |   const compareState = state !== undefined ? state : stateObj?.state; | ||||||
|  |  | ||||||
|   switch (domain) { |   switch (domain) { | ||||||
| @@ -88,6 +102,15 @@ export const domainIcon = ( | |||||||
|         ? mdiCheckCircleOutline |         ? mdiCheckCircleOutline | ||||||
|         : mdiCloseCircleOutline; |         : mdiCloseCircleOutline; | ||||||
|  |  | ||||||
|  |     case "input_datetime": | ||||||
|  |       if (!stateObj?.attributes.has_date) { | ||||||
|  |         return mdiClock; | ||||||
|  |       } | ||||||
|  |       if (!stateObj.attributes.has_time) { | ||||||
|  |         return mdiCalendar; | ||||||
|  |       } | ||||||
|  |       break; | ||||||
|  |  | ||||||
|     case "lock": |     case "lock": | ||||||
|       switch (compareState) { |       switch (compareState) { | ||||||
|         case "unlocked": |         case "unlocked": | ||||||
| @@ -109,9 +132,11 @@ export const domainIcon = ( | |||||||
|         case "outlet": |         case "outlet": | ||||||
|           return compareState === "on" ? mdiPowerPlug : mdiPowerPlugOff; |           return compareState === "on" ? mdiPowerPlug : mdiPowerPlugOff; | ||||||
|         case "switch": |         case "switch": | ||||||
|           return compareState === "on" ? mdiToggleSwitch : mdiToggleSwitchOff; |           return compareState === "on" | ||||||
|  |             ? mdiToggleSwitchVariant | ||||||
|  |             : mdiToggleSwitchVariantOff; | ||||||
|         default: |         default: | ||||||
|           return mdiLightSwitch; |           return mdiToggleSwitchVariant; | ||||||
|       } |       } | ||||||
|  |  | ||||||
|     case "sensor": { |     case "sensor": { | ||||||
| @@ -123,15 +148,6 @@ export const domainIcon = ( | |||||||
|       break; |       break; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     case "input_datetime": |  | ||||||
|       if (!stateObj?.attributes.has_date) { |  | ||||||
|         return mdiClock; |  | ||||||
|       } |  | ||||||
|       if (!stateObj.attributes.has_time) { |  | ||||||
|         return mdiCalendar; |  | ||||||
|       } |  | ||||||
|       break; |  | ||||||
|  |  | ||||||
|     case "sun": |     case "sun": | ||||||
|       return stateObj?.state === "above_horizon" |       return stateObj?.state === "above_horizon" | ||||||
|         ? FIXED_DOMAIN_ICONS[domain] |         ? FIXED_DOMAIN_ICONS[domain] | ||||||
| @@ -143,13 +159,14 @@ export const domainIcon = ( | |||||||
|           ? mdiPackageDown |           ? mdiPackageDown | ||||||
|           : mdiPackageUp |           : mdiPackageUp | ||||||
|         : mdiPackage; |         : mdiPackage; | ||||||
|  |  | ||||||
|  |     case "weather": | ||||||
|  |       return weatherIcon(stateObj?.state); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   if (domain in FIXED_DOMAIN_ICONS) { |   if (domain in FIXED_DOMAIN_ICONS) { | ||||||
|     return FIXED_DOMAIN_ICONS[domain]; |     return FIXED_DOMAIN_ICONS[domain]; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   // eslint-disable-next-line |   return undefined; | ||||||
|   console.warn(`Unable to find icon for domain ${domain}`); |  | ||||||
|   return DEFAULT_DOMAIN_ICON; |  | ||||||
| }; | }; | ||||||
|   | |||||||
| @@ -3,6 +3,13 @@ import { HassEntity } from "home-assistant-js-websocket"; | |||||||
| export const supportsFeature = ( | export const supportsFeature = ( | ||||||
|   stateObj: HassEntity, |   stateObj: HassEntity, | ||||||
|   feature: number |   feature: number | ||||||
|  | ): boolean => supportsFeatureFromAttributes(stateObj.attributes, feature); | ||||||
|  |  | ||||||
|  | export const supportsFeatureFromAttributes = ( | ||||||
|  |   attributes: { | ||||||
|  |     [key: string]: any; | ||||||
|  |   }, | ||||||
|  |   feature: number | ||||||
| ): boolean => | ): boolean => | ||||||
|   // eslint-disable-next-line no-bitwise |   // eslint-disable-next-line no-bitwise | ||||||
|   (stateObj.attributes.supported_features! & feature) !== 0; |   (attributes.supported_features! & feature) !== 0; | ||||||
|   | |||||||
| @@ -7,8 +7,11 @@ import { round } from "./round"; | |||||||
|  * @param stateObj The entity state object |  * @param stateObj The entity state object | ||||||
|  */ |  */ | ||||||
| export const isNumericState = (stateObj: HassEntity): boolean => | export const isNumericState = (stateObj: HassEntity): boolean => | ||||||
|   !!stateObj.attributes.unit_of_measurement || |   isNumericFromAttributes(stateObj.attributes); | ||||||
|   !!stateObj.attributes.state_class; |  | ||||||
|  | export const isNumericFromAttributes = (attributes: { | ||||||
|  |   [key: string]: any; | ||||||
|  | }): boolean => !!attributes.unit_of_measurement || !!attributes.state_class; | ||||||
|  |  | ||||||
| export const numberFormatToLocale = ( | export const numberFormatToLocale = ( | ||||||
|   localeOptions: FrontendLocaleData |   localeOptions: FrontendLocaleData | ||||||
|   | |||||||
| @@ -1,4 +1,4 @@ | |||||||
| export const promiseTimeout = (ms: number, promise: Promise<any>) => { | export const promiseTimeout = (ms: number, promise: Promise<any> | any) => { | ||||||
|   const timeout = new Promise((_resolve, reject) => { |   const timeout = new Promise((_resolve, reject) => { | ||||||
|     setTimeout(() => { |     setTimeout(() => { | ||||||
|       reject(`Timed out in ${ms} ms.`); |       reject(`Timed out in ${ms} ms.`); | ||||||
|   | |||||||
							
								
								
									
										18
									
								
								src/common/util/subscribe-polling.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										18
									
								
								src/common/util/subscribe-polling.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,18 @@ | |||||||
|  | import { HomeAssistant } from "../../types"; | ||||||
|  |  | ||||||
|  | export const subscribePollingCollection = ( | ||||||
|  |   hass: HomeAssistant, | ||||||
|  |   updateData: (hass: HomeAssistant) => void, | ||||||
|  |   interval: number | ||||||
|  | ) => { | ||||||
|  |   let timeout; | ||||||
|  |   const fetchData = async () => { | ||||||
|  |     try { | ||||||
|  |       await updateData(hass); | ||||||
|  |     } finally { | ||||||
|  |       timeout = setTimeout(() => fetchData(), interval); | ||||||
|  |     } | ||||||
|  |   }; | ||||||
|  |   fetchData(); | ||||||
|  |   return () => clearTimeout(timeout); | ||||||
|  | }; | ||||||
| @@ -13,7 +13,7 @@ export const throttle = <T extends any[]>( | |||||||
| ) => { | ) => { | ||||||
|   let timeout: number | undefined; |   let timeout: number | undefined; | ||||||
|   let previous = 0; |   let previous = 0; | ||||||
|   return (...args: T): void => { |   const throttledFunc = (...args: T): void => { | ||||||
|     const later = () => { |     const later = () => { | ||||||
|       previous = leading === false ? 0 : Date.now(); |       previous = leading === false ? 0 : Date.now(); | ||||||
|       timeout = undefined; |       timeout = undefined; | ||||||
| @@ -35,4 +35,10 @@ export const throttle = <T extends any[]>( | |||||||
|       timeout = window.setTimeout(later, remaining); |       timeout = window.setTimeout(later, remaining); | ||||||
|     } |     } | ||||||
|   }; |   }; | ||||||
|  |   throttledFunc.cancel = () => { | ||||||
|  |     clearTimeout(timeout); | ||||||
|  |     timeout = undefined; | ||||||
|  |     previous = 0; | ||||||
|  |   }; | ||||||
|  |   return throttledFunc; | ||||||
| }; | }; | ||||||
|   | |||||||
| @@ -34,7 +34,7 @@ import { | |||||||
|   endOfMonth, |   endOfMonth, | ||||||
|   endOfQuarter, |   endOfQuarter, | ||||||
|   endOfYear, |   endOfYear, | ||||||
| } from "date-fns"; | } from "date-fns/esm"; | ||||||
| import { | import { | ||||||
|   formatDate, |   formatDate, | ||||||
|   formatDateMonth, |   formatDateMonth, | ||||||
|   | |||||||
| @@ -347,8 +347,8 @@ class StatisticsChart extends LitElement { | |||||||
|         statTypes.forEach((type) => { |         statTypes.forEach((type) => { | ||||||
|           let val: number | null; |           let val: number | null; | ||||||
|           if (type === "sum") { |           if (type === "sum") { | ||||||
|             if (!initVal) { |             if (initVal === null) { | ||||||
|               initVal = val = stat.state; |               initVal = val = stat.state || 0; | ||||||
|               prevSum = stat.sum; |               prevSum = stat.sum; | ||||||
|             } else { |             } else { | ||||||
|               val = initVal + ((stat.sum || 0) - prevSum!); |               val = initVal + ((stat.sum || 0) - prevSum!); | ||||||
|   | |||||||
| @@ -1,7 +1,4 @@ | |||||||
| export const createCurrencyListEl = () => { | export const currencies = [ | ||||||
|   const list = document.createElement("datalist"); |  | ||||||
|   list.id = "currencies"; |  | ||||||
|   for (const currency of [ |  | ||||||
|   "AED", |   "AED", | ||||||
|   "AFN", |   "AFN", | ||||||
|   "ALL", |   "ALL", | ||||||
| @@ -159,7 +156,12 @@ export const createCurrencyListEl = () => { | |||||||
|   "ZAR", |   "ZAR", | ||||||
|   "ZMK", |   "ZMK", | ||||||
|   "ZWL", |   "ZWL", | ||||||
|   ]) { | ]; | ||||||
|  |  | ||||||
|  | export const createCurrencyListEl = () => { | ||||||
|  |   const list = document.createElement("datalist"); | ||||||
|  |   list.id = "currencies"; | ||||||
|  |   for (const currency of currencies) { | ||||||
|     const option = document.createElement("option"); |     const option = document.createElement("option"); | ||||||
|     option.value = currency; |     option.value = currency; | ||||||
|     option.innerHTML = currency; |     option.innerHTML = currency; | ||||||
|   | |||||||
| @@ -269,8 +269,8 @@ export class HaDataTable extends LitElement { | |||||||
|                       @change=${this._handleHeaderRowCheckboxClick} |                       @change=${this._handleHeaderRowCheckboxClick} | ||||||
|                       .indeterminate=${this._checkedRows.length && |                       .indeterminate=${this._checkedRows.length && | ||||||
|                       this._checkedRows.length !== this._checkableRowsCount} |                       this._checkedRows.length !== this._checkableRowsCount} | ||||||
|                       .checked=${this._checkedRows.length === |                       .checked=${this._checkedRows.length && | ||||||
|                       this._checkableRowsCount} |                       this._checkedRows.length === this._checkableRowsCount} | ||||||
|                     > |                     > | ||||||
|                     </ha-checkbox> |                     </ha-checkbox> | ||||||
|                   </div> |                   </div> | ||||||
|   | |||||||
| @@ -5,6 +5,7 @@ import { fireEvent } from "../../common/dom/fire_event"; | |||||||
| import { | import { | ||||||
|   DeviceAutomation, |   DeviceAutomation, | ||||||
|   deviceAutomationsEqual, |   deviceAutomationsEqual, | ||||||
|  |   sortDeviceAutomations, | ||||||
| } from "../../data/device_automation"; | } from "../../data/device_automation"; | ||||||
| import { HomeAssistant } from "../../types"; | import { HomeAssistant } from "../../types"; | ||||||
| import "../ha-select"; | import "../ha-select"; | ||||||
| @@ -127,7 +128,9 @@ export abstract class HaDeviceAutomationPicker< | |||||||
|  |  | ||||||
|   private async _updateDeviceInfo() { |   private async _updateDeviceInfo() { | ||||||
|     this._automations = this.deviceId |     this._automations = this.deviceId | ||||||
|       ? await this._fetchDeviceAutomations(this.hass, this.deviceId) |       ? (await this._fetchDeviceAutomations(this.hass, this.deviceId)).sort( | ||||||
|  |           sortDeviceAutomations | ||||||
|  |         ) | ||||||
|       : // No device, clear the list of automations |       : // No device, clear the list of automations | ||||||
|         []; |         []; | ||||||
|  |  | ||||||
| @@ -161,8 +164,9 @@ export abstract class HaDeviceAutomationPicker< | |||||||
|     if (this.value && deviceAutomationsEqual(automation, this.value)) { |     if (this.value && deviceAutomationsEqual(automation, this.value)) { | ||||||
|       return; |       return; | ||||||
|     } |     } | ||||||
|     fireEvent(this, "change"); |     const value = { ...automation }; | ||||||
|     fireEvent(this, "value-changed", { value: automation }); |     delete value.metadata; | ||||||
|  |     fireEvent(this, "value-changed", { value }); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   static get styles(): CSSResultGroup { |   static get styles(): CSSResultGroup { | ||||||
|   | |||||||
| @@ -198,7 +198,8 @@ export class HaDevicePicker extends SubscribeMixin(LitElement) { | |||||||
|           this.hass, |           this.hass, | ||||||
|           deviceEntityLookup[device.id] |           deviceEntityLookup[device.id] | ||||||
|         ), |         ), | ||||||
|         area: device.area_id |         area: | ||||||
|  |           device.area_id && areaLookup[device.area_id] | ||||||
|             ? areaLookup[device.area_id].name |             ? areaLookup[device.area_id].name | ||||||
|             : this.hass.localize("ui.components.device-picker.no_area"), |             : this.hass.localize("ui.components.device-picker.no_area"), | ||||||
|       })); |       })); | ||||||
|   | |||||||
| @@ -4,6 +4,7 @@ import { fireEvent } from "../../common/dom/fire_event"; | |||||||
| import { PolymerChangedEvent } from "../../polymer-types"; | import { PolymerChangedEvent } from "../../polymer-types"; | ||||||
| import { HomeAssistant } from "../../types"; | import { HomeAssistant } from "../../types"; | ||||||
| import "./ha-device-picker"; | import "./ha-device-picker"; | ||||||
|  | import type { HaDevicePickerDeviceFilterFunc } from "./ha-device-picker"; | ||||||
|  |  | ||||||
| @customElement("ha-devices-picker") | @customElement("ha-devices-picker") | ||||||
| class HaDevicesPicker extends LitElement { | class HaDevicesPicker extends LitElement { | ||||||
| @@ -13,6 +14,8 @@ class HaDevicesPicker extends LitElement { | |||||||
|  |  | ||||||
|   @property() public helper?: string; |   @property() public helper?: string; | ||||||
|  |  | ||||||
|  |   @property({ type: Boolean }) public disabled?: boolean; | ||||||
|  |  | ||||||
|   @property({ type: Boolean }) public required?: boolean; |   @property({ type: Boolean }) public required?: boolean; | ||||||
|  |  | ||||||
|   /** |   /** | ||||||
| @@ -39,6 +42,8 @@ class HaDevicesPicker extends LitElement { | |||||||
|  |  | ||||||
|   @property({ attribute: "pick-device-label" }) public pickDeviceLabel?: string; |   @property({ attribute: "pick-device-label" }) public pickDeviceLabel?: string; | ||||||
|  |  | ||||||
|  |   @property() public deviceFilter?: HaDevicePickerDeviceFilterFunc; | ||||||
|  |  | ||||||
|   protected render(): TemplateResult { |   protected render(): TemplateResult { | ||||||
|     if (!this.hass) { |     if (!this.hass) { | ||||||
|       return html``; |       return html``; | ||||||
| @@ -53,11 +58,13 @@ class HaDevicesPicker extends LitElement { | |||||||
|               allow-custom-entity |               allow-custom-entity | ||||||
|               .curValue=${entityId} |               .curValue=${entityId} | ||||||
|               .hass=${this.hass} |               .hass=${this.hass} | ||||||
|  |               .deviceFilter=${this.deviceFilter} | ||||||
|               .includeDomains=${this.includeDomains} |               .includeDomains=${this.includeDomains} | ||||||
|               .excludeDomains=${this.excludeDomains} |               .excludeDomains=${this.excludeDomains} | ||||||
|               .includeDeviceClasses=${this.includeDeviceClasses} |               .includeDeviceClasses=${this.includeDeviceClasses} | ||||||
|               .value=${entityId} |               .value=${entityId} | ||||||
|               .label=${this.pickedDeviceLabel} |               .label=${this.pickedDeviceLabel} | ||||||
|  |               .disabled=${this.disabled} | ||||||
|               @value-changed=${this._deviceChanged} |               @value-changed=${this._deviceChanged} | ||||||
|             ></ha-device-picker> |             ></ha-device-picker> | ||||||
|           </div> |           </div> | ||||||
| @@ -65,12 +72,15 @@ class HaDevicesPicker extends LitElement { | |||||||
|       )} |       )} | ||||||
|       <div> |       <div> | ||||||
|         <ha-device-picker |         <ha-device-picker | ||||||
|  |           allow-custom-entity | ||||||
|           .hass=${this.hass} |           .hass=${this.hass} | ||||||
|           .helper=${this.helper} |           .helper=${this.helper} | ||||||
|  |           .deviceFilter=${this.deviceFilter} | ||||||
|           .includeDomains=${this.includeDomains} |           .includeDomains=${this.includeDomains} | ||||||
|           .excludeDomains=${this.excludeDomains} |           .excludeDomains=${this.excludeDomains} | ||||||
|           .includeDeviceClasses=${this.includeDeviceClasses} |           .includeDeviceClasses=${this.includeDeviceClasses} | ||||||
|           .label=${this.pickDeviceLabel} |           .label=${this.pickDeviceLabel} | ||||||
|  |           .disabled=${this.disabled} | ||||||
|           .required=${this.required && !currentDevices.length} |           .required=${this.required && !currentDevices.length} | ||||||
|           @value-changed=${this._addDevice} |           @value-changed=${this._addDevice} | ||||||
|         ></ha-device-picker> |         ></ha-device-picker> | ||||||
|   | |||||||
| @@ -14,6 +14,8 @@ class HaEntitiesPickerLight extends LitElement { | |||||||
|  |  | ||||||
|   @property({ type: Array }) public value?: string[]; |   @property({ type: Array }) public value?: string[]; | ||||||
|  |  | ||||||
|  |   @property({ type: Boolean }) public disabled?: boolean; | ||||||
|  |  | ||||||
|   @property({ type: Boolean }) public required?: boolean; |   @property({ type: Boolean }) public required?: boolean; | ||||||
|  |  | ||||||
|   @property() public helper?: string; |   @property() public helper?: string; | ||||||
| @@ -96,6 +98,7 @@ class HaEntitiesPickerLight extends LitElement { | |||||||
|               .entityFilter=${this._entityFilter} |               .entityFilter=${this._entityFilter} | ||||||
|               .value=${entityId} |               .value=${entityId} | ||||||
|               .label=${this.pickedEntityLabel} |               .label=${this.pickedEntityLabel} | ||||||
|  |               .disabled=${this.disabled} | ||||||
|               @value-changed=${this._entityChanged} |               @value-changed=${this._entityChanged} | ||||||
|             ></ha-entity-picker> |             ></ha-entity-picker> | ||||||
|           </div> |           </div> | ||||||
| @@ -103,6 +106,7 @@ class HaEntitiesPickerLight extends LitElement { | |||||||
|       )} |       )} | ||||||
|       <div> |       <div> | ||||||
|         <ha-entity-picker |         <ha-entity-picker | ||||||
|  |           allow-custom-entity | ||||||
|           .hass=${this.hass} |           .hass=${this.hass} | ||||||
|           .includeDomains=${this.includeDomains} |           .includeDomains=${this.includeDomains} | ||||||
|           .excludeDomains=${this.excludeDomains} |           .excludeDomains=${this.excludeDomains} | ||||||
| @@ -113,6 +117,7 @@ class HaEntitiesPickerLight extends LitElement { | |||||||
|           .entityFilter=${this._entityFilter} |           .entityFilter=${this._entityFilter} | ||||||
|           .label=${this.pickEntityLabel} |           .label=${this.pickEntityLabel} | ||||||
|           .helper=${this.helper} |           .helper=${this.helper} | ||||||
|  |           .disabled=${this.disabled} | ||||||
|           .required=${this.required && !currentEntities.length} |           .required=${this.required && !currentEntities.length} | ||||||
|           @value-changed=${this._addEntity} |           @value-changed=${this._addEntity} | ||||||
|         ></ha-entity-picker> |         ></ha-entity-picker> | ||||||
|   | |||||||
| @@ -20,7 +20,7 @@ interface HassEntityWithCachedName extends HassEntity { | |||||||
|   friendly_name: string; |   friendly_name: string; | ||||||
| } | } | ||||||
|  |  | ||||||
| export type HaEntityPickerEntityFilterFunc = (entityId: HassEntity) => boolean; | export type HaEntityPickerEntityFilterFunc = (entity: HassEntity) => boolean; | ||||||
|  |  | ||||||
| // eslint-disable-next-line lit/prefer-static-styles | // eslint-disable-next-line lit/prefer-static-styles | ||||||
| const rowRenderer: ComboBoxLitRenderer<HassEntityWithCachedName> = (item) => | const rowRenderer: ComboBoxLitRenderer<HassEntityWithCachedName> = (item) => | ||||||
|   | |||||||
| @@ -2,12 +2,12 @@ import "@polymer/paper-tooltip/paper-tooltip"; | |||||||
| import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; | import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; | ||||||
| import { customElement, property } from "lit/decorators"; | import { customElement, property } from "lit/decorators"; | ||||||
| import { fireEvent } from "../common/dom/fire_event"; | import { fireEvent } from "../common/dom/fire_event"; | ||||||
| import { Analytics, AnalyticsPreferences } from "../data/analytics"; | import type { Analytics, AnalyticsPreferences } from "../data/analytics"; | ||||||
| import { haStyle } from "../resources/styles"; | import { haStyle } from "../resources/styles"; | ||||||
| import { HomeAssistant } from "../types"; | import type { HomeAssistant } from "../types"; | ||||||
| import "./ha-checkbox"; |  | ||||||
| import type { HaCheckbox } from "./ha-checkbox"; |  | ||||||
| import "./ha-settings-row"; | import "./ha-settings-row"; | ||||||
|  | import "./ha-switch"; | ||||||
|  | import type { HaSwitch } from "./ha-switch"; | ||||||
|  |  | ||||||
| const ADDITIONAL_PREFERENCES = [ | const ADDITIONAL_PREFERENCES = [ | ||||||
|   { |   { | ||||||
| @@ -40,62 +40,62 @@ export class HaAnalytics extends LitElement { | |||||||
|  |  | ||||||
|     return html` |     return html` | ||||||
|       <ha-settings-row> |       <ha-settings-row> | ||||||
|         <span slot="prefix"> |         <span slot="heading" data-for="base"> Basic analytics </span> | ||||||
|           <ha-checkbox |         <span slot="description" data-for="base"> | ||||||
|             @change=${this._handleRowCheckboxClick} |           This includes information about your system. | ||||||
|  |         </span> | ||||||
|  |         <ha-switch | ||||||
|  |           @change=${this._handleRowClick} | ||||||
|           .checked=${baseEnabled} |           .checked=${baseEnabled} | ||||||
|           .preference=${"base"} |           .preference=${"base"} | ||||||
|           .disabled=${loading} |           .disabled=${loading} | ||||||
|           name="base" |           name="base" | ||||||
|         > |         > | ||||||
|           </ha-checkbox> |         </ha-switch> | ||||||
|         </span> |  | ||||||
|         <span slot="heading" data-for="base"> Basic analytics </span> |  | ||||||
|         <span slot="description" data-for="base"> |  | ||||||
|           This includes information about your system. |  | ||||||
|         </span> |  | ||||||
|       </ha-settings-row> |       </ha-settings-row> | ||||||
|       ${ADDITIONAL_PREFERENCES.map( |       ${ADDITIONAL_PREFERENCES.map( | ||||||
|         (preference) => |         (preference) => | ||||||
|           html`<ha-settings-row> |           html` | ||||||
|             <span slot="prefix"> |             <ha-settings-row> | ||||||
|               <ha-checkbox |  | ||||||
|                 @change=${this._handleRowCheckboxClick} |  | ||||||
|                 .checked=${this.analytics?.preferences[preference.key]} |  | ||||||
|                 .preference=${preference.key} |  | ||||||
|                 name=${preference.key} |  | ||||||
|               > |  | ||||||
|               </ha-checkbox> |  | ||||||
|               ${!baseEnabled |  | ||||||
|                 ? html`<paper-tooltip animation-delay="0" position="right"> |  | ||||||
|                     You need to enable basic analytics for this option to be |  | ||||||
|                     available |  | ||||||
|                   </paper-tooltip>` |  | ||||||
|                 : ""} |  | ||||||
|             </span> |  | ||||||
|               <span slot="heading" data-for=${preference.key}> |               <span slot="heading" data-for=${preference.key}> | ||||||
|                 ${preference.title} |                 ${preference.title} | ||||||
|               </span> |               </span> | ||||||
|               <span slot="description" data-for=${preference.key}> |               <span slot="description" data-for=${preference.key}> | ||||||
|                 ${preference.description} |                 ${preference.description} | ||||||
|               </span> |               </span> | ||||||
|           </ha-settings-row>` |               <span> | ||||||
|  |                 <ha-switch | ||||||
|  |                   @change=${this._handleRowClick} | ||||||
|  |                   .checked=${this.analytics?.preferences[preference.key]} | ||||||
|  |                   .preference=${preference.key} | ||||||
|  |                   name=${preference.key} | ||||||
|  |                 > | ||||||
|  |                 </ha-switch> | ||||||
|  |                 ${!baseEnabled | ||||||
|  |                   ? html` | ||||||
|  |                       <paper-tooltip animation-delay="0" position="right"> | ||||||
|  |                         You need to enable basic analytics for this option to be | ||||||
|  |                         available | ||||||
|  |                       </paper-tooltip> | ||||||
|  |                     ` | ||||||
|  |                   : ""} | ||||||
|  |               </span> | ||||||
|  |             </ha-settings-row> | ||||||
|  |           ` | ||||||
|       )} |       )} | ||||||
|       <ha-settings-row> |       <ha-settings-row> | ||||||
|         <span slot="prefix"> |         <span slot="heading" data-for="diagnostics"> Diagnostics </span> | ||||||
|           <ha-checkbox |         <span slot="description" data-for="diagnostics"> | ||||||
|             @change=${this._handleRowCheckboxClick} |           Share crash reports when unexpected errors occur. | ||||||
|  |         </span> | ||||||
|  |         <ha-switch | ||||||
|  |           @change=${this._handleRowClick} | ||||||
|           .checked=${this.analytics?.preferences.diagnostics} |           .checked=${this.analytics?.preferences.diagnostics} | ||||||
|           .preference=${"diagnostics"} |           .preference=${"diagnostics"} | ||||||
|           .disabled=${loading} |           .disabled=${loading} | ||||||
|           name="diagnostics" |           name="diagnostics" | ||||||
|         > |         > | ||||||
|           </ha-checkbox> |         </ha-switch> | ||||||
|         </span> |  | ||||||
|         <span slot="heading" data-for="diagnostics"> Diagnostics </span> |  | ||||||
|         <span slot="description" data-for="diagnostics"> |  | ||||||
|           Share crash reports when unexpected errors occur. |  | ||||||
|         </span> |  | ||||||
|       </ha-settings-row> |       </ha-settings-row> | ||||||
|     `; |     `; | ||||||
|   } |   } | ||||||
| @@ -120,23 +120,23 @@ export class HaAnalytics extends LitElement { | |||||||
|     }); |     }); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   private _handleRowCheckboxClick(ev: Event) { |   private _handleRowClick(ev: Event) { | ||||||
|     const checkbox = ev.currentTarget as HaCheckbox; |     const target = ev.currentTarget as HaSwitch; | ||||||
|     const preference = (checkbox as any).preference; |     const preference = (target as any).preference; | ||||||
|     const preferences = this.analytics ? { ...this.analytics.preferences } : {}; |     const preferences = this.analytics ? { ...this.analytics.preferences } : {}; | ||||||
|  |  | ||||||
|     if (preferences[preference] === checkbox.checked) { |     if (preferences[preference] === target.checked) { | ||||||
|       return; |       return; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     preferences[preference] = checkbox.checked; |     preferences[preference] = target.checked; | ||||||
|  |  | ||||||
|     if ( |     if ( | ||||||
|       ADDITIONAL_PREFERENCES.some((entry) => entry.key === preference) && |       ADDITIONAL_PREFERENCES.some((entry) => entry.key === preference) && | ||||||
|       checkbox.checked |       target.checked | ||||||
|     ) { |     ) { | ||||||
|       preferences.base = true; |       preferences.base = true; | ||||||
|     } else if (preference === "base" && !checkbox.checked) { |     } else if (preference === "base" && !target.checked) { | ||||||
|       preferences.usage = false; |       preferences.usage = false; | ||||||
|       preferences.statistics = false; |       preferences.statistics = false; | ||||||
|     } |     } | ||||||
|   | |||||||
| @@ -10,8 +10,8 @@ interface State { | |||||||
|   backgroundColor: null | string; |   backgroundColor: null | string; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| @customElement("hassio-ansi-to-html") | @customElement("ha-ansi-to-html") | ||||||
| class HassioAnsiToHtml extends LitElement { | class HaAnsiToHtml extends LitElement { | ||||||
|   @property() public content!: string; |   @property() public content!: string; | ||||||
| 
 | 
 | ||||||
|   protected render(): TemplateResult | void { |   protected render(): TemplateResult | void { | ||||||
| @@ -241,6 +241,6 @@ class HassioAnsiToHtml extends LitElement { | |||||||
| 
 | 
 | ||||||
| declare global { | declare global { | ||||||
|   interface HTMLElementTagNameMap { |   interface HTMLElementTagNameMap { | ||||||
|     "hassio-ansi-to-html": HassioAnsiToHtml; |     "ha-ansi-to-html": HaAnsiToHtml; | ||||||
|   } |   } | ||||||
| } | } | ||||||
| @@ -409,7 +409,7 @@ export class HaAreaPicker extends SubscribeMixin(LitElement) { | |||||||
|             name, |             name, | ||||||
|           }); |           }); | ||||||
|           this._areas = [...this._areas!, area]; |           this._areas = [...this._areas!, area]; | ||||||
|           (this.comboBox as any).items = this._getAreas( |           (this.comboBox as any).filteredItems = this._getAreas( | ||||||
|             this._areas!, |             this._areas!, | ||||||
|             this._devices!, |             this._devices!, | ||||||
|             this._entities!, |             this._entities!, | ||||||
|   | |||||||
| @@ -131,7 +131,7 @@ export class HaBaseTimeInput extends LitElement { | |||||||
|   protected render(): TemplateResult { |   protected render(): TemplateResult { | ||||||
|     return html` |     return html` | ||||||
|       ${this.label |       ${this.label | ||||||
|         ? html`<label>${this.label}${this.required ? "*" : ""}</label>` |         ? html`<label>${this.label}${this.required ? " *" : ""}</label>` | ||||||
|         : ""} |         : ""} | ||||||
|       <div class="time-input-wrap"> |       <div class="time-input-wrap"> | ||||||
|         ${this.enableDay |         ${this.enableDay | ||||||
| @@ -310,6 +310,7 @@ export class HaBaseTimeInput extends LitElement { | |||||||
|       border-radius: var(--mdc-shape-small, 4px) var(--mdc-shape-small, 4px) 0 0; |       border-radius: var(--mdc-shape-small, 4px) var(--mdc-shape-small, 4px) 0 0; | ||||||
|       overflow: hidden; |       overflow: hidden; | ||||||
|       position: relative; |       position: relative; | ||||||
|  |       direction: ltr; | ||||||
|     } |     } | ||||||
|     ha-textfield { |     ha-textfield { | ||||||
|       width: 40px; |       width: 40px; | ||||||
|   | |||||||
| @@ -1,17 +1,22 @@ | |||||||
|  | import type { Button } from "@material/mwc-button"; | ||||||
| import "@material/mwc-menu"; | import "@material/mwc-menu"; | ||||||
| import type { Corner, Menu, MenuCorner } from "@material/mwc-menu"; | import type { Corner, Menu, MenuCorner } from "@material/mwc-menu"; | ||||||
| import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; | import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; | ||||||
| import { customElement, property, query } from "lit/decorators"; | import { customElement, property, query } from "lit/decorators"; | ||||||
|  | import { FOCUS_TARGET } from "../dialogs/make-dialog-manager"; | ||||||
|  | import type { HaIconButton } from "./ha-icon-button"; | ||||||
|  |  | ||||||
| @customElement("ha-button-menu") | @customElement("ha-button-menu") | ||||||
| export class HaButtonMenu extends LitElement { | export class HaButtonMenu extends LitElement { | ||||||
|  |   protected readonly [FOCUS_TARGET]; | ||||||
|  |  | ||||||
|   @property() public corner: Corner = "TOP_START"; |   @property() public corner: Corner = "TOP_START"; | ||||||
|  |  | ||||||
|   @property() public menuCorner: MenuCorner = "START"; |   @property() public menuCorner: MenuCorner = "START"; | ||||||
|  |  | ||||||
|   @property({ type: Number }) public x?: number; |   @property({ type: Number }) public x: number | null = null; | ||||||
|  |  | ||||||
|   @property({ type: Number }) public y?: number; |   @property({ type: Number }) public y: number | null = null; | ||||||
|  |  | ||||||
|   @property({ type: Boolean }) public multi = false; |   @property({ type: Boolean }) public multi = false; | ||||||
|  |  | ||||||
| @@ -31,10 +36,18 @@ export class HaButtonMenu extends LitElement { | |||||||
|     return this._menu?.selected; |     return this._menu?.selected; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|  |   public override focus() { | ||||||
|  |     if (this._menu?.open) { | ||||||
|  |       this._menu.focusItemAtIndex(0); | ||||||
|  |     } else { | ||||||
|  |       this._triggerButton?.focus(); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|   protected render(): TemplateResult { |   protected render(): TemplateResult { | ||||||
|     return html` |     return html` | ||||||
|       <div @click=${this._handleClick}> |       <div @click=${this._handleClick}> | ||||||
|         <slot name="trigger"></slot> |         <slot name="trigger" @slotchange=${this._setTriggerAria}></slot> | ||||||
|       </div> |       </div> | ||||||
|       <mwc-menu |       <mwc-menu | ||||||
|         .corner=${this.corner} |         .corner=${this.corner} | ||||||
| @@ -50,6 +63,21 @@ export class HaButtonMenu extends LitElement { | |||||||
|     `; |     `; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|  |   protected firstUpdated(changedProps): void { | ||||||
|  |     super.firstUpdated(changedProps); | ||||||
|  |  | ||||||
|  |     if (document.dir === "rtl") { | ||||||
|  |       this.updateComplete.then(() => { | ||||||
|  |         this.querySelectorAll("mwc-list-item").forEach((item) => { | ||||||
|  |           const style = document.createElement("style"); | ||||||
|  |           style.innerHTML = | ||||||
|  |             "span.material-icons:first-of-type { margin-left: var(--mdc-list-item-graphic-margin, 32px) !important; margin-right: 0px !important;}"; | ||||||
|  |           item!.shadowRoot!.appendChild(style); | ||||||
|  |         }); | ||||||
|  |       }); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|   private _handleClick(): void { |   private _handleClick(): void { | ||||||
|     if (this.disabled) { |     if (this.disabled) { | ||||||
|       return; |       return; | ||||||
| @@ -58,6 +86,18 @@ export class HaButtonMenu extends LitElement { | |||||||
|     this._menu!.show(); |     this._menu!.show(); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|  |   private get _triggerButton() { | ||||||
|  |     return this.querySelector( | ||||||
|  |       'ha-icon-button[slot="trigger"], mwc-button[slot="trigger"]' | ||||||
|  |     ) as HaIconButton | Button | null; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   private _setTriggerAria() { | ||||||
|  |     if (this._triggerButton) { | ||||||
|  |       this._triggerButton.ariaHasPopup = "menu"; | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|   static get styles(): CSSResultGroup { |   static get styles(): CSSResultGroup { | ||||||
|     return css` |     return css` | ||||||
|       :host { |       :host { | ||||||
|   | |||||||
| @@ -117,6 +117,19 @@ export class HaButtonToggleGroup extends LitElement { | |||||||
|         --mdc-shape-small: 4px; |         --mdc-shape-small: 4px; | ||||||
|         border-right-width: 1px; |         border-right-width: 1px; | ||||||
|       } |       } | ||||||
|  |  | ||||||
|  |       :host([dir="rtl"]) ha-icon-button:first-child, | ||||||
|  |       :host([dir="rtl"]) mwc-button:first-child { | ||||||
|  |         border-radius: 0 4px 4px 0; | ||||||
|  |         border-right-width: 1px; | ||||||
|  |         --mdc-shape-small: 0 4px 4px 0; | ||||||
|  |         --mdc-button-outline-width: 1px; | ||||||
|  |       } | ||||||
|  |       :host([dir="rtl"]) ha-icon-button:last-child, | ||||||
|  |       :host([dir="rtl"]) mwc-button:last-child { | ||||||
|  |         --mdc-shape-small: 4px 0 0 4px; | ||||||
|  |         border-radius: 4px 0 0 4px; | ||||||
|  |       } | ||||||
|     `; |     `; | ||||||
|   } |   } | ||||||
| } | } | ||||||
|   | |||||||
| @@ -66,9 +66,13 @@ export class HaChip extends LitElement { | |||||||
|         line-height: 14px; |         line-height: 14px; | ||||||
|         color: var(--ha-chip-icon-color, var(--ha-chip-text-color)); |         color: var(--ha-chip-icon-color, var(--ha-chip-text-color)); | ||||||
|       } |       } | ||||||
|  |       .mdc-chip.mdc-chip--selected .mdc-chip__checkmark, | ||||||
|       .mdc-chip.no-text |       .mdc-chip.no-text | ||||||
|         .mdc-chip__icon--leading:not(.mdc-chip__icon--leading-hidden) { |         .mdc-chip__icon--leading:not(.mdc-chip__icon--leading-hidden) { | ||||||
|         margin-right: -4px; |         margin-right: -4px; | ||||||
|  |         margin-inline-start: -4px; | ||||||
|  |         margin-inline-end: 4px; | ||||||
|  |         direction: var(--direction); | ||||||
|       } |       } | ||||||
|  |  | ||||||
|       span[role="gridcell"] { |       span[role="gridcell"] { | ||||||
|   | |||||||
							
								
								
									
										85
									
								
								src/components/ha-clickable-list-item.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										85
									
								
								src/components/ha-clickable-list-item.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,85 @@ | |||||||
|  | import { ListItemBase } from "@material/mwc-list/mwc-list-item-base"; | ||||||
|  | import { styles } from "@material/mwc-list/mwc-list-item.css"; | ||||||
|  | import { css, CSSResult, html } from "lit"; | ||||||
|  | import { customElement, property, query } from "lit/decorators"; | ||||||
|  |  | ||||||
|  | @customElement("ha-clickable-list-item") | ||||||
|  | export class HaClickableListItem extends ListItemBase { | ||||||
|  |   @property() public href?: string; | ||||||
|  |  | ||||||
|  |   @property({ type: Boolean }) public disableHref = false; | ||||||
|  |  | ||||||
|  |   // property used only in css | ||||||
|  |   @property({ type: Boolean, reflect: true }) public rtl = false; | ||||||
|  |  | ||||||
|  |   @property({ type: Boolean, reflect: true }) public openNewTab = false; | ||||||
|  |  | ||||||
|  |   @query("a") private _anchor!: HTMLAnchorElement; | ||||||
|  |  | ||||||
|  |   public render() { | ||||||
|  |     const r = super.render(); | ||||||
|  |     const href = this.href || ""; | ||||||
|  |  | ||||||
|  |     return html`${this.disableHref | ||||||
|  |       ? html`<a aria-role="option">${r}</a>` | ||||||
|  |       : html`<a | ||||||
|  |           aria-role="option" | ||||||
|  |           target=${this.openNewTab ? "_blank" : ""} | ||||||
|  |           href=${href} | ||||||
|  |           >${r}</a | ||||||
|  |         >`}`; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   firstUpdated() { | ||||||
|  |     super.firstUpdated(); | ||||||
|  |     this.addEventListener("keydown", (ev) => { | ||||||
|  |       if (ev.key === "Enter" || ev.key === " ") { | ||||||
|  |         this._anchor.click(); | ||||||
|  |       } | ||||||
|  |     }); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   static get styles(): CSSResult[] { | ||||||
|  |     return [ | ||||||
|  |       styles, | ||||||
|  |       css` | ||||||
|  |         :host { | ||||||
|  |           padding-left: 0px; | ||||||
|  |           padding-right: 0px; | ||||||
|  |         } | ||||||
|  |         :host([graphic="avatar"]:not([twoLine])), | ||||||
|  |         :host([graphic="icon"]:not([twoLine])) { | ||||||
|  |           height: 48px; | ||||||
|  |         } | ||||||
|  |         a { | ||||||
|  |           width: 100%; | ||||||
|  |           height: 100%; | ||||||
|  |           display: flex; | ||||||
|  |           align-items: center; | ||||||
|  |           padding-left: var(--mdc-list-side-padding, 20px); | ||||||
|  |           padding-right: var(--mdc-list-side-padding, 20px); | ||||||
|  |           overflow: hidden; | ||||||
|  |         } | ||||||
|  |         span.material-icons:first-of-type { | ||||||
|  |           margin-inline-start: 0px !important; | ||||||
|  |           margin-inline-end: var( | ||||||
|  |             --mdc-list-item-graphic-margin, | ||||||
|  |             16px | ||||||
|  |           ) !important; | ||||||
|  |           direction: var(--direction); | ||||||
|  |         } | ||||||
|  |         span.material-icons:last-of-type { | ||||||
|  |           margin-inline-start: auto !important; | ||||||
|  |           margin-inline-end: 0px !important; | ||||||
|  |           direction: var(--direction); | ||||||
|  |         } | ||||||
|  |       `, | ||||||
|  |     ]; | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | declare global { | ||||||
|  |   interface HTMLElementTagNameMap { | ||||||
|  |     "ha-clickable-list-item": HaClickableListItem; | ||||||
|  |   } | ||||||
|  | } | ||||||
| @@ -3,6 +3,7 @@ import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; | |||||||
| import { customElement, property } from "lit/decorators"; | import { customElement, property } from "lit/decorators"; | ||||||
| import { formatNumber } from "../common/number/format_number"; | import { formatNumber } from "../common/number/format_number"; | ||||||
| import { CLIMATE_PRESET_NONE } from "../data/climate"; | import { CLIMATE_PRESET_NONE } from "../data/climate"; | ||||||
|  | import { UNAVAILABLE_STATES } from "../data/entity"; | ||||||
| import type { HomeAssistant } from "../types"; | import type { HomeAssistant } from "../types"; | ||||||
|  |  | ||||||
| @customElement("ha-climate-state") | @customElement("ha-climate-state") | ||||||
| @@ -15,7 +16,7 @@ class HaClimateState extends LitElement { | |||||||
|     const currentStatus = this._computeCurrentStatus(); |     const currentStatus = this._computeCurrentStatus(); | ||||||
|  |  | ||||||
|     return html`<div class="target"> |     return html`<div class="target"> | ||||||
|         ${this.stateObj.state !== "unknown" |         ${!UNAVAILABLE_STATES.includes(this.stateObj.state) | ||||||
|           ? html`<span class="state-label"> |           ? html`<span class="state-label"> | ||||||
|                 ${this._localizeState()} |                 ${this._localizeState()} | ||||||
|                 ${this.stateObj.attributes.preset_mode && |                 ${this.stateObj.attributes.preset_mode && | ||||||
| @@ -25,12 +26,12 @@ class HaClimateState extends LitElement { | |||||||
|                       `state_attributes.climate.preset_mode.${this.stateObj.attributes.preset_mode}` |                       `state_attributes.climate.preset_mode.${this.stateObj.attributes.preset_mode}` | ||||||
|                     ) || this.stateObj.attributes.preset_mode}` |                     ) || this.stateObj.attributes.preset_mode}` | ||||||
|                   : ""} |                   : ""} | ||||||
|             </span>` |               </span> | ||||||
|           : ""} |               <div class="unit">${this._computeTarget()}</div>` | ||||||
|         <div class="unit">${this._computeTarget()}</div> |           : this._localizeState()} | ||||||
|       </div> |       </div> | ||||||
|  |  | ||||||
|       ${currentStatus |       ${currentStatus && !UNAVAILABLE_STATES.includes(this.stateObj.state) | ||||||
|         ? html`<div class="current"> |         ? html`<div class="current"> | ||||||
|             ${this.hass.localize("ui.card.climate.currently")}: |             ${this.hass.localize("ui.card.climate.currently")}: | ||||||
|             <div class="unit">${currentStatus}</div> |             <div class="unit">${currentStatus}</div> | ||||||
| @@ -108,6 +109,10 @@ class HaClimateState extends LitElement { | |||||||
|   } |   } | ||||||
|  |  | ||||||
|   private _localizeState(): string { |   private _localizeState(): string { | ||||||
|  |     if (UNAVAILABLE_STATES.includes(this.stateObj.state)) { | ||||||
|  |       return this.hass.localize(`state.default.${this.stateObj.state}`); | ||||||
|  |     } | ||||||
|  |  | ||||||
|     const stateString = this.hass.localize( |     const stateString = this.hass.localize( | ||||||
|       `component.climate.state._.${this.stateObj.state}` |       `component.climate.state._.${this.stateObj.state}` | ||||||
|     ); |     ); | ||||||
|   | |||||||
| @@ -241,6 +241,9 @@ export class HaComboBox extends LitElement { | |||||||
|       .toggle-button { |       .toggle-button { | ||||||
|         right: 12px; |         right: 12px; | ||||||
|         top: -10px; |         top: -10px; | ||||||
|  |         inset-inline-start: initial; | ||||||
|  |         inset-inline-end: 12px; | ||||||
|  |         direction: var(--direction); | ||||||
|       } |       } | ||||||
|       :host([opened]) .toggle-button { |       :host([opened]) .toggle-button { | ||||||
|         color: var(--primary-color); |         color: var(--primary-color); | ||||||
| @@ -249,6 +252,9 @@ export class HaComboBox extends LitElement { | |||||||
|         --mdc-icon-size: 20px; |         --mdc-icon-size: 20px; | ||||||
|         top: -7px; |         top: -7px; | ||||||
|         right: 36px; |         right: 36px; | ||||||
|  |         inset-inline-start: initial; | ||||||
|  |         inset-inline-end: 36px; | ||||||
|  |         direction: var(--direction); | ||||||
|       } |       } | ||||||
|     `; |     `; | ||||||
|   } |   } | ||||||
|   | |||||||
| @@ -140,6 +140,9 @@ export class HaDateRangePicker extends LitElement { | |||||||
|     return css` |     return css` | ||||||
|       ha-svg-icon { |       ha-svg-icon { | ||||||
|         margin-right: 8px; |         margin-right: 8px; | ||||||
|  |         margin-inline-end: 8px; | ||||||
|  |         margin-inline-start: initial; | ||||||
|  |         direction: var(--direction); | ||||||
|       } |       } | ||||||
|  |  | ||||||
|       .date-range-inputs { |       .date-range-inputs { | ||||||
| @@ -166,6 +169,9 @@ export class HaDateRangePicker extends LitElement { | |||||||
|  |  | ||||||
|       ha-textfield:last-child { |       ha-textfield:last-child { | ||||||
|         margin-left: 8px; |         margin-left: 8px; | ||||||
|  |         margin-inline-start: 8px; | ||||||
|  |         margin-inline-end: initial; | ||||||
|  |         direction: var(--direction); | ||||||
|       } |       } | ||||||
|  |  | ||||||
|       @media only screen and (max-width: 800px) { |       @media only screen and (max-width: 800px) { | ||||||
|   | |||||||
| @@ -3,8 +3,8 @@ import { styles } from "@material/mwc-dialog/mwc-dialog.css"; | |||||||
| import { mdiClose } from "@mdi/js"; | import { mdiClose } from "@mdi/js"; | ||||||
| import { css, html, TemplateResult } from "lit"; | import { css, html, TemplateResult } from "lit"; | ||||||
| import { customElement } from "lit/decorators"; | import { customElement } from "lit/decorators"; | ||||||
| import { computeRTLDirection } from "../common/util/compute_rtl"; |  | ||||||
| import type { HomeAssistant } from "../types"; | import type { HomeAssistant } from "../types"; | ||||||
|  | import { FOCUS_TARGET } from "../dialogs/make-dialog-manager"; | ||||||
| import "./ha-icon-button"; | import "./ha-icon-button"; | ||||||
|  |  | ||||||
| export const createCloseHeading = ( | export const createCloseHeading = ( | ||||||
| @@ -17,12 +17,13 @@ export const createCloseHeading = ( | |||||||
|     .path=${mdiClose} |     .path=${mdiClose} | ||||||
|     dialogAction="close" |     dialogAction="close" | ||||||
|     class="header_button" |     class="header_button" | ||||||
|     dir=${computeRTLDirection(hass)} |  | ||||||
|   ></ha-icon-button> |   ></ha-icon-button> | ||||||
| `; | `; | ||||||
|  |  | ||||||
| @customElement("ha-dialog") | @customElement("ha-dialog") | ||||||
| export class HaDialog extends DialogBase { | export class HaDialog extends DialogBase { | ||||||
|  |   protected readonly [FOCUS_TARGET]; | ||||||
|  |  | ||||||
|   public scrollToPos(x: number, y: number) { |   public scrollToPos(x: number, y: number) { | ||||||
|     this.contentElement?.scrollTo(x, y); |     this.contentElement?.scrollTo(x, y); | ||||||
|   } |   } | ||||||
| @@ -89,14 +90,18 @@ export class HaDialog extends DialogBase { | |||||||
|       } |       } | ||||||
|       .header_title { |       .header_title { | ||||||
|         margin-right: 40px; |         margin-right: 40px; | ||||||
|  |         margin-inline-end: 40px; | ||||||
|  |         direction: var(--direction); | ||||||
|       } |       } | ||||||
|       [dir="rtl"].header_button { |       .header_button { | ||||||
|         right: auto; |         inset-inline-start: initial; | ||||||
|         left: 16px; |         inset-inline-end: 16px; | ||||||
|  |         direction: var(--direction); | ||||||
|       } |       } | ||||||
|       [dir="rtl"].header_title { |       .dialog-actions { | ||||||
|         margin-left: 40px; |         inset-inline-start: initial !important; | ||||||
|         margin-right: 0px; |         inset-inline-end: 0px !important; | ||||||
|  |         direction: var(--direction); | ||||||
|       } |       } | ||||||
|     `, |     `, | ||||||
|   ]; |   ]; | ||||||
|   | |||||||
| @@ -133,6 +133,9 @@ class HaExpansionPanel extends LitElement { | |||||||
|       .summary-icon { |       .summary-icon { | ||||||
|         transition: transform 150ms cubic-bezier(0.4, 0, 0.2, 1); |         transition: transform 150ms cubic-bezier(0.4, 0, 0.2, 1); | ||||||
|         margin-left: auto; |         margin-left: auto; | ||||||
|  |         margin-inline-start: auto; | ||||||
|  |         margin-inline-end: initial; | ||||||
|  |         direction: var(--direction); | ||||||
|       } |       } | ||||||
|  |  | ||||||
|       .summary-icon.expanded { |       .summary-icon.expanded { | ||||||
|   | |||||||
| @@ -1,12 +1,25 @@ | |||||||
| import { Fab } from "@material/mwc-fab"; | import { FabBase } from "@material/mwc-fab/mwc-fab-base"; | ||||||
|  | import { styles } from "@material/mwc-fab/mwc-fab.css"; | ||||||
| import { customElement } from "lit/decorators"; | import { customElement } from "lit/decorators"; | ||||||
|  | import { css } from "lit"; | ||||||
|  |  | ||||||
| @customElement("ha-fab") | @customElement("ha-fab") | ||||||
| export class HaFab extends Fab { | export class HaFab extends FabBase { | ||||||
|   protected firstUpdated(changedProperties) { |   protected firstUpdated(changedProperties) { | ||||||
|     super.firstUpdated(changedProperties); |     super.firstUpdated(changedProperties); | ||||||
|     this.style.setProperty("--mdc-theme-secondary", "var(--primary-color)"); |     this.style.setProperty("--mdc-theme-secondary", "var(--primary-color)"); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|  |   static override styles = [ | ||||||
|  |     styles, | ||||||
|  |     css` | ||||||
|  |       :host .mdc-fab--extended .mdc-fab__icon { | ||||||
|  |         margin-inline-start: -8px; | ||||||
|  |         margin-inline-end: 12px; | ||||||
|  |         direction: var(--direction); | ||||||
|  |       } | ||||||
|  |     `, | ||||||
|  |   ]; | ||||||
| } | } | ||||||
|  |  | ||||||
| declare global { | declare global { | ||||||
|   | |||||||
| @@ -175,11 +175,24 @@ export class HaFileUpload extends LitElement { | |||||||
|         } |         } | ||||||
|         .mdc-text-field__icon--leading { |         .mdc-text-field__icon--leading { | ||||||
|           margin-bottom: 12px; |           margin-bottom: 12px; | ||||||
|  |           inset-inline-start: initial; | ||||||
|  |           inset-inline-end: 0px; | ||||||
|  |           direction: var(--direction); | ||||||
|         } |         } | ||||||
|         .mdc-text-field--filled .mdc-floating-label--float-above { |         .mdc-text-field--filled .mdc-floating-label--float-above { | ||||||
|           transform: scale(0.75); |           transform: scale(0.75); | ||||||
|           top: 8px; |           top: 8px; | ||||||
|         } |         } | ||||||
|  |         .mdc-floating-label { | ||||||
|  |           inset-inline-start: 16px !important; | ||||||
|  |           inset-inline-end: initial !important; | ||||||
|  |           direction: var(--direction); | ||||||
|  |         } | ||||||
|  |         .mdc-text-field--filled .mdc-floating-label { | ||||||
|  |           inset-inline-start: 48px !important; | ||||||
|  |           inset-inline-end: initial !important; | ||||||
|  |           direction: var(--direction); | ||||||
|  |         } | ||||||
|         .dragged:before { |         .dragged:before { | ||||||
|           position: var(--layout-fit_-_position); |           position: var(--layout-fit_-_position); | ||||||
|           top: var(--layout-fit_-_top); |           top: var(--layout-fit_-_top); | ||||||
|   | |||||||
| @@ -132,6 +132,12 @@ export class HaFormString extends LitElement implements HaFormElement { | |||||||
|         --mdc-icon-button-size: 24px; |         --mdc-icon-button-size: 24px; | ||||||
|         color: var(--secondary-text-color); |         color: var(--secondary-text-color); | ||||||
|       } |       } | ||||||
|  |  | ||||||
|  |       ha-icon-button { | ||||||
|  |         inset-inline-start: initial; | ||||||
|  |         inset-inline-end: 12px; | ||||||
|  |         direction: var(--direction); | ||||||
|  |       } | ||||||
|     `; |     `; | ||||||
|   } |   } | ||||||
| } | } | ||||||
|   | |||||||
| @@ -1,6 +1,8 @@ | |||||||
| import "@material/mwc-icon-button"; | import "@material/mwc-icon-button"; | ||||||
|  | import type { IconButton } from "@material/mwc-icon-button"; | ||||||
| import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; | import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; | ||||||
| import { customElement, property } from "lit/decorators"; | import { customElement, property, query } from "lit/decorators"; | ||||||
|  | import { ifDefined } from "lit/directives/if-defined"; | ||||||
| import "./ha-svg-icon"; | import "./ha-svg-icon"; | ||||||
|  |  | ||||||
| @customElement("ha-icon-button") | @customElement("ha-icon-button") | ||||||
| @@ -11,21 +13,32 @@ export class HaIconButton extends LitElement { | |||||||
|   @property({ type: String }) path?: string; |   @property({ type: String }) path?: string; | ||||||
|  |  | ||||||
|   // Label that is used for ARIA support and as tooltip |   // Label that is used for ARIA support and as tooltip | ||||||
|   @property({ type: String }) label = ""; |   @property({ type: String }) label?: string; | ||||||
|  |  | ||||||
|  |   // These should always be set as properties, not attributes, | ||||||
|  |   // so that only the <button> element gets the attribute | ||||||
|  |   @property({ type: String, attribute: "aria-haspopup" }) | ||||||
|  |   override ariaHasPopup!: IconButton["ariaHasPopup"]; | ||||||
|  |  | ||||||
|   @property({ type: Boolean }) hideTitle = false; |   @property({ type: Boolean }) hideTitle = false; | ||||||
|  |  | ||||||
|  |   @query("mwc-icon-button", true) private _button?: IconButton; | ||||||
|  |  | ||||||
|  |   public override focus() { | ||||||
|  |     this._button?.focus(); | ||||||
|  |   } | ||||||
|  |  | ||||||
|   static shadowRootOptions: ShadowRootInit = { |   static shadowRootOptions: ShadowRootInit = { | ||||||
|     mode: "open", |     mode: "open", | ||||||
|     delegatesFocus: true, |     delegatesFocus: true, | ||||||
|   }; |   }; | ||||||
|  |  | ||||||
|   protected render(): TemplateResult { |   protected render(): TemplateResult { | ||||||
|     // Note: `ariaLabel` required despite the `mwc-icon-button` docs saying `label` should be enough |  | ||||||
|     return html` |     return html` | ||||||
|       <mwc-icon-button |       <mwc-icon-button | ||||||
|         .ariaLabel=${this.label} |         aria-label=${ifDefined(this.label)} | ||||||
|         .title=${this.hideTitle ? "" : this.label} |         title=${ifDefined(this.hideTitle ? undefined : this.label)} | ||||||
|  |         aria-haspopup=${ifDefined(this.ariaHasPopup)} | ||||||
|         .disabled=${this.disabled} |         .disabled=${this.disabled} | ||||||
|       > |       > | ||||||
|         ${this.path |         ${this.path | ||||||
|   | |||||||
| @@ -53,7 +53,7 @@ class HaLabeledSlider extends PolymerElement { | |||||||
|   } |   } | ||||||
|  |  | ||||||
|   _getTitle() { |   _getTitle() { | ||||||
|     return `${this.caption}${this.required ? "*" : ""}`; |     return `${this.caption}${this.caption && this.required ? " *" : ""}`; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   static get properties() { |   static get properties() { | ||||||
|   | |||||||
							
								
								
									
										79
									
								
								src/components/ha-metric.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										79
									
								
								src/components/ha-metric.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,79 @@ | |||||||
|  | import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; | ||||||
|  | import { customElement, property } from "lit/decorators"; | ||||||
|  | import { classMap } from "lit/directives/class-map"; | ||||||
|  | import { roundWithOneDecimal } from "../util/calculate"; | ||||||
|  | import "./ha-bar"; | ||||||
|  | import "./ha-settings-row"; | ||||||
|  |  | ||||||
|  | @customElement("ha-metric") | ||||||
|  | class HaMetric extends LitElement { | ||||||
|  |   @property({ type: Number }) public value!: number; | ||||||
|  |  | ||||||
|  |   @property({ type: String }) public heading!: string; | ||||||
|  |  | ||||||
|  |   @property({ type: String }) public tooltip?: string; | ||||||
|  |  | ||||||
|  |   protected render(): TemplateResult { | ||||||
|  |     const roundedValue = roundWithOneDecimal(this.value); | ||||||
|  |     return html` | ||||||
|  |       <ha-settings-row> | ||||||
|  |         <span slot="heading"> ${this.heading} </span> | ||||||
|  |         <div slot="description" .title=${this.tooltip ?? ""}> | ||||||
|  |           <span class="value"> ${roundedValue} % </span> | ||||||
|  |           <ha-bar | ||||||
|  |             class=${classMap({ | ||||||
|  |               "target-warning": roundedValue > 50, | ||||||
|  |               "target-critical": roundedValue > 85, | ||||||
|  |             })} | ||||||
|  |             .value=${this.value} | ||||||
|  |           ></ha-bar> | ||||||
|  |         </div> | ||||||
|  |       </ha-settings-row> | ||||||
|  |     `; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   static get styles(): CSSResultGroup { | ||||||
|  |     return css` | ||||||
|  |       ha-settings-row { | ||||||
|  |         padding: 0; | ||||||
|  |         height: 54px; | ||||||
|  |         width: 100%; | ||||||
|  |       } | ||||||
|  |       ha-settings-row > div[slot="description"] { | ||||||
|  |         white-space: normal; | ||||||
|  |         color: var(--secondary-text-color); | ||||||
|  |         display: flex; | ||||||
|  |         justify-content: space-between; | ||||||
|  |       } | ||||||
|  |       ha-bar { | ||||||
|  |         --ha-bar-primary-color: var( | ||||||
|  |           --metric-bar-ok-color, | ||||||
|  |           var(--success-color) | ||||||
|  |         ); | ||||||
|  |       } | ||||||
|  |       .target-warning { | ||||||
|  |         --ha-bar-primary-color: var( | ||||||
|  |           --metric-bar-warning-color, | ||||||
|  |           var(--warning-color) | ||||||
|  |         ); | ||||||
|  |       } | ||||||
|  |       .target-critical { | ||||||
|  |         --ha-bar-primary-color: var( | ||||||
|  |           --metric-bar-critical-color, | ||||||
|  |           var(--error-color) | ||||||
|  |         ); | ||||||
|  |       } | ||||||
|  |       .value { | ||||||
|  |         width: 48px; | ||||||
|  |         padding-right: 4px; | ||||||
|  |         flex-shrink: 0; | ||||||
|  |       } | ||||||
|  |     `; | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | declare global { | ||||||
|  |   interface HTMLElementTagNameMap { | ||||||
|  |     "ha-metric": HaMetric; | ||||||
|  |   } | ||||||
|  | } | ||||||
							
								
								
									
										90
									
								
								src/components/ha-navigation-list.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										90
									
								
								src/components/ha-navigation-list.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,90 @@ | |||||||
|  | import "@material/mwc-list/mwc-list"; | ||||||
|  | import "@material/mwc-list/mwc-list-item"; | ||||||
|  | import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; | ||||||
|  | import { customElement, property } from "lit/decorators"; | ||||||
|  | import type { PageNavigation } from "../layouts/hass-tabs-subpage"; | ||||||
|  | import type { HomeAssistant } from "../types"; | ||||||
|  | import "./ha-clickable-list-item"; | ||||||
|  | import "./ha-icon-next"; | ||||||
|  | import "./ha-svg-icon"; | ||||||
|  |  | ||||||
|  | @customElement("ha-navigation-list") | ||||||
|  | class HaNavigationList extends LitElement { | ||||||
|  |   @property({ attribute: false }) public hass!: HomeAssistant; | ||||||
|  |  | ||||||
|  |   @property({ type: Boolean }) public narrow!: boolean; | ||||||
|  |  | ||||||
|  |   @property({ attribute: false }) public pages!: PageNavigation[]; | ||||||
|  |  | ||||||
|  |   @property({ type: Boolean }) public hasSecondary = false; | ||||||
|  |  | ||||||
|  |   public render(): TemplateResult { | ||||||
|  |     return html` | ||||||
|  |       <mwc-list> | ||||||
|  |         ${this.pages.map( | ||||||
|  |           (page) => html` | ||||||
|  |             <ha-clickable-list-item | ||||||
|  |               graphic="avatar" | ||||||
|  |               .twoline=${this.hasSecondary} | ||||||
|  |               .hasMeta=${!this.narrow} | ||||||
|  |               @click=${this._entryClicked} | ||||||
|  |               href=${page.path} | ||||||
|  |             > | ||||||
|  |               <div | ||||||
|  |                 slot="graphic" | ||||||
|  |                 class=${page.iconColor ? "icon-background" : ""} | ||||||
|  |                 .style="background-color: ${page.iconColor || "undefined"}" | ||||||
|  |               > | ||||||
|  |                 <ha-svg-icon .path=${page.iconPath}></ha-svg-icon> | ||||||
|  |               </div> | ||||||
|  |               <span>${page.name}</span> | ||||||
|  |               ${this.hasSecondary | ||||||
|  |                 ? html`<span slot="secondary">${page.description}</span>` | ||||||
|  |                 : ""} | ||||||
|  |               ${!this.narrow | ||||||
|  |                 ? html`<ha-icon-next slot="meta"></ha-icon-next>` | ||||||
|  |                 : ""} | ||||||
|  |             </ha-clickable-list-item> | ||||||
|  |           ` | ||||||
|  |         )} | ||||||
|  |       </mwc-list> | ||||||
|  |     `; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   private _entryClicked(ev) { | ||||||
|  |     ev.currentTarget.blur(); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   static styles: CSSResultGroup = css` | ||||||
|  |     :host { | ||||||
|  |       --mdc-list-vertical-padding: 0; | ||||||
|  |     } | ||||||
|  |     ha-svg-icon, | ||||||
|  |     ha-icon-next { | ||||||
|  |       color: var(--secondary-text-color); | ||||||
|  |       height: 24px; | ||||||
|  |       width: 24px; | ||||||
|  |       display: block; | ||||||
|  |     } | ||||||
|  |     ha-svg-icon { | ||||||
|  |       padding: 8px; | ||||||
|  |     } | ||||||
|  |     .icon-background { | ||||||
|  |       border-radius: 50%; | ||||||
|  |     } | ||||||
|  |     .icon-background ha-svg-icon { | ||||||
|  |       color: #fff; | ||||||
|  |     } | ||||||
|  |     ha-clickable-list-item { | ||||||
|  |       cursor: pointer; | ||||||
|  |       font-size: var(--navigation-list-item-title-font-size); | ||||||
|  |       padding: var(--navigation-list-item-padding) 0; | ||||||
|  |     } | ||||||
|  |   `; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | declare global { | ||||||
|  |   interface HTMLElementTagNameMap { | ||||||
|  |     "ha-navigation-list": HaNavigationList; | ||||||
|  |   } | ||||||
|  | } | ||||||
| @@ -163,6 +163,9 @@ export class HaNetwork extends LitElement { | |||||||
|  |  | ||||||
|         ha-settings-row { |         ha-settings-row { | ||||||
|           padding: 0; |           padding: 0; | ||||||
|  |           --paper-time-input-justify-content: flex-end; | ||||||
|  |           --settings-row-content-display: contents; | ||||||
|  |           --settings-row-prefix-display: contents; | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         span[slot="heading"], |         span[slot="heading"], | ||||||
|   | |||||||
| @@ -47,6 +47,19 @@ export class HaSelect extends SelectBase { | |||||||
|       .mdc-select__anchor { |       .mdc-select__anchor { | ||||||
|         width: var(--ha-select-min-width, 200px); |         width: var(--ha-select-min-width, 200px); | ||||||
|       } |       } | ||||||
|  |       .mdc-select--filled .mdc-floating-label { | ||||||
|  |         inset-inline-start: 12px; | ||||||
|  |         inset-inline-end: initial; | ||||||
|  |         direction: var(--direction); | ||||||
|  |       } | ||||||
|  |       .mdc-select .mdc-select__anchor { | ||||||
|  |         padding-inline-start: 12px; | ||||||
|  |         padding-inline-end: 0px; | ||||||
|  |         direction: var(--direction); | ||||||
|  |       } | ||||||
|  |       .mdc-select__anchor .mdc-floating-label--float-above { | ||||||
|  |         transform-origin: var(--float-start); | ||||||
|  |       } | ||||||
|     `, |     `, | ||||||
|   ]; |   ]; | ||||||
| } | } | ||||||
|   | |||||||
| @@ -1,15 +1,24 @@ | |||||||
|  | import { UnsubscribeFunc } from "home-assistant-js-websocket"; | ||||||
| import { html, LitElement } from "lit"; | import { html, LitElement } from "lit"; | ||||||
| import { customElement, property, state } from "lit/decorators"; | import { customElement, property, state } from "lit/decorators"; | ||||||
| import { ConfigEntry, getConfigEntries } from "../../data/config_entries"; | import memoizeOne from "memoize-one"; | ||||||
| import { DeviceRegistryEntry } from "../../data/device_registry"; | import { DeviceRegistryEntry } from "../../data/device_registry"; | ||||||
| import { EntityRegistryEntry } from "../../data/entity_registry"; | import { | ||||||
|  |   EntityRegistryEntry, | ||||||
|  |   subscribeEntityRegistry, | ||||||
|  | } from "../../data/entity_registry"; | ||||||
|  | import { | ||||||
|  |   EntitySources, | ||||||
|  |   fetchEntitySourcesWithCache, | ||||||
|  | } from "../../data/entity_sources"; | ||||||
| import { AreaSelector } from "../../data/selector"; | import { AreaSelector } from "../../data/selector"; | ||||||
|  | import { SubscribeMixin } from "../../mixins/subscribe-mixin"; | ||||||
| import { HomeAssistant } from "../../types"; | import { HomeAssistant } from "../../types"; | ||||||
| import "../ha-area-picker"; | import "../ha-area-picker"; | ||||||
| import "../ha-areas-picker"; | import "../ha-areas-picker"; | ||||||
|  |  | ||||||
| @customElement("ha-selector-area") | @customElement("ha-selector-area") | ||||||
| export class HaAreaSelector extends LitElement { | export class HaAreaSelector extends SubscribeMixin(LitElement) { | ||||||
|   @property() public hass!: HomeAssistant; |   @property() public hass!: HomeAssistant; | ||||||
|  |  | ||||||
|   @property() public selector!: AreaSelector; |   @property() public selector!: AreaSelector; | ||||||
| @@ -20,29 +29,44 @@ export class HaAreaSelector extends LitElement { | |||||||
|  |  | ||||||
|   @property() public helper?: string; |   @property() public helper?: string; | ||||||
|  |  | ||||||
|   @state() public _configEntries?: ConfigEntry[]; |   @state() private _entitySources?: EntitySources; | ||||||
|  |  | ||||||
|  |   @state() private _entities?: EntityRegistryEntry[]; | ||||||
|  |  | ||||||
|   @property({ type: Boolean }) public disabled = false; |   @property({ type: Boolean }) public disabled = false; | ||||||
|  |  | ||||||
|   @property({ type: Boolean }) public required = true; |   @property({ type: Boolean }) public required = true; | ||||||
|  |  | ||||||
|   protected updated(changedProperties) { |   public hassSubscribe(): UnsubscribeFunc[] { | ||||||
|     if (changedProperties.has("selector")) { |     return [ | ||||||
|       const oldSelector = changedProperties.get("selector"); |       subscribeEntityRegistry(this.hass.connection!, (entities) => { | ||||||
|       if ( |         this._entities = entities.filter((entity) => entity.device_id !== null); | ||||||
|         oldSelector !== this.selector && |       }), | ||||||
|         this.selector.area.device?.integration |     ]; | ||||||
|       ) { |  | ||||||
|         getConfigEntries(this.hass, { |  | ||||||
|           domain: this.selector.area.device.integration, |  | ||||||
|         }).then((entries) => { |  | ||||||
|           this._configEntries = entries; |  | ||||||
|         }); |  | ||||||
|   } |   } | ||||||
|  |  | ||||||
|  |   protected updated(changedProperties) { | ||||||
|  |     if ( | ||||||
|  |       changedProperties.has("selector") && | ||||||
|  |       (this.selector.area.device?.integration || | ||||||
|  |         this.selector.area.entity?.integration) && | ||||||
|  |       !this._entitySources | ||||||
|  |     ) { | ||||||
|  |       fetchEntitySourcesWithCache(this.hass).then((sources) => { | ||||||
|  |         this._entitySources = sources; | ||||||
|  |       }); | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   protected render() { |   protected render() { | ||||||
|  |     if ( | ||||||
|  |       (this.selector.area.device?.integration || | ||||||
|  |         this.selector.area.entity?.integration) && | ||||||
|  |       !this._entitySources | ||||||
|  |     ) { | ||||||
|  |       return html``; | ||||||
|  |     } | ||||||
|  |  | ||||||
|     if (!this.selector.area.multiple) { |     if (!this.selector.area.multiple) { | ||||||
|       return html` |       return html` | ||||||
|         <ha-area-picker |         <ha-area-picker | ||||||
| @@ -87,39 +111,62 @@ export class HaAreaSelector extends LitElement { | |||||||
|   } |   } | ||||||
|  |  | ||||||
|   private _filterEntities = (entity: EntityRegistryEntry): boolean => { |   private _filterEntities = (entity: EntityRegistryEntry): boolean => { | ||||||
|     if (this.selector.area.entity?.integration) { |     const filterIntegration = this.selector.area.entity?.integration; | ||||||
|       if (entity.platform !== this.selector.area.entity.integration) { |     if ( | ||||||
|  |       filterIntegration && | ||||||
|  |       this._entitySources?.[entity.entity_id]?.domain !== filterIntegration | ||||||
|  |     ) { | ||||||
|  |       return false; | ||||||
|  |     } | ||||||
|  |     return true; | ||||||
|  |   }; | ||||||
|  |  | ||||||
|  |   private _filterDevices = (device: DeviceRegistryEntry): boolean => { | ||||||
|  |     if (!this.selector.area.device) { | ||||||
|  |       return true; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     const { | ||||||
|  |       manufacturer: filterManufacturer, | ||||||
|  |       model: filterModel, | ||||||
|  |       integration: filterIntegration, | ||||||
|  |     } = this.selector.area.device; | ||||||
|  |  | ||||||
|  |     if (filterManufacturer && device.manufacturer !== filterManufacturer) { | ||||||
|  |       return false; | ||||||
|  |     } | ||||||
|  |     if (filterModel && device.model !== filterModel) { | ||||||
|  |       return false; | ||||||
|  |     } | ||||||
|  |     if (filterIntegration && this._entitySources && this._entities) { | ||||||
|  |       const deviceIntegrations = this._deviceIntegrations( | ||||||
|  |         this._entitySources, | ||||||
|  |         this._entities | ||||||
|  |       ); | ||||||
|  |       if (!deviceIntegrations?.[device.id]?.includes(filterIntegration)) { | ||||||
|         return false; |         return false; | ||||||
|       } |       } | ||||||
|     } |     } | ||||||
|     return true; |     return true; | ||||||
|   }; |   }; | ||||||
|  |  | ||||||
|   private _filterDevices = (device: DeviceRegistryEntry): boolean => { |   private _deviceIntegrations = memoizeOne( | ||||||
|     if ( |     (entitySources: EntitySources, entities: EntityRegistryEntry[]) => { | ||||||
|       this.selector.area.device?.manufacturer && |       const deviceIntegrations: Record<string, string[]> = {}; | ||||||
|       device.manufacturer !== this.selector.area.device.manufacturer |  | ||||||
|     ) { |       for (const entity of entities) { | ||||||
|       return false; |         const source = entitySources[entity.entity_id]; | ||||||
|  |         if (!source?.domain) { | ||||||
|  |           continue; | ||||||
|         } |         } | ||||||
|     if ( |         if (!deviceIntegrations[entity.device_id!]) { | ||||||
|       this.selector.area.device?.model && |           deviceIntegrations[entity.device_id!] = []; | ||||||
|       device.model !== this.selector.area.device.model |  | ||||||
|     ) { |  | ||||||
|       return false; |  | ||||||
|         } |         } | ||||||
|     if (this.selector.area.device?.integration) { |         deviceIntegrations[entity.device_id!].push(source.domain); | ||||||
|       if ( |  | ||||||
|         this._configEntries && |  | ||||||
|         !this._configEntries.some((entry) => |  | ||||||
|           device.config_entries.includes(entry.entry_id) |  | ||||||
|         ) |  | ||||||
|       ) { |  | ||||||
|         return false; |  | ||||||
|       } |       } | ||||||
|  |       return deviceIntegrations; | ||||||
|     } |     } | ||||||
|     return true; |   ); | ||||||
|   }; |  | ||||||
| } | } | ||||||
|  |  | ||||||
| declare global { | declare global { | ||||||
|   | |||||||
| @@ -26,9 +26,9 @@ export class HaColorTempSelector extends LitElement { | |||||||
|       <ha-labeled-slider |       <ha-labeled-slider | ||||||
|         pin |         pin | ||||||
|         icon="hass:thermometer" |         icon="hass:thermometer" | ||||||
|         .caption=${this.label} |         .caption=${this.label || ""} | ||||||
|         .min=${this.selector.color_temp.min_mireds ?? 153} |         .min=${this.selector.color_temp?.min_mireds ?? 153} | ||||||
|         .max=${this.selector.color_temp.max_mireds ?? 500} |         .max=${this.selector.color_temp?.max_mireds ?? 500} | ||||||
|         .value=${this.value} |         .value=${this.value} | ||||||
|         .disabled=${this.disabled} |         .disabled=${this.disabled} | ||||||
|         .helper=${this.helper} |         .helper=${this.helper} | ||||||
|   | |||||||
| @@ -1,18 +1,33 @@ | |||||||
|  | import { UnsubscribeFunc } from "home-assistant-js-websocket"; | ||||||
| import { html, LitElement } from "lit"; | import { html, LitElement } from "lit"; | ||||||
| import { customElement, property, state } from "lit/decorators"; | import { customElement, property, state } from "lit/decorators"; | ||||||
| import { ConfigEntry, getConfigEntries } from "../../data/config_entries"; | import memoizeOne from "memoize-one"; | ||||||
|  | import { ConfigEntry } from "../../data/config_entries"; | ||||||
| import type { DeviceRegistryEntry } from "../../data/device_registry"; | import type { DeviceRegistryEntry } from "../../data/device_registry"; | ||||||
|  | import { | ||||||
|  |   EntityRegistryEntry, | ||||||
|  |   subscribeEntityRegistry, | ||||||
|  | } from "../../data/entity_registry"; | ||||||
|  | import { | ||||||
|  |   EntitySources, | ||||||
|  |   fetchEntitySourcesWithCache, | ||||||
|  | } from "../../data/entity_sources"; | ||||||
| import type { DeviceSelector } from "../../data/selector"; | import type { DeviceSelector } from "../../data/selector"; | ||||||
|  | import { SubscribeMixin } from "../../mixins/subscribe-mixin"; | ||||||
| import type { HomeAssistant } from "../../types"; | import type { HomeAssistant } from "../../types"; | ||||||
| import "../device/ha-device-picker"; | import "../device/ha-device-picker"; | ||||||
| import "../device/ha-devices-picker"; | import "../device/ha-devices-picker"; | ||||||
|  |  | ||||||
| @customElement("ha-selector-device") | @customElement("ha-selector-device") | ||||||
| export class HaDeviceSelector extends LitElement { | export class HaDeviceSelector extends SubscribeMixin(LitElement) { | ||||||
|   @property() public hass!: HomeAssistant; |   @property() public hass!: HomeAssistant; | ||||||
|  |  | ||||||
|   @property() public selector!: DeviceSelector; |   @property() public selector!: DeviceSelector; | ||||||
|  |  | ||||||
|  |   @state() private _entitySources?: EntitySources; | ||||||
|  |  | ||||||
|  |   @state() private _entities?: EntityRegistryEntry[]; | ||||||
|  |  | ||||||
|   @property() public value?: any; |   @property() public value?: any; | ||||||
|  |  | ||||||
|   @property() public label?: string; |   @property() public label?: string; | ||||||
| @@ -25,20 +40,32 @@ export class HaDeviceSelector extends LitElement { | |||||||
|  |  | ||||||
|   @property({ type: Boolean }) public required = true; |   @property({ type: Boolean }) public required = true; | ||||||
|  |  | ||||||
|   protected updated(changedProperties) { |   public hassSubscribe(): UnsubscribeFunc[] { | ||||||
|     if (changedProperties.has("selector")) { |     return [ | ||||||
|       const oldSelector = changedProperties.get("selector"); |       subscribeEntityRegistry(this.hass.connection!, (entities) => { | ||||||
|       if (oldSelector !== this.selector && this.selector.device?.integration) { |         this._entities = entities.filter((entity) => entity.device_id !== null); | ||||||
|         getConfigEntries(this.hass, { |       }), | ||||||
|           domain: this.selector.device.integration, |     ]; | ||||||
|         }).then((entries) => { |  | ||||||
|           this._configEntries = entries; |  | ||||||
|         }); |  | ||||||
|   } |   } | ||||||
|  |  | ||||||
|  |   protected updated(changedProperties): void { | ||||||
|  |     super.updated(changedProperties); | ||||||
|  |     if ( | ||||||
|  |       changedProperties.has("selector") && | ||||||
|  |       this.selector.device.integration && | ||||||
|  |       !this._entitySources | ||||||
|  |     ) { | ||||||
|  |       fetchEntitySourcesWithCache(this.hass).then((sources) => { | ||||||
|  |         this._entitySources = sources; | ||||||
|  |       }); | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   protected render() { |   protected render() { | ||||||
|  |     if (this.selector.device.integration && !this._entitySources) { | ||||||
|  |       return html``; | ||||||
|  |     } | ||||||
|  |  | ||||||
|     if (!this.selector.device.multiple) { |     if (!this.selector.device.multiple) { | ||||||
|       return html` |       return html` | ||||||
|         <ha-device-picker |         <ha-device-picker | ||||||
| @@ -66,42 +93,62 @@ export class HaDeviceSelector extends LitElement { | |||||||
|         .hass=${this.hass} |         .hass=${this.hass} | ||||||
|         .value=${this.value} |         .value=${this.value} | ||||||
|         .helper=${this.helper} |         .helper=${this.helper} | ||||||
|  |         .deviceFilter=${this._filterDevices} | ||||||
|         .includeDeviceClasses=${this.selector.device.entity?.device_class |         .includeDeviceClasses=${this.selector.device.entity?.device_class | ||||||
|           ? [this.selector.device.entity.device_class] |           ? [this.selector.device.entity.device_class] | ||||||
|           : undefined} |           : undefined} | ||||||
|         .includeDomains=${this.selector.device.entity?.domain |         .includeDomains=${this.selector.device.entity?.domain | ||||||
|           ? [this.selector.device.entity.domain] |           ? [this.selector.device.entity.domain] | ||||||
|           : undefined} |           : undefined} | ||||||
|  |         .disabled=${this.disabled} | ||||||
|         .required=${this.required} |         .required=${this.required} | ||||||
|       ></ha-devices-picker> |       ></ha-devices-picker> | ||||||
|     `; |     `; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   private _filterDevices = (device: DeviceRegistryEntry): boolean => { |   private _filterDevices = (device: DeviceRegistryEntry): boolean => { | ||||||
|     if ( |     const { | ||||||
|       this.selector.device?.manufacturer && |       manufacturer: filterManufacturer, | ||||||
|       device.manufacturer !== this.selector.device.manufacturer |       model: filterModel, | ||||||
|     ) { |       integration: filterIntegration, | ||||||
|  |     } = this.selector.device; | ||||||
|  |  | ||||||
|  |     if (filterManufacturer && device.manufacturer !== filterManufacturer) { | ||||||
|       return false; |       return false; | ||||||
|     } |     } | ||||||
|     if ( |     if (filterModel && device.model !== filterModel) { | ||||||
|       this.selector.device?.model && |  | ||||||
|       device.model !== this.selector.device.model |  | ||||||
|     ) { |  | ||||||
|       return false; |       return false; | ||||||
|     } |     } | ||||||
|     if (this.selector.device?.integration) { |     if (filterIntegration && this._entitySources && this._entities) { | ||||||
|       if ( |       const deviceIntegrations = this._deviceIntegrations( | ||||||
|         this._configEntries && |         this._entitySources, | ||||||
|         !this._configEntries.some((entry) => |         this._entities | ||||||
|           device.config_entries.includes(entry.entry_id) |       ); | ||||||
|         ) |       if (!deviceIntegrations?.[device.id]?.includes(filterIntegration)) { | ||||||
|       ) { |  | ||||||
|         return false; |         return false; | ||||||
|       } |       } | ||||||
|     } |     } | ||||||
|     return true; |     return true; | ||||||
|   }; |   }; | ||||||
|  |  | ||||||
|  |   private _deviceIntegrations = memoizeOne( | ||||||
|  |     (entitySources: EntitySources, entities: EntityRegistryEntry[]) => { | ||||||
|  |       const deviceIntegrations: Record<string, string[]> = {}; | ||||||
|  |  | ||||||
|  |       for (const entity of entities) { | ||||||
|  |         const source = entitySources[entity.entity_id]; | ||||||
|  |         if (!source?.domain) { | ||||||
|  |           continue; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         if (!deviceIntegrations[entity.device_id!]) { | ||||||
|  |           deviceIntegrations[entity.device_id!] = []; | ||||||
|  |         } | ||||||
|  |         deviceIntegrations[entity.device_id!].push(source.domain); | ||||||
|  |       } | ||||||
|  |       return deviceIntegrations; | ||||||
|  |     } | ||||||
|  |   ); | ||||||
| } | } | ||||||
|  |  | ||||||
| declare global { | declare global { | ||||||
|   | |||||||
| @@ -51,9 +51,10 @@ export class HaEntitySelector extends LitElement { | |||||||
|         .hass=${this.hass} |         .hass=${this.hass} | ||||||
|         .value=${this.value} |         .value=${this.value} | ||||||
|         .helper=${this.helper} |         .helper=${this.helper} | ||||||
|         .entityFilter=${this._filterEntities} |  | ||||||
|         .includeEntities=${this.selector.entity.include_entities} |         .includeEntities=${this.selector.entity.include_entities} | ||||||
|         .excludeEntities=${this.selector.entity.exclude_entities} |         .excludeEntities=${this.selector.entity.exclude_entities} | ||||||
|  |         .entityFilter=${this._filterEntities} | ||||||
|  |         .disabled=${this.disabled} | ||||||
|         .required=${this.required} |         .required=${this.required} | ||||||
|       ></ha-entities-picker> |       ></ha-entities-picker> | ||||||
|     `; |     `; | ||||||
|   | |||||||
| @@ -1,4 +1,4 @@ | |||||||
| import { html, LitElement } from "lit"; | import { css, html, LitElement } from "lit"; | ||||||
| import { customElement, property } from "lit/decorators"; | import { customElement, property } from "lit/decorators"; | ||||||
| import memoizeOne from "memoize-one"; | import memoizeOne from "memoize-one"; | ||||||
| import { fireEvent } from "../../common/dom/fire_event"; | import { fireEvent } from "../../common/dom/fire_event"; | ||||||
| @@ -76,6 +76,13 @@ export class HaLocationSelector extends LitElement { | |||||||
|     const radius = ev.detail.radius; |     const radius = ev.detail.radius; | ||||||
|     fireEvent(this, "value-changed", { value: { ...this.value, radius } }); |     fireEvent(this, "value-changed", { value: { ...this.value, radius } }); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|  |   static styles = css` | ||||||
|  |     :host { | ||||||
|  |       display: block; | ||||||
|  |       height: 400px; | ||||||
|  |     } | ||||||
|  |   `; | ||||||
| } | } | ||||||
|  |  | ||||||
| declare global { | declare global { | ||||||
|   | |||||||
| @@ -30,7 +30,7 @@ export class HaNumberSelector extends LitElement { | |||||||
|     const isBox = this.selector.number.mode === "box"; |     const isBox = this.selector.number.mode === "box"; | ||||||
|  |  | ||||||
|     return html` |     return html` | ||||||
|       ${this.label}${this.required ? "*" : ""} |       ${this.label ? html`${this.label}${this.required ? " *" : ""}` : ""} | ||||||
|       <div class="input"> |       <div class="input"> | ||||||
|         ${!isBox |         ${!isBox | ||||||
|           ? html`<ha-slider |           ? html`<ha-slider | ||||||
| @@ -107,6 +107,7 @@ export class HaNumberSelector extends LitElement { | |||||||
|         display: flex; |         display: flex; | ||||||
|         justify-content: space-between; |         justify-content: space-between; | ||||||
|         align-items: center; |         align-items: center; | ||||||
|  |         direction: ltr; | ||||||
|       } |       } | ||||||
|       ha-slider { |       ha-slider { | ||||||
|         flex: 1; |         flex: 1; | ||||||
|   | |||||||
| @@ -3,6 +3,7 @@ import { customElement, property } from "lit/decorators"; | |||||||
| import { fireEvent } from "../../common/dom/fire_event"; | import { fireEvent } from "../../common/dom/fire_event"; | ||||||
| import { HomeAssistant } from "../../types"; | import { HomeAssistant } from "../../types"; | ||||||
| import "../ha-yaml-editor"; | import "../ha-yaml-editor"; | ||||||
|  | import "../ha-input-helper-text"; | ||||||
|  |  | ||||||
| @customElement("ha-selector-object") | @customElement("ha-selector-object") | ||||||
| export class HaObjectSelector extends LitElement { | export class HaObjectSelector extends LitElement { | ||||||
| @@ -12,6 +13,8 @@ export class HaObjectSelector extends LitElement { | |||||||
|  |  | ||||||
|   @property() public label?: string; |   @property() public label?: string; | ||||||
|  |  | ||||||
|  |   @property() public helper?: string; | ||||||
|  |  | ||||||
|   @property() public placeholder?: string; |   @property() public placeholder?: string; | ||||||
|  |  | ||||||
|   @property({ type: Boolean }) public disabled = false; |   @property({ type: Boolean }) public disabled = false; | ||||||
| @@ -22,11 +25,15 @@ export class HaObjectSelector extends LitElement { | |||||||
|     return html`<ha-yaml-editor |     return html`<ha-yaml-editor | ||||||
|         .hass=${this.hass} |         .hass=${this.hass} | ||||||
|         .readonly=${this.disabled} |         .readonly=${this.disabled} | ||||||
|  |         .label=${this.label} | ||||||
|         .required=${this.required} |         .required=${this.required} | ||||||
|         .placeholder=${this.placeholder} |         .placeholder=${this.placeholder} | ||||||
|         .defaultValue=${this.value} |         .defaultValue=${this.value} | ||||||
|         @value-changed=${this._handleChange} |         @value-changed=${this._handleChange} | ||||||
|     ></ha-yaml-editor>`; |       ></ha-yaml-editor> | ||||||
|  |       ${this.helper | ||||||
|  |         ? html`<ha-input-helper-text>${this.helper}</ha-input-helper-text>` | ||||||
|  |         : ""} `; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   private _handleChange(ev) { |   private _handleChange(ev) { | ||||||
|   | |||||||
							
								
								
									
										56
									
								
								src/components/ha-selector/ha-selector-template.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										56
									
								
								src/components/ha-selector/ha-selector-template.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,56 @@ | |||||||
|  | import { html, LitElement } from "lit"; | ||||||
|  | import { customElement, property } from "lit/decorators"; | ||||||
|  | import { fireEvent } from "../../common/dom/fire_event"; | ||||||
|  | import { HomeAssistant } from "../../types"; | ||||||
|  | import "../ha-code-editor"; | ||||||
|  | import "../ha-input-helper-text"; | ||||||
|  |  | ||||||
|  | @customElement("ha-selector-template") | ||||||
|  | export class HaTemplateSelector extends LitElement { | ||||||
|  |   @property() public hass!: HomeAssistant; | ||||||
|  |  | ||||||
|  |   @property() public value?: string; | ||||||
|  |  | ||||||
|  |   @property() public label?: string; | ||||||
|  |  | ||||||
|  |   @property() public helper?: string; | ||||||
|  |  | ||||||
|  |   @property({ type: Boolean }) public disabled = false; | ||||||
|  |  | ||||||
|  |   @property({ type: Boolean }) public required = true; | ||||||
|  |  | ||||||
|  |   protected render() { | ||||||
|  |     return html` | ||||||
|  |       ${this.label | ||||||
|  |         ? html`<p>${this.label}${this.required ? " *" : ""}</p>` | ||||||
|  |         : ""} | ||||||
|  |       <ha-code-editor | ||||||
|  |         mode="jinja2" | ||||||
|  |         .hass=${this.hass} | ||||||
|  |         .value=${this.value} | ||||||
|  |         .readOnly=${this.disabled} | ||||||
|  |         autofocus | ||||||
|  |         autocomplete-entities | ||||||
|  |         @value-changed=${this._handleChange} | ||||||
|  |         dir="ltr" | ||||||
|  |       ></ha-code-editor> | ||||||
|  |       ${this.helper | ||||||
|  |         ? html`<ha-input-helper-text>${this.helper}</ha-input-helper-text>` | ||||||
|  |         : ""} | ||||||
|  |     `; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   private _handleChange(ev) { | ||||||
|  |     const value = ev.target.value; | ||||||
|  |     if (this.value === value) { | ||||||
|  |       return; | ||||||
|  |     } | ||||||
|  |     fireEvent(this, "value-changed", { value }); | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | declare global { | ||||||
|  |   interface HTMLElementTagNameMap { | ||||||
|  |     "ha-selector-template": HaTemplateSelector; | ||||||
|  |   } | ||||||
|  | } | ||||||
| @@ -18,6 +18,7 @@ import "./ha-selector-number"; | |||||||
| import "./ha-selector-object"; | import "./ha-selector-object"; | ||||||
| import "./ha-selector-select"; | import "./ha-selector-select"; | ||||||
| import "./ha-selector-target"; | import "./ha-selector-target"; | ||||||
|  | import "./ha-selector-template"; | ||||||
| import "./ha-selector-text"; | import "./ha-selector-text"; | ||||||
| import "./ha-selector-time"; | import "./ha-selector-time"; | ||||||
| import "./ha-selector-icon"; | import "./ha-selector-icon"; | ||||||
|   | |||||||
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user