Compare commits
	
		
			343 Commits
		
	
	
		
			0.2.0
			...
			0.6.0-alph
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|  | 4e96b9adbb | ||
|  | b9581d3762 | ||
|  | 7c010359c3 | ||
|  | 4a75243994 | ||
|  | d29d7e5b3b | ||
|  | 5ebd25e0d1 | ||
|  | b7d5a53e86 | ||
|  | 20d3498bfd | ||
|  | 67d7bb45f5 | ||
|  | 6a03105d01 | ||
|  | 5ae580ecf1 | ||
|  | 0efef33e53 | ||
|  | ccb88884a7 | ||
|  | d70ba0a55a | ||
|  | 5140840d3a | ||
|  | 14759fd3c9 | ||
|  | fed35be517 | ||
|  | db77cc43aa | ||
|  | b2269cc96d | ||
|  | 8b28bb2e9e | ||
|  | fb456878bc | ||
|  | 8b961ebd69 | ||
|  | 9bd3a41cf5 | ||
|  | 491ae55a2a | ||
|  | e1d2981782 | ||
|  | 74572168ae | ||
|  | 92d0b5c055 | ||
|  | 3504d3276c | ||
|  | 736b38b64c | ||
|  | cb118b599a | ||
|  | a08a056cff | ||
|  | 0ef2ebfe31 | ||
|  | 4f4ac3b574 | ||
|  | 7064cb0e30 | ||
|  | 91a99e17e0 | ||
|  | 2e9b7d20b9 | ||
|  | b8aa808de4 | ||
|  | 2cfa92a42b | ||
|  | 146efef72d | ||
|  | 8c9804e16f | ||
|  | a4736bfb5a | ||
|  | 15c54df629 | ||
|  | 32ffef21e9 | ||
|  | 848d3cb510 | ||
|  | 8a4caeebba | ||
|  | aa923f0fba | ||
|  | 4d8f50ddd5 | ||
|  | fe06b21a6c | ||
|  | efed7fb1b5 | ||
|  | df2cbb7d13 | ||
|  | 03edaa9ca2 | ||
|  | 1a7457abf9 | ||
|  | 00889b13e0 | ||
|  | 0615073ec4 | ||
|  | eb7d17d147 | ||
|  | 24f80feeee | ||
|  | 4b6dda5a9c | ||
|  | 4099fa0c83 | ||
|  | 76057e8797 | ||
|  | 538d3603dc | ||
|  | bc0e72ca52 | ||
|  | f25a47beb2 | ||
|  | cc3c6b0087 | ||
|  | 6cf80c0bfd | ||
|  | 8ce9bdb7a5 | ||
|  | 31e50150b1 | ||
|  | e359150d97 | ||
|  | 93680c981c | ||
|  | e06b66c523 | ||
|  | 3dea844e1e | ||
|  | 62b1af30e0 | ||
|  | e006c4e403 | ||
|  | 983573388e | ||
|  | bdd1dc7e17 | ||
|  | 9c1970ee14 | ||
|  | d0e0bf3571 | ||
|  | b399357517 | ||
|  | 0290cd3a32 | ||
|  | d8a1d03179 | ||
|  | 216fad3cb9 | ||
|  | fead6ea348 | ||
|  | 8814687be6 | ||
|  | 71c0e2caa0 | ||
|  | 1531c41542 | ||
|  | bc90d013e8 | ||
|  | 2adfaca0c4 | ||
|  | 6cc1a37d9d | ||
|  | 4bb616b327 | ||
|  | 38219618ba | ||
|  | 6774b53758 | ||
|  | 29a94c882f | ||
|  | 5897fa3a99 | ||
|  | 7af92c2dc9 | ||
|  | 1094177a42 | ||
|  | 5e814e8109 | ||
|  | 24c7675fa4 | ||
|  | dc3ca38c78 | ||
|  | 96b528e055 | ||
|  | 3858036631 | ||
|  | 19d42ceeb3 | ||
|  | a2836a3603 | ||
|  | 2a45758a6d | ||
|  | dc1bf4d878 | ||
|  | e82ba60c4e | ||
|  | 09199d30e8 | ||
|  | 724d32dbe2 | ||
|  | 949c8ee44e | ||
|  | 1a446d34c7 | ||
|  | 22a5847285 | ||
|  | 1c8f770f10 | ||
|  | be5ea55f6b | ||
|  | c65ade9827 | ||
|  | d3c1422b9e | ||
|  | b6ac9f985f | ||
|  | a59de4b6dc | ||
|  | f507d5df0c | ||
|  | f77e46de37 | ||
|  | cda17b1217 | ||
|  | be560769ef | ||
|  | 3815800e32 | ||
|  | a3226311a2 | ||
|  | 79669243c2 | ||
|  | fdc81f6ea4 | ||
|  | 7fe44459e7 | ||
|  | a8500d44e1 | ||
|  | b4d4c5abec | ||
|  | c19a3f272a | ||
|  | b264534858 | ||
|  | ab53f77f9e | ||
|  | c73956720c | ||
|  | 051041e794 | ||
|  | 5c83be9fee | ||
|  | 4bece42693 | ||
|  | 4ae107fe4c | ||
|  | 9523ed2562 | ||
|  | 9c403480e2 | ||
|  | 20b1b90e39 | ||
|  | 5633e30448 | ||
|  | 4492fb9f0c | ||
|  | 36410752e4 | ||
|  | 0219f7bfbb | ||
|  | 5f3c77f4b9 | ||
|  | a36c7a9ca3 | ||
|  | 56ce6dfeeb | ||
|  | 67c214454f | ||
|  | 73398378c4 | ||
|  | 215871ce9e | ||
|  | fd8ea6befd | ||
|  | 809a1a1c8c | ||
|  | fc8f2f200f | ||
|  | f41c9f9197 | ||
|  | cdf55ce68b | ||
|  | 12088d9516 | ||
|  | a0235ee385 | ||
|  | 67fbdb13c6 | ||
|  | c5960de0be | ||
|  | da15e880ec | ||
|  | efbe33f4e3 | ||
|  | af84c99a2d | ||
|  | 438449cad8 | ||
|  | d9ca55c3b7 | ||
|  | f248268984 | ||
|  | 8ee096595c | ||
|  | a8e79c289b | ||
|  | 2cd8533882 | ||
|  | 0a21d9c690 | ||
|  | e77bb533b1 | ||
|  | 96f1211395 | ||
|  | 1e4cb03470 | ||
|  | ab67b557ca | ||
|  | 82c9bd26d1 | ||
|  | 1bd04abd37 | ||
|  | c5942d22b3 | ||
|  | 37ad5e81cf | ||
|  | 26187e6233 | ||
|  | b8f6fda8d3 | ||
|  | 62b4e99810 | ||
|  | 25bf10a64e | ||
|  | 874410964d | ||
|  | 57c30917b3 | ||
|  | 87f89b63e1 | ||
|  | 3190b45db3 | ||
|  | f5434e26e5 | ||
|  | 86b6ad6bba | ||
|  | 8a9641fbed | ||
|  | 5142391da2 | ||
|  | 01090dc3b1 | ||
|  | 0a7bbb5a38 | ||
|  | c347eee9f0 | ||
|  | 90f197ba54 | ||
|  | e09917c687 | ||
|  | a69da832cb | ||
|  | c1708fd980 | ||
|  | c85a9bbe27 | ||
|  | d9790dedbb | ||
|  | 30e4eaa023 | ||
|  | 54e00c3403 | ||
|  | 0e3474bbcb | ||
|  | efd06ca547 | ||
|  | 69fd37d4fe | ||
|  | 4a49372410 | ||
|  | 478f58e2d8 | ||
|  | a87aff67ac | ||
|  | 644f5e7fc6 | ||
|  | 3cddac3dc6 | ||
|  | ab30c64eab | ||
|  | 6d79487219 | ||
|  | 9f7444eae0 | ||
|  | 788d682f2f | ||
|  | 66f84952f0 | ||
|  | 5d95c3702d | ||
|  | 1f0bd8059b | ||
|  | a7830df628 | ||
|  | 790446d592 | ||
|  | bb17885b4a | ||
|  | 04d8681656 | ||
|  | 71c4ac7fed | ||
|  | 3f7e21e97e | ||
|  | e24c47b041 | ||
|  | 73b32b30a8 | ||
|  | 5b6155057c | ||
|  | ff4185effe | ||
|  | b2da9fc04d | ||
|  | f281fab744 | ||
|  | 3b99f4feeb | ||
|  | efab8b60b1 | ||
|  | 0e96406573 | ||
|  | ed8757c08d | ||
|  | 813770329c | ||
|  | 1853bd466e | ||
|  | 07258477b3 | ||
|  | a3adb72cf8 | ||
|  | e25162f7b5 | ||
|  | d30c9d574b | ||
|  | efa5a1958c | ||
|  | 37f20fae5a | ||
|  | 91db34badb | ||
|  | c20200b609 | ||
|  | fcd4ac7292 | ||
|  | e16338c3f2 | ||
|  | 6e038b0685 | ||
|  | 052cd3894e | ||
|  | 809c7d6355 | ||
|  | 9edfec7dff | ||
|  | df56f6ceda | ||
|  | 5e834b0645 | ||
|  | 8fb0d61a84 | ||
|  | 54979b583b | ||
|  | 4e955e98d8 | ||
|  | 88cfcb4382 | ||
|  | 5338e45ddc | ||
|  | 24d071e2f8 | ||
|  | 988cd4a72f | ||
|  | d1ea916781 | ||
|  | ce9f25b86c | ||
|  | f29762c931 | ||
|  | 30e4496ef1 | ||
|  | 7f9dc5dd3a | ||
|  | 0f6babc243 | ||
|  | 6a43e04b31 | ||
|  | 36fa5a50c4 | ||
|  | 9ad6d92ccd | ||
|  | fafa8f43f4 | ||
|  | 9b490d33d5 | ||
|  | 33f9a1075e | ||
|  | b83006e2c3 | ||
|  | ba09c36bd2 | ||
|  | c71ee568b0 | ||
|  | 75041f5c23 | ||
|  | 14da471774 | ||
|  | 369b44f1c8 | ||
|  | 8284bb6e76 | ||
|  | 9b3b4dfbbc | ||
|  | 5ca4424933 | ||
|  | a308aa29a4 | ||
|  | 9e80b0eaaf | ||
|  | 85379cf491 | ||
|  | 758376a891 | ||
|  | 2ebba364e3 | ||
|  | 6e604440c0 | ||
|  | c23034688e | ||
|  | 69f45b52cf | ||
|  | ffc053fbe6 | ||
|  | b5f9ecf601 | ||
|  | 948d1d4e23 | ||
|  | 136297c18b | ||
|  | 164800951d | ||
|  | 84d283de2b | ||
|  | 2fa35d771a | ||
|  | 326cd073b9 | ||
|  | e99c3f5742 | ||
|  | 16a9392fa6 | ||
|  | 5bf063969b | ||
|  | c19a0511a6 | ||
|  | a4ac40b366 | ||
|  | ce69f044fb | ||
|  | 70b6469bd1 | ||
|  | 253316fb1f | ||
|  | ec71200ab0 | ||
|  | bc1f4eab2e | ||
|  | 4085006446 | ||
|  | b7fb821abe | ||
|  | 284e7ba451 | ||
|  | 17a3bd8d35 | ||
|  | c2b88c8a12 | ||
|  | c975af4c79 | ||
|  | debf1b71f1 | ||
|  | 4725953b32 | ||
|  | e7ca1209e2 | ||
|  | f9afa663f5 | ||
|  | 5068cbbcf4 | ||
|  | 043d3a9905 | ||
|  | 77c5f80c13 | ||
|  | e0d35d07dc | ||
|  | 285447a5b7 | ||
|  | ed3e4ba272 | ||
|  | 908563063a | ||
|  | 7f2611b410 | ||
|  | 648750655c | ||
|  | 8a0d5581d9 | ||
|  | 98d716109b | ||
|  | ebb2f2b4e5 | ||
|  | d910e4dd43 | ||
|  | 95d80fbbfc | ||
|  | 41297150c2 | ||
|  | b14b248f2f | ||
|  | 13fc1bff27 | ||
|  | eee8f21e76 | ||
|  | 8ce3560d8d | ||
|  | 9e97bac85b | ||
|  | 4a0b447f00 | ||
|  | bc4969dae8 | ||
|  | 5025b3d384 | ||
|  | 0d7e7eb6f7 | ||
|  | 062392b38c | ||
|  | acd468ae75 | ||
|  | 60f216df13 | ||
|  | 9de8a659d3 | ||
|  | 7dd8f65af7 | ||
|  | 9e83a3e447 | ||
|  | 2f135169a9 | ||
|  | 76d2750ad6 | ||
|  | 571778fbd4 | 
							
								
								
									
										76
									
								
								CODE_OF_CONDUCT.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,76 @@ | ||||
| # Contributor Covenant Code of Conduct | ||||
|  | ||||
| ## Our Pledge | ||||
|  | ||||
| In the interest of fostering an open and welcoming environment, we as | ||||
| contributors and maintainers pledge to making participation in our project and | ||||
| our community a harassment-free experience for everyone, regardless of age, body | ||||
| size, disability, ethnicity, sex characteristics, gender identity and expression, | ||||
| level of experience, education, socio-economic status, nationality, personal | ||||
| appearance, race, religion, or sexual identity and orientation. | ||||
|  | ||||
| ## Our Standards | ||||
|  | ||||
| Examples of behavior that contributes to creating a positive environment | ||||
| include: | ||||
|  | ||||
| * Using welcoming and inclusive language | ||||
| * Being respectful of differing viewpoints and experiences | ||||
| * Gracefully accepting constructive criticism | ||||
| * Focusing on what is best for the community | ||||
| * Showing empathy towards other community members | ||||
|  | ||||
| Examples of unacceptable behavior by participants include: | ||||
|  | ||||
| * The use of sexualized language or imagery and unwelcome sexual attention or | ||||
|  advances | ||||
| * Trolling, insulting/derogatory comments, and personal or political attacks | ||||
| * Public or private harassment | ||||
| * Publishing others' private information, such as a physical or electronic | ||||
|  address, without explicit permission | ||||
| * Other conduct which could reasonably be considered inappropriate in a | ||||
|  professional setting | ||||
|  | ||||
| ## Our Responsibilities | ||||
|  | ||||
| Project maintainers are responsible for clarifying the standards of acceptable | ||||
| behavior and are expected to take appropriate and fair corrective action in | ||||
| response to any instances of unacceptable behavior. | ||||
|  | ||||
| Project maintainers have the right and responsibility to remove, edit, or | ||||
| reject comments, commits, code, wiki edits, issues, and other contributions | ||||
| that are not aligned to this Code of Conduct, or to ban temporarily or | ||||
| permanently any contributor for other behaviors that they deem inappropriate, | ||||
| threatening, offensive, or harmful. | ||||
|  | ||||
| ## Scope | ||||
|  | ||||
| This Code of Conduct applies both within project spaces and in public spaces | ||||
| when an individual is representing the project or its community. Examples of | ||||
| representing a project or community include using an official project e-mail | ||||
| address, posting via an official social media account, or acting as an appointed | ||||
| representative at an online or offline event. Representation of a project may be | ||||
| further defined and clarified by project maintainers. | ||||
|  | ||||
| ## Enforcement | ||||
|  | ||||
| Instances of abusive, harassing, or otherwise unacceptable behavior may be | ||||
| reported by contacting the project team at vyalov.egor@gmail.com. All | ||||
| complaints will be reviewed and investigated and will result in a response that | ||||
| is deemed necessary and appropriate to the circumstances. The project team is | ||||
| obligated to maintain confidentiality with regard to the reporter of an incident. | ||||
| Further details of specific enforcement policies may be posted separately. | ||||
|  | ||||
| Project maintainers who do not follow or enforce the Code of Conduct in good | ||||
| faith may face temporary or permanent repercussions as determined by other | ||||
| members of the project's leadership. | ||||
|  | ||||
| ## Attribution | ||||
|  | ||||
| This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, | ||||
| available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html | ||||
|  | ||||
| [homepage]: https://www.contributor-covenant.org | ||||
|  | ||||
| For answers to common questions about this code of conduct, see | ||||
| https://www.contributor-covenant.org/faq | ||||
							
								
								
									
										201
									
								
								LICENSE
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,201 @@ | ||||
|                                  Apache License | ||||
|                            Version 2.0, January 2004 | ||||
|                         http://www.apache.org/licenses/ | ||||
|  | ||||
|    TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION | ||||
|  | ||||
|    1. Definitions. | ||||
|  | ||||
|       "License" shall mean the terms and conditions for use, reproduction, | ||||
|       and distribution as defined by Sections 1 through 9 of this document. | ||||
|  | ||||
|       "Licensor" shall mean the copyright owner or entity authorized by | ||||
|       the copyright owner that is granting the License. | ||||
|  | ||||
|       "Legal Entity" shall mean the union of the acting entity and all | ||||
|       other entities that control, are controlled by, or are under common | ||||
|       control with that entity. For the purposes of this definition, | ||||
|       "control" means (i) the power, direct or indirect, to cause the | ||||
|       direction or management of such entity, whether by contract or | ||||
|       otherwise, or (ii) ownership of fifty percent (50%) or more of the | ||||
|       outstanding shares, or (iii) beneficial ownership of such entity. | ||||
|  | ||||
|       "You" (or "Your") shall mean an individual or Legal Entity | ||||
|       exercising permissions granted by this License. | ||||
|  | ||||
|       "Source" form shall mean the preferred form for making modifications, | ||||
|       including but not limited to software source code, documentation | ||||
|       source, and configuration files. | ||||
|  | ||||
|       "Object" form shall mean any form resulting from mechanical | ||||
|       transformation or translation of a Source form, including but | ||||
|       not limited to compiled object code, generated documentation, | ||||
|       and conversions to other media types. | ||||
|  | ||||
|       "Work" shall mean the work of authorship, whether in Source or | ||||
|       Object form, made available under the License, as indicated by a | ||||
|       copyright notice that is included in or attached to the work | ||||
|       (an example is provided in the Appendix below). | ||||
|  | ||||
|       "Derivative Works" shall mean any work, whether in Source or Object | ||||
|       form, that is based on (or derived from) the Work and for which the | ||||
|       editorial revisions, annotations, elaborations, or other modifications | ||||
|       represent, as a whole, an original work of authorship. For the purposes | ||||
|       of this License, Derivative Works shall not include works that remain | ||||
|       separable from, or merely link (or bind by name) to the interfaces of, | ||||
|       the Work and Derivative Works thereof. | ||||
|  | ||||
|       "Contribution" shall mean any work of authorship, including | ||||
|       the original version of the Work and any modifications or additions | ||||
|       to that Work or Derivative Works thereof, that is intentionally | ||||
|       submitted to Licensor for inclusion in the Work by the copyright owner | ||||
|       or by an individual or Legal Entity authorized to submit on behalf of | ||||
|       the copyright owner. For the purposes of this definition, "submitted" | ||||
|       means any form of electronic, verbal, or written communication sent | ||||
|       to the Licensor or its representatives, including but not limited to | ||||
|       communication on electronic mailing lists, source code control systems, | ||||
|       and issue tracking systems that are managed by, or on behalf of, the | ||||
|       Licensor for the purpose of discussing and improving the Work, but | ||||
|       excluding communication that is conspicuously marked or otherwise | ||||
|       designated in writing by the copyright owner as "Not a Contribution." | ||||
|  | ||||
|       "Contributor" shall mean Licensor and any individual or Legal Entity | ||||
|       on behalf of whom a Contribution has been received by Licensor and | ||||
|       subsequently incorporated within the Work. | ||||
|  | ||||
|    2. Grant of Copyright License. Subject to the terms and conditions of | ||||
|       this License, each Contributor hereby grants to You a perpetual, | ||||
|       worldwide, non-exclusive, no-charge, royalty-free, irrevocable | ||||
|       copyright license to reproduce, prepare Derivative Works of, | ||||
|       publicly display, publicly perform, sublicense, and distribute the | ||||
|       Work and such Derivative Works in Source or Object form. | ||||
|  | ||||
|    3. Grant of Patent License. Subject to the terms and conditions of | ||||
|       this License, each Contributor hereby grants to You a perpetual, | ||||
|       worldwide, non-exclusive, no-charge, royalty-free, irrevocable | ||||
|       (except as stated in this section) patent license to make, have made, | ||||
|       use, offer to sell, sell, import, and otherwise transfer the Work, | ||||
|       where such license applies only to those patent claims licensable | ||||
|       by such Contributor that are necessarily infringed by their | ||||
|       Contribution(s) alone or by combination of their Contribution(s) | ||||
|       with the Work to which such Contribution(s) was submitted. If You | ||||
|       institute patent litigation against any entity (including a | ||||
|       cross-claim or counterclaim in a lawsuit) alleging that the Work | ||||
|       or a Contribution incorporated within the Work constitutes direct | ||||
|       or contributory patent infringement, then any patent licenses | ||||
|       granted to You under this License for that Work shall terminate | ||||
|       as of the date such litigation is filed. | ||||
|  | ||||
|    4. Redistribution. You may reproduce and distribute copies of the | ||||
|       Work or Derivative Works thereof in any medium, with or without | ||||
|       modifications, and in Source or Object form, provided that You | ||||
|       meet the following conditions: | ||||
|  | ||||
|       (a) You must give any other recipients of the Work or | ||||
|           Derivative Works a copy of this License; and | ||||
|  | ||||
|       (b) You must cause any modified files to carry prominent notices | ||||
|           stating that You changed the files; and | ||||
|  | ||||
|       (c) You must retain, in the Source form of any Derivative Works | ||||
|           that You distribute, all copyright, patent, trademark, and | ||||
|           attribution notices from the Source form of the Work, | ||||
|           excluding those notices that do not pertain to any part of | ||||
|           the Derivative Works; and | ||||
|  | ||||
|       (d) If the Work includes a "NOTICE" text file as part of its | ||||
|           distribution, then any Derivative Works that You distribute must | ||||
|           include a readable copy of the attribution notices contained | ||||
|           within such NOTICE file, excluding those notices that do not | ||||
|           pertain to any part of the Derivative Works, in at least one | ||||
|           of the following places: within a NOTICE text file distributed | ||||
|           as part of the Derivative Works; within the Source form or | ||||
|           documentation, if provided along with the Derivative Works; or, | ||||
|           within a display generated by the Derivative Works, if and | ||||
|           wherever such third-party notices normally appear. The contents | ||||
|           of the NOTICE file are for informational purposes only and | ||||
|           do not modify the License. You may add Your own attribution | ||||
|           notices within Derivative Works that You distribute, alongside | ||||
|           or as an addendum to the NOTICE text from the Work, provided | ||||
|           that such additional attribution notices cannot be construed | ||||
|           as modifying the License. | ||||
|  | ||||
|       You may add Your own copyright statement to Your modifications and | ||||
|       may provide additional or different license terms and conditions | ||||
|       for use, reproduction, or distribution of Your modifications, or | ||||
|       for any such Derivative Works as a whole, provided Your use, | ||||
|       reproduction, and distribution of the Work otherwise complies with | ||||
|       the conditions stated in this License. | ||||
|  | ||||
|    5. Submission of Contributions. Unless You explicitly state otherwise, | ||||
|       any Contribution intentionally submitted for inclusion in the Work | ||||
|       by You to the Licensor shall be under the terms and conditions of | ||||
|       this License, without any additional terms or conditions. | ||||
|       Notwithstanding the above, nothing herein shall supersede or modify | ||||
|       the terms of any separate license agreement you may have executed | ||||
|       with Licensor regarding such Contributions. | ||||
|  | ||||
|    6. Trademarks. This License does not grant permission to use the trade | ||||
|       names, trademarks, service marks, or product names of the Licensor, | ||||
|       except as required for reasonable and customary use in describing the | ||||
|       origin of the Work and reproducing the content of the NOTICE file. | ||||
|  | ||||
|    7. Disclaimer of Warranty. Unless required by applicable law or | ||||
|       agreed to in writing, Licensor provides the Work (and each | ||||
|       Contributor provides its Contributions) on an "AS IS" BASIS, | ||||
|       WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or | ||||
|       implied, including, without limitation, any warranties or conditions | ||||
|       of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A | ||||
|       PARTICULAR PURPOSE. You are solely responsible for determining the | ||||
|       appropriateness of using or redistributing the Work and assume any | ||||
|       risks associated with Your exercise of permissions under this License. | ||||
|  | ||||
|    8. Limitation of Liability. In no event and under no legal theory, | ||||
|       whether in tort (including negligence), contract, or otherwise, | ||||
|       unless required by applicable law (such as deliberate and grossly | ||||
|       negligent acts) or agreed to in writing, shall any Contributor be | ||||
|       liable to You for damages, including any direct, indirect, special, | ||||
|       incidental, or consequential damages of any character arising as a | ||||
|       result of this License or out of the use or inability to use the | ||||
|       Work (including but not limited to damages for loss of goodwill, | ||||
|       work stoppage, computer failure or malfunction, or any and all | ||||
|       other commercial damages or losses), even if such Contributor | ||||
|       has been advised of the possibility of such damages. | ||||
|  | ||||
|    9. Accepting Warranty or Additional Liability. While redistributing | ||||
|       the Work or Derivative Works thereof, You may choose to offer, | ||||
|       and charge a fee for, acceptance of support, warranty, indemnity, | ||||
|       or other liability obligations and/or rights consistent with this | ||||
|       License. However, in accepting such obligations, You may act only | ||||
|       on Your own behalf and on Your sole responsibility, not on behalf | ||||
|       of any other Contributor, and only if You agree to indemnify, | ||||
|       defend, and hold each Contributor harmless for any liability | ||||
|       incurred by, or claims asserted against, such Contributor by reason | ||||
|       of your accepting any such warranty or additional liability. | ||||
|  | ||||
|    END OF TERMS AND CONDITIONS | ||||
|  | ||||
|    APPENDIX: How to apply the Apache License to your work. | ||||
|  | ||||
|       To apply the Apache License to your work, attach the following | ||||
|       boilerplate notice, with the fields enclosed by brackets "[]" | ||||
|       replaced with your own identifying information. (Don't include | ||||
|       the brackets!)  The text should be enclosed in the appropriate | ||||
|       comment syntax for the file format. We also recommend that a | ||||
|       file or class name and description of purpose be included on the | ||||
|       same "printed page" as the copyright notice for easier | ||||
|       identification within third-party archives. | ||||
|  | ||||
|    Copyright [yyyy] [name of copyright owner] | ||||
|  | ||||
|    Licensed under the Apache License, Version 2.0 (the "License"); | ||||
|    you may not use this file except in compliance with the License. | ||||
|    You may obtain a copy of the License at | ||||
|  | ||||
|        http://www.apache.org/licenses/LICENSE-2.0 | ||||
|  | ||||
|    Unless required by applicable law or agreed to in writing, software | ||||
|    distributed under the License is distributed on an "AS IS" BASIS, | ||||
|    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||||
|    See the License for the specific language governing permissions and | ||||
|    limitations under the License. | ||||
							
								
								
									
										13
									
								
								README.md
									
									
									
									
									
								
							
							
						
						| @@ -1,3 +1,12 @@ | ||||
| # Android client for Home Assistant | ||||
| [](https://somegeeky.website/badges/flutter) [](https://somegeeky.website/badges/dart) | ||||
| # HA Client | ||||
| ## Native Android client for Home Assistant | ||||
| ### With Lovelace UI support | ||||
|  | ||||
| Home Assistant Android client using Flutter and Dart. | ||||
| Visit [homemade.systems](http://ha-client.homemade.systems/) for more info. | ||||
|  | ||||
| Join [Google Group](https://groups.google.com/d/forum/ha-client-alpha-testing) to become an alpha tester | ||||
|  | ||||
| Download the app from [Google Play](https://play.google.com/apps/testing/com.keyboardcrumbs.haclient) after joining the group | ||||
|  | ||||
| Discuss it in [Home Assistant community](https://community.home-assistant.io/t/alpha-testing-ha-client-native-android-client-for-home-assistant/69912) | ||||
|   | ||||
| @@ -29,7 +29,12 @@ def keystoreProperties = new Properties() | ||||
| keystoreProperties.load(new FileInputStream(keystorePropertiesFile)) | ||||
|  | ||||
| android { | ||||
|     compileSdkVersion 27 | ||||
|     compileSdkVersion 28 | ||||
|  | ||||
|     compileOptions { | ||||
|         sourceCompatibility JavaVersion.VERSION_1_8 | ||||
|         targetCompatibility JavaVersion.VERSION_1_8 | ||||
|     } | ||||
|  | ||||
|     lintOptions { | ||||
|         disable 'InvalidPackage' | ||||
| @@ -38,7 +43,7 @@ android { | ||||
|     defaultConfig { | ||||
|         applicationId "com.keyboardcrumbs.haclient" | ||||
|         minSdkVersion 21 | ||||
|         targetSdkVersion 27 | ||||
|         targetSdkVersion 28 | ||||
|         versionCode flutterVersionCode.toInteger() | ||||
|         versionName flutterVersionName | ||||
|         testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" | ||||
| @@ -65,7 +70,10 @@ flutter { | ||||
| } | ||||
|  | ||||
| dependencies { | ||||
|     implementation 'com.google.firebase:firebase-core:16.0.8' | ||||
|     testImplementation 'junit:junit:4.12' | ||||
|     androidTestImplementation 'com.android.support.test:runner:1.0.2' | ||||
|     androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2' | ||||
| } | ||||
|  | ||||
| apply plugin: 'com.google.gms.google-services' | ||||
|   | ||||
							
								
								
									
										42
									
								
								android/app/google-services.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,42 @@ | ||||
| { | ||||
|   "project_info": { | ||||
|     "project_number": "441874387819", | ||||
|     "firebase_url": "https://ha-client-c73c4.firebaseio.com", | ||||
|     "project_id": "ha-client-c73c4", | ||||
|     "storage_bucket": "ha-client-c73c4.appspot.com" | ||||
|   }, | ||||
|   "client": [ | ||||
|     { | ||||
|       "client_info": { | ||||
|         "mobilesdk_app_id": "1:441874387819:android:92c7efc892dc3d45", | ||||
|         "android_client_info": { | ||||
|           "package_name": "com.keyboardcrumbs.haclient" | ||||
|         } | ||||
|       }, | ||||
|       "oauth_client": [ | ||||
|         { | ||||
|           "client_id": "441874387819-id0hqsfprj3b5kc312faqv3lmdfpm7l8.apps.googleusercontent.com", | ||||
|           "client_type": 3 | ||||
|         } | ||||
|       ], | ||||
|       "api_key": [ | ||||
|         { | ||||
|           "current_key": "AIzaSyBsl9cjBY633IrdrTyCsLFlO9lfsYJ0OJU" | ||||
|         } | ||||
|       ], | ||||
|       "services": { | ||||
|         "analytics_service": { | ||||
|           "status": 1 | ||||
|         }, | ||||
|         "appinvite_service": { | ||||
|           "status": 1, | ||||
|           "other_platform_oauth_client": [] | ||||
|         }, | ||||
|         "ads_service": { | ||||
|           "status": 2 | ||||
|         } | ||||
|       } | ||||
|     } | ||||
|   ], | ||||
|   "configuration_version": "1" | ||||
| } | ||||
| @@ -15,7 +15,8 @@ | ||||
|     <application | ||||
|         android:name="io.flutter.app.FlutterApplication" | ||||
|         android:label="HA Client" | ||||
|         android:icon="@mipmap/ic_launcher"> | ||||
|         android:icon="@mipmap/ic_launcher" | ||||
|         android:usesCleartextTraffic="true"> | ||||
|         <activity | ||||
|             android:name=".MainActivity" | ||||
|             android:launchMode="singleTop" | ||||
| @@ -30,10 +31,14 @@ | ||||
|             <meta-data | ||||
|                 android:name="io.flutter.app.android.SplashScreenUntilFirstFrame" | ||||
|                 android:value="true" /> | ||||
|             <intent-filter> | ||||
|                 <action android:name="FLUTTER_NOTIFICATION_CLICK" /> | ||||
|                 <category android:name="android.intent.category.DEFAULT" /> | ||||
|             </intent-filter> | ||||
|             <intent-filter> | ||||
|                 <action android:name="android.intent.action.MAIN"/> | ||||
|                 <category android:name="android.intent.category.LAUNCHER"/> | ||||
|             </intent-filter> | ||||
|         </activity> | ||||
|     </application> | ||||
| </manifest> | ||||
| </manifest> | ||||
|   | ||||
| Before Width: | Height: | Size: 2.2 KiB After Width: | Height: | Size: 4.0 KiB | 
| Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 2.6 KiB | 
| Before Width: | Height: | Size: 3.0 KiB After Width: | Height: | Size: 5.4 KiB | 
| Before Width: | Height: | Size: 4.5 KiB After Width: | Height: | Size: 8.0 KiB | 
| Before Width: | Height: | Size: 5.6 KiB After Width: | Height: | Size: 11 KiB | 
| @@ -5,7 +5,8 @@ buildscript { | ||||
|     } | ||||
|  | ||||
|     dependencies { | ||||
|         classpath 'com.android.tools.build:gradle:3.1.2' | ||||
|         classpath 'com.android.tools.build:gradle:3.3.2' | ||||
|         classpath 'com.google.gms:google-services:4.2.0' | ||||
|     } | ||||
| } | ||||
|  | ||||
|   | ||||
| @@ -1 +1,5 @@ | ||||
| org.gradle.jvmargs=-Xmx1536M | ||||
| org.gradle.jvmargs=-Xmx2g | ||||
| org.gradle.daemon=true | ||||
| org.gradle.caching=true | ||||
| android.useAndroidX=true | ||||
| android.enableJetifier=true | ||||
| @@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME | ||||
| distributionPath=wrapper/dists | ||||
| zipStoreBase=GRADLE_USER_HOME | ||||
| zipStorePath=wrapper/dists | ||||
| distributionUrl=https\://services.gradle.org/distributions/gradle-4.4-all.zip | ||||
| distributionUrl=https\://services.gradle.org/distributions/gradle-4.10.1-all.zip | ||||
|   | ||||
							
								
								
									
										0
									
								
								android/gradlew
									
									
									
									
										vendored
									
									
										
										
										Normal file → Executable file
									
								
							
							
						
						
							
								
								
									
										1
									
								
								docs/empty
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1 @@ | ||||
|  | ||||
							
								
								
									
										
											BIN
										
									
								
								docs/ha_access_tokens.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 23 KiB | 
							
								
								
									
										
											BIN
										
									
								
								docs/ha_profile-300x247.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 16 KiB | 
							
								
								
									
										
											BIN
										
									
								
								docs/settings-869x1024.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 102 KiB | 
							
								
								
									
										
											BIN
										
									
								
								fonts/materialdesignicons-webfont-3-5-95.ttf
									
									
									
									
									
										Normal file
									
								
							
							
						
						| Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 24 KiB | 
| Before Width: | Height: | Size: 43 KiB After Width: | Height: | Size: 48 KiB | 
| Before Width: | Height: | Size: 711 B After Width: | Height: | Size: 715 B | 
| Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 1.4 KiB | 
| Before Width: | Height: | Size: 2.2 KiB After Width: | Height: | Size: 2.0 KiB | 
| Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 1.0 KiB | 
| Before Width: | Height: | Size: 2.1 KiB After Width: | Height: | Size: 2.0 KiB | 
| Before Width: | Height: | Size: 3.0 KiB After Width: | Height: | Size: 2.9 KiB | 
| Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 1.4 KiB | 
| Before Width: | Height: | Size: 2.6 KiB After Width: | Height: | Size: 2.7 KiB | 
| Before Width: | Height: | Size: 3.8 KiB After Width: | Height: | Size: 3.9 KiB | 
| Before Width: | Height: | Size: 3.8 KiB After Width: | Height: | Size: 3.9 KiB | 
| Before Width: | Height: | Size: 5.9 KiB After Width: | Height: | Size: 5.7 KiB | 
| Before Width: | Height: | Size: 2.7 KiB After Width: | Height: | Size: 2.5 KiB | 
| Before Width: | Height: | Size: 4.7 KiB After Width: | Height: | Size: 4.9 KiB | 
| Before Width: | Height: | Size: 5.5 KiB After Width: | Height: | Size: 5.3 KiB | 
							
								
								
									
										44
									
								
								lib/auth_manager.class.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,44 @@ | ||||
| part of 'main.dart'; | ||||
|  | ||||
| class AuthManager { | ||||
|  | ||||
|   static final AuthManager _instance = AuthManager._internal(); | ||||
|  | ||||
|   factory AuthManager() { | ||||
|     return _instance; | ||||
|   } | ||||
|  | ||||
|   AuthManager._internal(); | ||||
|  | ||||
|   Future getTempToken({String httpWebHost, String oauthUrl}) { | ||||
|     Completer completer = Completer(); | ||||
|     final flutterWebviewPlugin = new FlutterWebviewPlugin(); | ||||
|     flutterWebviewPlugin.onUrlChanged.listen((String url) { | ||||
|       if (url.startsWith("http://ha-client.homemade.systems/service/auth_callback.html")) { | ||||
|         String authCode = url.split("=")[1]; | ||||
|         Logger.d("We have auth code. Getting temporary access token..."); | ||||
|         Connection().sendHTTPPost( | ||||
|             host: httpWebHost, | ||||
|             endPoint: "/auth/token", | ||||
|             contentType: "application/x-www-form-urlencoded", | ||||
|             includeAuthHeader: false, | ||||
|             data: "grant_type=authorization_code&code=$authCode&client_id=${Uri.encodeComponent('http://ha-client.homemade.systems/')}" | ||||
|         ).then((response) { | ||||
|           Logger.d("Gottemp token"); | ||||
|           String tempToken = json.decode(response)['access_token']; | ||||
|           Logger.d("Closing webview..."); | ||||
|           flutterWebviewPlugin.close(); | ||||
|           completer.complete(tempToken); | ||||
|         }).catchError((e) { | ||||
|           flutterWebviewPlugin.close(); | ||||
|           completer.completeError({"errorCode": 61, "errorMessage": "Error getting temp token"}); | ||||
|           Logger.e("Error getting temp token: ${e.toString()}"); | ||||
|         }); | ||||
|       } | ||||
|     }); | ||||
|     Logger.d("Launching OAuth..."); | ||||
|     eventBus.fire(StartAuthEvent(oauthUrl)); | ||||
|     return completer.future; | ||||
|   } | ||||
|  | ||||
| } | ||||
| @@ -1,9 +0,0 @@ | ||||
| part of 'main.dart'; | ||||
|  | ||||
| class Badge { | ||||
|   String _entityId; | ||||
|  | ||||
|   Badge(String groupId) { | ||||
|     _entityId = groupId; | ||||
|   } | ||||
| } | ||||
| @@ -1,25 +0,0 @@ | ||||
| part of 'main.dart'; | ||||
|  | ||||
| class HACard { | ||||
|   String _entityId; | ||||
|   List _entities; | ||||
|   String _friendlyName; | ||||
|  | ||||
|   List get entities => _entities; | ||||
|   String get friendlyName => _friendlyName; | ||||
|  | ||||
|   HACard(String groupId, String friendlyName) { | ||||
|     _entityId = groupId; | ||||
|     _entities = []; | ||||
|     _friendlyName = friendlyName; | ||||
|   } | ||||
|  | ||||
|   void addEntity(String entityId) { | ||||
|     _entities.add(entityId); | ||||
|   } | ||||
|  | ||||
|   void addEntities(List entities) { | ||||
|     _entities.addAll(entities); | ||||
|   } | ||||
|  | ||||
| } | ||||
							
								
								
									
										343
									
								
								lib/connection.class.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,343 @@ | ||||
| part of 'main.dart'; | ||||
|  | ||||
| class Connection { | ||||
|  | ||||
|   static final Connection _instance = Connection._internal(); | ||||
|  | ||||
|   factory Connection() { | ||||
|     return _instance; | ||||
|   } | ||||
|  | ||||
|   Connection._internal(); | ||||
|  | ||||
|   String displayHostname; | ||||
|   String _webSocketAPIEndpoint; | ||||
|   String httpWebHost; | ||||
|   String _token; | ||||
|   String _tempToken; | ||||
|   String oauthUrl; | ||||
|   bool get isAuthenticated => _token != null; | ||||
|   StreamSubscription _socketSubscription; | ||||
|   Duration connectTimeout = Duration(seconds: 15); | ||||
|  | ||||
|   bool isConnected = false; | ||||
|  | ||||
|   var onStateChangeCallback; | ||||
|  | ||||
|   IOWebSocketChannel _socket; | ||||
|  | ||||
|   int _currentMessageId = 0; | ||||
|   Map<String, Completer> _messageResolver = {}; | ||||
|  | ||||
|   Future init(onStateChange) async { | ||||
|     Completer completer = Completer(); | ||||
|     onStateChangeCallback = onStateChange; | ||||
|     SharedPreferences prefs = await SharedPreferences.getInstance(); | ||||
|     String domain = prefs.getString('hassio-domain'); | ||||
|     String port = prefs.getString('hassio-port'); | ||||
|     displayHostname = "$domain:$port"; | ||||
|     _webSocketAPIEndpoint = "${prefs.getString('hassio-protocol')}://$domain:$port/api/websocket"; | ||||
|     httpWebHost = "${prefs.getString('hassio-res-protocol')}://$domain:$port"; | ||||
|     //_token = prefs.getString('hassio-token'); | ||||
|     final storage = new FlutterSecureStorage(); | ||||
|     try { | ||||
|       _token = await storage.read(key: "hacl_llt"); | ||||
|     } catch (e) { | ||||
|       Logger.e("Cannt read secure storage. Need to relogin."); | ||||
|       _token = null; | ||||
|       await storage.delete(key: "hacl_llt"); | ||||
|     } | ||||
|     if ((domain == null) || (port == null) || | ||||
|         (domain.length == 0) || (port.length == 0)) { | ||||
|       completer.completeError({"errorCode": 5, "errorMessage": "Check connection settings"}); | ||||
|     } else { | ||||
|       oauthUrl = "$httpWebHost/auth/authorize?client_id=${Uri.encodeComponent('http://ha-client.homemade.systems/')}&redirect_uri=${Uri.encodeComponent('http://ha-client.homemade.systems/service/auth_callback.html')}"; | ||||
|       if (_token == null) { | ||||
|         await AuthManager().getTempToken( | ||||
|             httpWebHost: httpWebHost, | ||||
|             oauthUrl: oauthUrl | ||||
|         ).then((token) { | ||||
|           Logger.d("Token from AuthManager recived"); | ||||
|           _tempToken = token; | ||||
|         }); | ||||
|       } | ||||
|       _connect().timeout(connectTimeout, onTimeout: () { | ||||
|         _disconnect().then((_) { | ||||
|           completer.completeError( | ||||
|               {"errorCode": 1, "errorMessage": "Connection timeout"}); | ||||
|         }); | ||||
|       }).then((_) => completer.complete()).catchError((e) { | ||||
|         completer.completeError(e); | ||||
|       }); | ||||
|     } | ||||
|     return completer.future; | ||||
|   } | ||||
|  | ||||
|   Completer connecting; | ||||
|  | ||||
|   Future _connect() async { | ||||
|     if (connecting != null && !connecting.isCompleted) { | ||||
|       Logger.w(""); | ||||
|       return connecting.future; | ||||
|     } | ||||
|     connecting = Completer(); | ||||
|     await _disconnect(); | ||||
|     Logger.d( "Socket connecting..."); | ||||
|     _socket = IOWebSocketChannel.connect( | ||||
|         _webSocketAPIEndpoint, pingInterval: Duration(seconds: 15)); | ||||
|     _socketSubscription = _socket.stream.listen( | ||||
|             (message) { | ||||
|           isConnected = true; | ||||
|           var data = json.decode(message); | ||||
|           if (data["type"] == "auth_required") { | ||||
|             Logger.d("[Received] <== ${data.toString()}"); | ||||
|             _authenticate().then((_) => connecting.complete()).catchError((e) { | ||||
|               if (!connecting.isCompleted) connecting.completeError(e); | ||||
|             }); | ||||
|           } else if (data["type"] == "auth_ok") { | ||||
|             Logger.d("[Received] <== ${data.toString()}"); | ||||
|             _messageResolver["auth"]?.complete(); | ||||
|             _messageResolver.remove("auth"); | ||||
|             if (!connecting.isCompleted) connecting.complete(sendSocketMessage( | ||||
|               type: "subscribe_events", | ||||
|               additionalData: {"event_type": "state_changed"}, | ||||
|             )); | ||||
|           } else if (data["type"] == "auth_invalid") { | ||||
|             Logger.d("[Received] <== ${data.toString()}"); | ||||
|             _messageResolver["auth"]?.completeError({"errorCode": 62, "errorMessage": "${data["message"]}"}); | ||||
|             _messageResolver.remove("auth"); | ||||
|             logout().then((_) { | ||||
|               if (!connecting.isCompleted) connecting.completeError({"errorCode": 62, "errorMessage": "${data["message"]}"}); | ||||
|             }); | ||||
|           } else { | ||||
|             _handleMessage(data); | ||||
|           } | ||||
|         }, | ||||
|         cancelOnError: true, | ||||
|         onDone: () => _handleSocketClose(connecting), | ||||
|         onError: (e) => _handleSocketError(e, connecting) | ||||
|     ); | ||||
|     return connecting.future; | ||||
|   } | ||||
|  | ||||
|   Future _disconnect() async { | ||||
|     Logger.d( "Socket disconnecting..."); | ||||
|     await _socketSubscription?.cancel(); | ||||
|     await _socket?.sink?.close()?.timeout(Duration(seconds: 4), | ||||
|         onTimeout: () => Logger.d( "Socket sink close timeout") | ||||
|     ); | ||||
|     Logger.d( "..Disconnected"); | ||||
|   } | ||||
|  | ||||
|   _handleMessage(data) { | ||||
|     if (data["type"] == "result") { | ||||
|       if (data["id"] != null && data["success"]) { | ||||
|         Logger.d("[Received] <== Request id ${data['id']} was successful"); | ||||
|         _messageResolver["${data["id"]}"]?.complete(data["result"]); | ||||
|       } else if (data["id"] != null) { | ||||
|         Logger.e("[Received] <== Error received on request id ${data['id']}: ${data['error']}"); | ||||
|         _messageResolver["${data["id"]}"]?.completeError({"errorMessage": "${data['error']["message"]}"}); | ||||
|       } | ||||
|       _messageResolver.remove("${data["id"]}"); | ||||
|     } else if (data["type"] == "event") { | ||||
|       if ((data["event"] != null) && (data["event"]["event_type"] == "state_changed")) { | ||||
|         Logger.d("[Received] <== ${data['type']}.${data["event"]["event_type"]}: ${data["event"]["data"]["entity_id"]}"); | ||||
|         onStateChangeCallback(data["event"]["data"]); | ||||
|       } else if (data["event"] != null) { | ||||
|         Logger.w("Unhandled event type: ${data["event"]["event_type"]}"); | ||||
|       } else { | ||||
|         Logger.e("Event is null: $data"); | ||||
|       } | ||||
|     } else { | ||||
|       Logger.d("[Received unhandled] <== ${data.toString()}"); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   void _handleSocketClose(Completer connectionCompleter) { | ||||
|     isConnected = false; | ||||
|     Logger.d("Socket disconnected."); | ||||
|     if (!connectionCompleter.isCompleted) { | ||||
|       connectionCompleter.completeError({"errorCode": 82, "errorMessage": "Disconnected"}); | ||||
|     } else { | ||||
|       _disconnect().then((_) { | ||||
|         Timer(Duration(seconds: 5), () { | ||||
|           Logger.d("Trying to reconnect..."); | ||||
|           _connect(); | ||||
|         }); | ||||
|       }); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   void _handleSocketError(e, Completer connectionCompleter) { | ||||
|     isConnected = false; | ||||
|     Logger.e("Socket stream Error: $e"); | ||||
|     if (!connectionCompleter.isCompleted) { | ||||
|       connectionCompleter.completeError({"errorCode": 81, "errorMessage": "Unable to connect to Home Assistant"}); | ||||
|     } else { | ||||
|       _disconnect().then((_) { | ||||
|         Timer(Duration(seconds: 5), () { | ||||
|           Logger.d("Trying to reconnect..."); | ||||
|           _connect(); | ||||
|         }); | ||||
|       }); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   Future _authenticate() { | ||||
|     Completer completer = Completer(); | ||||
|     if (_token != null) { | ||||
|       Logger.d( "Long-lived token exist"); | ||||
|       Logger.d( "[Sending] ==> auth request"); | ||||
|       sendSocketMessage( | ||||
|           type: "auth", | ||||
|           additionalData: {"access_token": "$_token"}, | ||||
|           auth: true | ||||
|       ).then((_) { | ||||
|         completer.complete(); | ||||
|       }).catchError((e) => completer.completeError(e)); | ||||
|     } else if (_tempToken != null) { | ||||
|       Logger.d("We have temp token. Loging in..."); | ||||
|       sendSocketMessage( | ||||
|           type: "auth", | ||||
|           additionalData: {"access_token": "$_tempToken"}, | ||||
|           auth: true | ||||
|       ).then((_) { | ||||
|         Logger.d("Requesting long-lived token..."); | ||||
|         _getLongLivedToken().then((_) { | ||||
|           completer.complete(); | ||||
|         }).catchError((e) { | ||||
|           Logger.e("Can't get long-lived token: $e"); | ||||
|           throw e; | ||||
|         }); | ||||
|       }).catchError((e) => completer.completeError(e)); | ||||
|     } else { | ||||
|       completer.completeError({"errorCode": 63, "errorMessage": "General login error"}); | ||||
|     } | ||||
|     return completer.future; | ||||
|   } | ||||
|  | ||||
|   Future logout() { | ||||
|     _token = null; | ||||
|     _tempToken = null; | ||||
|     final storage = new FlutterSecureStorage(); | ||||
|     return storage.delete(key: "hacl_llt"); | ||||
|   } | ||||
|  | ||||
|   Future _getLongLivedToken() { | ||||
|     Completer completer = Completer(); | ||||
|     sendSocketMessage(type: "auth/long_lived_access_token", additionalData: {"client_name": "HA Client app ${DateTime.now().millisecondsSinceEpoch}", "lifespan": 365}).then((data) { | ||||
|       Logger.d("Got long-lived token."); | ||||
|       _token = data; | ||||
|       _tempToken = null; | ||||
|       final storage = new FlutterSecureStorage(); | ||||
|       storage.write(key: "hacl_llt", value: "$_token").then((_) { | ||||
|         completer.complete(); | ||||
|       }).catchError((e) { | ||||
|         throw e; | ||||
|       }); | ||||
|     }).catchError((e) { | ||||
|       logout(); | ||||
|       completer.completeError({"errorCode": 63, "errorMessage": "Authentication error: $e"}); | ||||
|     }); | ||||
|     return completer.future; | ||||
|   } | ||||
|  | ||||
|   Future sendSocketMessage({String type, Map additionalData, bool auth: false}) { | ||||
|     Completer _completer = Completer(); | ||||
|     Map dataObject = {"type": "$type"}; | ||||
|     String callbackName; | ||||
|     if (!auth) { | ||||
|       _incrementMessageId(); | ||||
|       dataObject["id"] = _currentMessageId; | ||||
|       callbackName = "$_currentMessageId"; | ||||
|     } else { | ||||
|       callbackName = "auth"; | ||||
|     } | ||||
|     if (additionalData != null) { | ||||
|       dataObject.addAll(additionalData); | ||||
|     } | ||||
|     _messageResolver[callbackName] = _completer; | ||||
|     String rawMessage = json.encode(dataObject); | ||||
|     Logger.d("[Sending] ==> $rawMessage"); | ||||
|     if (!isConnected) { | ||||
|       _connect().timeout(connectTimeout, onTimeout: (){ | ||||
|         _completer.completeError({"errorCode": 8, "errorMessage": "No connection to Home Assistant"}); | ||||
|       }).then((_) { | ||||
|         _socket.sink.add(rawMessage); | ||||
|       }).catchError((e) { | ||||
|         _completer.completeError(e); | ||||
|       }); | ||||
|     } else { | ||||
|       _socket.sink.add(rawMessage); | ||||
|     } | ||||
|     return _completer.future; | ||||
|   } | ||||
|  | ||||
|   void _incrementMessageId() { | ||||
|     _currentMessageId += 1; | ||||
|   } | ||||
|  | ||||
|   Future callService({String domain, String service, String entityId, Map additionalServiceData}) { | ||||
|     Map serviceData = {}; | ||||
|     if (entityId != null) { | ||||
|       serviceData["entity_id"] = entityId; | ||||
|     } | ||||
|     if (additionalServiceData != null && additionalServiceData.isNotEmpty) { | ||||
|       serviceData.addAll(additionalServiceData); | ||||
|     } | ||||
|     if (serviceData.isNotEmpty) | ||||
|       return sendSocketMessage(type: "call_service", additionalData: {"domain": domain, "service": service, "service_data": serviceData}); | ||||
|     else | ||||
|       return sendSocketMessage(type: "call_service", additionalData: {"domain": domain, "service": service}); | ||||
|   } | ||||
|  | ||||
|   Future<List> getHistory(String entityId) async { | ||||
|     DateTime now = DateTime.now(); | ||||
|     //String endTime = formatDate(now, [yyyy, '-', mm, '-', dd, 'T', HH, ':', nn, ':', ss, z]); | ||||
|     String startTime = formatDate(now.subtract(Duration(hours: 24)), [yyyy, '-', mm, '-', dd, 'T', HH, ':', nn, ':', ss, z]); | ||||
|     String url = "$httpWebHost/api/history/period/$startTime?&filter_entity_id=$entityId"; | ||||
|     Logger.d("[Sending] ==> $url"); | ||||
|     http.Response historyResponse; | ||||
|     historyResponse = await http.get(url, headers: { | ||||
|       "authorization": "Bearer $_token", | ||||
|       "Content-Type": "application/json" | ||||
|     }); | ||||
|     var history = json.decode(historyResponse.body); | ||||
|     if (history is List) { | ||||
|       Logger.d( "[Received] <== ${history.first.length} history recors"); | ||||
|       return history; | ||||
|     } else { | ||||
|       return []; | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   Future sendHTTPPost({String host, String endPoint, String data, String contentType: "application/json", bool includeAuthHeader: true, String authToken}) async { | ||||
|     Completer completer = Completer(); | ||||
|     String url = "$host$endPoint"; | ||||
|     Logger.d("[Sending] ==> $url"); | ||||
|     Map<String, String> headers = {}; | ||||
|     if (contentType != null) { | ||||
|       headers["Content-Type"] = contentType; | ||||
|     } | ||||
|     if (includeAuthHeader) { | ||||
|       headers["authorization"] = "Bearer $authToken"; | ||||
|     } | ||||
|     http.post( | ||||
|         url, | ||||
|         headers: headers, | ||||
|         body: data | ||||
|     ).then((response) { | ||||
|       Logger.d("[Received] <== ${response.statusCode}, ${response.body}"); | ||||
|       if (response.statusCode == 200) { | ||||
|         completer.complete(response.body); | ||||
|       } else { | ||||
|         completer.completeError({"code": response.statusCode, "message": "${response.body}"}); | ||||
|       } | ||||
|     }).catchError((e) { | ||||
|       completer.completeError(e); | ||||
|     }); | ||||
|  | ||||
|     return completer.future; | ||||
|   } | ||||
|  | ||||
| } | ||||
| @@ -1,314 +0,0 @@ | ||||
| part of 'main.dart'; | ||||
|  | ||||
| class Entity { | ||||
|   static const STATE_ICONS_COLORS = { | ||||
|     "on": Colors.amber, | ||||
|     "off": Color.fromRGBO(68, 115, 158, 1.0), | ||||
|     "unavailable": Colors.black12, | ||||
|     "unknown": Colors.black12, | ||||
|     "playing": Colors.amber | ||||
|   }; | ||||
|   static const RIGTH_WIDGET_PADDING = 14.0; | ||||
|   static const LEFT_WIDGET_PADDING = 8.0; | ||||
|   static const EXTENDED_WIDGET_HEIGHT = 50.0; | ||||
|   static const WIDGET_HEIGHT = 34.0; | ||||
|  | ||||
|   Map _attributes; | ||||
|   String _domain; | ||||
|   String _entityId; | ||||
|   String _state; | ||||
|   String _entityPicture; | ||||
|   DateTime _lastUpdated; | ||||
|  | ||||
|   String get displayName => _attributes["friendly_name"] ?? (_attributes["name"] ?? "_"); | ||||
|   String get domain => _domain; | ||||
|   String get entityId => _entityId; | ||||
|   String get state => _state; | ||||
|   set state(value) => _state = value; | ||||
|  | ||||
|   double get minValue => _attributes["min"] ?? 0.0; | ||||
|   double get maxValue => _attributes["max"] ?? 100.0; | ||||
|   double get valueStep => _attributes["step"] ?? 1.0; | ||||
|   double get doubleState => double.tryParse(_state) ?? 0.0; | ||||
|   bool get isSliderField => _attributes["mode"] == "slider"; | ||||
|   bool get isTextField => _attributes["mode"] == "text"; | ||||
|   bool get isPasswordField => _attributes["mode"] == "password"; | ||||
|  | ||||
|   String get deviceClass => _attributes["device_class"] ?? null; | ||||
|   bool get isView => (_domain == "group") && (_attributes != null ? _attributes["view"] ?? false : false); | ||||
|   bool get isGroup => _domain == "group"; | ||||
|   String get icon => _attributes["icon"] ?? ""; | ||||
|   bool get isOn => state == "on"; | ||||
|   String get entityPicture => _attributes["entity_picture"]; | ||||
|   String get unitOfMeasurement => _attributes["unit_of_measurement"] ?? ""; | ||||
|   List get childEntities => _attributes["entity_id"] ?? []; | ||||
|   String get lastUpdated => _getLastUpdatedFormatted(); | ||||
|  | ||||
|   Entity(Map rawData) { | ||||
|     update(rawData); | ||||
|   } | ||||
|  | ||||
|   int getValueDivisions() { | ||||
|     return ((maxValue - minValue)/valueStep).round().round(); | ||||
|   } | ||||
|  | ||||
|   void update(Map rawData) { | ||||
|     _attributes = rawData["attributes"] ?? {}; | ||||
|     _domain = rawData["entity_id"].split(".")[0]; | ||||
|     _entityId = rawData["entity_id"]; | ||||
|     _state = rawData["state"]; | ||||
|     _lastUpdated = DateTime.tryParse(rawData["last_updated"]); | ||||
|   } | ||||
|  | ||||
|   String _getLastUpdatedFormatted() { | ||||
|     if (_lastUpdated == null) { | ||||
|       return "-"; | ||||
|     } else { | ||||
|       return formatDate(_lastUpdated, [yy, '-', M, '-', d, ' ', HH, ':', nn, ':', ss]); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   void openEntityPage() { | ||||
|     eventBus.fire(new ShowEntityPageEvent(this)); | ||||
|   } | ||||
|  | ||||
|   Widget buildWidget(BuildContext context) { | ||||
|     return SizedBox( | ||||
|       height: Entity.WIDGET_HEIGHT, | ||||
|       child: Row( | ||||
|         children: <Widget>[ | ||||
|           GestureDetector( | ||||
|             child: _buildIconWidget(), | ||||
|             onTap: openEntityPage, | ||||
|           ), | ||||
|           Expanded( | ||||
|             child: GestureDetector( | ||||
|               child: _buildNameWidget(), | ||||
|               onTap: openEntityPage, | ||||
|             ), | ||||
|           ), | ||||
|           _buildActionWidget(context) | ||||
|         ], | ||||
|       ), | ||||
|     ); | ||||
|   } | ||||
|  | ||||
|   Widget buildExtendedWidget(BuildContext context, String staticState) { | ||||
|     return Row( | ||||
|       children: <Widget>[ | ||||
|         _buildIconWidget(), | ||||
|         Expanded( | ||||
|           child: Column( | ||||
|             crossAxisAlignment: CrossAxisAlignment.start, | ||||
|             children: <Widget>[ | ||||
|               Row( | ||||
|                 children: <Widget>[ | ||||
|                   Expanded( | ||||
|                     child: _buildNameWidget(), | ||||
|                   ), | ||||
|                   _buildExtendedActionWidget(context, staticState) | ||||
|                 ], | ||||
|               ), | ||||
|               _buildLastUpdatedWidget() | ||||
|             ], | ||||
|           ), | ||||
|         ) | ||||
|       ], | ||||
|     ); | ||||
|   } | ||||
|  | ||||
|   Widget _buildIconWidget() { | ||||
|     return Padding( | ||||
|       padding: EdgeInsets.fromLTRB(Entity.LEFT_WIDGET_PADDING, 0.0, 12.0, 0.0), | ||||
|       child: MaterialDesignIcons.createIconWidgetFromEntityData(this, 28.0, Entity.STATE_ICONS_COLORS[_state] ?? Colors.blueGrey), | ||||
|     ); | ||||
|   } | ||||
|  | ||||
|   Widget _buildLastUpdatedWidget() { | ||||
|     return Text( | ||||
|       '${this.lastUpdated}', | ||||
|       textAlign: TextAlign.left, | ||||
|       style: TextStyle( | ||||
|           fontSize: 12.0, | ||||
|           color: Colors.black26 | ||||
|       ), | ||||
|     ); | ||||
|   } | ||||
|  | ||||
|   Widget _buildNameWidget() { | ||||
|     return Padding( | ||||
|       padding: EdgeInsets.only(right: 10.0), | ||||
|       child: Text( | ||||
|         "${this.displayName}", | ||||
|         overflow: TextOverflow.ellipsis, | ||||
|         softWrap: false, | ||||
|         style: TextStyle( | ||||
|             fontSize: 16.0 | ||||
|         ), | ||||
|       ), | ||||
|     ); | ||||
|   } | ||||
|  | ||||
|   Widget _buildActionWidget(BuildContext context) { | ||||
|     return Padding( | ||||
|         padding: EdgeInsets.fromLTRB(0.0, 0.0, Entity.RIGTH_WIDGET_PADDING, 0.0), | ||||
|         child: GestureDetector( | ||||
|           child: Text( | ||||
|               this.isPasswordField ? "******" : | ||||
|               "$_state${this.unitOfMeasurement}", | ||||
|               textAlign: TextAlign.right, | ||||
|               style: new TextStyle( | ||||
|                 fontSize: 16.0, | ||||
|               ) | ||||
|           ), | ||||
|           onTap: openEntityPage, | ||||
|         ) | ||||
|     ); | ||||
|   } | ||||
|  | ||||
|   Widget _buildExtendedActionWidget(BuildContext context, String staticState) { | ||||
|     return _buildActionWidget(context); | ||||
|   } | ||||
| } | ||||
|  | ||||
| class SwitchEntity extends Entity { | ||||
|  | ||||
|   SwitchEntity(Map rawData) : super(rawData); | ||||
|  | ||||
|   @override | ||||
|   Widget _buildActionWidget(BuildContext context) { | ||||
|     return Switch( | ||||
|       value: this.isOn, | ||||
|       onChanged: ((switchState) { | ||||
|         eventBus.fire(new ServiceCallEvent(_domain, switchState ? "turn_on" : "turn_off", entityId, null)); | ||||
|       }), | ||||
|     ); | ||||
|   } | ||||
|  | ||||
| } | ||||
|  | ||||
| class ButtonEntity extends Entity { | ||||
|  | ||||
|   ButtonEntity(Map rawData) : super(rawData); | ||||
|  | ||||
|   @override | ||||
|   Widget _buildActionWidget(BuildContext context) { | ||||
|     return FlatButton( | ||||
|       onPressed: (() { | ||||
|         eventBus.fire(new ServiceCallEvent(_domain, "turn_on", _entityId, null)); | ||||
|       }), | ||||
|       child: Text( | ||||
|         "EXECUTE", | ||||
|         textAlign: TextAlign.right, | ||||
|         style: new TextStyle(fontSize: 16.0, color: Colors.blue), | ||||
|       ), | ||||
|     ); | ||||
|   } | ||||
|  | ||||
| } | ||||
|  | ||||
| class InputEntity extends Entity { | ||||
|  | ||||
|   InputEntity(Map rawData) : super(rawData); | ||||
|  | ||||
|   @override | ||||
|   Widget buildExtendedWidget(BuildContext context, String staticState) { | ||||
|     return Column( | ||||
|       children: <Widget>[ | ||||
|         SizedBox( | ||||
|           height: Entity.EXTENDED_WIDGET_HEIGHT, | ||||
|           child: Row( | ||||
|             children: <Widget>[ | ||||
|               _buildIconWidget(), | ||||
|               Expanded( | ||||
|                 child: _buildNameWidget(), | ||||
|               ), | ||||
|               _buildLastUpdatedWidget() | ||||
|             ], | ||||
|           ), | ||||
|         ), | ||||
|         SizedBox( | ||||
|           height: Entity.EXTENDED_WIDGET_HEIGHT, | ||||
|           child: _buildExtendedActionWidget(context, staticState), | ||||
|         ) | ||||
|       ], | ||||
|     ); | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   Widget _buildActionWidget(BuildContext context) { | ||||
|     if (this.isSliderField) { | ||||
|       return Container( | ||||
|         width: 200.0, | ||||
|         child: Row( | ||||
|           children: <Widget>[ | ||||
|             Expanded( | ||||
|               child: Slider( | ||||
|                 min: this.minValue*10, | ||||
|                 max: this.maxValue*10, | ||||
|                 value: (this.doubleState <= this.maxValue) && (this.doubleState >= this.minValue) ? this.doubleState*10 : this.minValue*10, | ||||
|                 divisions: this.getValueDivisions(), | ||||
|                 onChanged: (value) { | ||||
|                   eventBus.fire(new StateChangedEvent(_entityId, (value.roundToDouble() / 10).toString(), true)); | ||||
|                 }, | ||||
|                 onChangeEnd: (value) { | ||||
|                   eventBus.fire(new ServiceCallEvent(_domain, "set_value", _entityId,{"value": "$_state"})); | ||||
|                 }, | ||||
|               ), | ||||
|             ), | ||||
|             Padding( | ||||
|               padding: EdgeInsets.only(right: 16.0), | ||||
|               child: Text( | ||||
|                   "$_state${this.unitOfMeasurement}", | ||||
|                   textAlign: TextAlign.right, | ||||
|                   style: new TextStyle( | ||||
|                     fontSize: 16.0, | ||||
|                   ) | ||||
|               ), | ||||
|             ) | ||||
|           ], | ||||
|         ), | ||||
|       ); | ||||
|     } else { | ||||
|       return super._buildActionWidget(context); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   Widget _buildExtendedActionWidget(BuildContext context, String staticState) { | ||||
|     return Padding( | ||||
|         padding: EdgeInsets.fromLTRB(Entity.LEFT_WIDGET_PADDING, 0.0, Entity.RIGTH_WIDGET_PADDING, 0.0), | ||||
|         child: Row( | ||||
|           crossAxisAlignment: CrossAxisAlignment.center, | ||||
|           children: <Widget>[ | ||||
|             Expanded( | ||||
|               child: TextField( | ||||
|                 obscureText: this.isPasswordField, | ||||
|                 controller: TextEditingController( | ||||
|                   text: staticState, | ||||
|                 ), | ||||
|                 onChanged: (value) { | ||||
|                   staticState = value; | ||||
|                 }, | ||||
|               ), | ||||
|             ), | ||||
|             SizedBox( | ||||
|               width: 63.0, | ||||
|               child: FlatButton( | ||||
|                 onPressed: () { | ||||
|                   eventBus.fire(new ServiceCallEvent(_domain, "set_value", _entityId,{"value": "$staticState"})); | ||||
|                   Navigator.pop(context); | ||||
|                 }, | ||||
|                 child: Text( | ||||
|                     "SET", | ||||
|                     textAlign: TextAlign.right, | ||||
|                   style: new TextStyle(fontSize: 16.0, color: Colors.blue), | ||||
|                 ), | ||||
|               ), | ||||
|             ) | ||||
|           ], | ||||
|         ) | ||||
|     ); | ||||
|   } | ||||
|  | ||||
| } | ||||
| @@ -1,9 +1,10 @@ | ||||
| part of 'main.dart'; | ||||
|  | ||||
| class EntityViewPage extends StatefulWidget { | ||||
|   EntityViewPage({Key key, this.entity}) : super(key: key); | ||||
|   EntityViewPage({Key key, @required this.entityId, @required this.homeAssistant }) : super(key: key); | ||||
|  | ||||
|   Entity entity; | ||||
|   final String entityId; | ||||
|   final HomeAssistant homeAssistant; | ||||
|  | ||||
|   @override | ||||
|   _EntityViewPageState createState() => new _EntityViewPageState(); | ||||
| @@ -11,30 +12,29 @@ class EntityViewPage extends StatefulWidget { | ||||
|  | ||||
| class _EntityViewPageState extends State<EntityViewPage> { | ||||
|   String _title; | ||||
|   Entity _entity; | ||||
|   String _lastState; | ||||
|   StreamSubscription _refreshDataSubscription; | ||||
|   StreamSubscription _stateSubscription; | ||||
|  | ||||
|   @override | ||||
|   void initState() { | ||||
|     super.initState(); | ||||
|     _entity = widget.entity; | ||||
|     _lastState = _entity.state; | ||||
|     if (_stateSubscription != null) _stateSubscription.cancel(); | ||||
|     _stateSubscription = eventBus.on<StateChangedEvent>().listen((event) { | ||||
|       setState(() { | ||||
|         if (event.entityId == _entity.entityId) { | ||||
|           _lastState = event.newState ?? _entity.state; | ||||
|         } | ||||
|       }); | ||||
|       if (event.entityId == widget.entityId) { | ||||
|         Logger.d("State change event handled by entity page: ${event.entityId}"); | ||||
|         setState(() {}); | ||||
|       } | ||||
|     }); | ||||
|     _refreshDataSubscription = eventBus.on<RefreshDataFinishedEvent>().listen((event) { | ||||
|       setState(() {}); | ||||
|     }); | ||||
|     _prepareData(); | ||||
|   } | ||||
|  | ||||
|   _prepareData() async { | ||||
|     _title = _entity.displayName; | ||||
|   void _prepareData() async { | ||||
|     _title = widget.homeAssistant.entities.get(widget.entityId).displayName; | ||||
|   } | ||||
|  | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context) { | ||||
|     return new Scaffold( | ||||
| @@ -46,13 +46,9 @@ class _EntityViewPageState extends State<EntityViewPage> { | ||||
|         // the App.build method, and use it to set our appbar title. | ||||
|         title: new Text(_title), | ||||
|       ), | ||||
|       body: Padding( | ||||
|           padding: EdgeInsets.all(10.0), | ||||
|           child: ListView( | ||||
|             children: <Widget>[ | ||||
|               _entity.buildExtendedWidget(context, _lastState) | ||||
|             ], | ||||
|           ), | ||||
|       body: HomeAssistantModel( | ||||
|           homeAssistant: widget.homeAssistant, | ||||
|           child: widget.homeAssistant.entities.get(widget.entityId).buildEntityPageWidget(context) | ||||
|       ), | ||||
|     ); | ||||
|   } | ||||
| @@ -60,6 +56,7 @@ class _EntityViewPageState extends State<EntityViewPage> { | ||||
|   @override | ||||
|   void dispose(){ | ||||
|     if (_stateSubscription != null) _stateSubscription.cancel(); | ||||
|     if (_refreshDataSubscription != null) _refreshDataSubscription.cancel(); | ||||
|     super.dispose(); | ||||
|   } | ||||
| } | ||||
							
								
								
									
										13
									
								
								lib/entity_class/alarm_control_panel.class.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,13 @@ | ||||
| part of '../main.dart'; | ||||
|  | ||||
| class AlarmControlPanelEntity extends Entity { | ||||
|   AlarmControlPanelEntity(Map rawData, String webHost) : super(rawData, webHost); | ||||
|  | ||||
|  | ||||
|   @override | ||||
|   Widget _buildAdditionalControlsForPage(BuildContext context) { | ||||
|     return AlarmControlPanelControlsWidget( | ||||
|       extended: false, | ||||
|     ); | ||||
|   } | ||||
| } | ||||
							
								
								
									
										27
									
								
								lib/entity_class/automation_entity.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,27 @@ | ||||
| part of '../main.dart'; | ||||
|  | ||||
| class AutomationEntity extends Entity { | ||||
|   AutomationEntity(Map rawData, String webHost) : super(rawData, webHost); | ||||
|  | ||||
|  | ||||
|   @override | ||||
|   Widget _buildStatePart(BuildContext context) { | ||||
|     return SwitchStateWidget(); | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   Widget _buildAdditionalControlsForPage(BuildContext context) { | ||||
|     return Row( | ||||
|       mainAxisAlignment: MainAxisAlignment.center, | ||||
|       mainAxisSize: MainAxisSize.max, | ||||
|       children: <Widget>[ | ||||
|         FlatServiceButton( | ||||
|           serviceDomain: domain, | ||||
|           entityId: entityId, | ||||
|           text: "TRIGGER", | ||||
|           serviceName: "trigger", | ||||
|         ) | ||||
|       ], | ||||
|     ); | ||||
|   } | ||||
| } | ||||
							
								
								
									
										16
									
								
								lib/entity_class/button_entity.class.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,16 @@ | ||||
| part of '../main.dart'; | ||||
|  | ||||
| class ButtonEntity extends Entity { | ||||
|   ButtonEntity(Map rawData, String webHost) : super(rawData, webHost); | ||||
|  | ||||
|  | ||||
|   @override | ||||
|   Widget _buildStatePart(BuildContext context) { | ||||
|     return FlatServiceButton( | ||||
|       entityId: entityId, | ||||
|       serviceDomain: domain, | ||||
|       serviceName: 'turn_on', | ||||
|       text: domain == "scene" ? "ACTIVATE" : "EXECUTE", | ||||
|     ); | ||||
|   } | ||||
| } | ||||
							
								
								
									
										17
									
								
								lib/entity_class/camera_entity.class.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,17 @@ | ||||
| part of '../main.dart'; | ||||
|  | ||||
| class CameraEntity extends Entity { | ||||
|  | ||||
|   static const SUPPORT_ON_OFF = 1; | ||||
|  | ||||
|   CameraEntity(Map rawData, String webHost) : super(rawData, webHost); | ||||
|  | ||||
|   bool get supportOnOff => ((supportedFeatures & | ||||
|   CameraEntity.SUPPORT_ON_OFF) == | ||||
|       CameraEntity.SUPPORT_ON_OFF); | ||||
|  | ||||
|   @override | ||||
|   Widget _buildAdditionalControlsForPage(BuildContext context) { | ||||
|     return CameraStreamView(); | ||||
|   } | ||||
| } | ||||
							
								
								
									
										129
									
								
								lib/entity_class/climate_entity.class.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,129 @@ | ||||
| part of '../main.dart'; | ||||
|  | ||||
| class ClimateEntity extends Entity { | ||||
|  | ||||
|   @override | ||||
|   EntityHistoryConfig historyConfig = EntityHistoryConfig( | ||||
|     chartType: EntityHistoryWidgetType.numericAttributes, | ||||
|     numericState: false, | ||||
|     numericAttributesToShow: ["current_temperature"] | ||||
|   ); | ||||
|  | ||||
|   static const SUPPORT_TARGET_TEMPERATURE = 1; | ||||
|   static const SUPPORT_TARGET_TEMPERATURE_HIGH = 2; | ||||
|   static const SUPPORT_TARGET_TEMPERATURE_LOW = 4; | ||||
|   static const SUPPORT_TARGET_HUMIDITY = 8; | ||||
|   static const SUPPORT_TARGET_HUMIDITY_HIGH = 16; | ||||
|   static const SUPPORT_TARGET_HUMIDITY_LOW = 32; | ||||
|   static const SUPPORT_FAN_MODE = 64; | ||||
|   static const SUPPORT_OPERATION_MODE = 128; | ||||
|   static const SUPPORT_HOLD_MODE = 256; | ||||
|   static const SUPPORT_SWING_MODE = 512; | ||||
|   static const SUPPORT_AWAY_MODE = 1024; | ||||
|   static const SUPPORT_AUX_HEAT = 2048; | ||||
|   static const SUPPORT_ON_OFF = 4096; | ||||
|  | ||||
|   ClimateEntity(Map rawData, String webHost) : super(rawData, webHost); | ||||
|  | ||||
|   bool get supportTargetTemperature => ((supportedFeatures & | ||||
|   ClimateEntity.SUPPORT_TARGET_TEMPERATURE) == | ||||
|       ClimateEntity.SUPPORT_TARGET_TEMPERATURE); | ||||
|   bool get supportTargetTemperatureHigh => ((supportedFeatures & | ||||
|   ClimateEntity.SUPPORT_TARGET_TEMPERATURE_HIGH) == | ||||
|       ClimateEntity.SUPPORT_TARGET_TEMPERATURE_HIGH); | ||||
|   bool get supportTargetTemperatureLow => ((supportedFeatures & | ||||
|   ClimateEntity.SUPPORT_TARGET_TEMPERATURE_LOW) == | ||||
|       ClimateEntity.SUPPORT_TARGET_TEMPERATURE_LOW); | ||||
|   bool get supportTargetHumidity => ((supportedFeatures & | ||||
|   ClimateEntity.SUPPORT_TARGET_HUMIDITY) == | ||||
|       ClimateEntity.SUPPORT_TARGET_HUMIDITY); | ||||
|   bool get supportTargetHumidityHigh => ((supportedFeatures & | ||||
|   ClimateEntity.SUPPORT_TARGET_HUMIDITY_HIGH) == | ||||
|       ClimateEntity.SUPPORT_TARGET_HUMIDITY_HIGH); | ||||
|   bool get supportTargetHumidityLow => ((supportedFeatures & | ||||
|   ClimateEntity.SUPPORT_TARGET_HUMIDITY_LOW) == | ||||
|       ClimateEntity.SUPPORT_TARGET_HUMIDITY_LOW); | ||||
|   bool get supportFanMode => | ||||
|       ((supportedFeatures & ClimateEntity.SUPPORT_FAN_MODE) == | ||||
|           ClimateEntity.SUPPORT_FAN_MODE); | ||||
|   bool get supportOperationMode => ((supportedFeatures & | ||||
|   ClimateEntity.SUPPORT_OPERATION_MODE) == | ||||
|       ClimateEntity.SUPPORT_OPERATION_MODE); | ||||
|   bool get supportHoldMode => | ||||
|       ((supportedFeatures & ClimateEntity.SUPPORT_HOLD_MODE) == | ||||
|           ClimateEntity.SUPPORT_HOLD_MODE); | ||||
|   bool get supportSwingMode => | ||||
|       ((supportedFeatures & ClimateEntity.SUPPORT_SWING_MODE) == | ||||
|           ClimateEntity.SUPPORT_SWING_MODE); | ||||
|   bool get supportAwayMode => | ||||
|       ((supportedFeatures & ClimateEntity.SUPPORT_AWAY_MODE) == | ||||
|           ClimateEntity.SUPPORT_AWAY_MODE); | ||||
|   bool get supportAuxHeat => | ||||
|       ((supportedFeatures & ClimateEntity.SUPPORT_AUX_HEAT) == | ||||
|           ClimateEntity.SUPPORT_AUX_HEAT); | ||||
|   bool get supportOnOff => | ||||
|       ((supportedFeatures & ClimateEntity.SUPPORT_ON_OFF) == | ||||
|           ClimateEntity.SUPPORT_ON_OFF); | ||||
|  | ||||
|   List<String> get operationList => attributes["operation_list"] != null | ||||
|       ? (attributes["operation_list"] as List).cast<String>() | ||||
|       : null; | ||||
|   List<String> get fanList => attributes["fan_list"] != null | ||||
|       ? (attributes["fan_list"] as List).cast<String>() | ||||
|       : null; | ||||
|   List<String> get swingList => attributes["swing_list"] != null | ||||
|       ? (attributes["swing_list"] as List).cast<String>() | ||||
|       : null; | ||||
|   double get temperature => _getDoubleAttributeValue('temperature'); | ||||
|   double get targetHigh => _getDoubleAttributeValue('target_temp_high'); | ||||
|   double get targetLow => _getDoubleAttributeValue('target_temp_low'); | ||||
|   double get maxTemp => _getDoubleAttributeValue('max_temp') ?? 100.0; | ||||
|   double get minTemp => _getDoubleAttributeValue('min_temp') ?? -100.0; | ||||
|   double get targetHumidity => _getDoubleAttributeValue('humidity'); | ||||
|   double get maxHumidity => _getDoubleAttributeValue('max_humidity'); | ||||
|   double get minHumidity => _getDoubleAttributeValue('min_humidity'); | ||||
|   double get temperatureStep => _getDoubleAttributeValue('target_temp_step') ?? 0.5; | ||||
|   String get operationMode => attributes['operation_mode']; | ||||
|   String get fanMode => attributes['fan_mode']; | ||||
|   String get swingMode => attributes['swing_mode']; | ||||
|   bool get awayMode => attributes['away_mode'] == "on"; | ||||
|   bool get isOff => state == EntityState.off; | ||||
|   bool get auxHeat => attributes['aux_heat'] == "on"; | ||||
|  | ||||
|   @override | ||||
|   void update(Map rawData, String webHost) { | ||||
|     super.update(rawData, webHost); | ||||
|     if (supportTargetTemperature) { | ||||
|       historyConfig.numericAttributesToShow.add("temperature"); | ||||
|     } | ||||
|     if (supportTargetTemperatureHigh) { | ||||
|       historyConfig.numericAttributesToShow.add("target_temp_high"); | ||||
|     } | ||||
|     if (supportTargetTemperatureLow) { | ||||
|       historyConfig.numericAttributesToShow.add("target_temp_low"); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   Widget _buildStatePart(BuildContext context) { | ||||
|     return ClimateStateWidget(); | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   Widget _buildAdditionalControlsForPage(BuildContext context) { | ||||
|     return ClimateControlWidget(); | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   double _getDoubleAttributeValue(String attributeName) { | ||||
|     var temp1 = attributes["$attributeName"]; | ||||
|     if (temp1 is int) { | ||||
|       return temp1.toDouble(); | ||||
|     } else if (temp1 is double) { | ||||
|       return temp1; | ||||
|     } else { | ||||
|       return null; | ||||
|     } | ||||
|   } | ||||
|  | ||||
| } | ||||
							
								
								
									
										99
									
								
								lib/entity_class/const.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,99 @@ | ||||
| part of '../main.dart'; | ||||
|  | ||||
| class EntityState { | ||||
|   static const on = 'on'; | ||||
|   static const off = 'off'; | ||||
|   static const home = 'home'; | ||||
|   static const not_home = 'not_home'; | ||||
|   static const unknown = 'unknown'; | ||||
|   static const open = 'open'; | ||||
|   static const opening = 'opening'; | ||||
|   static const closed = 'closed'; | ||||
|   static const closing = 'closing'; | ||||
|   static const playing = 'playing'; | ||||
|   static const paused = 'paused'; | ||||
|   static const idle = 'idle'; | ||||
|   static const standby = 'standby'; | ||||
|   static const alarm_disarmed = 'disarmed'; | ||||
|   static const alarm_armed_home = 'armed_home'; | ||||
|   static const alarm_armed_away = 'armed_away'; | ||||
|   static const alarm_armed_night = 'armed_night'; | ||||
|   static const alarm_armed_custom_bypass = 'armed_custom_bypass'; | ||||
|   static const alarm_pending = 'pending'; | ||||
|   static const alarm_arming = 'arming'; | ||||
|   static const alarm_disarming = 'disarming'; | ||||
|   static const alarm_triggered = 'triggered'; | ||||
|   static const locked = 'locked'; | ||||
|   static const unlocked = 'unlocked'; | ||||
|   static const unavailable = 'unavailable'; | ||||
|   static const ok = 'ok'; | ||||
|   static const problem = 'problem'; | ||||
|   static const active = 'active'; | ||||
| } | ||||
|  | ||||
| class EntityUIAction { | ||||
|   static const moreInfo = 'more-info'; | ||||
|   static const toggle = 'toggle'; | ||||
|   static const callService = 'call-service'; | ||||
|   static const navigate = 'navigate'; | ||||
|   static const none = 'none'; | ||||
|  | ||||
|   String tapAction = EntityUIAction.moreInfo; | ||||
|   String tapNavigationPath; | ||||
|   String tapService; | ||||
|   Map<String, dynamic> tapServiceData; | ||||
|   String holdAction = EntityUIAction.none; | ||||
|   String holdNavigationPath; | ||||
|   String holdService; | ||||
|   Map<String, dynamic> holdServiceData; | ||||
|  | ||||
|   EntityUIAction({rawEntityData}) { | ||||
|     if (rawEntityData != null) { | ||||
|       if (rawEntityData["tap_action"] != null) { | ||||
|         if (rawEntityData["tap_action"] is String) { | ||||
|           tapAction = rawEntityData["tap_action"]; | ||||
|         } else { | ||||
|           tapAction = | ||||
|               rawEntityData["tap_action"]["action"] ?? EntityUIAction.moreInfo; | ||||
|           tapNavigationPath = rawEntityData["tap_action"]["navigation_path"]; | ||||
|           tapService = rawEntityData["tap_action"]["service"]; | ||||
|           tapServiceData = rawEntityData["tap_action"]["service_data"]; | ||||
|         } | ||||
|       } | ||||
|       if (rawEntityData["hold_action"] != null) { | ||||
|         if (rawEntityData["hold_action"] is String) { | ||||
|           holdAction = rawEntityData["hold_action"]; | ||||
|         } else { | ||||
|           holdAction = | ||||
|               rawEntityData["hold_action"]["action"] ?? EntityUIAction.none; | ||||
|           holdNavigationPath = rawEntityData["hold_action"]["navigation_path"]; | ||||
|           holdService = rawEntityData["hold_action"]["service"]; | ||||
|           holdServiceData = rawEntityData["hold_action"]["service_data"]; | ||||
|         } | ||||
|       } | ||||
|     } | ||||
|   } | ||||
|  | ||||
| } | ||||
|  | ||||
| class CardType { | ||||
|   static const horizontalStack = "horizontal-stack"; | ||||
|   static const verticalStack = "vertical-stack"; | ||||
|   static const entities = "entities"; | ||||
|   static const glance = "glance"; | ||||
|   static const mediaControl = "media-control"; | ||||
|   static const weatherForecast = "weather-forecast"; | ||||
|   static const thermostat = "thermostat"; | ||||
|   static const sensor = "sensor"; | ||||
|   static const plantStatus = "plant-status"; | ||||
|   static const pictureEntity = "picture-entity"; | ||||
|   static const pictureElements = "picture-elements"; | ||||
|   static const picture = "picture"; | ||||
|   static const map = "map"; | ||||
|   static const iframe = "iframe"; | ||||
|   static const gauge = "gauge"; | ||||
|   static const entityButton = "entity-button"; | ||||
|   static const conditional = "conditional"; | ||||
|   static const alarmPanel = "alarm-panel"; | ||||
|   static const markdown = "markdown"; | ||||
| } | ||||
							
								
								
									
										60
									
								
								lib/entity_class/cover_entity.class.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,60 @@ | ||||
| part of '../main.dart'; | ||||
|  | ||||
| class CoverEntity extends Entity { | ||||
|  | ||||
|   static const SUPPORT_OPEN = 1; | ||||
|   static const SUPPORT_CLOSE = 2; | ||||
|   static const SUPPORT_SET_POSITION = 4; | ||||
|   static const SUPPORT_STOP = 8; | ||||
|   static const SUPPORT_OPEN_TILT = 16; | ||||
|   static const SUPPORT_CLOSE_TILT = 32; | ||||
|   static const SUPPORT_STOP_TILT = 64; | ||||
|   static const SUPPORT_SET_TILT_POSITION = 128; | ||||
|  | ||||
|   CoverEntity(Map rawData, String webHost) : super(rawData, webHost); | ||||
|  | ||||
|   bool get supportOpen => ((supportedFeatures & | ||||
|   CoverEntity.SUPPORT_OPEN) == | ||||
|       CoverEntity.SUPPORT_OPEN); | ||||
|   bool get supportClose => ((supportedFeatures & | ||||
|   CoverEntity.SUPPORT_CLOSE) == | ||||
|       CoverEntity.SUPPORT_CLOSE); | ||||
|   bool get supportSetPosition => ((supportedFeatures & | ||||
|   CoverEntity.SUPPORT_SET_POSITION) == | ||||
|       CoverEntity.SUPPORT_SET_POSITION); | ||||
|   bool get supportStop => ((supportedFeatures & | ||||
|   CoverEntity.SUPPORT_STOP) == | ||||
|       CoverEntity.SUPPORT_STOP); | ||||
|  | ||||
|   bool get supportOpenTilt => ((supportedFeatures & | ||||
|   CoverEntity.SUPPORT_OPEN_TILT) == | ||||
|       CoverEntity.SUPPORT_OPEN_TILT); | ||||
|   bool get supportCloseTilt => ((supportedFeatures & | ||||
|   CoverEntity.SUPPORT_CLOSE_TILT) == | ||||
|       CoverEntity.SUPPORT_CLOSE_TILT); | ||||
|   bool get supportStopTilt => ((supportedFeatures & | ||||
|   CoverEntity.SUPPORT_STOP_TILT) == | ||||
|       CoverEntity.SUPPORT_STOP_TILT); | ||||
|   bool get supportSetTiltPosition => ((supportedFeatures & | ||||
|   CoverEntity.SUPPORT_SET_TILT_POSITION) == | ||||
|       CoverEntity.SUPPORT_SET_TILT_POSITION); | ||||
|  | ||||
|  | ||||
|   double get currentPosition => _getDoubleAttributeValue('current_position'); | ||||
|   double get currentTiltPosition => _getDoubleAttributeValue('current_tilt_position'); | ||||
|   bool get canBeOpened => ((state != EntityState.opening) && (state != EntityState.open)) || (state == EntityState.open && currentPosition != null && currentPosition > 0.0 && currentPosition < 100.0); | ||||
|   bool get canBeClosed => ((state != EntityState.closing) && (state != EntityState.closed)); | ||||
|   bool get canTiltBeOpened => currentTiltPosition < 100; | ||||
|   bool get canTiltBeClosed => currentTiltPosition > 0; | ||||
|  | ||||
|   @override | ||||
|   Widget _buildStatePart(BuildContext context) { | ||||
|     return CoverStateWidget(); | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   Widget _buildAdditionalControlsForPage(BuildContext context) { | ||||
|     return CoverControlWidget(); | ||||
|   } | ||||
|  | ||||
| } | ||||
							
								
								
									
										42
									
								
								lib/entity_class/date_time_entity.class.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,42 @@ | ||||
| part of '../main.dart'; | ||||
|  | ||||
| class DateTimeEntity extends Entity { | ||||
|   DateTimeEntity(Map rawData, String webHost) : super(rawData, webHost); | ||||
|  | ||||
|   bool get hasDate => attributes["has_date"] ?? false; | ||||
|   bool get hasTime => attributes["has_time"] ?? false; | ||||
|   int get year => attributes["year"] ?? 1970; | ||||
|   int get month => attributes["month"] ?? 1; | ||||
|   int get day => attributes["day"] ?? 1; | ||||
|   int get hour => attributes["hour"] ?? 0; | ||||
|   int get minute => attributes["minute"] ?? 0; | ||||
|   int get second => attributes["second"] ?? 0; | ||||
|   String get formattedState => _getFormattedState(); | ||||
|   DateTime get dateTimeState => _getDateTimeState(); | ||||
|  | ||||
|   @override | ||||
|   Widget _buildStatePart(BuildContext context) { | ||||
|     return DateTimeStateWidget(); | ||||
|   } | ||||
|  | ||||
|   DateTime _getDateTimeState() { | ||||
|     return DateTime( | ||||
|         this.year, this.month, this.day, this.hour, this.minute, this.second); | ||||
|   } | ||||
|  | ||||
|   String _getFormattedState() { | ||||
|     String formattedState = ""; | ||||
|     if (this.hasDate) { | ||||
|       formattedState += formatDate(dateTimeState, [M, ' ', d, ', ', yyyy]); | ||||
|     } | ||||
|     if (this.hasTime) { | ||||
|       formattedState += " " + formatDate(dateTimeState, [HH, ':', nn]); | ||||
|     } | ||||
|     return formattedState; | ||||
|   } | ||||
|  | ||||
|   void setNewState(newValue) { | ||||
|     eventBus | ||||
|         .fire(new ServiceCallEvent(domain, "set_datetime", entityId, newValue)); | ||||
|   } | ||||
| } | ||||
							
								
								
									
										282
									
								
								lib/entity_class/entity.class.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,282 @@ | ||||
| part of '../main.dart'; | ||||
|  | ||||
| class StatelessEntityType { | ||||
|   static const NONE = 0; | ||||
|   static const MISSED = 1; | ||||
|   static const DIVIDER = 2; | ||||
|   static const SECTION = 3; | ||||
|   static const CALL_SERVICE = 4; | ||||
|   static const WEBLINK = 5; | ||||
| } | ||||
|  | ||||
| class Entity { | ||||
|  | ||||
|   static List badgeDomains = [ | ||||
|     "alarm_control_panel", | ||||
|     "binary_sensor", | ||||
|     "device_tracker", | ||||
|     "updater", | ||||
|     "sun", | ||||
|     "timer", | ||||
|     "sensor" | ||||
|   ]; | ||||
|  | ||||
|   static Map StateByDeviceClass = { | ||||
|     "battery.on": "Low", | ||||
|     "battery.off": "Normal", | ||||
|     "cold.on": "Cold", | ||||
|     "cold.off": "Normal", | ||||
|     "connectivity.on": "Connected", | ||||
|     "connectivity.off": "Diconnected", | ||||
|     "door.on": "Open", | ||||
|     "door.off": "Closed", | ||||
|     "garage_door.on": "Open", | ||||
|     "garage_door.off": "Closed", | ||||
|     "gas.on": "Detected", | ||||
|     "gas.off": "Clear", | ||||
|     "heat.on": "Hot", | ||||
|     "heat.off": "Normal", | ||||
|     "light.on": "Detected", | ||||
|     "lignt.off": "No light", | ||||
|     "lock.on": "Unlocked", | ||||
|     "lock.off": "Locked", | ||||
|     "moisture.on": "Wet", | ||||
|     "moisture.off": "Dry", | ||||
|     "motion.on": "Detected", | ||||
|     "motion.off": "Clear", | ||||
|     "moving.on": "Moving", | ||||
|     "moving.off": "Stopped", | ||||
|     "occupancy.on": "Occupied", | ||||
|     "occupancy.off": "Clear", | ||||
|     "opening.on": "Open", | ||||
|     "opening.off": "Closed", | ||||
|     "plug.on": "Plugged in", | ||||
|     "plug.off": "Unplugged", | ||||
|     "power.on": "Powered", | ||||
|     "power.off": "No power", | ||||
|     "presence.on": "Home", | ||||
|     "presence.off": "Away", | ||||
|     "problem.on": "Problem", | ||||
|     "problem.off": "OK", | ||||
|     "safety.on": "Unsafe", | ||||
|     "safety.off": "Safe", | ||||
|     "smoke.on": "Detected", | ||||
|     "smoke.off": "Clear", | ||||
|     "sound.on": "Detected", | ||||
|     "sound.off": "Clear", | ||||
|     "vibration.on": "Detected", | ||||
|     "vibration.off": "Clear", | ||||
|     "window.on": "Open", | ||||
|     "window.off": "Closed" | ||||
|   }; | ||||
|  | ||||
|   Map attributes; | ||||
|   String domain; | ||||
|   String entityId; | ||||
|   String entityPicture; | ||||
|   String state; | ||||
|   String displayState; | ||||
|   DateTime _lastUpdated; | ||||
|   int statelessType = 0; | ||||
|  | ||||
|   List<Entity> childEntities = []; | ||||
|   String deviceClass; | ||||
|   EntityHistoryConfig historyConfig = EntityHistoryConfig( | ||||
|     chartType: EntityHistoryWidgetType.simple | ||||
|   ); | ||||
|  | ||||
|   String get displayName => | ||||
|       attributes["friendly_name"] ?? (attributes["name"] ?? entityId.split(".")[1].replaceAll("_", " ")); | ||||
|  | ||||
|   bool get isView => | ||||
|       (domain == "group") && | ||||
|       (attributes != null ? attributes["view"] ?? false : false); | ||||
|   bool get isGroup => domain == "group"; | ||||
|   bool get isBadge => Entity.badgeDomains.contains(domain); | ||||
|   String get icon => attributes["icon"] ?? ""; | ||||
|   bool get isOn => state == EntityState.on; | ||||
|   String get unitOfMeasurement => attributes["unit_of_measurement"] ?? ""; | ||||
|   List get childEntityIds => attributes["entity_id"] ?? []; | ||||
|   String get lastUpdated => _getLastUpdatedFormatted(); | ||||
|   bool get isHidden => attributes["hidden"] ?? false; | ||||
|   double get doubleState => double.tryParse(state) ?? 0.0; | ||||
|   int get supportedFeatures => attributes["supported_features"] ?? 0; | ||||
|  | ||||
|   String _getEntityPictureUrl(String webHost) { | ||||
|     String result = attributes["entity_picture"]; | ||||
|     if (result == null) return result; | ||||
|     if (!result.startsWith("http")) { | ||||
|       if (result.startsWith("/")) { | ||||
|         result = "$webHost$result"; | ||||
|       } else { | ||||
|         result = "$webHost/$result"; | ||||
|       } | ||||
|     } | ||||
|     return result; | ||||
|   } | ||||
|  | ||||
|   Entity(Map rawData, String webHost) { | ||||
|     update(rawData, webHost); | ||||
|   } | ||||
|  | ||||
|   Entity.missed(String entityId) { | ||||
|     statelessType = StatelessEntityType.MISSED; | ||||
|     attributes = {"hidden": false}; | ||||
|     this.entityId = entityId; | ||||
|   } | ||||
|  | ||||
|   Entity.divider() { | ||||
|     statelessType = StatelessEntityType.DIVIDER; | ||||
|     attributes = {"hidden": false}; | ||||
|   } | ||||
|  | ||||
|   Entity.section(String label) { | ||||
|     statelessType = StatelessEntityType.SECTION; | ||||
|     attributes = {"hidden": false, "friendly_name": "$label"}; | ||||
|   } | ||||
|  | ||||
|   Entity.callService({String icon, String name, String service, String actionName}) { | ||||
|     statelessType = StatelessEntityType.CALL_SERVICE; | ||||
|     entityId = service; | ||||
|     displayState = actionName?.toUpperCase() ?? "RUN"; | ||||
|     attributes = {"hidden": false, "friendly_name": "$name", "icon": "$icon"}; | ||||
|   } | ||||
|  | ||||
|   Entity.weblink({String url, String name, String icon}) { | ||||
|     statelessType = StatelessEntityType.WEBLINK; | ||||
|     entityId = "custom.custom"; //TODO wtf?? | ||||
|     attributes = {"hidden": false, "friendly_name": "${name ?? url}", "icon": "${icon ?? 'mdi:link'}"}; | ||||
|   } | ||||
|  | ||||
|   void update(Map rawData, String webHost) { | ||||
|     attributes = rawData["attributes"] ?? {}; | ||||
|     domain = rawData["entity_id"].split(".")[0]; | ||||
|     entityId = rawData["entity_id"]; | ||||
|     deviceClass = attributes["device_class"]; | ||||
|     state = rawData["state"]; | ||||
|     displayState = Entity.StateByDeviceClass["$deviceClass.$state"] ?? state; | ||||
|     _lastUpdated = DateTime.tryParse(rawData["last_updated"]); | ||||
|     entityPicture = _getEntityPictureUrl(webHost); | ||||
|   } | ||||
|  | ||||
|   double _getDoubleAttributeValue(String attributeName) { | ||||
|     var temp1 = attributes["$attributeName"]; | ||||
|     if (temp1 is int) { | ||||
|       return temp1.toDouble(); | ||||
|     } else if (temp1 is double) { | ||||
|       return temp1; | ||||
|     } else { | ||||
|       return double.tryParse("$temp1"); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   int _getIntAttributeValue(String attributeName) { | ||||
|     var temp1 = attributes["$attributeName"]; | ||||
|     if (temp1 is int) { | ||||
|       return temp1; | ||||
|     } else if (temp1 is double) { | ||||
|       return temp1.round(); | ||||
|     } else { | ||||
|       return int.tryParse("$temp1"); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   List<String> getStringListAttributeValue(String attribute) { | ||||
|     if (attributes["$attribute"] != null) { | ||||
|       List<String> result = (attributes["$attribute"] as List).cast<String>(); | ||||
|       return result; | ||||
|     } else { | ||||
|       return null; | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   Widget buildDefaultWidget(BuildContext context) { | ||||
|     return DefaultEntityContainer( | ||||
|         state: _buildStatePart(context) | ||||
|     ); | ||||
|   } | ||||
|  | ||||
|   Widget _buildStatePart(BuildContext context) { | ||||
|     return SimpleEntityState(); | ||||
|   } | ||||
|  | ||||
|   Widget _buildStatePartForPage(BuildContext context) { | ||||
|     return _buildStatePart(context); | ||||
|   } | ||||
|  | ||||
|   Widget _buildAdditionalControlsForPage(BuildContext context) { | ||||
|     return Container( | ||||
|       width: 0.0, | ||||
|       height: 0.0, | ||||
|     ); | ||||
|   } | ||||
|  | ||||
|   Widget buildEntityPageWidget(BuildContext context) { | ||||
|     return EntityModel( | ||||
|       entityWrapper: EntityWrapper(entity: this), | ||||
|       child: EntityPageContainer(children: <Widget>[ | ||||
|         Padding( | ||||
|           padding: EdgeInsets.only(top: Sizes.rowPadding), | ||||
|           child: DefaultEntityContainer(state: _buildStatePartForPage(context)), | ||||
|         ), | ||||
|         LastUpdatedWidget(), | ||||
|         Divider(), | ||||
|         _buildAdditionalControlsForPage(context), | ||||
|         Divider(), | ||||
|         buildHistoryWidget(), | ||||
|         EntityAttributesList() | ||||
|       ]), | ||||
|       handleTap: false, | ||||
|     ); | ||||
|   } | ||||
|  | ||||
|   Widget buildHistoryWidget() { | ||||
|     return EntityHistoryWidget( | ||||
|       config: historyConfig, | ||||
|     ); | ||||
|   } | ||||
|  | ||||
|   Widget buildBadgeWidget(BuildContext context) { | ||||
|     return EntityModel( | ||||
|       entityWrapper: EntityWrapper(entity: this), | ||||
|       child: BadgeWidget(), | ||||
|       handleTap: true, | ||||
|     ); | ||||
|   } | ||||
|  | ||||
|   String getAttribute(String attributeName) { | ||||
|     if (attributes != null) { | ||||
|       return attributes["$attributeName"]; | ||||
|     } | ||||
|     return null; | ||||
|   } | ||||
|  | ||||
|   String _getLastUpdatedFormatted() { | ||||
|     if (_lastUpdated == null) { | ||||
|       return "-"; | ||||
|     } else { | ||||
|       DateTime now = DateTime.now(); | ||||
|       Duration d = now.difference(_lastUpdated); | ||||
|       String text; | ||||
|       int v; | ||||
|       if (d.inDays == 0) { | ||||
|         if (d.inHours == 0) { | ||||
|           if (d.inMinutes == 0) { | ||||
|             text = "seconds ago"; | ||||
|             v = d.inSeconds; | ||||
|           } else { | ||||
|             text = "minutes ago"; | ||||
|             v = d.inMinutes; | ||||
|           } | ||||
|         } else { | ||||
|           text = "hours ago"; | ||||
|           v = d.inHours; | ||||
|         } | ||||
|       } else { | ||||
|         text = "days ago"; | ||||
|         v = d.inDays; | ||||
|       } | ||||
|       return "$v $text"; | ||||
|     } | ||||
|   } | ||||
| } | ||||
							
								
								
									
										114
									
								
								lib/entity_class/entity_wrapper.class.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,114 @@ | ||||
| part of '../main.dart'; | ||||
|  | ||||
| class EntityWrapper { | ||||
|  | ||||
|   String displayName; | ||||
|   String icon; | ||||
|   String entityPicture; | ||||
|   EntityUIAction uiAction; | ||||
|   Entity entity; | ||||
|  | ||||
|  | ||||
|   EntityWrapper({ | ||||
|     this.entity, | ||||
|     String icon, | ||||
|     String displayName, | ||||
|     this.uiAction | ||||
|   }) { | ||||
|     if (entity.statelessType == StatelessEntityType.NONE || entity.statelessType == StatelessEntityType.CALL_SERVICE || entity.statelessType == StatelessEntityType.WEBLINK) { | ||||
|       this.icon = icon ?? entity.icon; | ||||
|       if (icon == null) { | ||||
|         entityPicture = entity.entityPicture; | ||||
|       } | ||||
|       this.displayName = displayName ?? entity.displayName; | ||||
|       if (uiAction == null) { | ||||
|         uiAction = EntityUIAction(); | ||||
|       } | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   void handleTap() { | ||||
|     switch (uiAction.tapAction) { | ||||
|       case EntityUIAction.toggle: { | ||||
|         eventBus.fire( | ||||
|             ServiceCallEvent("homeassistant", "toggle", entity.entityId, null)); | ||||
|         break; | ||||
|       } | ||||
|  | ||||
|       case EntityUIAction.callService: { | ||||
|         if (uiAction.tapService != null) { | ||||
|           eventBus.fire( | ||||
|               ServiceCallEvent(uiAction.tapService.split(".")[0], | ||||
|                   uiAction.tapService.split(".")[1], null, | ||||
|                   uiAction.tapServiceData)); | ||||
|         } | ||||
|         break; | ||||
|       } | ||||
|  | ||||
|       case EntityUIAction.none: { | ||||
|         break; | ||||
|       } | ||||
|  | ||||
|       case EntityUIAction.moreInfo: { | ||||
|         eventBus.fire( | ||||
|             new ShowEntityPageEvent(entity)); | ||||
|         break; | ||||
|       } | ||||
|  | ||||
|       case EntityUIAction.navigate: { | ||||
|         if (uiAction.tapService.startsWith("/")) { | ||||
|           //TODO handle local urls | ||||
|           Logger.w("Local urls is not supported yet"); | ||||
|         } else { | ||||
|           HAUtils.launchURL(uiAction.tapService); | ||||
|         } | ||||
|         break; | ||||
|       } | ||||
|  | ||||
|       default: { | ||||
|         break; | ||||
|       } | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   void handleHold() { | ||||
|       switch (uiAction.holdAction) { | ||||
|         case EntityUIAction.toggle: { | ||||
|           eventBus.fire( | ||||
|               ServiceCallEvent("homeassistant", "toggle", entity.entityId, null)); | ||||
|           break; | ||||
|         } | ||||
|  | ||||
|         case EntityUIAction.callService: { | ||||
|           if (uiAction.holdService != null) { | ||||
|             eventBus.fire( | ||||
|                 ServiceCallEvent(uiAction.holdService.split(".")[0], | ||||
|                     uiAction.holdService.split(".")[1], null, | ||||
|                     uiAction.holdServiceData)); | ||||
|           } | ||||
|           break; | ||||
|         } | ||||
|  | ||||
|         case EntityUIAction.moreInfo: { | ||||
|           eventBus.fire( | ||||
|               new ShowEntityPageEvent(entity)); | ||||
|           break; | ||||
|         } | ||||
|  | ||||
|         case EntityUIAction.navigate: { | ||||
|           if (uiAction.holdService.startsWith("/")) { | ||||
|             //TODO handle local urls | ||||
|             Logger.w("Local urls is not supported yet"); | ||||
|           } else { | ||||
|             HAUtils.launchURL(uiAction.holdService); | ||||
|           } | ||||
|           break; | ||||
|         } | ||||
|  | ||||
|         default: { | ||||
|           break; | ||||
|         } | ||||
|       } | ||||
|   } | ||||
|  | ||||
| } | ||||
							
								
								
									
										32
									
								
								lib/entity_class/fan_entity.class.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,32 @@ | ||||
| part of '../main.dart'; | ||||
|  | ||||
| class FanEntity extends Entity { | ||||
|  | ||||
|   static const SUPPORT_SET_SPEED = 1; | ||||
|   static const SUPPORT_OSCILLATE = 2; | ||||
|   static const SUPPORT_DIRECTION = 4; | ||||
|  | ||||
|   FanEntity(Map rawData, String webHost) : super(rawData, webHost); | ||||
|  | ||||
|   bool get supportSetSpeed => ((supportedFeatures & | ||||
|   FanEntity.SUPPORT_SET_SPEED) == | ||||
|       FanEntity.SUPPORT_SET_SPEED); | ||||
|   bool get supportOscillate => ((supportedFeatures & | ||||
|   FanEntity.SUPPORT_OSCILLATE) == | ||||
|       FanEntity.SUPPORT_OSCILLATE); | ||||
|   bool get supportDirection => ((supportedFeatures & | ||||
|   FanEntity.SUPPORT_DIRECTION) == | ||||
|       FanEntity.SUPPORT_DIRECTION); | ||||
|  | ||||
|   List<String> get speedList => getStringListAttributeValue("speed_list"); | ||||
|  | ||||
|   @override | ||||
|   Widget _buildStatePart(BuildContext context) { | ||||
|     return SwitchStateWidget(); | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   Widget _buildAdditionalControlsForPage(BuildContext context) { | ||||
|     return FanControlsWidget(); | ||||
|   } | ||||
| } | ||||
							
								
								
									
										44
									
								
								lib/entity_class/group_entity.class.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,44 @@ | ||||
| part of '../main.dart'; | ||||
|  | ||||
| class GroupEntity extends Entity { | ||||
|  | ||||
|   final List<String> _domainsForSwitchableGroup = ["switch", "light", "automation", "input_boolean"]; | ||||
|   String mutualDomain; | ||||
|   bool switchable = false; | ||||
|  | ||||
|   GroupEntity(Map rawData, String webHost) : super(rawData, webHost); | ||||
|  | ||||
|   @override | ||||
|   Widget _buildStatePart(BuildContext context) { | ||||
|     if (switchable) { | ||||
|       return SwitchStateWidget( | ||||
|         domainForService: "homeassistant", | ||||
|       ); | ||||
|     } else { | ||||
|       return super._buildStatePart(context); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   void update(Map rawData, String webHost) { | ||||
|     super.update(rawData, webHost); | ||||
|     if (_isOneDomain()) { | ||||
|       mutualDomain = attributes['entity_id'][0].split(".")[0]; | ||||
|       switchable = _domainsForSwitchableGroup.contains(mutualDomain); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   bool _isOneDomain() { | ||||
|     bool result = false; | ||||
|     if (attributes['entity_id'] != null && attributes['entity_id'] is List && attributes['entity_id'].isNotEmpty) { | ||||
|       String firstChildDomain = attributes['entity_id'][0].split(".")[0]; | ||||
|       result = true; | ||||
|       attributes['entity_id'].forEach((childEntityId){ | ||||
|         if (childEntityId.split(".")[0] != firstChildDomain) { | ||||
|           result = false; | ||||
|         } | ||||
|       }); | ||||
|     } | ||||
|     return result; | ||||
|   } | ||||
| } | ||||
							
								
								
									
										79
									
								
								lib/entity_class/light_entity.class.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,79 @@ | ||||
| part of '../main.dart'; | ||||
|  | ||||
| class LightEntity extends Entity { | ||||
|  | ||||
|   static const SUPPORT_BRIGHTNESS = 1; | ||||
|   static const SUPPORT_COLOR_TEMP = 2; | ||||
|   static const SUPPORT_EFFECT = 4; | ||||
|   static const SUPPORT_FLASH = 8; | ||||
|   static const SUPPORT_COLOR = 16; | ||||
|   static const SUPPORT_TRANSITION = 32; | ||||
|   static const SUPPORT_WHITE_VALUE = 128; | ||||
|  | ||||
|   bool get supportBrightness => ((supportedFeatures & | ||||
|   LightEntity.SUPPORT_BRIGHTNESS) == | ||||
|       LightEntity.SUPPORT_BRIGHTNESS); | ||||
|   bool get supportColorTemp => ((supportedFeatures & | ||||
|   LightEntity.SUPPORT_COLOR_TEMP) == | ||||
|       LightEntity.SUPPORT_COLOR_TEMP); | ||||
|   bool get supportEffect => ((supportedFeatures & | ||||
|   LightEntity.SUPPORT_EFFECT) == | ||||
|       LightEntity.SUPPORT_EFFECT); | ||||
|   bool get supportFlash => ((supportedFeatures & | ||||
|   LightEntity.SUPPORT_FLASH) == | ||||
|       LightEntity.SUPPORT_FLASH); | ||||
|   bool get supportColor => ((supportedFeatures & | ||||
|   LightEntity.SUPPORT_COLOR) == | ||||
|       LightEntity.SUPPORT_COLOR); | ||||
|   bool get supportTransition => ((supportedFeatures & | ||||
|   LightEntity.SUPPORT_TRANSITION) == | ||||
|       LightEntity.SUPPORT_TRANSITION); | ||||
|   bool get supportWhiteValue => ((supportedFeatures & | ||||
|   LightEntity.SUPPORT_WHITE_VALUE) == | ||||
|       LightEntity.SUPPORT_WHITE_VALUE); | ||||
|  | ||||
|   int get brightness => _getIntAttributeValue("brightness"); | ||||
|   int get whiteValue => _getIntAttributeValue("white_value"); | ||||
|   String get effect => attributes["effect"]; | ||||
|   int get colorTemp => _getIntAttributeValue("color_temp"); | ||||
|   double get maxMireds => _getDoubleAttributeValue("max_mireds"); | ||||
|   double get minMireds => _getDoubleAttributeValue("min_mireds"); | ||||
|   HSVColor get color => _getColor(); | ||||
|   bool get isAdditionalControls => ((supportedFeatures != null) && (supportedFeatures != 0)); | ||||
|   List<String> get effectList => getStringListAttributeValue("effect_list"); | ||||
|  | ||||
|   LightEntity(Map rawData, String webHost) : super(rawData, webHost); | ||||
|  | ||||
|   HSVColor _getColor() { | ||||
|     List hs = attributes["hs_color"]; | ||||
|     List rgb = attributes["rgb_color"]; | ||||
|     try { | ||||
|       if (hs != null && hs.isNotEmpty) { | ||||
|         double sat = hs[1]/100; | ||||
|         String ssat = sat.toStringAsFixed(2); | ||||
|         return HSVColor.fromAHSV(1.0, hs[0], double.parse(ssat), 1.0); | ||||
|       } else if (rgb != null && rgb.isNotEmpty) { | ||||
|         return HSVColor.fromColor(Color.fromARGB(255, rgb[0], rgb[1], rgb[2])); | ||||
|       } else { | ||||
|         return null; | ||||
|       } | ||||
|     } catch (e) { | ||||
|       return null; | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   Widget _buildStatePart(BuildContext context) { | ||||
|     return SwitchStateWidget(); | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   Widget _buildAdditionalControlsForPage(BuildContext context) { | ||||
|     if (!isAdditionalControls || state == EntityState.unavailable) { | ||||
|       return Container(height: 0.0, width: 0.0); | ||||
|     } else { | ||||
|       return LightControlsWidget(); | ||||
|     } | ||||
|   } | ||||
|  | ||||
| } | ||||
							
								
								
									
										21
									
								
								lib/entity_class/lock_entity.class.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,21 @@ | ||||
| part of '../main.dart'; | ||||
|  | ||||
| class LockEntity extends Entity { | ||||
|   LockEntity(Map rawData, String webHost) : super(rawData, webHost); | ||||
|  | ||||
|   bool get isLocked => state == "locked"; | ||||
|  | ||||
|   @override | ||||
|   Widget _buildStatePart(BuildContext context) { | ||||
|     return LockStateWidget( | ||||
|       assumedState: false, | ||||
|     ); | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   Widget _buildStatePartForPage(BuildContext context) { | ||||
|     return LockStateWidget( | ||||
|       assumedState: true, | ||||
|     ); | ||||
|   } | ||||
| } | ||||
							
								
								
									
										83
									
								
								lib/entity_class/media_player_entity.class.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,83 @@ | ||||
| part of '../main.dart'; | ||||
|  | ||||
| class MediaPlayerEntity extends Entity { | ||||
|  | ||||
|   static const SUPPORT_PAUSE = 1; | ||||
|   static const SUPPORT_SEEK = 2; | ||||
|   static const SUPPORT_VOLUME_SET = 4; | ||||
|   static const SUPPORT_VOLUME_MUTE = 8; | ||||
|   static const SUPPORT_PREVIOUS_TRACK = 16; | ||||
|   static const SUPPORT_NEXT_TRACK = 32; | ||||
|  | ||||
|   static const SUPPORT_TURN_ON = 128; | ||||
|   static const SUPPORT_TURN_OFF = 256; | ||||
|   static const SUPPORT_PLAY_MEDIA = 512; | ||||
|   static const SUPPORT_VOLUME_STEP = 1024; | ||||
|   static const SUPPORT_SELECT_SOURCE = 2048; | ||||
|   static const SUPPORT_STOP = 4096; | ||||
|   static const SUPPORT_CLEAR_PLAYLIST = 8192; | ||||
|   static const SUPPORT_PLAY = 16384; | ||||
|   static const SUPPORT_SHUFFLE_SET = 32768; | ||||
|   static const SUPPORT_SELECT_SOUND_MODE = 65536; | ||||
|  | ||||
|   MediaPlayerEntity(Map rawData, String webHost) : super(rawData, webHost); | ||||
|  | ||||
|   bool get supportPause => ((supportedFeatures & | ||||
|   MediaPlayerEntity.SUPPORT_PAUSE) == | ||||
|       MediaPlayerEntity.SUPPORT_PAUSE); | ||||
|   bool get supportSeek => ((supportedFeatures & | ||||
|   MediaPlayerEntity.SUPPORT_SEEK) == | ||||
|       MediaPlayerEntity.SUPPORT_SEEK); | ||||
|   bool get supportVolumeSet => ((supportedFeatures & | ||||
|   MediaPlayerEntity.SUPPORT_VOLUME_SET) == | ||||
|       MediaPlayerEntity.SUPPORT_VOLUME_SET); | ||||
|   bool get supportVolumeMute => ((supportedFeatures & | ||||
|   MediaPlayerEntity.SUPPORT_VOLUME_MUTE) == | ||||
|       MediaPlayerEntity.SUPPORT_VOLUME_MUTE); | ||||
|   bool get supportPreviousTrack => ((supportedFeatures & | ||||
|   MediaPlayerEntity.SUPPORT_PREVIOUS_TRACK) == | ||||
|       MediaPlayerEntity.SUPPORT_PREVIOUS_TRACK); | ||||
|   bool get supportNextTrack => ((supportedFeatures & | ||||
|   MediaPlayerEntity.SUPPORT_NEXT_TRACK) == | ||||
|       MediaPlayerEntity.SUPPORT_NEXT_TRACK); | ||||
|  | ||||
|   bool get supportTurnOn => ((supportedFeatures & | ||||
|   MediaPlayerEntity.SUPPORT_TURN_ON) == | ||||
|       MediaPlayerEntity.SUPPORT_TURN_ON); | ||||
|   bool get supportTurnOff => ((supportedFeatures & | ||||
|   MediaPlayerEntity.SUPPORT_TURN_OFF) == | ||||
|       MediaPlayerEntity.SUPPORT_TURN_OFF); | ||||
|   bool get supportPlayMedia => ((supportedFeatures & | ||||
|   MediaPlayerEntity.SUPPORT_PLAY_MEDIA) == | ||||
|       MediaPlayerEntity.SUPPORT_PLAY_MEDIA); | ||||
|   bool get supportVolumeStep => ((supportedFeatures & | ||||
|   MediaPlayerEntity.SUPPORT_VOLUME_STEP) == | ||||
|       MediaPlayerEntity.SUPPORT_VOLUME_STEP); | ||||
|   bool get supportSelectSource => ((supportedFeatures & | ||||
|   MediaPlayerEntity.SUPPORT_SELECT_SOURCE) == | ||||
|       MediaPlayerEntity.SUPPORT_SELECT_SOURCE); | ||||
|   bool get supportStop => ((supportedFeatures & | ||||
|   MediaPlayerEntity.SUPPORT_STOP) == | ||||
|       MediaPlayerEntity.SUPPORT_STOP); | ||||
|   bool get supportClearPlaylist => ((supportedFeatures & | ||||
|   MediaPlayerEntity.SUPPORT_CLEAR_PLAYLIST) == | ||||
|       MediaPlayerEntity.SUPPORT_CLEAR_PLAYLIST); | ||||
|   bool get supportPlay => ((supportedFeatures & | ||||
|   MediaPlayerEntity.SUPPORT_PLAY) == | ||||
|       MediaPlayerEntity.SUPPORT_PLAY); | ||||
|   bool get supportShuffleSet => ((supportedFeatures & | ||||
|   MediaPlayerEntity.SUPPORT_SHUFFLE_SET) == | ||||
|       MediaPlayerEntity.SUPPORT_SHUFFLE_SET); | ||||
|   bool get supportSelectSoundMode => ((supportedFeatures & | ||||
|   MediaPlayerEntity.SUPPORT_SELECT_SOUND_MODE) == | ||||
|       MediaPlayerEntity.SUPPORT_SELECT_SOUND_MODE); | ||||
|  | ||||
|   List<String> get soundModeList => getStringListAttributeValue("sound_mode_list"); | ||||
|   List<String> get sourceList => getStringListAttributeValue("source_list"); | ||||
|  | ||||
|   @override | ||||
|   Widget _buildAdditionalControlsForPage(BuildContext context) { | ||||
|     return MediaPlayerControls(); | ||||
|   } | ||||
|  | ||||
| } | ||||
							
								
								
									
										17
									
								
								lib/entity_class/other_entity.class.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,17 @@ | ||||
| part of '../main.dart'; | ||||
|  | ||||
| class SunEntity extends Entity { | ||||
|   SunEntity(Map rawData, String webHost) : super(rawData, webHost); | ||||
| } | ||||
|  | ||||
| class SensorEntity extends Entity { | ||||
|  | ||||
|   @override | ||||
|   EntityHistoryConfig historyConfig = EntityHistoryConfig( | ||||
|       chartType: EntityHistoryWidgetType.numericState, | ||||
|       numericState: true | ||||
|   ); | ||||
|  | ||||
|   SensorEntity(Map rawData, String webHost) : super(rawData, webHost); | ||||
|  | ||||
| } | ||||
							
								
								
									
										14
									
								
								lib/entity_class/select_entity.class.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,14 @@ | ||||
| part of '../main.dart'; | ||||
|  | ||||
| class SelectEntity extends Entity { | ||||
|   List<String> get listOptions => attributes["options"] != null | ||||
|       ? (attributes["options"] as List).cast<String>() | ||||
|       : []; | ||||
|  | ||||
|   SelectEntity(Map rawData, String webHost) : super(rawData, webHost); | ||||
|  | ||||
|   @override | ||||
|   Widget _buildStatePart(BuildContext context) { | ||||
|     return SelectStateWidget(); | ||||
|   } | ||||
| } | ||||
							
								
								
									
										44
									
								
								lib/entity_class/slider_entity.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,44 @@ | ||||
| part of '../main.dart'; | ||||
|  | ||||
| class SliderEntity extends Entity { | ||||
|   SliderEntity(Map rawData, String webHost) : super(rawData, webHost); | ||||
|  | ||||
|   double get minValue => _getDoubleAttributeValue("min") ?? 0.0; | ||||
|   double get maxValue =>_getDoubleAttributeValue("max") ?? 100.0; | ||||
|   double get valueStep => _getDoubleAttributeValue("step") ?? 1.0; | ||||
|  | ||||
|   @override | ||||
|   EntityHistoryConfig historyConfig = EntityHistoryConfig( | ||||
|       chartType: EntityHistoryWidgetType.numericState, | ||||
|       numericState: true | ||||
|   ); | ||||
|  | ||||
|   /*@override | ||||
|   Widget _buildStatePart(BuildContext context) { | ||||
|     return Expanded( | ||||
|       //width: 200.0, | ||||
|       child: Row( | ||||
|         children: <Widget>[ | ||||
|           SliderStateWidget( | ||||
|             expanded: true, | ||||
|           ), | ||||
|           SimpleEntityState( | ||||
|             expanded: false, | ||||
|           ), | ||||
|         ], | ||||
|       ), | ||||
|     ); | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   Widget _buildStatePartForPage(BuildContext context) { | ||||
|     return SimpleEntityState( | ||||
|       expanded: false, | ||||
|     ); | ||||
|   }*/ | ||||
|  | ||||
|   @override | ||||
|   Widget _buildAdditionalControlsForPage(BuildContext context) { | ||||
|     return SliderControlsWidget(); | ||||
|   } | ||||
| } | ||||
							
								
								
									
										10
									
								
								lib/entity_class/switch_entity.class.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,10 @@ | ||||
| part of '../main.dart'; | ||||
|  | ||||
| class SwitchEntity extends Entity { | ||||
|   SwitchEntity(Map rawData, String webHost) : super(rawData, webHost); | ||||
|  | ||||
|   @override | ||||
|   Widget _buildStatePart(BuildContext context) { | ||||
|     return SwitchStateWidget(); | ||||
|   } | ||||
| } | ||||
							
								
								
									
										16
									
								
								lib/entity_class/text_entity.class.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,16 @@ | ||||
| part of '../main.dart'; | ||||
|  | ||||
| class TextEntity extends Entity { | ||||
|   TextEntity(Map rawData, String webHost) : super(rawData, webHost); | ||||
|  | ||||
|   int get valueMinLength => attributes["min"] ?? -1; | ||||
|   int get valueMaxLength => attributes["max"] ?? -1; | ||||
|   String get valuePattern => attributes["pattern"] ?? null; | ||||
|   bool get isTextField => attributes["mode"] == "text"; | ||||
|   bool get isPasswordField => attributes["mode"] == "password"; | ||||
|  | ||||
|   @override | ||||
|   Widget _buildStatePart(BuildContext context) { | ||||
|     return TextInputStateWidget(); | ||||
|   } | ||||
| } | ||||
							
								
								
									
										45
									
								
								lib/entity_class/timer_entity.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,45 @@ | ||||
| part of '../main.dart'; | ||||
|  | ||||
| class TimerEntity extends Entity { | ||||
|   TimerEntity(Map rawData, String webHost) : super(rawData, webHost); | ||||
|  | ||||
|   Duration duration; | ||||
|  | ||||
|   @override | ||||
|   void update(Map rawData, String webHost) { | ||||
|     super.update(rawData, webHost); | ||||
|     String durationSource = "${attributes["duration"]}"; | ||||
|     if (durationSource != null && durationSource.isNotEmpty) { | ||||
|       try { | ||||
|         List<String> durationList = durationSource.split(":"); | ||||
|         if (durationList.length == 1) { | ||||
|           duration = Duration(seconds: int.tryParse(durationList[0] ?? 0)); | ||||
|         } else if (durationList.length == 2) { | ||||
|           duration = Duration( | ||||
|               hours: int.tryParse(durationList[0]) ?? 0, | ||||
|               minutes: int.tryParse(durationList[1]) ?? 0 | ||||
|           ); | ||||
|         } else if (durationList.length == 3) { | ||||
|           duration = Duration( | ||||
|               hours: int.tryParse(durationList[0]) ?? 0, | ||||
|               minutes: int.tryParse(durationList[1]) ?? 0, | ||||
|               seconds: int.tryParse(durationList[2]) ?? 0 | ||||
|           ); | ||||
|         } else { | ||||
|           Logger.e("Strange $entityId duration format: $durationSource"); | ||||
|           duration = Duration(seconds: 0); | ||||
|         } | ||||
|       } catch (e) { | ||||
|         Logger.e("Error parsing duration for $entityId: ${e.toString()}"); | ||||
|         duration = Duration(seconds: 0); | ||||
|       } | ||||
|     } else { | ||||
|       duration = Duration(seconds: 0); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   Widget _buildStatePart(BuildContext context) { | ||||
|     return TimerState(); | ||||
|   } | ||||
| } | ||||
| @@ -2,111 +2,179 @@ part of 'main.dart'; | ||||
|  | ||||
| class EntityCollection { | ||||
|  | ||||
|   Map<String, Entity> _entities; | ||||
|   List<String> viewList; | ||||
|   final homeAssistantWebHost; | ||||
|  | ||||
|   EntityCollection() { | ||||
|     _entities = {}; | ||||
|     viewList = []; | ||||
|   Map<String, Entity> _allEntities; | ||||
|   //Map<String, Entity> views; | ||||
|  | ||||
|   bool get isEmpty => _allEntities.isEmpty; | ||||
|   List<Entity> get viewEntities => _allEntities.values.where((entity) => entity.isView).toList(); | ||||
|  | ||||
|   EntityCollection(this.homeAssistantWebHost) { | ||||
|     _allEntities = {}; | ||||
|     //views = {}; | ||||
|   } | ||||
|  | ||||
|   bool get hasDefaultView => _entities["group.default_view"] != null; | ||||
|   bool get hasDefaultView => _allEntities.keys.contains("group.default_view"); | ||||
|  | ||||
|   void parse(List rawData) { | ||||
|     _entities.clear(); | ||||
|     viewList.clear(); | ||||
|     _allEntities.clear(); | ||||
|     //views.clear(); | ||||
|  | ||||
|     TheLogger.log("Debug","Parsing ${rawData.length} Home Assistant entities"); | ||||
|     Logger.d("Parsing ${rawData.length} Home Assistant entities"); | ||||
|     rawData.forEach((rawEntityData) { | ||||
|       Entity newEntity = addFromRaw(rawEntityData); | ||||
|  | ||||
|       if (newEntity.isView) { | ||||
|         viewList.add(newEntity.entityId); | ||||
|       } | ||||
|       addFromRaw(rawEntityData); | ||||
|     }); | ||||
|     _allEntities.forEach((entityId, entity){ | ||||
|       if ((entity.isGroup) && (entity.childEntityIds != null)) { | ||||
|         entity.childEntities = getAll(entity.childEntityIds); | ||||
|       } | ||||
|       /*if (entity.isView) { | ||||
|         views[entityId] = entity; | ||||
|       }*/ | ||||
|     }); | ||||
|   } | ||||
|  | ||||
|   void clear() { | ||||
|     _allEntities.clear(); | ||||
|   } | ||||
|  | ||||
|   Entity _createEntityInstance(rawEntityData) { | ||||
|     switch (rawEntityData["entity_id"].split(".")[0]) { | ||||
|       case "automation": | ||||
|       case "input_boolean ": | ||||
|       case "switch": | ||||
|       case "light": { | ||||
|         return SwitchEntity(rawEntityData); | ||||
|       case 'sun': { | ||||
|         return SunEntity(rawEntityData, homeAssistantWebHost); | ||||
|       } | ||||
|       case "media_player": { | ||||
|         return MediaPlayerEntity(rawEntityData, homeAssistantWebHost); | ||||
|       } | ||||
|       case 'sensor': { | ||||
|         return SensorEntity(rawEntityData, homeAssistantWebHost); | ||||
|       } | ||||
|       case 'lock': { | ||||
|         return LockEntity(rawEntityData, homeAssistantWebHost); | ||||
|       } | ||||
|       case "automation": { | ||||
|         return AutomationEntity(rawEntityData, homeAssistantWebHost); | ||||
|       } | ||||
|  | ||||
|       case "input_boolean": | ||||
|       case "switch": { | ||||
|         return SwitchEntity(rawEntityData, homeAssistantWebHost); | ||||
|       } | ||||
|       case "light": { | ||||
|         return LightEntity(rawEntityData, homeAssistantWebHost); | ||||
|       } | ||||
|       case "group": { | ||||
|         return GroupEntity(rawEntityData, homeAssistantWebHost); | ||||
|       } | ||||
|       case "script": | ||||
|       case "scene": { | ||||
|         return ButtonEntity(rawEntityData); | ||||
|         return ButtonEntity(rawEntityData, homeAssistantWebHost); | ||||
|       } | ||||
|       case "input_datetime": { | ||||
|         return DateTimeEntity(rawEntityData, homeAssistantWebHost); | ||||
|       } | ||||
|       case "input_select": { | ||||
|         return SelectEntity(rawEntityData, homeAssistantWebHost); | ||||
|       } | ||||
|  | ||||
|       case "input_text": | ||||
|       case "input_number": { | ||||
|         return InputEntity(rawEntityData); | ||||
|         return SliderEntity(rawEntityData, homeAssistantWebHost); | ||||
|       } | ||||
|       case "input_text": { | ||||
|         return TextEntity(rawEntityData, homeAssistantWebHost); | ||||
|       } | ||||
|       case "climate": { | ||||
|         return ClimateEntity(rawEntityData, homeAssistantWebHost); | ||||
|       } | ||||
|       case "cover": { | ||||
|         return CoverEntity(rawEntityData, homeAssistantWebHost); | ||||
|       } | ||||
|       case "fan": { | ||||
|         return FanEntity(rawEntityData, homeAssistantWebHost); | ||||
|       } | ||||
|       case "camera": { | ||||
|         return CameraEntity(rawEntityData, homeAssistantWebHost); | ||||
|       } | ||||
|       case "alarm_control_panel": { | ||||
|         return AlarmControlPanelEntity(rawEntityData, homeAssistantWebHost); | ||||
|       } | ||||
|       case "timer": { | ||||
|         return TimerEntity(rawEntityData, homeAssistantWebHost); | ||||
|       } | ||||
|  | ||||
|       default: { | ||||
|         return Entity(rawEntityData); | ||||
|         return Entity(rawEntityData, homeAssistantWebHost); | ||||
|       } | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   void updateState(Map rawStateData) { | ||||
|   bool updateState(Map rawStateData) { | ||||
|     if (isExist(rawStateData["entity_id"])) { | ||||
|       updateFromRaw(rawStateData["new_state"] ?? rawStateData["old_state"]); | ||||
|       return false; | ||||
|     } else { | ||||
|       addFromRaw(rawStateData["new_state"] ?? rawStateData["old_state"]); | ||||
|       return true; | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   void add(Entity entity) { | ||||
|     _entities[entity.entityId] = entity; | ||||
|     _allEntities[entity.entityId] = entity; | ||||
|   } | ||||
|  | ||||
|   Entity addFromRaw(Map rawEntityData) { | ||||
|   void addFromRaw(Map rawEntityData) { | ||||
|     Entity entity = _createEntityInstance(rawEntityData); | ||||
|     _entities[entity.entityId] = entity; | ||||
|     return entity; | ||||
|     _allEntities[entity.entityId] = entity; | ||||
|   } | ||||
|  | ||||
|   void updateFromRaw(Map rawEntityData) { | ||||
|     //TODO pass entity in this function and call update from it | ||||
|     _entities[rawEntityData["entity_id"]].update(rawEntityData); | ||||
|     get("${rawEntityData["entity_id"]}")?.update(rawEntityData, homeAssistantWebHost); | ||||
|   } | ||||
|  | ||||
|   Entity get(String entityId) { | ||||
|     return _entities[entityId]; | ||||
|     return _allEntities[entityId]; | ||||
|   } | ||||
|  | ||||
|   List<Entity> getAll(List ids) { | ||||
|     List<Entity> result = []; | ||||
|     ids.forEach((id){ | ||||
|       Entity en = get(id); | ||||
|       if (en != null) { | ||||
|         result.add(en); | ||||
|       } | ||||
|     }); | ||||
|     return result; | ||||
|   } | ||||
|  | ||||
|   bool isExist(String entityId) { | ||||
|     return _entities[entityId] != null; | ||||
|     return _allEntities[entityId] != null; | ||||
|   } | ||||
|  | ||||
|   Map<String,List<String>> getDefaultViewTopLevelEntities() { | ||||
|     Map<String,List<String>> result = {"userGroups": [], "notGroupedEntities": []}; | ||||
|     List<String> entities = []; | ||||
|     _entities.forEach((id, entity){ | ||||
|       if ((id.indexOf("group.") == 0) && (id.indexOf(".all_") == -1) && (!entity.isView)) { | ||||
|         result["userGroups"].add(id); | ||||
|   List<Entity> filterEntitiesForDefaultView() { | ||||
|     List<Entity> result = []; | ||||
|     List<Entity> groups = []; | ||||
|     List<Entity> nonGroupEntities = []; | ||||
|     _allEntities.forEach((id, entity){ | ||||
|       if (entity.isGroup && (entity.attributes['auto'] == null || (entity.attributes['auto'] && !entity.isHidden)) && (!entity.isView)) { | ||||
|         groups.add(entity); | ||||
|       } | ||||
|       if (!entity.isGroup) { | ||||
|         entities.add(id); | ||||
|         nonGroupEntities.add(entity); | ||||
|       } | ||||
|     }); | ||||
|  | ||||
|     entities.forEach((entiyId) { | ||||
|     nonGroupEntities.forEach((entity) { | ||||
|       bool foundInGroup = false; | ||||
|       result["userGroups"].forEach((userGroupId) { | ||||
|         if (_entities[userGroupId].childEntities.contains(entiyId)) { | ||||
|       groups.forEach((groupEntity) { | ||||
|         if (groupEntity.childEntityIds.contains(entity.entityId)) { | ||||
|           foundInGroup = true; | ||||
|         } | ||||
|       }); | ||||
|       if (!foundInGroup) { | ||||
|         result["notGroupedEntities"].add(entiyId); | ||||
|         result.add(entity); | ||||
|       } | ||||
|     }); | ||||
|     result.insertAll(0, groups); | ||||
|  | ||||
|     return result; | ||||
|   } | ||||
|  | ||||
| } | ||||
							
								
								
									
										50
									
								
								lib/entity_widgets/button_entity_container.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,50 @@ | ||||
| part of '../main.dart'; | ||||
|  | ||||
| class ButtonEntityContainer extends StatelessWidget { | ||||
|  | ||||
|   ButtonEntityContainer({ | ||||
|     Key key, | ||||
|   }) : super(key: key); | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context) { | ||||
|     final EntityWrapper entityWrapper = EntityModel.of(context).entityWrapper; | ||||
|     if (entityWrapper.entity.statelessType == StatelessEntityType.MISSED) { | ||||
|       return MissedEntityWidget(); | ||||
|     } | ||||
|     if (entityWrapper.entity.statelessType > StatelessEntityType.MISSED) { | ||||
|       return Container(width: 0.0, height: 0.0,); | ||||
|     } | ||||
|     return InkWell( | ||||
|       onTap: () => entityWrapper.handleTap(), | ||||
|       onLongPress: () => entityWrapper.handleHold(), | ||||
|       child: Column( | ||||
|         mainAxisSize: MainAxisSize.min, | ||||
|         children: <Widget>[ | ||||
|           FractionallySizedBox( | ||||
|             widthFactor: 0.4, | ||||
|             child: FittedBox( | ||||
|                 fit: BoxFit.fitHeight, | ||||
|                 child: EntityIcon( | ||||
|                   padding: EdgeInsets.fromLTRB(2.0, 6.0, 2.0, 2.0), | ||||
|                   size: Sizes.iconSize, | ||||
|                 ) | ||||
|             ), | ||||
|           ), | ||||
|           _buildName() | ||||
|         ], | ||||
|       ), | ||||
|     ); | ||||
|   } | ||||
|  | ||||
|   Widget _buildName() { | ||||
|     return EntityName( | ||||
|       padding: EdgeInsets.fromLTRB(Sizes.buttonPadding, 0.0, Sizes.buttonPadding, Sizes.rowPadding), | ||||
|       textOverflow: TextOverflow.ellipsis, | ||||
|       maxLines: 3, | ||||
|       wordsWrap: true, | ||||
|       textAlign: TextAlign.center, | ||||
|       fontSize: Sizes.nameFontSize, | ||||
|     ); | ||||
|   } | ||||
| } | ||||
							
								
								
									
										134
									
								
								lib/entity_widgets/common/badge.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,134 @@ | ||||
| part of '../../main.dart'; | ||||
|  | ||||
| class BadgeWidget extends StatelessWidget { | ||||
|   @override | ||||
|   Widget build(BuildContext context) { | ||||
|     final entityModel = EntityModel.of(context); | ||||
|     double iconSize = 26.0; | ||||
|     Widget badgeIcon; | ||||
|     String onBadgeTextValue; | ||||
|     Color iconColor = EntityColor.badgeColors[entityModel.entityWrapper.entity.domain] ?? | ||||
|         EntityColor.badgeColors["default"]; | ||||
|     switch (entityModel.entityWrapper.entity.domain) { | ||||
|       case "sun": | ||||
|         { | ||||
|           badgeIcon = entityModel.entityWrapper.entity.state == "below_horizon" | ||||
|               ? Icon( | ||||
|             MaterialDesignIcons.getIconDataFromIconCode(0xf0dc), | ||||
|             size: iconSize, | ||||
|           ) | ||||
|               : Icon( | ||||
|             MaterialDesignIcons.getIconDataFromIconCode(0xf5a8), | ||||
|             size: iconSize, | ||||
|           ); | ||||
|           break; | ||||
|         } | ||||
|       case "camera": | ||||
|       case "media_player": | ||||
|       case "binary_sensor": | ||||
|         { | ||||
|           badgeIcon = EntityIcon( | ||||
|             padding: EdgeInsets.all(0.0), | ||||
|             size: iconSize, | ||||
|             color: Colors.black | ||||
|           ); | ||||
|           break; | ||||
|         } | ||||
|       case "device_tracker": | ||||
|         { | ||||
|           badgeIcon = EntityIcon( | ||||
|               padding: EdgeInsets.all(0.0), | ||||
|               size: iconSize, | ||||
|               color: Colors.black | ||||
|           ); | ||||
|           onBadgeTextValue = entityModel.entityWrapper.entity.state; | ||||
|           break; | ||||
|         } | ||||
|       default: | ||||
|         { | ||||
|           onBadgeTextValue = entityModel.entityWrapper.entity.unitOfMeasurement; | ||||
|           badgeIcon = Center( | ||||
|             child: Text( | ||||
|               "${entityModel.entityWrapper.entity.state}", | ||||
|               overflow: TextOverflow.fade, | ||||
|               softWrap: false, | ||||
|               textAlign: TextAlign.center, | ||||
|               style: TextStyle(fontSize: 17.0), | ||||
|             ), | ||||
|           ); | ||||
|           break; | ||||
|         } | ||||
|     } | ||||
|     Widget onBadgeText; | ||||
|     if (onBadgeTextValue == null || onBadgeTextValue.length == 0) { | ||||
|       onBadgeText = Container(width: 0.0, height: 0.0); | ||||
|     } else { | ||||
|       onBadgeText = Container( | ||||
|           padding: EdgeInsets.fromLTRB(6.0, 2.0, 6.0, 2.0), | ||||
|           child: Text("$onBadgeTextValue", | ||||
|               style: TextStyle(fontSize: 12.0, color: Colors.white), | ||||
|               textAlign: TextAlign.center, | ||||
|               softWrap: false, | ||||
|               overflow: TextOverflow.fade), | ||||
|           decoration: new BoxDecoration( | ||||
|             // Circle shape | ||||
|             //shape: BoxShape.circle, | ||||
|             color: iconColor, | ||||
|             borderRadius: BorderRadius.circular(9.0), | ||||
|           )); | ||||
|     } | ||||
|     return GestureDetector( | ||||
|         child: Column( | ||||
|           children: <Widget>[ | ||||
|             Container( | ||||
|               margin: EdgeInsets.fromLTRB(0.0, 10.0, 0.0, 10.0), | ||||
|               width: 50.0, | ||||
|               height: 50.0, | ||||
|               decoration: new BoxDecoration( | ||||
|                 // Circle shape | ||||
|                 shape: BoxShape.circle, | ||||
|                 color: Colors.white, | ||||
|                 // The border you want | ||||
|                 border: new Border.all( | ||||
|                   width: 2.0, | ||||
|                   color: iconColor, | ||||
|                 ), | ||||
|               ), | ||||
|               child: Stack( | ||||
|                 overflow: Overflow.visible, | ||||
|                 children: <Widget>[ | ||||
|                   Positioned( | ||||
|                     width: 46.0, | ||||
|                     height: 46.0, | ||||
|                     top: 0.0, | ||||
|                     left: 0.0, | ||||
|                     child: badgeIcon, | ||||
|                   ), | ||||
|                   Positioned( | ||||
|                     //width: 50.0, | ||||
|                       bottom: -9.0, | ||||
|                       left: -10.0, | ||||
|                       right: -10.0, | ||||
|                       child: Center( | ||||
|                         child: onBadgeText, | ||||
|                       )) | ||||
|                 ], | ||||
|               ), | ||||
|             ), | ||||
|             Container( | ||||
|               width: 60.0, | ||||
|               child: Text( | ||||
|                 "${entityModel.entityWrapper.displayName}", | ||||
|                 textAlign: TextAlign.center, | ||||
|                 style: TextStyle(fontSize: 12.0), | ||||
|                 softWrap: true, | ||||
|                 maxLines: 3, | ||||
|                 overflow: TextOverflow.ellipsis, | ||||
|               ), | ||||
|             ), | ||||
|           ], | ||||
|         ), | ||||
|         onTap: () => | ||||
|             eventBus.fire(new ShowEntityPageEvent(entityModel.entityWrapper.entity))); | ||||
|   } | ||||
| } | ||||
							
								
								
									
										177
									
								
								lib/entity_widgets/common/camera_stream_view.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,177 @@ | ||||
| part of '../../main.dart'; | ||||
|  | ||||
| class CameraStreamView extends StatefulWidget { | ||||
|  | ||||
|   CameraStreamView({Key key}) : super(key: key); | ||||
|  | ||||
|   @override | ||||
|   _CameraStreamViewState createState() => _CameraStreamViewState(); | ||||
| } | ||||
|  | ||||
| class _CameraStreamViewState extends State<CameraStreamView> { | ||||
|  | ||||
|   @override | ||||
|   void initState() { | ||||
|     super.initState(); | ||||
|   } | ||||
|  | ||||
|   CameraEntity _entity; | ||||
|   String _webHost; | ||||
|  | ||||
|   http.Client client; | ||||
|   http.StreamedResponse response; | ||||
|   List<int> binaryImage = []; | ||||
|   bool timeToStop = false; | ||||
|   Completer streamCompleter; | ||||
|   bool started = false; | ||||
|   bool useSVG = false; | ||||
|  | ||||
|   void _connect() async { | ||||
|     started = true; | ||||
|     timeToStop = false; | ||||
|     String streamUrl = '$_webHost/api/camera_proxy_stream/${_entity.entityId}?token=${_entity.attributes['access_token']}'; | ||||
|     client = new http.Client(); // create a client to make api calls | ||||
|     http.Request request = new http.Request("GET", Uri.parse(streamUrl));  // create get request | ||||
|     Logger.d("[Sending] ==> $streamUrl"); | ||||
|     response = await client.send(request); | ||||
|     Logger.d("[Received] <== ${response.headers}"); | ||||
|     String frameBoundary = response.headers['content-type'].split('boundary=')[1]; | ||||
|     final int frameBoundarySize = frameBoundary.length; | ||||
|     List<int> primaryBuffer=[]; | ||||
|     int imageSizeStart = 59; | ||||
|     int imageSizeEnd = 0; | ||||
|     int imageStart = 0; | ||||
|     int imageSize = 0; | ||||
|     String strBuffer = ""; | ||||
|     String contentType = ""; | ||||
|     streamCompleter = Completer(); | ||||
|     response.stream.transform( | ||||
|         StreamTransformer.fromHandlers( | ||||
|           handleData: (data, sink) { | ||||
|             primaryBuffer.addAll(data); | ||||
|             imageStart = 0; | ||||
|             imageSizeEnd = 0; | ||||
|             if (primaryBuffer.length >= imageSizeStart + 10) { | ||||
|               contentType = utf8.decode( | ||||
|                   primaryBuffer.sublist(frameBoundarySize+16, imageSizeStart + 10), allowMalformed: true).split("\r\n")[0]; | ||||
|               useSVG = contentType == "image/svg+xml"; | ||||
|               imageSizeStart = frameBoundarySize + 16 + contentType.length + 18; | ||||
|               for (int i = imageSizeStart; i < primaryBuffer.length - 4; i++) { | ||||
|                 strBuffer = utf8.decode( | ||||
|                     primaryBuffer.sublist(i, i + 4), allowMalformed: true); | ||||
|                 if (strBuffer == "\r\n\r\n") { | ||||
|                   imageSizeEnd = i; | ||||
|                   imageStart = i + 4; | ||||
|                   break; | ||||
|                 } | ||||
|               } | ||||
|               if (imageSizeEnd > 0) { | ||||
|                 imageSize = int.tryParse(utf8.decode( | ||||
|                     primaryBuffer.sublist(imageSizeStart, imageSizeEnd), | ||||
|                     allowMalformed: true)); | ||||
|                 //Logger.d("content-length: $imageSize"); | ||||
|                 if (imageSize != null && | ||||
|                     primaryBuffer.length >= imageStart + imageSize + 2) { | ||||
|                   sink.add( | ||||
|                       primaryBuffer.sublist( | ||||
|                           imageStart, imageStart + imageSize)); | ||||
|                   primaryBuffer.removeRange(0, imageStart + imageSize + 2); | ||||
|                 } | ||||
|               } | ||||
|             } | ||||
|             if (timeToStop) { | ||||
|               sink?.close(); | ||||
|               streamCompleter.complete(); | ||||
|             } | ||||
|           }, | ||||
|           handleError: (error, stack, sink) { | ||||
|             Logger.e("Error parsing MJPEG stream: $error"); | ||||
|           }, | ||||
|           handleDone: (sink) { | ||||
|             Logger.d("Camera stream finished. Reconnecting..."); | ||||
|             sink?.close(); | ||||
|             streamCompleter?.complete(); | ||||
|             _reconnect(); | ||||
|           }, | ||||
|         ) | ||||
|     ).listen((d) { | ||||
|       if (!timeToStop) { | ||||
|         setState(() { | ||||
|           binaryImage = d; | ||||
|         }); | ||||
|       } | ||||
|     }); | ||||
|   } | ||||
|  | ||||
|   void _reconnect() { | ||||
|     disconnect().then((_){ | ||||
|       _connect(); | ||||
|     }); | ||||
|   } | ||||
|  | ||||
|   Future disconnect() { | ||||
|     Completer disconF = Completer(); | ||||
|     timeToStop = true; | ||||
|     if (streamCompleter != null && !streamCompleter.isCompleted) { | ||||
|       streamCompleter.future.then((_) { | ||||
|         client?.close(); | ||||
|         disconF.complete(); | ||||
|       }); | ||||
|     } else { | ||||
|       client?.close(); | ||||
|       disconF.complete(); | ||||
|     } | ||||
|     return disconF.future; | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context) { | ||||
|     if (!started) { | ||||
|       _entity = EntityModel | ||||
|           .of(context) | ||||
|           .entityWrapper | ||||
|           .entity; | ||||
|       _webHost = HomeAssistantModel.of(context).homeAssistant.connection.httpWebHost; | ||||
|       _connect(); | ||||
|     } | ||||
|  | ||||
|     if (binaryImage.isEmpty) { | ||||
|       return Column( | ||||
|         children: <Widget>[ | ||||
|           Container( | ||||
|               padding: const EdgeInsets.all(20.0), | ||||
|               child: const CircularProgressIndicator() | ||||
|           ) | ||||
|         ], | ||||
|       ); | ||||
|     } else { | ||||
|       if (useSVG) { | ||||
|         return Column( | ||||
|           children: <Widget>[ | ||||
|             SvgPicture.memory( | ||||
|               Uint8List.fromList(binaryImage), | ||||
|               placeholderBuilder: (BuildContext context) => | ||||
|               new Container( | ||||
|                   padding: const EdgeInsets.all(20.0), | ||||
|                   child: const CircularProgressIndicator() | ||||
|               ), | ||||
|             ) | ||||
|           ], | ||||
|         ); | ||||
|       } else { | ||||
|         return Column( | ||||
|           children: <Widget>[ | ||||
|             Image.memory( | ||||
|                 Uint8List.fromList(binaryImage), gaplessPlayback: true), | ||||
|           ], | ||||
|         ); | ||||
|       } | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   void dispose() { | ||||
|     disconnect(); | ||||
|     super.dispose(); | ||||
|   } | ||||
| } | ||||
							
								
								
									
										49
									
								
								lib/entity_widgets/common/entity_attributes_list.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,49 @@ | ||||
| part of '../../main.dart'; | ||||
|  | ||||
| class EntityAttributesList extends StatelessWidget { | ||||
|   EntityAttributesList({Key key}) : super(key: key); | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context) { | ||||
|     final entityModel = EntityModel.of(context); | ||||
|     List<Widget> attrs = []; | ||||
|     entityModel.entityWrapper.entity.attributes.forEach((name, value) { | ||||
|       attrs.add(_buildSingleAttribute("$name", "${value ?? '-'}")); | ||||
|     }); | ||||
|     return Padding( | ||||
|       padding: EdgeInsets.only(bottom: Sizes.rowPadding), | ||||
|       child: Column( | ||||
|         children: attrs, | ||||
|         crossAxisAlignment: CrossAxisAlignment.start, | ||||
|         mainAxisSize: MainAxisSize.min, | ||||
|       ) | ||||
|     ); | ||||
|   } | ||||
|  | ||||
|   Widget _buildSingleAttribute(String name, String value) { | ||||
|     return Row( | ||||
|       children: <Widget>[ | ||||
|         Expanded( | ||||
|           child: Padding( | ||||
|             padding: EdgeInsets.fromLTRB( | ||||
|                 Sizes.leftWidgetPadding, Sizes.rowPadding, 0.0, 0.0), | ||||
|             child: Text( | ||||
|               "$name", | ||||
|               textAlign: TextAlign.left, | ||||
|             ), | ||||
|           ), | ||||
|         ), | ||||
|         Expanded( | ||||
|           child: Padding( | ||||
|             padding: EdgeInsets.fromLTRB( | ||||
|                 0.0, Sizes.rowPadding, Sizes.rightWidgetPadding, 0.0), | ||||
|             child: Text( | ||||
|               "${value}", | ||||
|               textAlign: TextAlign.right, | ||||
|             ), | ||||
|           ), | ||||
|         ) | ||||
|       ], | ||||
|     ); | ||||
|   } | ||||
| } | ||||
							
								
								
									
										41
									
								
								lib/entity_widgets/common/flat_service_button.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,41 @@ | ||||
| part of '../../main.dart'; | ||||
|  | ||||
| class FlatServiceButton extends StatelessWidget { | ||||
|  | ||||
|   final String serviceDomain; | ||||
|   final String serviceName; | ||||
|   final String entityId; | ||||
|   final String text; | ||||
|   final double fontSize; | ||||
|  | ||||
|   FlatServiceButton({ | ||||
|     Key key, | ||||
|     @required this.serviceDomain, | ||||
|     @required this.serviceName, | ||||
|     @required this.entityId, | ||||
|     @required this.text, | ||||
|     this.fontSize: Sizes.stateFontSize | ||||
|   }) : super(key: key); | ||||
|  | ||||
|   void _setNewState() { | ||||
|     eventBus.fire(new ServiceCallEvent(serviceDomain, serviceName, entityId, null)); | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context) { | ||||
|     return SizedBox( | ||||
|         height: fontSize*2.5, | ||||
|         child: FlatButton( | ||||
|           onPressed: (() { | ||||
|             _setNewState(); | ||||
|           }), | ||||
|           child: Text( | ||||
|             text, | ||||
|             textAlign: TextAlign.right, | ||||
|             style: | ||||
|             new TextStyle(fontSize: fontSize, color: Colors.blue), | ||||
|           ), | ||||
|         ) | ||||
|     ); | ||||
|   } | ||||
| } | ||||
							
								
								
									
										18
									
								
								lib/entity_widgets/common/last_updated.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,18 @@ | ||||
| part of '../../main.dart'; | ||||
|  | ||||
| class LastUpdatedWidget extends StatelessWidget { | ||||
|   @override | ||||
|   Widget build(BuildContext context) { | ||||
|     final entityModel = EntityModel.of(context); | ||||
|     return Padding( | ||||
|       padding: EdgeInsets.fromLTRB( | ||||
|           Sizes.leftWidgetPadding, Sizes.rowPadding, 0.0, 0.0), | ||||
|       child: Text( | ||||
|         '${entityModel.entityWrapper.entity.lastUpdated}', | ||||
|         textAlign: TextAlign.left, | ||||
|         style: TextStyle( | ||||
|             fontSize: Sizes.smallFontSize, color: Colors.black26), | ||||
|       ), | ||||
|     ); | ||||
|   } | ||||
| } | ||||
							
								
								
									
										101
									
								
								lib/entity_widgets/common/light_color_picker.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,101 @@ | ||||
| part of '../../main.dart'; | ||||
|  | ||||
| class LightColorPicker extends StatefulWidget { | ||||
|  | ||||
|   final HSVColor color; | ||||
|   final onColorSelected; | ||||
|   final double hueStep; | ||||
|   final double saturationStep; | ||||
|   final EdgeInsets padding; | ||||
|  | ||||
|   LightColorPicker({this.color, this.onColorSelected, this.hueStep: 15.0, this.saturationStep: 0.2, this.padding: const EdgeInsets.fromLTRB(Sizes.leftWidgetPadding, Sizes.rowPadding, Sizes.rightWidgetPadding, 0.0)}); | ||||
|  | ||||
|   @override | ||||
|   LightColorPickerState createState() => new LightColorPickerState(); | ||||
| } | ||||
|  | ||||
| class LightColorPickerState extends State<LightColorPicker> { | ||||
|  | ||||
|   @override | ||||
|   void initState() { | ||||
|     super.initState(); | ||||
|  | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context) { | ||||
|     List<Widget> colorRows = []; | ||||
|     Border border; | ||||
|     bool isSomethingSelected = false; | ||||
|     Logger.d("Current colotfor picker: [${widget.color.hue}, ${widget.color.saturation}]"); | ||||
|     for (double saturation = 1.0; saturation >= (0.0 + widget.saturationStep); saturation = double.parse((saturation - widget.saturationStep).toStringAsFixed(2))) { | ||||
|       List<Widget> rowChildren = []; | ||||
|       //Logger.d("$saturation"); | ||||
|       double roundedSaturation = double.parse(widget.color.saturation.toStringAsFixed(1)); | ||||
|       //Logger.d("Rounded saturation=$roundedSaturation"); | ||||
|       for (double hue = 0; hue <= (365 - widget.hueStep); | ||||
|       hue += widget.hueStep) { | ||||
|         bool isExactHue = widget.color.hue.round() == hue; | ||||
|         bool isHueInRange = widget.color.hue.round() > hue && widget.color.hue.round() < (hue+widget.hueStep); | ||||
|         bool isExactSaturation = roundedSaturation == saturation; | ||||
|         bool isSaturationInRange = roundedSaturation > saturation && roundedSaturation < double.parse((saturation+widget.saturationStep).toStringAsFixed(1)); | ||||
|         if ((isExactHue || isHueInRange) && (isExactSaturation || isSaturationInRange)) { | ||||
|           //Logger.d("$isExactHue $isHueInRange $isExactSaturation $isSaturationInRange (${saturation+widget.saturationStep})"); | ||||
|           border = Border.all( | ||||
|             width: 2.0, | ||||
|             color: Colors.white, | ||||
|           ); | ||||
|           isSomethingSelected = true; | ||||
|         } else { | ||||
|           border = null; | ||||
|         } | ||||
|         HSVColor currentColor = HSVColor.fromAHSV(1.0, hue, double.parse(saturation.toStringAsFixed(2)), 1.0); | ||||
|         rowChildren.add( | ||||
|             Flexible( | ||||
|                 child: GestureDetector( | ||||
|                   child: Container( | ||||
|                     height: 40.0, | ||||
|                     decoration: BoxDecoration( | ||||
|                         color: currentColor.toColor(), | ||||
|                         border: border, | ||||
|                     ), | ||||
|                   ), | ||||
|                   onTap: () => widget.onColorSelected(currentColor), | ||||
|                 ) | ||||
|             ) | ||||
|         ); | ||||
|       } | ||||
|       colorRows.add( | ||||
|           Row( | ||||
|             children: rowChildren, | ||||
|           ) | ||||
|       ); | ||||
|     } | ||||
|     colorRows.add( | ||||
|         Flexible( | ||||
|             child: GestureDetector( | ||||
|               child: Container( | ||||
|                 height: 40.0, | ||||
|                 decoration: BoxDecoration( | ||||
|                   color: Colors.white, | ||||
|                   border: isSomethingSelected ? null : Border.all( | ||||
|                     width: 2.0, | ||||
|                     color: Colors.amber[200], | ||||
|                   ) | ||||
|                 ), | ||||
|               ), | ||||
|               onTap: () => widget.onColorSelected(HSVColor.fromAHSV(1.0, 30.0, 0.0, 1.0)), | ||||
|             ) | ||||
|         ) | ||||
|     ); | ||||
|     return Padding( | ||||
|       child: Column( | ||||
|         mainAxisSize: MainAxisSize.min, | ||||
|         crossAxisAlignment: CrossAxisAlignment.center, | ||||
|         children: colorRows, | ||||
|       ), | ||||
|       padding: widget.padding, | ||||
|     ); | ||||
|  | ||||
|   } | ||||
| } | ||||
							
								
								
									
										64
									
								
								lib/entity_widgets/common/mode_selector.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,64 @@ | ||||
| part of '../../main.dart'; | ||||
|  | ||||
| class ModeSelectorWidget extends StatelessWidget { | ||||
|  | ||||
|   final String caption; | ||||
|   final List<String> options; | ||||
|   final String value; | ||||
|   final double captionFontSize; | ||||
|   final double valueFontSize; | ||||
|   final onChange; | ||||
|   final EdgeInsets padding; | ||||
|  | ||||
|   ModeSelectorWidget({ | ||||
|     Key key, | ||||
|     @required this.caption, | ||||
|     @required this.options, | ||||
|     this.value, | ||||
|     @required this.onChange, | ||||
|     this.captionFontSize, | ||||
|     this.valueFontSize, | ||||
|     this.padding: const EdgeInsets.fromLTRB(Sizes.leftWidgetPadding, Sizes.rowPadding, Sizes.rightWidgetPadding, 0.0), | ||||
|   }) : super(key: key); | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context) { | ||||
|     return Padding( | ||||
|       padding: padding, | ||||
|       child: Column( | ||||
|         crossAxisAlignment: CrossAxisAlignment.start, | ||||
|         children: <Widget>[ | ||||
|           Text("$caption", style: TextStyle( | ||||
|               fontSize: captionFontSize ?? Sizes.stateFontSize | ||||
|           )), | ||||
|           Row( | ||||
|             children: <Widget>[ | ||||
|               Expanded( | ||||
|                 child: ButtonTheme( | ||||
|                   alignedDropdown: true, | ||||
|                   child: DropdownButton<String>( | ||||
|                     value: value, | ||||
|                     iconSize: 30.0, | ||||
|                     isExpanded: true, | ||||
|                     style: TextStyle( | ||||
|                       fontSize: valueFontSize ?? Sizes.largeFontSize, | ||||
|                       color: Colors.black, | ||||
|                     ), | ||||
|                     hint: Text("Select ${caption.toLowerCase()}"), | ||||
|                     items: options.map((String value) { | ||||
|                       return new DropdownMenuItem<String>( | ||||
|                         value: value, | ||||
|                         child: Text(value), | ||||
|                       ); | ||||
|                     }).toList(), | ||||
|                     onChanged: (mode) => onChange(mode), | ||||
|                   ), | ||||
|                 ), | ||||
|               ) | ||||
|             ], | ||||
|           ) | ||||
|         ], | ||||
|       ), | ||||
|     ); | ||||
|   } | ||||
| } | ||||
							
								
								
									
										53
									
								
								lib/entity_widgets/common/mode_swicth.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,53 @@ | ||||
| part of '../../main.dart'; | ||||
|  | ||||
| class ModeSwitchWidget extends StatelessWidget { | ||||
|  | ||||
|   final String caption; | ||||
|   final onChange; | ||||
|   final double captionFontSize; | ||||
|   final bool value; | ||||
|   final bool expanded; | ||||
|   final EdgeInsets padding; | ||||
|  | ||||
|   ModeSwitchWidget({ | ||||
|     Key key, | ||||
|     @required this.caption, | ||||
|     @required this.onChange, | ||||
|     this.captionFontSize, | ||||
|     this.value, | ||||
|     this.expanded: true, | ||||
|     this.padding: const EdgeInsets.only(left: Sizes.leftWidgetPadding, right: Sizes.rightWidgetPadding) | ||||
|   }) : super(key: key); | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context) { | ||||
|     return Padding( | ||||
|       padding: this.padding, | ||||
|       child: Row( | ||||
|         children: <Widget>[ | ||||
|           _buildCaption(), | ||||
|           Switch( | ||||
|             onChanged: (value) => onChange(value), | ||||
|             value: value ?? false, | ||||
|           ) | ||||
|         ], | ||||
|       ) | ||||
|     ); | ||||
|   } | ||||
|  | ||||
|   Widget _buildCaption() { | ||||
|     Widget captionWidget = Text( | ||||
|       "$caption", | ||||
|       style: TextStyle( | ||||
|           fontSize: captionFontSize ?? Sizes.stateFontSize | ||||
|       ), | ||||
|     ); | ||||
|     if (expanded) { | ||||
|       return Expanded( | ||||
|         child: captionWidget, | ||||
|       ); | ||||
|     } | ||||
|     return captionWidget; | ||||
|   } | ||||
|  | ||||
| } | ||||
							
								
								
									
										58
									
								
								lib/entity_widgets/common/universal_slider.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,58 @@ | ||||
| part of '../../main.dart'; | ||||
|  | ||||
| class UniversalSlider extends StatelessWidget { | ||||
|  | ||||
|   final onChanged; | ||||
|   final onChangeEnd; | ||||
|   final Widget leading; | ||||
|   final Widget closing; | ||||
|   final String title; | ||||
|   final double min; | ||||
|   final double max; | ||||
|   final double value; | ||||
|   final EdgeInsets padding; | ||||
|  | ||||
|   const UniversalSlider({Key key, this.onChanged, this.onChangeEnd, this.leading, this.closing, this.title, this.min, this.max, this.value, this.padding: const EdgeInsets.fromLTRB(Sizes.leftWidgetPadding, Sizes.rowPadding, Sizes.rightWidgetPadding, 0.0)}) : super(key: key); | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context) { | ||||
|     List <Widget> row = []; | ||||
|     if (leading != null) { | ||||
|       row.add(leading); | ||||
|     } | ||||
|     row.add( | ||||
|         Flexible( | ||||
|           child: Slider( | ||||
|             value: value, | ||||
|             min: min, | ||||
|             max: max, | ||||
|             onChanged: (value) => onChanged(value), | ||||
|             onChangeEnd: (value) => onChangeEnd(value), | ||||
|           ), | ||||
|         ) | ||||
|     ); | ||||
|     if (closing != null) { | ||||
|       row.add(closing); | ||||
|     } | ||||
|     return Padding( | ||||
|       padding: padding, | ||||
|       child: Column( | ||||
|         crossAxisAlignment: CrossAxisAlignment.start, | ||||
|         children: <Widget>[ | ||||
|           Container(height: Sizes.rowPadding,), | ||||
|           Text( | ||||
|             "$title", | ||||
|             style: TextStyle(fontSize: Sizes.stateFontSize), | ||||
|           ), | ||||
|           Container(height: Sizes.rowPadding,), | ||||
|           Row( | ||||
|             mainAxisSize: MainAxisSize.min, | ||||
|             children: row, | ||||
|           ), | ||||
|           Container(height: Sizes.rowPadding,) | ||||
|         ], | ||||
|       ), | ||||
|     ); | ||||
|   } | ||||
|  | ||||
| } | ||||
							
								
								
									
										262
									
								
								lib/entity_widgets/controls/alarm_control_panel_controls.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,262 @@ | ||||
| part of '../../main.dart'; | ||||
|  | ||||
| class AlarmControlPanelControlsWidget extends StatefulWidget { | ||||
|  | ||||
|   final bool extended; | ||||
|   final List states; | ||||
|  | ||||
|   const AlarmControlPanelControlsWidget({Key key, @required this.extended, this.states}) : super(key: key); | ||||
|  | ||||
|   @override | ||||
|   _AlarmControlPanelControlsWidgetWidgetState createState() => _AlarmControlPanelControlsWidgetWidgetState(); | ||||
|  | ||||
| } | ||||
|  | ||||
| class _AlarmControlPanelControlsWidgetWidgetState extends State<AlarmControlPanelControlsWidget> { | ||||
|  | ||||
|   String code = ""; | ||||
|   List supportedStates; | ||||
|  | ||||
|   @override | ||||
|   void initState() { | ||||
|     super.initState(); | ||||
|     supportedStates = widget.states ?? ["arm_home", "arm_away"]; | ||||
|   } | ||||
|  | ||||
|  | ||||
|   void _callService(AlarmControlPanelEntity entity, String service) { | ||||
|     eventBus.fire(new ServiceCallEvent( | ||||
|           entity.domain, service, entity.entityId, | ||||
|           {"code": "$code"})); | ||||
|     setState(() { | ||||
|       code = ""; | ||||
|     }); | ||||
|   } | ||||
|  | ||||
|   void _pinPadHandler(value) { | ||||
|     setState(() { | ||||
|       code += "$value"; | ||||
|     }); | ||||
|   } | ||||
|  | ||||
|   void _pinPadClear() { | ||||
|     setState(() { | ||||
|       code = ""; | ||||
|     }); | ||||
|   } | ||||
|  | ||||
|   void _askToTrigger(AlarmControlPanelEntity entity) { | ||||
|     // flutter defined function | ||||
|     showDialog( | ||||
|       context: context, | ||||
|       builder: (BuildContext context) { | ||||
|         // return object of type Dialog | ||||
|         return AlertDialog( | ||||
|           title: new Text("Are you sure?"), | ||||
|           content: new Text("Are you sure want to trigger alarm ${entity.displayName}?"), | ||||
|           actions: <Widget>[ | ||||
|             FlatButton( | ||||
|               child: new Text("Yes"), | ||||
|               onPressed: () { | ||||
|                 eventBus.fire(new ServiceCallEvent(entity.domain, "alarm_trigger", entity.entityId, null)); | ||||
|                 Navigator.of(context).pop(); | ||||
|               }, | ||||
|             ), | ||||
|             FlatButton( | ||||
|               child: new Text("No"), | ||||
|               onPressed: () { | ||||
|                 Navigator.of(context).pop(); | ||||
|               }, | ||||
|             ), | ||||
|           ], | ||||
|         ); | ||||
|       }, | ||||
|     ); | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context) { | ||||
|     final entityModel = EntityModel.of(context); | ||||
|     final AlarmControlPanelEntity entity = entityModel.entityWrapper.entity; | ||||
|     List<Widget> buttons = []; | ||||
|     if (entity.state == EntityState.alarm_disarmed) { | ||||
|       if (supportedStates.contains("arm_home")) { | ||||
|         buttons.add( | ||||
|           RaisedButton( | ||||
|             onPressed: () => _callService(entity, "alarm_arm_home"), | ||||
|             child: Text("ARM HOME"), | ||||
|           ) | ||||
|         ); | ||||
|       } | ||||
|       if (supportedStates.contains("arm_away")) { | ||||
|         buttons.add( | ||||
|             RaisedButton( | ||||
|               onPressed: () => _callService(entity, "alarm_arm_away"), | ||||
|               child: Text("ARM AWAY"), | ||||
|             ) | ||||
|         ); | ||||
|       } | ||||
|       if (widget.extended) { | ||||
|         if (supportedStates.contains("arm_night")) { | ||||
|           buttons.add( | ||||
|               RaisedButton( | ||||
|                 onPressed: () => _callService(entity, "alarm_arm_night"), | ||||
|                 child: Text("ARM NIGHT"), | ||||
|               ) | ||||
|           ); | ||||
|         } | ||||
|         if (supportedStates.contains("arm_custom_bypass")) { | ||||
|           buttons.add( | ||||
|               RaisedButton( | ||||
|                 onPressed: () => | ||||
|                     _callService(entity, "alarm_arm_custom_bypass"), | ||||
|                 child: Text("ARM CUSTOM BYPASS"), | ||||
|               ) | ||||
|           ); | ||||
|         } | ||||
|       } | ||||
|     } else { | ||||
|       buttons.add( | ||||
|         RaisedButton( | ||||
|           onPressed: () => _callService(entity, "alarm_disarm"), | ||||
|           child: Text("DISARM"), | ||||
|         ) | ||||
|       ); | ||||
|     } | ||||
|     Widget pinPad; | ||||
|     if (entity.attributes["code_format"] == null) { | ||||
|       pinPad = Container(width: 0.0, height: 0.0,); | ||||
|     } else { | ||||
|       pinPad = Padding( | ||||
|           padding: EdgeInsets.only(bottom: Sizes.rowPadding), | ||||
|           child: Column( | ||||
|             mainAxisSize: MainAxisSize.min, | ||||
|             children: <Widget>[ | ||||
|               Wrap( | ||||
|                 spacing: 5.0, | ||||
|                 children: <Widget>[ | ||||
|                   RaisedButton( | ||||
|                     onPressed: () => _pinPadHandler("1"), | ||||
|                     child: Text("1"), | ||||
|                   ), | ||||
|                   RaisedButton( | ||||
|                     onPressed: () => _pinPadHandler("2"), | ||||
|                     child: Text("2"), | ||||
|                   ), | ||||
|                   RaisedButton( | ||||
|                     onPressed: () => _pinPadHandler("3"), | ||||
|                     child: Text("3"), | ||||
|                   ) | ||||
|                 ], | ||||
|               ), | ||||
|               Wrap( | ||||
|                 spacing: 5.0, | ||||
|                 children: <Widget>[ | ||||
|                   RaisedButton( | ||||
|                     onPressed: () => _pinPadHandler("4"), | ||||
|                     child: Text("4"), | ||||
|                   ), | ||||
|                   RaisedButton( | ||||
|                     onPressed: () => _pinPadHandler("5"), | ||||
|                     child: Text("5"), | ||||
|                   ), | ||||
|                   RaisedButton( | ||||
|                     onPressed: () => _pinPadHandler("6"), | ||||
|                     child: Text("6"), | ||||
|                   ) | ||||
|                 ], | ||||
|               ), | ||||
|               Wrap( | ||||
|                 spacing: 5.0, | ||||
|                 children: <Widget>[ | ||||
|                   RaisedButton( | ||||
|                     onPressed: () => _pinPadHandler("7"), | ||||
|                     child: Text("7"), | ||||
|                   ), | ||||
|                   RaisedButton( | ||||
|                     onPressed: () => _pinPadHandler("8"), | ||||
|                     child: Text("8"), | ||||
|                   ), | ||||
|                   RaisedButton( | ||||
|                     onPressed: () => _pinPadHandler("9"), | ||||
|                     child: Text("9"), | ||||
|                   ) | ||||
|                 ], | ||||
|               ), | ||||
|               Wrap( | ||||
|                 spacing: 5.0, | ||||
|                 alignment: WrapAlignment.end, | ||||
|                 children: <Widget>[ | ||||
|                   RaisedButton( | ||||
|                     onPressed: () => _pinPadHandler("0"), | ||||
|                     child: Text("0"), | ||||
|                   ), | ||||
|                   RaisedButton( | ||||
|                     onPressed: () => _pinPadClear(), | ||||
|                     child: Text("CLEAR"), | ||||
|                   ) | ||||
|                 ], | ||||
|               ) | ||||
|             ], | ||||
|           ) | ||||
|       ); | ||||
|     } | ||||
|     Widget inputWrapper; | ||||
|     if (entity.attributes["code_format"] == null) { | ||||
|       inputWrapper = Container(width: 0.0, height: 0.0,); | ||||
|     } else { | ||||
|       inputWrapper = Container( | ||||
|           width: 150.0, | ||||
|           child: TextField( | ||||
|               decoration: InputDecoration( | ||||
|                   labelText: "Alarm Code" | ||||
|               ), | ||||
|               //focusNode: _focusNode, | ||||
|               obscureText: true, | ||||
|               controller: new TextEditingController.fromValue( | ||||
|                   new TextEditingValue( | ||||
|                       text: code, | ||||
|                       selection: | ||||
|                       new TextSelection.collapsed(offset: code.length) | ||||
|                   ) | ||||
|               ), | ||||
|               onChanged: (value) { | ||||
|                 code = value; | ||||
|               } | ||||
|           ) | ||||
|       ); | ||||
|     } | ||||
|     Widget buttonsWrapper = Padding( | ||||
|         padding: EdgeInsets.symmetric(vertical: Sizes.rowPadding), | ||||
|         child: Wrap( | ||||
|             alignment: WrapAlignment.center, | ||||
|             spacing: 15.0, | ||||
|             runSpacing: Sizes.rowPadding, | ||||
|             children: buttons | ||||
|         ) | ||||
|     ); | ||||
|     Widget triggerButton = Row( | ||||
|       mainAxisAlignment: MainAxisAlignment.end, | ||||
|       children: [ | ||||
|         FlatButton( | ||||
|           child: Text( | ||||
|             "TRIGGER", | ||||
|             style: TextStyle(color: Colors.redAccent) | ||||
|           ), | ||||
|           onPressed: () => _askToTrigger(entity), | ||||
|         ) | ||||
|       ] | ||||
|     ); | ||||
|     return Column( | ||||
|       crossAxisAlignment: CrossAxisAlignment.center, | ||||
|       children: <Widget>[ | ||||
|         widget.extended ? buttonsWrapper : inputWrapper, | ||||
|         widget.extended ? inputWrapper : buttonsWrapper, | ||||
|         widget.extended ? pinPad : triggerButton | ||||
|       ] | ||||
|  | ||||
|     ); | ||||
|   } | ||||
|  | ||||
|  | ||||
| } | ||||
							
								
								
									
										459
									
								
								lib/entity_widgets/controls/climate_controls.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,459 @@ | ||||
| part of '../../main.dart'; | ||||
|  | ||||
| class ClimateControlWidget extends StatefulWidget { | ||||
|  | ||||
|   ClimateControlWidget({Key key}) : super(key: key); | ||||
|  | ||||
|   @override | ||||
|   _ClimateControlWidgetState createState() => _ClimateControlWidgetState(); | ||||
| } | ||||
|  | ||||
| class _ClimateControlWidgetState extends State<ClimateControlWidget> { | ||||
|  | ||||
|   bool _showPending = false; | ||||
|   bool _changedHere = false; | ||||
|   Timer _resetTimer; | ||||
|   Timer _tempThrottleTimer; | ||||
|   Timer _targetTempThrottleTimer; | ||||
|   double _tmpTemperature = 0.0; | ||||
|   double _tmpTargetLow = 0.0; | ||||
|   double _tmpTargetHigh = 0.0; | ||||
|   double _tmpTargetHumidity = 0.0; | ||||
|   String _tmpOperationMode; | ||||
|   String _tmpFanMode; | ||||
|   String _tmpSwingMode; | ||||
|   bool _tmpAwayMode = false; | ||||
|   bool _tmpIsOff = false; | ||||
|   bool _tmpAuxHeat = false; | ||||
|  | ||||
|   void _resetVars(ClimateEntity entity) { | ||||
|     _tmpTemperature = entity.temperature; | ||||
|     _tmpTargetHigh = entity.targetHigh; | ||||
|     _tmpTargetLow = entity.targetLow; | ||||
|     _tmpOperationMode = entity.operationMode; | ||||
|     _tmpFanMode = entity.fanMode; | ||||
|     _tmpSwingMode = entity.swingMode; | ||||
|     _tmpAwayMode = entity.awayMode; | ||||
|     _tmpIsOff = entity.isOff; | ||||
|     _tmpAuxHeat = entity.auxHeat; | ||||
|     _tmpTargetHumidity = entity.targetHumidity; | ||||
|  | ||||
|     _showPending = false; | ||||
|     _changedHere = false; | ||||
|   } | ||||
|  | ||||
|   void _temperatureUp(ClimateEntity entity) { | ||||
|     _tmpTemperature = ((_tmpTemperature + entity.temperatureStep) <= entity.maxTemp) ? _tmpTemperature + entity.temperatureStep : entity.maxTemp; | ||||
|     _setTemperature(entity); | ||||
|   } | ||||
|  | ||||
|   void _temperatureDown(ClimateEntity entity) { | ||||
|     _tmpTemperature = ((_tmpTemperature - entity.temperatureStep) >= entity.minTemp) ? _tmpTemperature - entity.temperatureStep : entity.minTemp; | ||||
|     _setTemperature(entity); | ||||
|   } | ||||
|  | ||||
|   void _targetLowUp(ClimateEntity entity) { | ||||
|     _tmpTargetLow = ((_tmpTargetLow + entity.temperatureStep) <= entity.maxTemp) ? _tmpTargetLow + entity.temperatureStep : entity.maxTemp; | ||||
|     _setTargetTemp(entity); | ||||
|   } | ||||
|  | ||||
|   void _targetLowDown(ClimateEntity entity) { | ||||
|     _tmpTargetLow = ((_tmpTargetLow - entity.temperatureStep) >= entity.minTemp) ? _tmpTargetLow - entity.temperatureStep : entity.minTemp; | ||||
|     _setTargetTemp(entity); | ||||
|   } | ||||
|  | ||||
|   void _targetHighUp(ClimateEntity entity) { | ||||
|     _tmpTargetHigh = ((_tmpTargetHigh + entity.temperatureStep) <= entity.maxTemp) ? _tmpTargetHigh + entity.temperatureStep : entity.maxTemp; | ||||
|     _setTargetTemp(entity); | ||||
|   } | ||||
|  | ||||
|   void _targetHighDown(ClimateEntity entity) { | ||||
|     _tmpTargetHigh = ((_tmpTargetHigh - entity.temperatureStep) >= entity.minTemp) ? _tmpTargetHigh - entity.temperatureStep : entity.minTemp; | ||||
|     _setTargetTemp(entity); | ||||
|   } | ||||
|  | ||||
|   void _setTemperature(ClimateEntity entity) { | ||||
|     if (_tempThrottleTimer!=null) { | ||||
|       _tempThrottleTimer.cancel(); | ||||
|     } | ||||
|     setState(() { | ||||
|       _changedHere = true; | ||||
|       _tmpTemperature = double.parse(_tmpTemperature.toStringAsFixed(1)); | ||||
|     }); | ||||
|     _tempThrottleTimer = Timer(Duration(seconds: 2), () { | ||||
|       setState(() { | ||||
|         _changedHere = true; | ||||
|         eventBus.fire(new ServiceCallEvent(entity.domain, "set_temperature", entity.entityId,{"temperature": "${_tmpTemperature.toStringAsFixed(1)}"})); | ||||
|         _resetStateTimer(entity); | ||||
|       }); | ||||
|     }); | ||||
|   } | ||||
|  | ||||
|   void _setTargetTemp(ClimateEntity entity) { | ||||
|     if (_targetTempThrottleTimer!=null) { | ||||
|       _targetTempThrottleTimer.cancel(); | ||||
|     } | ||||
|     setState(() { | ||||
|       _changedHere = true; | ||||
|       _tmpTargetLow = double.parse(_tmpTargetLow.toStringAsFixed(1)); | ||||
|       _tmpTargetHigh = double.parse(_tmpTargetHigh.toStringAsFixed(1)); | ||||
|     }); | ||||
|     _targetTempThrottleTimer = Timer(Duration(seconds: 2), () { | ||||
|       setState(() { | ||||
|         _changedHere = true; | ||||
|         eventBus.fire(new ServiceCallEvent(entity.domain, "set_temperature", entity.entityId,{"target_temp_high": "${_tmpTargetHigh.toStringAsFixed(1)}", "target_temp_low": "${_tmpTargetLow.toStringAsFixed(1)}"})); | ||||
|         _resetStateTimer(entity); | ||||
|       }); | ||||
|     }); | ||||
|   } | ||||
|  | ||||
|   void _setTargetHumidity(ClimateEntity entity, double value) { | ||||
|     setState(() { | ||||
|       _tmpTargetHumidity = value.roundToDouble(); | ||||
|       _changedHere = true; | ||||
|       eventBus.fire(new ServiceCallEvent(entity.domain, "set_humidity", entity.entityId,{"humidity": "$_tmpTargetHumidity"})); | ||||
|       _resetStateTimer(entity); | ||||
|     }); | ||||
|   } | ||||
|  | ||||
|   void _setOperationMode(ClimateEntity entity, value) { | ||||
|     setState(() { | ||||
|       _tmpOperationMode = value; | ||||
|       _changedHere = true; | ||||
|       eventBus.fire(new ServiceCallEvent(entity.domain, "set_operation_mode", entity.entityId,{"operation_mode": "$_tmpOperationMode"})); | ||||
|       _resetStateTimer(entity); | ||||
|     }); | ||||
|   } | ||||
|  | ||||
|   void _setSwingMode(ClimateEntity entity, value) { | ||||
|     setState(() { | ||||
|       _tmpSwingMode = value; | ||||
|       _changedHere = true; | ||||
|       eventBus.fire(new ServiceCallEvent(entity.domain, "set_swing_mode", entity.entityId,{"swing_mode": "$_tmpSwingMode"})); | ||||
|       _resetStateTimer(entity); | ||||
|     }); | ||||
|   } | ||||
|  | ||||
|   void _setFanMode(ClimateEntity entity, value) { | ||||
|     setState(() { | ||||
|       _tmpFanMode = value; | ||||
|       _changedHere = true; | ||||
|       eventBus.fire(new ServiceCallEvent(entity.domain, "set_fan_mode", entity.entityId,{"fan_mode": "$_tmpFanMode"})); | ||||
|       _resetStateTimer(entity); | ||||
|     }); | ||||
|   } | ||||
|  | ||||
|   void _setAwayMode(ClimateEntity entity, value) { | ||||
|     setState(() { | ||||
|       _tmpAwayMode = value; | ||||
|       _changedHere = true; | ||||
|       eventBus.fire(new ServiceCallEvent(entity.domain, "set_away_mode", entity.entityId,{"away_mode": "${_tmpAwayMode ? 'on' : 'off'}"})); | ||||
|       _resetStateTimer(entity); | ||||
|     }); | ||||
|   } | ||||
|  | ||||
|   void _setOnOf(ClimateEntity entity, value) { | ||||
|     setState(() { | ||||
|       _tmpIsOff = !value; | ||||
|       _changedHere = true; | ||||
|       eventBus.fire(new ServiceCallEvent(entity.domain, "${_tmpIsOff ? 'turn_off' : 'turn_on'}", entity.entityId, null)); | ||||
|       _resetStateTimer(entity); | ||||
|     }); | ||||
|   } | ||||
|  | ||||
|   void _setAuxHeat(ClimateEntity entity, value) { | ||||
|     setState(() { | ||||
|       _tmpAuxHeat = value; | ||||
|       _changedHere = true; | ||||
|       eventBus.fire(new ServiceCallEvent(entity.domain, "set_aux_heat", entity.entityId, {"aux_heat": "$_tmpAuxHeat"})); | ||||
|       _resetStateTimer(entity); | ||||
|     }); | ||||
|   } | ||||
|  | ||||
|   void _resetStateTimer(ClimateEntity entity) { | ||||
|     if (_resetTimer!=null) { | ||||
|       _resetTimer.cancel(); | ||||
|     } | ||||
|     _resetTimer = Timer(Duration(seconds: 3), () { | ||||
|       setState(() {}); | ||||
|       _resetVars(entity); | ||||
|     }); | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context) { | ||||
|     final entityModel = EntityModel.of(context); | ||||
|     final ClimateEntity entity = entityModel.entityWrapper.entity; | ||||
|     if (_changedHere) { | ||||
|       _showPending = (_tmpTemperature != entity.temperature || _tmpTargetHigh != entity.targetHigh || _tmpTargetLow != entity.targetLow); | ||||
|       _changedHere = false; | ||||
|     } else { | ||||
|       _resetTimer?.cancel(); | ||||
|       _resetVars(entity); | ||||
|     } | ||||
|     return Padding( | ||||
|       padding: EdgeInsets.fromLTRB(Sizes.leftWidgetPadding, Sizes.rowPadding, Sizes.rightWidgetPadding, 0.0), | ||||
|       child: Column( | ||||
|         crossAxisAlignment: CrossAxisAlignment.start, | ||||
|         children: <Widget>[ | ||||
|           _buildOnOffControl(entity), | ||||
|           _buildTemperatureControls(entity), | ||||
|           _buildTargetTemperatureControls(entity), | ||||
|           _buildHumidityControls(entity), | ||||
|           _buildOperationControl(entity), | ||||
|           _buildFanControl(entity), | ||||
|           _buildSwingControl(entity), | ||||
|           _buildAwayModeControl(entity), | ||||
|           _buildAuxHeatControl(entity) | ||||
|         ], | ||||
|       ), | ||||
|     ); | ||||
|   } | ||||
|  | ||||
|   Widget _buildAwayModeControl(ClimateEntity entity) { | ||||
|     if (entity.supportAwayMode) { | ||||
|       return ModeSwitchWidget( | ||||
|         caption: "Away mode", | ||||
|         onChange: (value) => _setAwayMode(entity, value), | ||||
|         value: _tmpAwayMode, | ||||
|       ); | ||||
|     } else { | ||||
|       return Container(height: 0.0, width: 0.0,); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   Widget _buildOnOffControl(ClimateEntity entity) { | ||||
|     if (entity.supportOnOff) { | ||||
|       return ModeSwitchWidget( | ||||
|           onChange: (value) => _setOnOf(entity, value), | ||||
|           caption: "On / Off", | ||||
|           value: !_tmpIsOff | ||||
|       ); | ||||
|     } else { | ||||
|       return Container(height: 0.0, width: 0.0,); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   Widget _buildAuxHeatControl(ClimateEntity entity) { | ||||
|     if (entity.supportAuxHeat ) { | ||||
|       return ModeSwitchWidget( | ||||
|           caption: "Aux heat", | ||||
|           onChange: (value) => _setAuxHeat(entity, value), | ||||
|           value: _tmpAuxHeat | ||||
|       ); | ||||
|     } else { | ||||
|       return Container(height: 0.0, width: 0.0,); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   Widget _buildOperationControl(ClimateEntity entity) { | ||||
|     if (entity.supportOperationMode) { | ||||
|       return ModeSelectorWidget( | ||||
|         onChange: (mode) => _setOperationMode(entity, mode), | ||||
|         options: entity.operationList, | ||||
|         caption: "Operation", | ||||
|         value: _tmpOperationMode, | ||||
|       ); | ||||
|     } else { | ||||
|       return Container(height: 0.0, width: 0.0); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   Widget _buildFanControl(ClimateEntity entity) { | ||||
|     if (entity.supportFanMode) { | ||||
|       return ModeSelectorWidget( | ||||
|         options: entity.fanList, | ||||
|         onChange: (mode) => _setFanMode(entity, mode), | ||||
|         caption: "Fan mode", | ||||
|         value: _tmpFanMode, | ||||
|       ); | ||||
|     } else { | ||||
|       return Container(height: 0.0, width: 0.0); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   Widget _buildSwingControl(ClimateEntity entity) { | ||||
|     if (entity.supportSwingMode) { | ||||
|       return ModeSelectorWidget( | ||||
|           onChange: (mode) => _setSwingMode(entity, mode), | ||||
|           options: entity.swingList, | ||||
|           value: _tmpSwingMode, | ||||
|           caption: "Swing mode" | ||||
|       ); | ||||
|     } else { | ||||
|       return Container(height: 0.0, width: 0.0); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   Widget _buildTemperatureControls(ClimateEntity entity) { | ||||
|     if ((entity.supportTargetTemperature) && (entity.temperature != null)) { | ||||
|       return Column( | ||||
|         crossAxisAlignment: CrossAxisAlignment.start, | ||||
|         children: <Widget>[ | ||||
|           Text("Target temperature", style: TextStyle( | ||||
|               fontSize: Sizes.stateFontSize | ||||
|           )), | ||||
|           TemperatureControlWidget( | ||||
|             value: _tmpTemperature, | ||||
|             fontColor: _showPending ? Colors.red : Colors.black, | ||||
|             onDec: () => _temperatureDown(entity), | ||||
|             onInc: () => _temperatureUp(entity), | ||||
|           ) | ||||
|         ], | ||||
|       ); | ||||
|     } else { | ||||
|       return Container(width: 0.0, height: 0.0,); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   Widget _buildTargetTemperatureControls(ClimateEntity entity) { | ||||
|     List<Widget> controls = []; | ||||
|     if ((entity.supportTargetTemperatureLow) && (entity.targetLow != null)) { | ||||
|       controls.addAll(<Widget>[ | ||||
|         TemperatureControlWidget( | ||||
|           value: _tmpTargetLow, | ||||
|           fontColor: _showPending ? Colors.red : Colors.black, | ||||
|           onDec: () => _targetLowDown(entity), | ||||
|           onInc: () => _targetLowUp(entity), | ||||
|         ), | ||||
|         Expanded( | ||||
|           child: Container(height: 10.0), | ||||
|         ) | ||||
|       ]); | ||||
|     } | ||||
|     if ((entity.supportTargetTemperatureHigh) && (entity.targetHigh != null)) { | ||||
|       controls.add( | ||||
|           TemperatureControlWidget( | ||||
|             value: _tmpTargetHigh, | ||||
|             fontColor: _showPending ? Colors.red : Colors.black, | ||||
|             onDec: () => _targetHighDown(entity), | ||||
|             onInc: () => _targetHighUp(entity), | ||||
|           ) | ||||
|       ); | ||||
|     } | ||||
|     if (controls.isNotEmpty) { | ||||
|       return Column( | ||||
|         crossAxisAlignment: CrossAxisAlignment.start, | ||||
|         children: <Widget>[ | ||||
|           Text("Target temperature range", style: TextStyle( | ||||
|               fontSize: Sizes.stateFontSize | ||||
|           )), | ||||
|           Row( | ||||
|             children: controls, | ||||
|           ) | ||||
|         ], | ||||
|       ); | ||||
|     } else { | ||||
|       return Container(width: 0.0, height: 0.0); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   Widget _buildHumidityControls(ClimateEntity entity) { | ||||
|     List<Widget> result = []; | ||||
|     if (entity.supportTargetHumidity) { | ||||
|       result.addAll(<Widget>[ | ||||
|         Text( | ||||
|           "$_tmpTargetHumidity%", | ||||
|           style: TextStyle(fontSize: Sizes.largeFontSize), | ||||
|         ), | ||||
|         Expanded( | ||||
|           child: Slider( | ||||
|             value: _tmpTargetHumidity, | ||||
|             max: entity.maxHumidity, | ||||
|             min: entity.minHumidity, | ||||
|             onChanged: ((double val) { | ||||
|               setState(() { | ||||
|                 _changedHere = true; | ||||
|                 _tmpTargetHumidity = val.roundToDouble(); | ||||
|               }); | ||||
|             }), | ||||
|             onChangeEnd: (double v) => _setTargetHumidity(entity, v), | ||||
|           ), | ||||
|         ) | ||||
|       ]); | ||||
|     } | ||||
|     if (result.isNotEmpty) { | ||||
|       return Column( | ||||
|         crossAxisAlignment: CrossAxisAlignment.start, | ||||
|         children: <Widget>[ | ||||
|           Padding( | ||||
|             padding: EdgeInsets.fromLTRB( | ||||
|                 0.0, Sizes.rowPadding, 0.0, Sizes.rowPadding), | ||||
|             child: Text("Target humidity", style: TextStyle( | ||||
|                 fontSize: Sizes.stateFontSize | ||||
|             )), | ||||
|           ), | ||||
|           Row( | ||||
|             crossAxisAlignment: CrossAxisAlignment.center, | ||||
|             children: result, | ||||
|           ), | ||||
|           Container( | ||||
|             height: Sizes.rowPadding, | ||||
|           ) | ||||
|         ], | ||||
|       ); | ||||
|     } else { | ||||
|       return Container( | ||||
|         width: 0.0, | ||||
|         height: 0.0, | ||||
|       ); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|  | ||||
|   @override | ||||
|   void dispose() { | ||||
|     _resetTimer?.cancel(); | ||||
|     super.dispose(); | ||||
|   } | ||||
|  | ||||
| } | ||||
|  | ||||
| class TemperatureControlWidget extends StatelessWidget { | ||||
|   final double value; | ||||
|   final double fontSize; | ||||
|   final Color fontColor; | ||||
|   final onInc; | ||||
|   final onDec; | ||||
|  | ||||
|   TemperatureControlWidget( | ||||
|       {Key key, | ||||
|         @required this.value, | ||||
|         @required this.onInc, | ||||
|         @required this.onDec, | ||||
|         this.fontSize, | ||||
|         this.fontColor}) | ||||
|       : super(key: key); | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context) { | ||||
|     return Row( | ||||
|       crossAxisAlignment: CrossAxisAlignment.center, | ||||
|       children: <Widget>[ | ||||
|         Text( | ||||
|           "$value", | ||||
|           style: TextStyle( | ||||
|               fontSize: fontSize ?? 24.0, | ||||
|               color: fontColor ?? Colors.black | ||||
|           ), | ||||
|         ), | ||||
|         Column( | ||||
|           children: <Widget>[ | ||||
|             IconButton( | ||||
|               icon: Icon(MaterialDesignIcons.getIconDataFromIconName( | ||||
|                   'mdi:chevron-up')), | ||||
|               iconSize: 30.0, | ||||
|               onPressed: () => onInc(), | ||||
|             ), | ||||
|             IconButton( | ||||
|               icon: Icon(MaterialDesignIcons.getIconDataFromIconName( | ||||
|                   'mdi:chevron-down')), | ||||
|               iconSize: 30.0, | ||||
|               onPressed: () => onDec(), | ||||
|             ) | ||||
|           ], | ||||
|         ) | ||||
|       ], | ||||
|     ); | ||||
|   } | ||||
| } | ||||
							
								
								
									
										200
									
								
								lib/entity_widgets/controls/cover_controls.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,200 @@ | ||||
| part of '../../main.dart'; | ||||
|  | ||||
| class CoverControlWidget extends StatefulWidget { | ||||
|  | ||||
|   CoverControlWidget({Key key}) : super(key: key); | ||||
|  | ||||
|   @override | ||||
|   _CoverControlWidgetState createState() => _CoverControlWidgetState(); | ||||
| } | ||||
|  | ||||
| class _CoverControlWidgetState extends State<CoverControlWidget> { | ||||
|  | ||||
|   double _tmpPosition = 0.0; | ||||
|   double _tmpTiltPosition = 0.0; | ||||
|   bool _changedHere = false; | ||||
|  | ||||
|   void _setNewPosition(CoverEntity entity, double position) { | ||||
|     setState(() { | ||||
|       _tmpPosition = position.roundToDouble(); | ||||
|       _changedHere = true; | ||||
|       eventBus.fire(new ServiceCallEvent(entity.domain, "set_cover_position", entity.entityId,{"position": _tmpPosition.round()})); | ||||
|     }); | ||||
|   } | ||||
|  | ||||
|   void _setNewTiltPosition(CoverEntity entity, double position) { | ||||
|     setState(() { | ||||
|       _tmpTiltPosition = position.roundToDouble(); | ||||
|       _changedHere = true; | ||||
|       eventBus.fire(new ServiceCallEvent(entity.domain, "set_cover_tilt_position", entity.entityId,{"tilt_position": _tmpTiltPosition.round()})); | ||||
|     }); | ||||
|   } | ||||
|  | ||||
|   void _resetVars(CoverEntity entity) { | ||||
|     _tmpPosition = entity.currentPosition; | ||||
|     _tmpTiltPosition = entity.currentTiltPosition; | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context) { | ||||
|     final entityModel = EntityModel.of(context); | ||||
|     final CoverEntity entity = entityModel.entityWrapper.entity; | ||||
|     if (_changedHere) { | ||||
|       _changedHere = false; | ||||
|     } else { | ||||
|       _resetVars(entity); | ||||
|     } | ||||
|     return Padding( | ||||
|       padding: EdgeInsets.fromLTRB(Sizes.leftWidgetPadding, Sizes.rowPadding, Sizes.rightWidgetPadding, 0.0), | ||||
|       child: Column( | ||||
|         crossAxisAlignment: CrossAxisAlignment.start, | ||||
|         children: <Widget>[ | ||||
|           _buildPositionControls(entity), | ||||
|           _buildTiltControls(entity) | ||||
|         ], | ||||
|       ), | ||||
|     ); | ||||
|   } | ||||
|  | ||||
|   Widget _buildPositionControls(CoverEntity entity) { | ||||
|     if (entity.supportSetPosition) { | ||||
|       return Column( | ||||
|         crossAxisAlignment: CrossAxisAlignment.start, | ||||
|         children: <Widget>[ | ||||
|           Padding( | ||||
|             padding: EdgeInsets.fromLTRB( | ||||
|                 0.0, Sizes.rowPadding, 0.0, Sizes.rowPadding), | ||||
|             child: Text("Position", style: TextStyle( | ||||
|                 fontSize: Sizes.stateFontSize | ||||
|             )), | ||||
|           ), | ||||
|           Slider( | ||||
|             value: _tmpPosition, | ||||
|             min: 0.0, | ||||
|             max: 100.0, | ||||
|             divisions: 10, | ||||
|             onChanged: (double value) { | ||||
|               setState(() { | ||||
|                 _tmpPosition = value.roundToDouble(); | ||||
|                 _changedHere = true; | ||||
|               }); | ||||
|             }, | ||||
|             onChangeEnd: (double value) => _setNewPosition(entity, value), | ||||
|           ), | ||||
|           Container(height: Sizes.rowPadding,) | ||||
|         ], | ||||
|       ); | ||||
|     } else { | ||||
|       return Container(width: 0.0, height: 0.0); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   Widget _buildTiltControls(CoverEntity entity) { | ||||
|     List<Widget> controls = []; | ||||
|     if (entity.supportCloseTilt || entity.supportOpenTilt || entity.supportStopTilt) { | ||||
|       controls.add( | ||||
|           CoverTiltControlsWidget() | ||||
|       ); | ||||
|     } | ||||
|     if (entity.supportSetTiltPosition) { | ||||
|       controls.addAll(<Widget>[ | ||||
|         Slider( | ||||
|           value: _tmpTiltPosition, | ||||
|           min: 0.0, | ||||
|           max: 100.0, | ||||
|           divisions: 10, | ||||
|           onChanged: (double value) { | ||||
|             setState(() { | ||||
|               _tmpTiltPosition = value.roundToDouble(); | ||||
|               _changedHere = true; | ||||
|             }); | ||||
|           }, | ||||
|           onChangeEnd: (double value) => _setNewTiltPosition(entity, value), | ||||
|         ), | ||||
|         Container(height: Sizes.rowPadding,) | ||||
|       ]); | ||||
|     } | ||||
|     if (controls.isNotEmpty) { | ||||
|       controls.insert(0, Padding( | ||||
|         padding: EdgeInsets.fromLTRB( | ||||
|             0.0, Sizes.rowPadding, 0.0, Sizes.rowPadding), | ||||
|         child: Text("Tilt position", style: TextStyle( | ||||
|             fontSize: Sizes.stateFontSize | ||||
|         )), | ||||
|       )); | ||||
|       return Column( | ||||
|         crossAxisAlignment: CrossAxisAlignment.start, | ||||
|         children: controls, | ||||
|       ); | ||||
|     } else { | ||||
|       return Container(width: 0.0, height: 0.0); | ||||
|     } | ||||
|   } | ||||
|  | ||||
| } | ||||
|  | ||||
| class CoverTiltControlsWidget extends StatelessWidget { | ||||
|   void _open(CoverEntity entity) { | ||||
|     eventBus.fire(new ServiceCallEvent( | ||||
|         entity.domain, "open_cover_tilt", entity.entityId, null)); | ||||
|   } | ||||
|  | ||||
|   void _close(CoverEntity entity) { | ||||
|     eventBus.fire(new ServiceCallEvent( | ||||
|         entity.domain, "close_cover_tilt", entity.entityId, null)); | ||||
|   } | ||||
|  | ||||
|   void _stop(CoverEntity entity) { | ||||
|     eventBus.fire(new ServiceCallEvent( | ||||
|         entity.domain, "stop_cover_tilt", entity.entityId, null)); | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context) { | ||||
|     final entityModel = EntityModel.of(context); | ||||
|     final CoverEntity entity = entityModel.entityWrapper.entity; | ||||
|     List<Widget> buttons = []; | ||||
|     if (entity.supportOpenTilt) { | ||||
|       buttons.add(IconButton( | ||||
|           icon: Icon( | ||||
|             MaterialDesignIcons.getIconDataFromIconName( | ||||
|                 "mdi:arrow-top-right"), | ||||
|             size: Sizes.iconSize, | ||||
|           ), | ||||
|           onPressed: entity.canTiltBeOpened ? () => _open(entity) : null)); | ||||
|     } else { | ||||
|       buttons.add(Container( | ||||
|         width: Sizes.iconSize + 20.0, | ||||
|       )); | ||||
|     } | ||||
|     if (entity.supportStopTilt) { | ||||
|       buttons.add(IconButton( | ||||
|           icon: Icon( | ||||
|             MaterialDesignIcons.getIconDataFromIconName("mdi:stop"), | ||||
|             size: Sizes.iconSize, | ||||
|           ), | ||||
|           onPressed: () => _stop(entity))); | ||||
|     } else { | ||||
|       buttons.add(Container( | ||||
|         width: Sizes.iconSize + 20.0, | ||||
|       )); | ||||
|     } | ||||
|     if (entity.supportCloseTilt) { | ||||
|       buttons.add(IconButton( | ||||
|           icon: Icon( | ||||
|             MaterialDesignIcons.getIconDataFromIconName( | ||||
|                 "mdi:arrow-bottom-left"), | ||||
|             size: Sizes.iconSize, | ||||
|           ), | ||||
|           onPressed: entity.canTiltBeClosed ? () => _close(entity) : null)); | ||||
|     } else { | ||||
|       buttons.add(Container( | ||||
|         width: Sizes.iconSize + 20.0, | ||||
|       )); | ||||
|     } | ||||
|  | ||||
|     return Row( | ||||
|       children: buttons, | ||||
|     ); | ||||
|   } | ||||
| } | ||||
							
								
								
									
										123
									
								
								lib/entity_widgets/controls/fan_controls.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,123 @@ | ||||
| part of '../../main.dart'; | ||||
|  | ||||
| class FanControlsWidget extends StatefulWidget { | ||||
|  | ||||
|   @override | ||||
|   _FanControlsWidgetState createState() => _FanControlsWidgetState(); | ||||
|  | ||||
| } | ||||
|  | ||||
| class _FanControlsWidgetState extends State<FanControlsWidget> { | ||||
|  | ||||
|   bool _tmpOscillate; | ||||
|   bool _tmpDirectionForward; | ||||
|   bool _changedHere = false; | ||||
|   String _tmpSpeed; | ||||
|  | ||||
|   void _resetState(FanEntity entity) { | ||||
|     _tmpOscillate = entity.attributes["oscillating"] ?? false; | ||||
|     _tmpDirectionForward = entity.attributes["direction"] == "forward"; | ||||
|     _tmpSpeed = entity.attributes["speed"]; | ||||
|   } | ||||
|  | ||||
|   void _setOscillate(FanEntity entity, bool oscillate) { | ||||
|     setState(() { | ||||
|       _tmpOscillate = oscillate; | ||||
|       _changedHere = true; | ||||
|       eventBus.fire(new ServiceCallEvent( | ||||
|           "fan", "oscillate", entity.entityId, | ||||
|           {"oscillating": oscillate})); | ||||
|     }); | ||||
|   } | ||||
|  | ||||
|   void _setDirection(FanEntity entity, bool forward) { | ||||
|     setState(() { | ||||
|       _tmpDirectionForward = forward; | ||||
|       _changedHere = true; | ||||
|       eventBus.fire(new ServiceCallEvent( | ||||
|           "fan", "set_direction", entity.entityId, | ||||
|           {"direction": forward ? "forward" : "reverse"})); | ||||
|     }); | ||||
|   } | ||||
|  | ||||
|   void _setSpeed(FanEntity entity, String value) { | ||||
|     setState(() { | ||||
|       _tmpSpeed = value; | ||||
|       _changedHere = true; | ||||
|       eventBus.fire(new ServiceCallEvent( | ||||
|             "fan", "set_speed", entity.entityId, | ||||
|             {"speed": value})); | ||||
|     }); | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context) { | ||||
|     final entityModel = EntityModel.of(context); | ||||
|     final FanEntity entity = entityModel.entityWrapper.entity; | ||||
|     if (!_changedHere) { | ||||
|       _resetState(entity); | ||||
|     } else { | ||||
|       _changedHere = false; | ||||
|     } | ||||
|     return Column( | ||||
|       crossAxisAlignment: CrossAxisAlignment.center, | ||||
|       children: <Widget>[ | ||||
|         _buildSpeedControl(entity), | ||||
|         _buildOscillateControl(entity), | ||||
|         _buildDirectionControl(entity) | ||||
|       ], | ||||
|     ); | ||||
|   } | ||||
|  | ||||
|   Widget _buildSpeedControl(FanEntity entity) { | ||||
|     if (entity.supportSetSpeed && entity.speedList != null && entity.speedList.isNotEmpty) { | ||||
|       return ModeSelectorWidget( | ||||
|         onChange: (effect) => _setSpeed(entity, effect), | ||||
|         caption: "Speed", | ||||
|         options: entity.speedList, | ||||
|         value: _tmpSpeed | ||||
|       ); | ||||
|     } else { | ||||
|       return Container(width: 0.0, height: 0.0); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   Widget _buildOscillateControl(FanEntity entity) { | ||||
|     if (entity.supportOscillate) { | ||||
|       return ModeSwitchWidget( | ||||
|           onChange: (value) => _setOscillate(entity, value), | ||||
|           caption: "Oscillate", | ||||
|           value: _tmpOscillate, | ||||
|           expanded: false, | ||||
|       ); | ||||
|     } else { | ||||
|       return Container(width: 0.0, height: 0.0); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   Widget _buildDirectionControl(FanEntity entity) { | ||||
|     if (entity.supportDirection) { | ||||
|       return Row( | ||||
|         mainAxisSize: MainAxisSize.min, | ||||
|         children: <Widget>[ | ||||
|           IconButton( | ||||
|             onPressed: _tmpDirectionForward ? | ||||
|               () => _setDirection(entity, false) : | ||||
|               null, | ||||
|             icon: Icon(Icons.rotate_left), | ||||
|           ), | ||||
|           IconButton( | ||||
|             onPressed: !_tmpDirectionForward ? | ||||
|               () => _setDirection(entity, true) : | ||||
|               null, | ||||
|             icon: Icon(Icons.rotate_right), | ||||
|           ), | ||||
|         ], | ||||
|       ); | ||||
|     } else { | ||||
|       return Container(width: 0.0, height: 0.0); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|  | ||||
| } | ||||
							
								
								
									
										227
									
								
								lib/entity_widgets/controls/light_controls.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,227 @@ | ||||
| part of '../../main.dart'; | ||||
|  | ||||
| class LightControlsWidget extends StatefulWidget { | ||||
|  | ||||
|   @override | ||||
|   _LightControlsWidgetState createState() => _LightControlsWidgetState(); | ||||
|  | ||||
| } | ||||
|  | ||||
| class _LightControlsWidgetState extends State<LightControlsWidget> { | ||||
|  | ||||
|   int _tmpBrightness; | ||||
|   int _tmpWhiteValue; | ||||
|   int _tmpColorTemp = 0; | ||||
|   HSVColor _tmpColor = HSVColor.fromAHSV(1.0, 30.0, 0.0, 1.0); | ||||
|   bool _changedHere = false; | ||||
|   String _tmpEffect; | ||||
|  | ||||
|   void _resetState(LightEntity entity) { | ||||
|     _tmpBrightness = entity.brightness ?? 0; | ||||
|     _tmpWhiteValue = entity.whiteValue ?? 0; | ||||
|     _tmpColorTemp = entity.colorTemp ?? entity.minMireds?.toInt(); | ||||
|     _tmpColor = entity.color ?? _tmpColor; | ||||
|     _tmpEffect = entity.effect; | ||||
|   } | ||||
|  | ||||
|   void _setBrightness(LightEntity entity, double value) { | ||||
|     setState(() { | ||||
|       _tmpBrightness = value.round(); | ||||
|       _changedHere = true; | ||||
|       if (_tmpBrightness > 0) { | ||||
|         eventBus.fire(new ServiceCallEvent( | ||||
|             entity.domain, "turn_on", entity.entityId, | ||||
|             {"brightness": _tmpBrightness})); | ||||
|       } else { | ||||
|         eventBus.fire(new ServiceCallEvent( | ||||
|             entity.domain, "turn_off", entity.entityId, | ||||
|             null)); | ||||
|       } | ||||
|     }); | ||||
|   } | ||||
|  | ||||
|   void _setWhiteValue(LightEntity entity, double value) { | ||||
|     setState(() { | ||||
|       _tmpWhiteValue = value.round(); | ||||
|       _changedHere = true; | ||||
|       eventBus.fire(new ServiceCallEvent( | ||||
|             entity.domain, "turn_on", entity.entityId, | ||||
|             {"white_value": _tmpWhiteValue})); | ||||
|  | ||||
|     }); | ||||
|   } | ||||
|  | ||||
|   void _setColorTemp(LightEntity entity, double value) { | ||||
|     setState(() { | ||||
|       _tmpColorTemp = value.round(); | ||||
|       _changedHere = true; | ||||
|       eventBus.fire(new ServiceCallEvent( | ||||
|           entity.domain, "turn_on", entity.entityId, | ||||
|           {"color_temp": _tmpColorTemp})); | ||||
|     }); | ||||
|   } | ||||
|  | ||||
|   void _setColor(LightEntity entity, HSVColor color) { | ||||
|     setState(() { | ||||
|       _tmpColor = color; | ||||
|       _changedHere = true; | ||||
|       Logger.d( "HS Color: [${color.hue}, ${color.saturation}]"); | ||||
|       eventBus.fire(new ServiceCallEvent( | ||||
|         entity.domain, "turn_on", entity.entityId, | ||||
|           {"hs_color": [color.hue, color.saturation*100]})); | ||||
|     }); | ||||
|   } | ||||
|  | ||||
|   void _setEffect(LightEntity entity, String value) { | ||||
|     setState(() { | ||||
|       _tmpEffect = value; | ||||
|       _changedHere = true; | ||||
|       if (_tmpEffect != null) { | ||||
|         eventBus.fire(new ServiceCallEvent( | ||||
|             entity.domain, "turn_on", entity.entityId, | ||||
|             {"effect": "$value"})); | ||||
|       } | ||||
|     }); | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context) { | ||||
|     final entityModel = EntityModel.of(context); | ||||
|     final LightEntity entity = entityModel.entityWrapper.entity; | ||||
|     if (!_changedHere) { | ||||
|       _resetState(entity); | ||||
|     } else { | ||||
|       _changedHere = false; | ||||
|     } | ||||
|     return Column( | ||||
|       crossAxisAlignment: CrossAxisAlignment.center, | ||||
|       children: <Widget>[ | ||||
|         _buildBrightnessControl(entity), | ||||
|         _buildWhiteValueControl(entity), | ||||
|         _buildColorTempControl(entity), | ||||
|         _buildColorControl(entity), | ||||
|         _buildEffectControl(entity) | ||||
|       ], | ||||
|     ); | ||||
|   } | ||||
|  | ||||
|   Widget _buildBrightnessControl(LightEntity entity) { | ||||
|     if ((entity.supportBrightness) && (_tmpBrightness != null)) { | ||||
|       return UniversalSlider( | ||||
|         onChanged: (value) { | ||||
|           setState(() { | ||||
|             _changedHere = true; | ||||
|             _tmpBrightness = value.round(); | ||||
|           }); | ||||
|         }, | ||||
|         min: 0.0, | ||||
|         max: 255.0, | ||||
|         onChangeEnd: (value) => _setBrightness(entity, value), | ||||
|         value: _tmpBrightness == null ? 0.0 : _tmpBrightness.toDouble(), | ||||
|         leading: Icon(Icons.brightness_5), | ||||
|         title: "Brightness", | ||||
|       ); | ||||
|     } else { | ||||
|       return Container(width: 0.0, height: 0.0); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   Widget _buildWhiteValueControl(LightEntity entity) { | ||||
|     if ((entity.supportWhiteValue) && (_tmpWhiteValue != null)) { | ||||
|       return UniversalSlider( | ||||
|         onChanged: (value) { | ||||
|           setState(() { | ||||
|             _changedHere = true; | ||||
|             _tmpWhiteValue = value.round(); | ||||
|           }); | ||||
|         }, | ||||
|         min: 0.0, | ||||
|         max: 255.0, | ||||
|         onChangeEnd: (value) => _setWhiteValue(entity, value), | ||||
|         value: _tmpWhiteValue == null ? 0.0 : _tmpWhiteValue.toDouble(), | ||||
|         leading: Icon(MaterialDesignIcons.getIconDataFromIconName("mdi:file-word-box")), | ||||
|         title: "White", | ||||
|       ); | ||||
|     } else { | ||||
|       return Container(width: 0.0, height: 0.0); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   Widget _buildColorTempControl(LightEntity entity) { | ||||
|     if (entity.supportColorTemp) { | ||||
|       return UniversalSlider( | ||||
|         title: "Color temperature", | ||||
|         leading: Text("Cold", style: TextStyle(color: Colors.lightBlue),), | ||||
|         value:  _tmpColorTemp == null ? entity.maxMireds : _tmpColorTemp.toDouble(), | ||||
|         onChangeEnd: (value) => _setColorTemp(entity, value), | ||||
|         max: entity.maxMireds, | ||||
|         min: entity.minMireds, | ||||
|         onChanged: (value) { | ||||
|           setState(() { | ||||
|             _changedHere = true; | ||||
|             _tmpColorTemp = value.round(); | ||||
|           }); | ||||
|         }, | ||||
|         closing: Text("Warm", style: TextStyle(color: Colors.amberAccent),), | ||||
|       ); | ||||
|     } else { | ||||
|       return Container(width: 0.0, height: 0.0); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   Widget _buildColorControl(LightEntity entity) { | ||||
|     if (entity.supportColor) { | ||||
|       HSVColor savedColor = HomeAssistantModel.of(context)?.homeAssistant?.savedColor; | ||||
|       return Column( | ||||
|         mainAxisSize: MainAxisSize.min, | ||||
|         children: <Widget>[ | ||||
|           LightColorPicker( | ||||
|             color: _tmpColor, | ||||
|             onColorSelected: (color) => _setColor(entity, color), | ||||
|           ), | ||||
|           Row( | ||||
|             mainAxisSize: MainAxisSize.min, | ||||
|             children: <Widget>[ | ||||
|               FlatButton( | ||||
|                 color: _tmpColor.toColor(), | ||||
|                 child: Text('Copy color'), | ||||
|                 onPressed: _tmpColor == null ? null : () { | ||||
|                   setState(() { | ||||
|                     HomeAssistantModel | ||||
|                         .of(context) | ||||
|                         .homeAssistant | ||||
|                         .savedColor = _tmpColor; | ||||
|                   }); | ||||
|                 }, | ||||
|               ), | ||||
|               FlatButton( | ||||
|                 color: savedColor?.toColor() ?? Colors.transparent, | ||||
|                 child: Text('Paste color'), | ||||
|                 onPressed: savedColor == null ? null : () { | ||||
|                   _setColor(entity, savedColor); | ||||
|                 }, | ||||
|               ) | ||||
|             ], | ||||
|           ) | ||||
|         ], | ||||
|       ); | ||||
|     } else { | ||||
|       return Container(width: 0.0, height: 0.0); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   Widget _buildEffectControl(LightEntity entity) { | ||||
|     if ((entity.supportEffect) && (entity.effectList != null)) { | ||||
|       return ModeSelectorWidget( | ||||
|           onChange: (effect) => _setEffect(entity, effect), | ||||
|           caption: "Effect", | ||||
|           options: entity.effectList, | ||||
|           value: _tmpEffect | ||||
|       ); | ||||
|     } else { | ||||
|       return Container(width: 0.0, height: 0.0); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|  | ||||
| } | ||||
							
								
								
									
										466
									
								
								lib/entity_widgets/controls/media_player_widgets.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,466 @@ | ||||
| part of '../../main.dart'; | ||||
|  | ||||
| class MediaPlayerWidget extends StatelessWidget { | ||||
|    | ||||
|   @override | ||||
|   Widget build(BuildContext context) { | ||||
|     final EntityModel entityModel = EntityModel.of(context); | ||||
|     final MediaPlayerEntity entity = entityModel.entityWrapper.entity; | ||||
|     //TheLogger.debug("stop: ${entity.supportStop}, seek: ${entity.supportSeek}"); | ||||
|     return Column( | ||||
|       children: <Widget>[ | ||||
|         Stack( | ||||
|           alignment: AlignmentDirectional.topEnd, | ||||
|           children: <Widget>[ | ||||
|             _buildImage(entity), | ||||
|             Positioned( | ||||
|               bottom: 0.0, | ||||
|               left: 0.0, | ||||
|               right: 0.0, | ||||
|               child: Container( | ||||
|                 color: Colors.black45, | ||||
|                 child: _buildState(entity), | ||||
|               ), | ||||
|             ), | ||||
|             Positioned( | ||||
|                 bottom: 0.0, | ||||
|                 left: 0.0, | ||||
|                 right: 0.0, | ||||
|                 child: MediaPlayerProgressWidget() | ||||
|             ) | ||||
|           ], | ||||
|         ), | ||||
|         MediaPlayerPlaybackControls() | ||||
|       ] | ||||
|     ); | ||||
|   } | ||||
|  | ||||
|   Widget _buildState(MediaPlayerEntity entity) { | ||||
|     TextStyle style = TextStyle( | ||||
|         fontSize: 14.0, | ||||
|         color: Colors.white, | ||||
|         fontWeight: FontWeight.normal, | ||||
|         height: 1.2 | ||||
|     ); | ||||
|     List<Widget> states = []; | ||||
|     states.add(Text("${entity.displayName}", style: style)); | ||||
|     String state = entity.state; | ||||
|     if (state == null || state == EntityState.off || state == EntityState.unavailable || state == EntityState.idle) { | ||||
|       states.add(Text("${entity.state}", style: style.apply(fontSizeDelta: 4.0),)); | ||||
|     } | ||||
|     if (entity.attributes['media_title'] != null) { | ||||
|       states.add(Text( | ||||
|         "${entity.attributes['media_title']}", | ||||
|         style: style.apply(fontSizeDelta: 6.0, fontWeightDelta: 50), | ||||
|         maxLines: 1, | ||||
|         softWrap: true, | ||||
|         overflow: TextOverflow.ellipsis, | ||||
|       )); | ||||
|     } | ||||
|     if (entity.attributes['media_content_type'] == "music") { | ||||
|       states.add(Text("${entity.attributes['media_artist'] ?? entity.attributes['app_name']}", style: style.apply(fontSizeDelta: 4.0),)); | ||||
|     } else if (entity.attributes['app_name'] != null) { | ||||
|       states.add(Text("${entity.attributes['app_name']}", style: style.apply(fontSizeDelta: 4.0),)); | ||||
|     } | ||||
|     return Padding( | ||||
|       padding: EdgeInsets.fromLTRB(Sizes.leftWidgetPadding, Sizes.rowPadding, Sizes.rightWidgetPadding, Sizes.rowPadding), | ||||
|       child: Column( | ||||
|         crossAxisAlignment: CrossAxisAlignment.start, | ||||
|         children: states, | ||||
|       ), | ||||
|     ); | ||||
|   } | ||||
|  | ||||
|   Widget _buildImage(MediaPlayerEntity entity) { | ||||
|     String state = entity.state; | ||||
|     if (entity.entityPicture != null && state != EntityState.off && state != EntityState.unavailable && state != EntityState.idle) { | ||||
|       return Container( | ||||
|         color: Colors.black, | ||||
|         child: Row( | ||||
|           mainAxisAlignment: MainAxisAlignment.center, | ||||
|           children: <Widget>[ | ||||
|             Flexible( | ||||
|               child: Image( | ||||
|                 image: CachedNetworkImageProvider("${entity.entityPicture}"), | ||||
|                 height: 240.0, | ||||
|                 //width: 320.0, | ||||
|                 fit: BoxFit.contain, | ||||
|               ), | ||||
|             ) | ||||
|           ], | ||||
|         ), | ||||
|       ); | ||||
|     } else { | ||||
|       return Row( | ||||
|         mainAxisAlignment: MainAxisAlignment.center, | ||||
|         children: <Widget>[ | ||||
|           Icon( | ||||
|             MaterialDesignIcons.getIconDataFromIconName("mdi:movie"), | ||||
|             size: 150.0, | ||||
|             color: EntityColor.stateColor("$state"), | ||||
|           ) | ||||
|         ], | ||||
|       ); | ||||
|       /*return Container( | ||||
|         color: Colors.blue, | ||||
|         height: 80.0, | ||||
|       );*/ | ||||
|     } | ||||
|   } | ||||
| } | ||||
|  | ||||
| class MediaPlayerPlaybackControls extends StatelessWidget { | ||||
|  | ||||
|   final bool showMenu; | ||||
|   final bool showStop; | ||||
|  | ||||
|   const MediaPlayerPlaybackControls({Key key, this.showMenu: true, this.showStop: false}) : super(key: key); | ||||
|  | ||||
|  | ||||
|   void _setPower(MediaPlayerEntity entity) { | ||||
|     if (entity.state != EntityState.unavailable && entity.state != EntityState.unknown) { | ||||
|       if (entity.state == EntityState.off) { | ||||
|         Logger.d("${entity.entityId} turn_on"); | ||||
|         eventBus.fire(new ServiceCallEvent( | ||||
|             entity.domain, "turn_on", entity.entityId, | ||||
|             null)); | ||||
|       } else { | ||||
|         Logger.d("${entity.entityId} turn_off"); | ||||
|         eventBus.fire(new ServiceCallEvent( | ||||
|             entity.domain, "turn_off", entity.entityId, | ||||
|             null)); | ||||
|       } | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   void _callAction(MediaPlayerEntity entity, String action) { | ||||
|     Logger.d("${entity.entityId} $action"); | ||||
|     eventBus.fire(new ServiceCallEvent( | ||||
|         entity.domain, "$action", entity.entityId, | ||||
|         null)); | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context) { | ||||
|     final MediaPlayerEntity entity = EntityModel.of(context).entityWrapper.entity; | ||||
|     List<Widget> result = []; | ||||
|     if (entity.supportTurnOn || entity.supportTurnOff) { | ||||
|       result.add( | ||||
|           IconButton( | ||||
|             icon: Icon(Icons.power_settings_new), | ||||
|             onPressed: () => _setPower(entity), | ||||
|             iconSize: Sizes.iconSize, | ||||
|           ) | ||||
|       ); | ||||
|     } else { | ||||
|       result.add( | ||||
|           Container( | ||||
|             width: Sizes.iconSize, | ||||
|           ) | ||||
|       ); | ||||
|     } | ||||
|     List <Widget> centeredControlsChildren = []; | ||||
|     if (entity.supportPreviousTrack && entity.state != EntityState.off && entity.state != EntityState.unavailable) { | ||||
|       centeredControlsChildren.add( | ||||
|           IconButton( | ||||
|             icon: Icon(Icons.skip_previous), | ||||
|             onPressed: () => _callAction(entity, "media_previous_track"), | ||||
|             iconSize: Sizes.iconSize, | ||||
|           ) | ||||
|       ); | ||||
|     } | ||||
|     if (entity.supportPlay || entity.supportPause) { | ||||
|       if (entity.state == EntityState.playing) { | ||||
|         centeredControlsChildren.add( | ||||
|             IconButton( | ||||
|               icon: Icon(Icons.pause_circle_filled), | ||||
|               color: Colors.blue, | ||||
|               onPressed: () => _callAction(entity, "media_pause"), | ||||
|               iconSize: Sizes.iconSize*1.8, | ||||
|             ) | ||||
|         ); | ||||
|       } else if (entity.state == EntityState.paused || entity.state == EntityState.idle) { | ||||
|         centeredControlsChildren.add( | ||||
|             IconButton( | ||||
|               icon: Icon(Icons.play_circle_filled), | ||||
|               color: Colors.blue, | ||||
|               onPressed: () => _callAction(entity, "media_play"), | ||||
|               iconSize: Sizes.iconSize*1.8, | ||||
|             ) | ||||
|         ); | ||||
|       } else { | ||||
|         centeredControlsChildren.add( | ||||
|             Container( | ||||
|               width: Sizes.iconSize*1.8, | ||||
|               height: Sizes.iconSize*2.0, | ||||
|             ) | ||||
|         ); | ||||
|       } | ||||
|     } | ||||
|     if (entity.supportNextTrack && entity.state != EntityState.off && entity.state != EntityState.unavailable) { | ||||
|       centeredControlsChildren.add( | ||||
|           IconButton( | ||||
|             icon: Icon(Icons.skip_next), | ||||
|             onPressed: () => _callAction(entity, "media_next_track"), | ||||
|             iconSize: Sizes.iconSize, | ||||
|           ) | ||||
|       ); | ||||
|     } | ||||
|     if (centeredControlsChildren.isNotEmpty) { | ||||
|       result.add( | ||||
|           Expanded( | ||||
|               child: Row( | ||||
|                 mainAxisAlignment: showMenu ? MainAxisAlignment.center : MainAxisAlignment.end, | ||||
|                 children: centeredControlsChildren, | ||||
|               ) | ||||
|           ) | ||||
|       ); | ||||
|     } else { | ||||
|       result.add( | ||||
|           Expanded( | ||||
|             child: Container( | ||||
|               height: 10.0, | ||||
|             ), | ||||
|           ) | ||||
|       ); | ||||
|     } | ||||
|     if (showMenu) { | ||||
|       result.add( | ||||
|           IconButton( | ||||
|               icon: Icon(MaterialDesignIcons.getIconDataFromIconName( | ||||
|                   "mdi:dots-vertical")), | ||||
|               onPressed: () => eventBus.fire(new ShowEntityPageEvent(entity)) | ||||
|           ) | ||||
|       ); | ||||
|     } else if (entity.supportStop && entity.state != EntityState.off && entity.state != EntityState.unavailable) { | ||||
|       result.add( | ||||
|           IconButton( | ||||
|               icon: Icon(Icons.stop), | ||||
|               onPressed: () => _callAction(entity, "media_stop") | ||||
|           ) | ||||
|       ); | ||||
|     } | ||||
|     return Row( | ||||
|       children: result, | ||||
|       mainAxisAlignment: MainAxisAlignment.center, | ||||
|     ); | ||||
|   } | ||||
|  | ||||
| } | ||||
|  | ||||
| class MediaPlayerControls extends StatefulWidget { | ||||
|   @override | ||||
|   _MediaPlayerControlsState createState() => _MediaPlayerControlsState(); | ||||
| } | ||||
|  | ||||
| class _MediaPlayerControlsState extends State<MediaPlayerControls> { | ||||
|  | ||||
|   double _newVolumeLevel; | ||||
|   bool _changedHere = false; | ||||
|   String _newSoundMode; | ||||
|   String _newSource; | ||||
|  | ||||
|   void _setVolume(double value, String entityId) { | ||||
|     setState(() { | ||||
|       _changedHere = true; | ||||
|       _newVolumeLevel = value; | ||||
|       eventBus.fire(ServiceCallEvent("media_player", "volume_set", entityId, {"volume_level": value})); | ||||
|     }); | ||||
|   } | ||||
|  | ||||
|   void _setVolumeMute(bool isMuted, String entityId) { | ||||
|     eventBus.fire(ServiceCallEvent("media_player", "volume_mute", entityId, {"is_volume_muted": isMuted})); | ||||
|   } | ||||
|  | ||||
|   void _setVolumeUp(String entityId) { | ||||
|     eventBus.fire(ServiceCallEvent("media_player", "volume_up", entityId, null)); | ||||
|   } | ||||
|  | ||||
|   void _setVolumeDown(String entityId) { | ||||
|     eventBus.fire(ServiceCallEvent("media_player", "volume_down", entityId, null)); | ||||
|   } | ||||
|  | ||||
|   void _setSoundMode(String value, String entityId) { | ||||
|     setState(() { | ||||
|       _newSoundMode = value; | ||||
|       _changedHere = true; | ||||
|       eventBus.fire(ServiceCallEvent("media_player", "select_sound_mode", entityId, {"sound_mode": "$value"})); | ||||
|     }); | ||||
|   } | ||||
|  | ||||
|   void _setSource(String source, String entityId) { | ||||
|     setState(() { | ||||
|       _newSource = source; | ||||
|       _changedHere = true; | ||||
|       eventBus.fire(ServiceCallEvent("media_player", "select_source", entityId, {"source": "$source"})); | ||||
|     }); | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context) { | ||||
|     final MediaPlayerEntity entity = EntityModel.of(context).entityWrapper.entity; | ||||
|     List<Widget> children = [ | ||||
|       MediaPlayerPlaybackControls( | ||||
|         showMenu: false, | ||||
|       ) | ||||
|     ]; | ||||
|     if (entity.state != EntityState.off && entity.state != EntityState.unknown && entity.state != EntityState.unavailable) { | ||||
|       Widget muteWidget; | ||||
|       Widget volumeStepWidget; | ||||
|       if (entity.supportVolumeMute  || entity.attributes["is_volume_muted"] != null) { | ||||
|         bool isMuted = entity.attributes["is_volume_muted"] ?? false; | ||||
|         muteWidget = | ||||
|             IconButton( | ||||
|                 icon: Icon(isMuted ? Icons.volume_up : Icons.volume_off), | ||||
|                 onPressed: () => _setVolumeMute(!isMuted, entity.entityId) | ||||
|             ); | ||||
|       } else { | ||||
|         muteWidget = Container(width: 0.0, height: 0.0,); | ||||
|       } | ||||
|       if (entity.supportVolumeStep) { | ||||
|         volumeStepWidget = Row( | ||||
|           mainAxisSize: MainAxisSize.min, | ||||
|           children: <Widget>[ | ||||
|             IconButton( | ||||
|                 icon: Icon(MaterialDesignIcons.getIconDataFromIconName("mdi:plus")), | ||||
|                 onPressed: () => _setVolumeUp(entity.entityId) | ||||
|             ), | ||||
|             IconButton( | ||||
|                 icon: Icon(MaterialDesignIcons.getIconDataFromIconName("mdi:minus")), | ||||
|                 onPressed: () => _setVolumeDown(entity.entityId) | ||||
|             ) | ||||
|           ], | ||||
|         ); | ||||
|       } else { | ||||
|         volumeStepWidget = Container(width: 0.0, height: 0.0,); | ||||
|       } | ||||
|       if (entity.supportVolumeSet) { | ||||
|         if (!_changedHere) { | ||||
|           _newVolumeLevel = entity._getDoubleAttributeValue("volume_level"); | ||||
|         } else { | ||||
|           _changedHere = false; | ||||
|         } | ||||
|         children.add( | ||||
|             UniversalSlider( | ||||
|               leading: muteWidget, | ||||
|               closing: volumeStepWidget, | ||||
|               title: "Volume", | ||||
|               onChanged: (value) { | ||||
|                 setState(() { | ||||
|                   _changedHere = true; | ||||
|                   _newVolumeLevel = value; | ||||
|                 }); | ||||
|               }, | ||||
|               value: _newVolumeLevel, | ||||
|               onChangeEnd: (value) => _setVolume(value, entity.entityId), | ||||
|               max: 1.0, | ||||
|               min: 0.0, | ||||
|             ) | ||||
|         ); | ||||
|       } else { | ||||
|         children.add(Row( | ||||
|           mainAxisSize: MainAxisSize.min, | ||||
|           children: <Widget>[ | ||||
|             muteWidget, | ||||
|             volumeStepWidget | ||||
|           ], | ||||
|         )); | ||||
|       } | ||||
|  | ||||
|       if (entity.supportSelectSoundMode && entity.soundModeList != null) { | ||||
|         if (!_changedHere) { | ||||
|           _newSoundMode = entity.attributes["sound_mode"]; | ||||
|         } else { | ||||
|           _changedHere = false; | ||||
|         } | ||||
|         children.add( | ||||
|           ModeSelectorWidget( | ||||
|               options: entity.soundModeList, | ||||
|               caption: "Sound mode", | ||||
|               value: _newSoundMode, | ||||
|               onChange: (value) => _setSoundMode(value, entity.entityId) | ||||
|           ) | ||||
|         ); | ||||
|       } | ||||
|  | ||||
|       if (entity.supportSelectSource && entity.sourceList != null) { | ||||
|         if (!_changedHere) { | ||||
|           _newSource = entity.attributes["source"]; | ||||
|         } else { | ||||
|           _changedHere = false; | ||||
|         } | ||||
|         children.add( | ||||
|             ModeSelectorWidget( | ||||
|                 options: entity.sourceList, | ||||
|                 caption: "Source", | ||||
|                 value: _newSource, | ||||
|                 onChange: (value) => _setSource(value, entity.entityId) | ||||
|             ) | ||||
|         ); | ||||
|       } | ||||
|  | ||||
|     } | ||||
|     return Column( | ||||
|       children: children, | ||||
|     ); | ||||
|   } | ||||
|  | ||||
| } | ||||
|  | ||||
| class MediaPlayerProgressWidget extends StatefulWidget { | ||||
|   @override | ||||
|   _MediaPlayerProgressWidgetState createState() => _MediaPlayerProgressWidgetState(); | ||||
| } | ||||
|  | ||||
| class _MediaPlayerProgressWidgetState extends State<MediaPlayerProgressWidget> { | ||||
|  | ||||
|   Timer _timer; | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context) { | ||||
|     final EntityModel entityModel = EntityModel.of(context); | ||||
|     final MediaPlayerEntity entity = entityModel.entityWrapper.entity; | ||||
|     double progress; | ||||
|     try { | ||||
|       DateTime lastUpdated = DateTime.parse( | ||||
|           entity.attributes["media_position_updated_at"]).toLocal(); | ||||
|       Duration duration = Duration(seconds: entity._getIntAttributeValue("media_duration") ?? 1); | ||||
|       Duration position = Duration(seconds: entity._getIntAttributeValue("media_position") ?? 0); | ||||
|       int currentPosition = position.inSeconds; | ||||
|       if (entity.state == EntityState.playing) { | ||||
|         _timer?.cancel(); | ||||
|         _timer = Timer(Duration(seconds: 1), () { | ||||
|           setState(() { | ||||
|           }); | ||||
|         }); | ||||
|         int differenceInSeconds = DateTime | ||||
|             .now() | ||||
|             .difference(lastUpdated) | ||||
|             .inSeconds; | ||||
|         currentPosition = currentPosition + differenceInSeconds; | ||||
|       } else { | ||||
|         _timer?.cancel(); | ||||
|       } | ||||
|       progress = currentPosition / duration.inSeconds; | ||||
|       return LinearProgressIndicator( | ||||
|         value: progress, | ||||
|         backgroundColor: Colors.black45, | ||||
|         valueColor: AlwaysStoppedAnimation<Color>(EntityColor.stateColor(EntityState.on)), | ||||
|       ); | ||||
|     } catch (e) { | ||||
|       _timer?.cancel(); | ||||
|       progress = 0.0; | ||||
|     } | ||||
|     return LinearProgressIndicator( | ||||
|       value: progress, | ||||
|       backgroundColor: Colors.black45, | ||||
|     ); | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   void dispose() { | ||||
|     _timer?.cancel(); | ||||
|     super.dispose(); | ||||
|   } | ||||
|  | ||||
| } | ||||
							
								
								
									
										70
									
								
								lib/entity_widgets/controls/slider_controls.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,70 @@ | ||||
| part of '../../main.dart'; | ||||
|  | ||||
| class SliderControlsWidget extends StatefulWidget { | ||||
|  | ||||
|   SliderControlsWidget({Key key}) : super(key: key); | ||||
|  | ||||
|   @override | ||||
|   _SliderControlsWidgetState createState() => _SliderControlsWidgetState(); | ||||
| } | ||||
|  | ||||
| class _SliderControlsWidgetState extends State<SliderControlsWidget> { | ||||
|   int _multiplier = 1; | ||||
|   double _newValue; | ||||
|   bool _changedHere = false; | ||||
|  | ||||
|   void setNewState(newValue, domain, entityId) { | ||||
|     setState(() { | ||||
|       _newValue = newValue; | ||||
|       _changedHere = true; | ||||
|     }); | ||||
|     eventBus.fire(new ServiceCallEvent(domain, "set_value", entityId, | ||||
|         {"value": "${newValue.toString()}"})); | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context) { | ||||
|     final entityModel = EntityModel.of(context); | ||||
|     final SliderEntity entity = entityModel.entityWrapper.entity; | ||||
|     if (entity.valueStep < 1) { | ||||
|       _multiplier = 10; | ||||
|     } else if (entity.valueStep < 0.1) { | ||||
|       _multiplier = 100; | ||||
|     } | ||||
|     if (!_changedHere) { | ||||
|       _newValue = entity.doubleState; | ||||
|     } else { | ||||
|       _changedHere = false; | ||||
|     } | ||||
|     Widget slider = Slider( | ||||
|       min: entity.minValue * _multiplier, | ||||
|       max: entity.maxValue * _multiplier, | ||||
|       value: (_newValue <= entity.maxValue) && | ||||
|           (_newValue >= entity.minValue) | ||||
|           ? _newValue * _multiplier | ||||
|           : entity.minValue * _multiplier, | ||||
|       onChanged: (value) { | ||||
|         setState(() { | ||||
|           _newValue = (value.roundToDouble() / _multiplier); | ||||
|           _changedHere = true; | ||||
|         }); | ||||
|       }, | ||||
|       onChangeEnd: (value) { | ||||
|         setNewState(value.roundToDouble() / _multiplier, entity.domain, entity.entityId); | ||||
|       }, | ||||
|     ); | ||||
|     return Column( | ||||
|       crossAxisAlignment: CrossAxisAlignment.center, | ||||
|       children: <Widget>[ | ||||
|         Text( | ||||
|           "$_newValue", | ||||
|           style: TextStyle( | ||||
|             fontSize: Sizes.largeFontSize, | ||||
|             color: Colors.blue | ||||
|           ), | ||||
|         ), | ||||
|         slider | ||||
|       ], | ||||
|     ); | ||||
|   } | ||||
| } | ||||
							
								
								
									
										61
									
								
								lib/entity_widgets/default_entity_container.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,61 @@ | ||||
| part of '../main.dart'; | ||||
|  | ||||
| class DefaultEntityContainer extends StatelessWidget { | ||||
|   DefaultEntityContainer({ | ||||
|     Key key, | ||||
|     @required this.state | ||||
|   }) : super(key: key); | ||||
|  | ||||
|   final Widget state; | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context) { | ||||
|     final EntityModel entityModel = EntityModel.of(context); | ||||
|     if (entityModel.entityWrapper.entity.statelessType == StatelessEntityType.MISSED) { | ||||
|       return MissedEntityWidget(); | ||||
|     } | ||||
|     if (entityModel.entityWrapper.entity.statelessType == StatelessEntityType.DIVIDER) { | ||||
|       return Divider(); | ||||
|     } | ||||
|     if (entityModel.entityWrapper.entity.statelessType == StatelessEntityType.SECTION) { | ||||
|       return Column( | ||||
|         crossAxisAlignment: CrossAxisAlignment.start, | ||||
|         mainAxisSize: MainAxisSize.min, | ||||
|         children: <Widget>[ | ||||
|           Divider(), | ||||
|           Text( | ||||
|               "${entityModel.entityWrapper.entity.displayName}", | ||||
|             style: TextStyle(color: Colors.blue), | ||||
|           ) | ||||
|         ], | ||||
|       ); | ||||
|     } | ||||
|     return InkWell( | ||||
|       onLongPress: () { | ||||
|         if (entityModel.handleTap) { | ||||
|           entityModel.entityWrapper.handleHold(); | ||||
|         } | ||||
|       }, | ||||
|       onTap: () { | ||||
|         if (entityModel.handleTap) { | ||||
|           entityModel.entityWrapper.handleTap(); | ||||
|         } | ||||
|       }, | ||||
|       child: Row( | ||||
|         mainAxisSize: MainAxisSize.max, | ||||
|         children: <Widget>[ | ||||
|           EntityIcon(), | ||||
|  | ||||
|           Flexible( | ||||
|             fit: FlexFit.tight, | ||||
|             flex: 3, | ||||
|             child: EntityName( | ||||
|               padding: EdgeInsets.fromLTRB(10.0, 2.0, 10.0, 2.0), | ||||
|             ), | ||||
|           ), | ||||
|           state | ||||
|         ], | ||||
|       ), | ||||
|     ); | ||||
|   } | ||||
| } | ||||
							
								
								
									
										74
									
								
								lib/entity_widgets/entity_colors.class.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,74 @@ | ||||
| part of '../main.dart'; | ||||
|  | ||||
| class EntityColor { | ||||
|  | ||||
|   static const defaultStateColor = Color.fromRGBO(68, 115, 158, 1.0); | ||||
|  | ||||
|   static const badgeColors = { | ||||
|     "default": Color.fromRGBO(223, 76, 30, 1.0), | ||||
|     "binary_sensor": Color.fromRGBO(3, 155, 229, 1.0) | ||||
|   }; | ||||
|  | ||||
|   static const _stateColors = { | ||||
|     EntityState.on: Colors.amber, | ||||
|     "auto": Colors.amber, | ||||
|     EntityState.active: Colors.amber, | ||||
|     EntityState.playing: Colors.amber, | ||||
|     "above_horizon": Colors.amber, | ||||
|     EntityState.home:  Colors.amber, | ||||
|     EntityState.open:  Colors.amber, | ||||
|     EntityState.off: defaultStateColor, | ||||
|     EntityState.closed: defaultStateColor, | ||||
|     "below_horizon": defaultStateColor, | ||||
|     "default": defaultStateColor, | ||||
|     EntityState.idle: defaultStateColor, | ||||
|     "heat": Colors.redAccent, | ||||
|     "cool": Colors.lightBlue, | ||||
|     EntityState.unavailable: Colors.black26, | ||||
|     EntityState.unknown: Colors.black26, | ||||
|     EntityState.alarm_disarmed: Colors.green, | ||||
|     EntityState.alarm_armed_away: Colors.redAccent, | ||||
|     EntityState.alarm_armed_custom_bypass: Colors.redAccent, | ||||
|     EntityState.alarm_armed_home: Colors.redAccent, | ||||
|     EntityState.alarm_armed_night: Colors.redAccent, | ||||
|     EntityState.alarm_triggered: Colors.redAccent, | ||||
|     EntityState.alarm_arming: Colors.amber, | ||||
|     EntityState.alarm_disarming: Colors.amber, | ||||
|     EntityState.alarm_pending: Colors.amber, | ||||
|   }; | ||||
|  | ||||
|   static Color stateColor(String state) { | ||||
|     return _stateColors[state] ?? _stateColors["default"]; | ||||
|   } | ||||
|  | ||||
|   static charts.Color chartHistoryStateColor(String state, int id) { | ||||
|     Color c = _stateColors[state]; | ||||
|     if (c != null) { | ||||
|       return charts.Color( | ||||
|           r: c.red, | ||||
|           g: c.green, | ||||
|           b: c.blue, | ||||
|           a: c.alpha | ||||
|       ); | ||||
|     } else { | ||||
|       double r = id.toDouble() % 10; | ||||
|       return charts.MaterialPalette.getOrderedPalettes(10)[r.round()].shadeDefault; | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   static Color historyStateColor(String state, int id) { | ||||
|     Color c = _stateColors[state]; | ||||
|     if (c != null) { | ||||
|       return c; | ||||
|     } else { | ||||
|       if (id > -1) { | ||||
|         double r = id.toDouble() % 10; | ||||
|         charts.Color c1 = charts.MaterialPalette.getOrderedPalettes(10)[r.round()].shadeDefault; | ||||
|         return Color.fromARGB(c1.a, c1.r, c1.g, c1.b); | ||||
|       } else { | ||||
|         return _stateColors[EntityState.on]; | ||||
|       } | ||||
|     } | ||||
|   } | ||||
|  | ||||
| } | ||||
							
								
								
									
										74
									
								
								lib/entity_widgets/entity_icon.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,74 @@ | ||||
| part of '../main.dart'; | ||||
|  | ||||
| class EntityIcon extends StatelessWidget { | ||||
|  | ||||
|   final EdgeInsetsGeometry padding; | ||||
|   final double size; | ||||
|   final Color color; | ||||
|  | ||||
|   const EntityIcon({Key key, this.color, this.size: Sizes.iconSize, this.padding: const EdgeInsets.all(0.0)}) : super(key: key); | ||||
|  | ||||
|   int getDefaultIconByEntityId(String entityId, String deviceClass, String state) { | ||||
|     String domain = entityId.split(".")[0]; | ||||
|     String iconNameByDomain = MaterialDesignIcons.defaultIconsByDomains["$domain.$state"] ?? MaterialDesignIcons.defaultIconsByDomains["$domain"]; | ||||
|     String iconNameByDeviceClass; | ||||
|     if (deviceClass != null) { | ||||
|       iconNameByDeviceClass = MaterialDesignIcons.defaultIconsByDeviceClass["$domain.$deviceClass.$state"] ?? MaterialDesignIcons.defaultIconsByDeviceClass["$domain.$deviceClass"]; | ||||
|     } | ||||
|     String iconName = iconNameByDeviceClass ?? iconNameByDomain; | ||||
|     if (iconName != null) { | ||||
|       return MaterialDesignIcons.iconsDataMap[iconName] ?? 0; | ||||
|     } else { | ||||
|       return 0; | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   Widget buildIcon(EntityWrapper data, Color color) { | ||||
|     if (data == null) { | ||||
|       return null; | ||||
|     } | ||||
|     if (data.entityPicture != null) { | ||||
|       return Container( | ||||
|         height: size+12, | ||||
|         width: size+12, | ||||
|         decoration: BoxDecoration( | ||||
|             shape: BoxShape.circle, | ||||
|             image: DecorationImage( | ||||
|               fit:BoxFit.cover, | ||||
|               image: CachedNetworkImageProvider( | ||||
|                 "${data.entityPicture}" | ||||
|               ), | ||||
|             ) | ||||
|         ), | ||||
|       ); | ||||
|     } | ||||
|     String iconName = data.icon; | ||||
|     int iconCode = 0; | ||||
|     if (iconName.length > 0) { | ||||
|       iconCode = MaterialDesignIcons.getIconCodeByIconName(iconName); | ||||
|     } else { | ||||
|       iconCode = getDefaultIconByEntityId(data.entity.entityId, | ||||
|           data.entity.deviceClass, data.entity.state); // | ||||
|     } | ||||
|     return Padding( | ||||
|         padding: EdgeInsets.fromLTRB(6.0, 6.0, 6.0, 6.0), | ||||
|         child: Icon( | ||||
|           IconData(iconCode, fontFamily: 'Material Design Icons'), | ||||
|           size: size, | ||||
|           color: color, | ||||
|         ) | ||||
|     ); | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context) { | ||||
|     final EntityWrapper entityWrapper = EntityModel.of(context).entityWrapper; | ||||
|     return Padding( | ||||
|       padding: padding, | ||||
|       child: buildIcon( | ||||
|           entityWrapper, | ||||
|           color ?? EntityColor.stateColor(entityWrapper.entity.state) | ||||
|       ), | ||||
|     ); | ||||
|   } | ||||
| } | ||||
							
								
								
									
										33
									
								
								lib/entity_widgets/entity_name.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,33 @@ | ||||
| part of '../main.dart'; | ||||
|  | ||||
| class EntityName extends StatelessWidget { | ||||
|  | ||||
|   final EdgeInsetsGeometry padding; | ||||
|   final TextOverflow textOverflow; | ||||
|   final bool wordsWrap; | ||||
|   final double fontSize; | ||||
|   final TextAlign textAlign; | ||||
|   final int maxLines; | ||||
|  | ||||
|   const EntityName({Key key, this.maxLines, this.padding: const EdgeInsets.only(right: 10.0), this.textOverflow: TextOverflow.ellipsis, this.wordsWrap: true, this.fontSize: Sizes.nameFontSize, this.textAlign: TextAlign.left}) : super(key: key); | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context) { | ||||
|     final EntityWrapper entityWrapper = EntityModel.of(context).entityWrapper; | ||||
|     TextStyle textStyle = TextStyle(fontSize: fontSize); | ||||
|     if (entityWrapper.entity.statelessType == StatelessEntityType.WEBLINK) { | ||||
|       textStyle = textStyle.apply(color: Colors.blue, decoration: TextDecoration.underline); | ||||
|     } | ||||
|     return Padding( | ||||
|       padding: padding, | ||||
|       child: Text( | ||||
|         "${entityWrapper.displayName}", | ||||
|         overflow: textOverflow, | ||||
|         softWrap: wordsWrap, | ||||
|         maxLines: maxLines, | ||||
|         style: textStyle, | ||||
|         textAlign: textAlign, | ||||
|       ), | ||||
|     ); | ||||
|   } | ||||
| } | ||||
							
								
								
									
										14
									
								
								lib/entity_widgets/entity_page_container.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,14 @@ | ||||
| part of '../main.dart'; | ||||
|  | ||||
| class EntityPageContainer extends StatelessWidget { | ||||
|   EntityPageContainer({Key key, @required this.children}) : super(key: key); | ||||
|  | ||||
|   final List<Widget> children; | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context) { | ||||
|     return ListView( | ||||
|       children: children, | ||||
|     ); | ||||
|   } | ||||
| } | ||||
							
								
								
									
										90
									
								
								lib/entity_widgets/glance_entity_container.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,90 @@ | ||||
| part of '../main.dart'; | ||||
|  | ||||
| class GlanceEntityContainer extends StatelessWidget { | ||||
|  | ||||
|   final bool showName; | ||||
|   final bool showState; | ||||
|   final bool nameInTheBottom; | ||||
|   final double iconSize; | ||||
|   final double nameFontSize; | ||||
|   final bool wordsWrapInName; | ||||
|  | ||||
|   GlanceEntityContainer({ | ||||
|     Key key, | ||||
|     @required this.showName, | ||||
|     @required this.showState, | ||||
|     this.nameInTheBottom: false, | ||||
|     this.iconSize: Sizes.iconSize, | ||||
|     this.nameFontSize: Sizes.smallFontSize, | ||||
|     this.wordsWrapInName: false | ||||
|   }) : super(key: key); | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context) { | ||||
|     final EntityWrapper entityWrapper = EntityModel.of(context).entityWrapper; | ||||
|     if (entityWrapper.entity.statelessType == StatelessEntityType.MISSED) { | ||||
|       return MissedEntityWidget(); | ||||
|     } | ||||
|     if (entityWrapper.entity.statelessType > StatelessEntityType.MISSED) { | ||||
|       return Container(width: 0.0, height: 0.0,); | ||||
|     } | ||||
|     List<Widget> result = []; | ||||
|     if (!nameInTheBottom) { | ||||
|       if (showName) { | ||||
|         result.add(_buildName()); | ||||
|       } | ||||
|     } else { | ||||
|       if (showState) { | ||||
|         result.add(_buildState()); | ||||
|       } | ||||
|     } | ||||
|     result.add( | ||||
|       EntityIcon( | ||||
|         padding: EdgeInsets.all(0.0), | ||||
|         size: iconSize, | ||||
|       ) | ||||
|     ); | ||||
|     if (!nameInTheBottom) { | ||||
|       if (showState) { | ||||
|         result.add(_buildState()); | ||||
|       } | ||||
|     } else { | ||||
|       result.add(_buildName()); | ||||
|     } | ||||
|  | ||||
|     return Center( | ||||
|       child: InkResponse( | ||||
|         child: ConstrainedBox( | ||||
|           constraints: BoxConstraints(minWidth: Sizes.iconSize * 2), | ||||
|           child: Column( | ||||
|             mainAxisSize: MainAxisSize.min, | ||||
|             //mainAxisAlignment: MainAxisAlignment.start, | ||||
|             //crossAxisAlignment: CrossAxisAlignment.center, | ||||
|             children: result, | ||||
|           ), | ||||
|         ), | ||||
|         onTap: () => entityWrapper.handleTap(), | ||||
|         onLongPress: () => entityWrapper.handleHold(), | ||||
|       ), | ||||
|     ); | ||||
|   } | ||||
|  | ||||
|   Widget _buildName() { | ||||
|     return EntityName( | ||||
|       padding: EdgeInsets.only(bottom: Sizes.rowPadding), | ||||
|       textOverflow: TextOverflow.ellipsis, | ||||
|       wordsWrap: wordsWrapInName, | ||||
|       textAlign: TextAlign.center, | ||||
|       fontSize: nameFontSize, | ||||
|     ); | ||||
|   } | ||||
|  | ||||
|   Widget _buildState() { | ||||
|     return SimpleEntityState( | ||||
|       textAlign: TextAlign.center, | ||||
|       expanded: false, | ||||
|       maxLines: 1, | ||||
|       padding: EdgeInsets.only(top: Sizes.rowPadding), | ||||
|     ); | ||||
|   } | ||||
| } | ||||
							
								
								
									
										230
									
								
								lib/entity_widgets/history_chart/combined_history_chart.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,230 @@ | ||||
| part of '../../main.dart'; | ||||
|  | ||||
| class CombinedHistoryChartWidget extends StatefulWidget { | ||||
|   final rawHistory; | ||||
|   final EntityHistoryConfig config; | ||||
|  | ||||
|   const CombinedHistoryChartWidget({Key key, @required this.rawHistory, @required this.config}) : super(key: key); | ||||
|  | ||||
|  | ||||
|   @override | ||||
|   State<StatefulWidget> createState() { | ||||
|     return new _CombinedHistoryChartWidgetState(); | ||||
|   } | ||||
|  | ||||
| } | ||||
|  | ||||
| class _CombinedHistoryChartWidgetState extends State<CombinedHistoryChartWidget> { | ||||
|  | ||||
|   int _selectedId = -1; | ||||
|   List<charts.Series<EntityHistoryMoment, DateTime>> _parsedHistory; | ||||
|  | ||||
|   @override | ||||
|   void initState() { | ||||
|     // TODO: implement initState | ||||
|     super.initState(); | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context) { | ||||
|     _parsedHistory = _parseHistory(); | ||||
|     DateTime selectedTime; | ||||
|     List<String> selectedStates = []; | ||||
|     List<int> colorIndexes = []; | ||||
|     if ((_selectedId > -1) && (_parsedHistory != null) && (_parsedHistory.first.data.length >= (_selectedId + 1))) { | ||||
|       selectedTime = _parsedHistory.first.data[_selectedId].startTime; | ||||
|       _parsedHistory.where((item) { return item.id == "state"; }).forEach((item) { | ||||
|         selectedStates.add(item.data[_selectedId].state); | ||||
|         colorIndexes.add(item.data[_selectedId].colorId); | ||||
|       }); | ||||
|       _parsedHistory.where((item) { return item.id == "value"; }).forEach((item) { | ||||
|         selectedStates.add("${item.data[_selectedId].value ?? '-'}"); | ||||
|         colorIndexes.add(item.data[_selectedId].colorId); | ||||
|       }); | ||||
|     } | ||||
|     return Column( | ||||
|       mainAxisAlignment: MainAxisAlignment.start, | ||||
|       crossAxisAlignment: CrossAxisAlignment.center, | ||||
|       children: <Widget>[ | ||||
|         HistoryControlWidget( | ||||
|           selectedTimeStart: selectedTime, | ||||
|           selectedStates: selectedStates, | ||||
|           onPrevTap: () => _selectPrev(), | ||||
|           onNextTap: () => _selectNext(), | ||||
|           colorIndexes: colorIndexes, | ||||
|         ), | ||||
|         SizedBox( | ||||
|           height: 150.0, | ||||
|           child: charts.TimeSeriesChart( | ||||
|             _parsedHistory, | ||||
|             animate: false, | ||||
|             primaryMeasureAxis: new charts.NumericAxisSpec( | ||||
|                 tickProviderSpec: | ||||
|                 new charts.BasicNumericTickProviderSpec(zeroBound: false)), | ||||
|             dateTimeFactory: const charts.LocalDateTimeFactory(), | ||||
|             defaultRenderer: charts.LineRendererConfig( | ||||
|               includeArea: false, | ||||
|               includePoints: true | ||||
|             ), | ||||
|             selectionModels: [ | ||||
|               new charts.SelectionModelConfig( | ||||
|                 type: charts.SelectionModelType.info, | ||||
|                 changedListener: (model) => _onSelectionChanged(model), | ||||
|               ) | ||||
|             ], | ||||
|             customSeriesRenderers: [ | ||||
|               new charts.SymbolAnnotationRendererConfig( | ||||
|                 customRendererId: "stateBars" | ||||
|               ) | ||||
|             ], | ||||
|           ), | ||||
|         ) | ||||
|       ], | ||||
|     ); | ||||
|   } | ||||
|  | ||||
|   double _parseToDouble(temp1) { | ||||
|     if (temp1 is int) { | ||||
|       return temp1.toDouble(); | ||||
|     } else if (temp1 is double) { | ||||
|       return temp1; | ||||
|     } else { | ||||
|       return double.tryParse("$temp1"); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   List<charts.Series<EntityHistoryMoment, DateTime>> _parseHistory() { | ||||
|     Logger.d("  parsing history..."); | ||||
|     Map<String, List<EntityHistoryMoment>> numericDataLists = {}; | ||||
|     int colorIdCounter = 0; | ||||
|     widget.config.numericAttributesToShow.forEach((String attrName) { | ||||
|       Logger.d("    parsing attribute $attrName"); | ||||
|       List<EntityHistoryMoment> data = []; | ||||
|       DateTime now = DateTime.now(); | ||||
|       for (var i = 0; i < widget.rawHistory.length; i++) { | ||||
|         var stateData = widget.rawHistory[i]; | ||||
|         DateTime startTime = DateTime.tryParse(stateData["last_updated"])?.toLocal(); | ||||
|         DateTime endTime; | ||||
|         bool hiddenLine; | ||||
|         double value; | ||||
|         double previousValue = 0.0; | ||||
|         value = _parseToDouble(stateData["attributes"]["$attrName"]); | ||||
|         bool hiddenDot = (value == null); | ||||
|         if (hiddenDot && i > 0) { | ||||
|           previousValue = data[i-1].value ?? data[i-1].previousValue; | ||||
|         } | ||||
|         if (i < (widget.rawHistory.length - 1)) { | ||||
|           endTime = DateTime.tryParse(widget.rawHistory[i+1]["last_updated"])?.toLocal(); | ||||
|           double nextValue = _parseToDouble(widget.rawHistory[i+1]["attributes"]["$attrName"]); | ||||
|           hiddenLine = (nextValue == null || hiddenDot); | ||||
|         } else { | ||||
|           hiddenLine = hiddenDot; | ||||
|           endTime = now; | ||||
|         } | ||||
|         data.add(EntityHistoryMoment( | ||||
|           value: value, | ||||
|           previousValue: previousValue, | ||||
|           hiddenDot: hiddenDot, | ||||
|           hiddenLine: hiddenLine, | ||||
|           state: stateData["state"], | ||||
|           startTime: startTime, | ||||
|           endTime: endTime, | ||||
|           id: i, | ||||
|           colorId: colorIdCounter | ||||
|         )); | ||||
|       } | ||||
|       data.add(EntityHistoryMoment( | ||||
|           value: data.last.value, | ||||
|           previousValue: data.last.previousValue, | ||||
|           hiddenDot: data.last.hiddenDot, | ||||
|           hiddenLine: data.last.hiddenLine, | ||||
|           state: data.last.state, | ||||
|           startTime: now, | ||||
|           id: widget.rawHistory.length, | ||||
|           colorId: colorIdCounter | ||||
|       )); | ||||
|       numericDataLists.addAll({attrName: data}); | ||||
|       colorIdCounter += 1; | ||||
|     }); | ||||
|  | ||||
|     if ((_selectedId == -1) && (numericDataLists.isNotEmpty)) { | ||||
|       _selectedId = 0; | ||||
|     } | ||||
|     List<charts.Series<EntityHistoryMoment, DateTime>> result = []; | ||||
|     numericDataLists.forEach((attrName, dataList) { | ||||
|       Logger.d("  adding ${dataList.length} data values"); | ||||
|       result.add( | ||||
|         new charts.Series<EntityHistoryMoment, DateTime>( | ||||
|           id: "value", | ||||
|           colorFn: (EntityHistoryMoment historyMoment, __) => EntityColor.chartHistoryStateColor("_", historyMoment.colorId), | ||||
|           radiusPxFn: (EntityHistoryMoment historyMoment, __) { | ||||
|               if (historyMoment.hiddenDot) { | ||||
|                 return 0.0; | ||||
|               } else if (historyMoment.id == _selectedId) { | ||||
|                 return 5.0; | ||||
|               } else { | ||||
|                 return 1.0; | ||||
|               } | ||||
|             }, | ||||
|           strokeWidthPxFn: (EntityHistoryMoment historyMoment, __) => historyMoment.hiddenLine ? 0.0 : 2.0, | ||||
|           domainFn: (EntityHistoryMoment historyMoment, _) => historyMoment.startTime, | ||||
|           measureFn: (EntityHistoryMoment historyMoment, _) => historyMoment.value ?? historyMoment.previousValue, | ||||
|           data: dataList, | ||||
|           /*domainLowerBoundFn: (CombinedEntityStateHistoryMoment historyMoment, _) => historyMoment.time.subtract(Duration(hours: 1)), | ||||
|           domainUpperBoundFn: (CombinedEntityStateHistoryMoment historyMoment, _) => historyMoment.time.add(Duration(hours: 1)),*/ | ||||
|         ) | ||||
|       ); | ||||
|     }); | ||||
|     result.add( | ||||
|         new charts.Series<EntityHistoryMoment, DateTime>( | ||||
|           id: 'state', | ||||
|           radiusPxFn: (EntityHistoryMoment historyMoment, __) => (historyMoment.id == _selectedId) ? 5.0 : 4.0, | ||||
|           colorFn: (EntityHistoryMoment historyMoment, __) => EntityColor.chartHistoryStateColor(historyMoment.state, historyMoment.colorId), | ||||
|           domainFn: (EntityHistoryMoment historyMoment, _) => historyMoment.startTime, | ||||
|           domainLowerBoundFn: (EntityHistoryMoment historyMoment, _) => historyMoment.startTime, | ||||
|           domainUpperBoundFn: (EntityHistoryMoment historyMoment, _) => historyMoment.endTime ?? DateTime.now(), | ||||
|           // No measure values are needed for symbol annotations. | ||||
|           measureFn: (_, __) => null, | ||||
|           data: numericDataLists[numericDataLists.keys.first], | ||||
|         ) | ||||
|         // Configure our custom symbol annotation renderer for this series. | ||||
|           ..setAttribute(charts.rendererIdKey, 'stateBars') | ||||
|         // Optional radius for the annotation shape. If not specified, this will | ||||
|         // default to the same radius as the points. | ||||
|           //..setAttribute(charts.boundsLineRadiusPxKey, 3.5) | ||||
|     ); | ||||
|     return result; | ||||
|   } | ||||
|  | ||||
|   void _selectPrev() { | ||||
|     if (_selectedId > 0) { | ||||
|       setState(() { | ||||
|         _selectedId -= 1; | ||||
|       }); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   void _selectNext() { | ||||
|     if (_selectedId < (_parsedHistory.first.data.length - 1)) { | ||||
|       setState(() { | ||||
|         _selectedId += 1; | ||||
|       }); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   void _onSelectionChanged(charts.SelectionModel model) { | ||||
|     final selectedDatum = model.selectedDatum; | ||||
|  | ||||
|     int selectedId; | ||||
|  | ||||
|     if (selectedDatum.isNotEmpty) { | ||||
|       selectedId = selectedDatum.first.datum.id; | ||||
|       setState(() { | ||||
|         _selectedId = selectedId; | ||||
|       }); | ||||
|     } else { | ||||
|       setState(() { | ||||
|       }); | ||||
|     } | ||||
|   } | ||||
| } | ||||
							
								
								
									
										145
									
								
								lib/entity_widgets/history_chart/entity_history.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,145 @@ | ||||
| part of '../../main.dart'; | ||||
|  | ||||
| class EntityHistoryWidgetType { | ||||
|   static const int simple = 0; | ||||
|   static const int numericState = 1; | ||||
|   static const int numericAttributes = 2; | ||||
| } | ||||
|  | ||||
| class EntityHistoryConfig { | ||||
|   final int chartType; | ||||
|   final List<String> numericAttributesToShow; | ||||
|   final bool numericState; | ||||
|  | ||||
|   EntityHistoryConfig({this.chartType, this.numericAttributesToShow, this.numericState: true}); | ||||
|  | ||||
| } | ||||
|  | ||||
| class EntityHistoryWidget extends StatefulWidget { | ||||
|  | ||||
|   final EntityHistoryConfig config; | ||||
|  | ||||
|   const EntityHistoryWidget({Key key, @required this.config}) : super(key: key); | ||||
|  | ||||
|   @override | ||||
|   _EntityHistoryWidgetState createState() { | ||||
|     return new _EntityHistoryWidgetState(); | ||||
|   } | ||||
| } | ||||
|  | ||||
| class _EntityHistoryWidgetState extends State<EntityHistoryWidget> { | ||||
|  | ||||
|   List _history; | ||||
|   bool _needToUpdateHistory; | ||||
|   DateTime _historyLastUpdated; | ||||
|   bool _disposed = false; | ||||
|  | ||||
|   @override | ||||
|   void initState() { | ||||
|     super.initState(); | ||||
|     _needToUpdateHistory = true; | ||||
|   } | ||||
|  | ||||
|   void _loadHistory(HomeAssistant ha, String entityId) { | ||||
|     DateTime now = DateTime.now(); | ||||
|     if (_historyLastUpdated != null) { | ||||
|       Logger.d("History was updated ${now.difference(_historyLastUpdated).inSeconds} seconds ago"); | ||||
|     } | ||||
|     if (_historyLastUpdated == null || now.difference(_historyLastUpdated).inSeconds > 30) { | ||||
|       _historyLastUpdated = now; | ||||
|       ha.connection.getHistory(entityId).then((history){ | ||||
|         if (!_disposed) { | ||||
|           setState(() { | ||||
|             _history = history.isNotEmpty ? history[0] : []; | ||||
|             _needToUpdateHistory = false; | ||||
|           }); | ||||
|         } | ||||
|       }).catchError((e) { | ||||
|         Logger.e("Error loading $entityId history: $e"); | ||||
|         if (!_disposed) { | ||||
|           setState(() { | ||||
|             _history = []; | ||||
|             _needToUpdateHistory = false; | ||||
|           }); | ||||
|         } | ||||
|       }); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context) { | ||||
|     final HomeAssistantModel homeAssistantModel = HomeAssistantModel.of(context); | ||||
|     final EntityModel entityModel = EntityModel.of(context); | ||||
|     final Entity entity = entityModel.entityWrapper.entity; | ||||
|     if (!_needToUpdateHistory) { | ||||
|       _needToUpdateHistory = true; | ||||
|     } else { | ||||
|       _loadHistory(homeAssistantModel.homeAssistant, entity.entityId); | ||||
|     } | ||||
|     return _buildChart(); | ||||
|   } | ||||
|  | ||||
|   Widget _buildChart() { | ||||
|     List<Widget> children = []; | ||||
|     if (_history == null) { | ||||
|       children.add( | ||||
|           Text("Loading history...") | ||||
|       ); | ||||
|     } else if (_history.isEmpty) { | ||||
|       children.add( | ||||
|           Text("No history") | ||||
|       ); | ||||
|     } else { | ||||
|       children.add( | ||||
|           _selectChartWidget() | ||||
|       ); | ||||
|     } | ||||
|     children.add(Divider()); | ||||
|     return Padding( | ||||
|       padding: EdgeInsets.fromLTRB(0.0, 0.0, 0.0, Sizes.rowPadding), | ||||
|       child: Column( | ||||
|         children: children, | ||||
|       ), | ||||
|     ); | ||||
|   } | ||||
|  | ||||
|   Widget _selectChartWidget() { | ||||
|     switch (widget.config.chartType) { | ||||
|  | ||||
|       case EntityHistoryWidgetType.simple: { | ||||
|           return SimpleStateHistoryChartWidget( | ||||
|             rawHistory: _history, | ||||
|           ); | ||||
|       } | ||||
|  | ||||
|       case EntityHistoryWidgetType.numericState: { | ||||
|         return NumericStateHistoryChartWidget( | ||||
|           rawHistory: _history, | ||||
|           config: widget.config, | ||||
|         ); | ||||
|       } | ||||
|  | ||||
|       case EntityHistoryWidgetType.numericAttributes: { | ||||
|         return CombinedHistoryChartWidget( | ||||
|           rawHistory: _history, | ||||
|           config: widget.config, | ||||
|         ); | ||||
|       } | ||||
|  | ||||
|       default: { | ||||
|         Logger.d("  Simple selected as default"); | ||||
|         return SimpleStateHistoryChartWidget( | ||||
|           rawHistory: _history, | ||||
|         ); | ||||
|       } | ||||
|     } | ||||
|  | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   void dispose() { | ||||
|     _disposed = true; | ||||
|     super.dispose(); | ||||
|   } | ||||
|  | ||||
| } | ||||
							
								
								
									
										25
									
								
								lib/entity_widgets/history_chart/entity_history_moment.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,25 @@ | ||||
| part of '../../main.dart'; | ||||
|  | ||||
| class EntityHistoryMoment { | ||||
|   final DateTime startTime; | ||||
|   final DateTime endTime; | ||||
|   final double value; | ||||
|   final double previousValue; | ||||
|   final int id; | ||||
|   final int colorId; | ||||
|   final String state; | ||||
|   final bool hiddenDot; | ||||
|   final bool hiddenLine; | ||||
|  | ||||
|   EntityHistoryMoment({ | ||||
|     this.value, | ||||
|     this.previousValue, | ||||
|     this.hiddenDot, | ||||
|     this.hiddenLine, | ||||
|     this.state, | ||||
|     @required this.startTime, | ||||
|     this.endTime, | ||||
|     @required this.id, | ||||
|     this.colorId | ||||
|   }); | ||||
| } | ||||
							
								
								
									
										86
									
								
								lib/entity_widgets/history_chart/history_control_widget.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,86 @@ | ||||
| part of '../../main.dart'; | ||||
|  | ||||
| class HistoryControlWidget extends StatelessWidget { | ||||
|  | ||||
|   final Function onPrevTap; | ||||
|   final Function onNextTap; | ||||
|   final DateTime selectedTimeStart; | ||||
|   final DateTime selectedTimeEnd; | ||||
|   final List<String> selectedStates; | ||||
|   final List<int> colorIndexes; | ||||
|  | ||||
|   const HistoryControlWidget({Key key, this.onPrevTap, this.onNextTap, this.selectedTimeStart, this.selectedTimeEnd, this.selectedStates, @ required this.colorIndexes}) : super(key: key); | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context) { | ||||
|     if (selectedTimeStart != null) { | ||||
|       return | ||||
|         Row( | ||||
|           mainAxisAlignment: MainAxisAlignment.center, | ||||
|           crossAxisAlignment: CrossAxisAlignment.center, | ||||
|           children: <Widget>[ | ||||
|             IconButton( | ||||
|               icon: Icon(Icons.chevron_left), | ||||
|               padding: EdgeInsets.all(0.0), | ||||
|               iconSize: 40.0, | ||||
|               onPressed: onPrevTap, | ||||
|             ), | ||||
|             Expanded( | ||||
|               child: Padding( | ||||
|                 padding: EdgeInsets.only(right: 10.0), | ||||
|                 child: _buildStates(), | ||||
|               ), | ||||
|             ), | ||||
|             _buildTime(), | ||||
|             IconButton( | ||||
|               icon: Icon(Icons.chevron_right), | ||||
|               padding: EdgeInsets.all(0.0), | ||||
|               iconSize: 40.0, | ||||
|               onPressed: onNextTap, | ||||
|             ), | ||||
|           ], | ||||
|         ); | ||||
|  | ||||
|     } else { | ||||
|       return Container(height: 48.0); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   Widget _buildStates() { | ||||
|     List<Widget> children = []; | ||||
|     for (int i = 0; i < selectedStates.length; i++) { | ||||
|       children.add( | ||||
|           Text( | ||||
|             "${selectedStates[i] ?? '-'}", | ||||
|             textAlign: TextAlign.right, | ||||
|             style: TextStyle( | ||||
|                 fontWeight: FontWeight.bold, | ||||
|                 color: EntityColor.historyStateColor(selectedStates[i], colorIndexes[i]), | ||||
|                 fontSize: 22.0 | ||||
|             ), | ||||
|           ) | ||||
|       ); | ||||
|     } | ||||
|     return Column( | ||||
|       crossAxisAlignment: CrossAxisAlignment.end, | ||||
|       children: children, | ||||
|     ); | ||||
|   } | ||||
|  | ||||
|   Widget _buildTime() { | ||||
|     List<Widget> children = []; | ||||
|     children.add( | ||||
|         Text("${formatDate(selectedTimeStart, [M, ' ', d, ', ', HH, ':', nn, ':', ss])}", textAlign: TextAlign.left,) | ||||
|     ); | ||||
|     if (selectedTimeEnd != null) { | ||||
|       children.add( | ||||
|           Text("${formatDate(selectedTimeEnd, [M, ' ', d, ', ', HH, ':', nn, ':', ss])}", textAlign: TextAlign.left,) | ||||
|       ); | ||||
|     } | ||||
|     return Column( | ||||
|       crossAxisAlignment: CrossAxisAlignment.start, | ||||
|       children: children, | ||||
|     ); | ||||
|   } | ||||
|  | ||||
| } | ||||
| @@ -0,0 +1,160 @@ | ||||
| part of '../../main.dart'; | ||||
|  | ||||
| class NumericStateHistoryChartWidget extends StatefulWidget { | ||||
|   final rawHistory; | ||||
|   final EntityHistoryConfig config; | ||||
|  | ||||
|   const NumericStateHistoryChartWidget({Key key, @required this.rawHistory, @required this.config}) : super(key: key); | ||||
|  | ||||
|  | ||||
|   @override | ||||
|   State<StatefulWidget> createState() { | ||||
|     return new _NumericStateHistoryChartWidgetState(); | ||||
|   } | ||||
|  | ||||
| } | ||||
|  | ||||
| class _NumericStateHistoryChartWidgetState extends State<NumericStateHistoryChartWidget> { | ||||
|  | ||||
|   int _selectedId = -1; | ||||
|   List<charts.Series<EntityHistoryMoment, DateTime>> _parsedHistory; | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context) { | ||||
|     _parsedHistory = _parseHistory(); | ||||
|     DateTime selectedTime; | ||||
|     double selectedState; | ||||
|     if ((_selectedId > -1) && (_parsedHistory != null) && (_parsedHistory.first.data.length >= (_selectedId + 1))) { | ||||
|       selectedTime = _parsedHistory.first.data[_selectedId].startTime; | ||||
|       selectedState = _parsedHistory.first.data[_selectedId].value; | ||||
|     } | ||||
|     return Column( | ||||
|       mainAxisAlignment: MainAxisAlignment.start, | ||||
|       crossAxisAlignment: CrossAxisAlignment.center, | ||||
|       children: <Widget>[ | ||||
|         HistoryControlWidget( | ||||
|           selectedTimeStart: selectedTime, | ||||
|           selectedStates: ["${selectedState ?? '-'}"], | ||||
|           onPrevTap: () => _selectPrev(), | ||||
|           onNextTap: () => _selectNext(), | ||||
|           colorIndexes: [-1], | ||||
|         ), | ||||
|         SizedBox( | ||||
|           height: 150.0, | ||||
|           child: charts.TimeSeriesChart( | ||||
|             _parsedHistory, | ||||
|             animate: false, | ||||
|             primaryMeasureAxis: new charts.NumericAxisSpec( | ||||
|                 tickProviderSpec: | ||||
|                 new charts.BasicNumericTickProviderSpec(zeroBound: false)), | ||||
|             dateTimeFactory: const charts.LocalDateTimeFactory(), | ||||
|             defaultRenderer: charts.LineRendererConfig( | ||||
|               includePoints: true | ||||
|             ), | ||||
|             /*primaryMeasureAxis: charts.NumericAxisSpec( | ||||
|                 renderSpec: charts.NoneRenderSpec() | ||||
|             ),*/ | ||||
|             selectionModels: [ | ||||
|               new charts.SelectionModelConfig( | ||||
|                 type: charts.SelectionModelType.info, | ||||
|                 changedListener: (model) => _onSelectionChanged(model), | ||||
|               ) | ||||
|             ], | ||||
|           ), | ||||
|         ) | ||||
|       ], | ||||
|     ); | ||||
|   } | ||||
|  | ||||
|   List<charts.Series<EntityHistoryMoment, DateTime>> _parseHistory() { | ||||
|     List<EntityHistoryMoment> data = []; | ||||
|     DateTime now = DateTime.now(); | ||||
|     for (var i = 0; i < widget.rawHistory.length; i++) { | ||||
|       var stateData = widget.rawHistory[i]; | ||||
|       DateTime time = DateTime.tryParse(stateData["last_updated"])?.toLocal(); | ||||
|       double value = double.tryParse(stateData["state"]); | ||||
|       double previousValue = 0.0; | ||||
|       bool hiddenDot = (value == null); | ||||
|       bool hiddenLine; | ||||
|       if (hiddenDot && i > 0) { | ||||
|         previousValue = data[i-1].value ?? data[i-1].previousValue; | ||||
|       } | ||||
|       if (i < (widget.rawHistory.length - 1)) { | ||||
|         double nextValue = double.tryParse(widget.rawHistory[i+1]["state"]); | ||||
|         hiddenLine = (nextValue == null || hiddenDot); | ||||
|       } else { | ||||
|         hiddenLine = hiddenDot; | ||||
|       } | ||||
|       data.add(EntityHistoryMoment( | ||||
|         value: value, | ||||
|         previousValue: previousValue, | ||||
|         hiddenDot: hiddenDot, | ||||
|         hiddenLine: hiddenLine, | ||||
|         startTime: time, | ||||
|         id: i | ||||
|       )); | ||||
|     } | ||||
|     data.add(EntityHistoryMoment( | ||||
|         value: data.last.value, | ||||
|         previousValue: data.last.previousValue, | ||||
|         hiddenDot: data.last.hiddenDot, | ||||
|         hiddenLine:  data.last.hiddenLine, | ||||
|         startTime: now, | ||||
|         id: widget.rawHistory.length | ||||
|     )); | ||||
|     if (_selectedId == -1) { | ||||
|       _selectedId = 0; | ||||
|     } | ||||
|     return [ | ||||
|       new charts.Series<EntityHistoryMoment, DateTime>( | ||||
|         id: 'State', | ||||
|         colorFn: (EntityHistoryMoment historyMoment, __) => EntityColor.chartHistoryStateColor(EntityState.on, -1), | ||||
|         domainFn: (EntityHistoryMoment historyMoment, _) => historyMoment.startTime, | ||||
|         measureFn: (EntityHistoryMoment historyMoment, _) => historyMoment.value ?? historyMoment.previousValue, | ||||
|         data: data, | ||||
|         strokeWidthPxFn: (EntityHistoryMoment historyMoment, __) => historyMoment.hiddenLine ? 0.0 : 2.0, | ||||
|         radiusPxFn: (EntityHistoryMoment historyMoment, __) { | ||||
|           if (historyMoment.hiddenDot) { | ||||
|             return 0.0; | ||||
|           } else if (historyMoment.id == _selectedId) { | ||||
|             return 5.0; | ||||
|           } else { | ||||
|             return 1.0; | ||||
|           } | ||||
|         }, | ||||
|       ) | ||||
|     ]; | ||||
|   } | ||||
|  | ||||
|   void _selectPrev() { | ||||
|     if (_selectedId > 0) { | ||||
|       setState(() { | ||||
|         _selectedId -= 1; | ||||
|       }); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   void _selectNext() { | ||||
|     if (_selectedId < (_parsedHistory.first.data.length - 1)) { | ||||
|       setState(() { | ||||
|         _selectedId += 1; | ||||
|       }); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   void _onSelectionChanged(charts.SelectionModel model) { | ||||
|     final selectedDatum = model.selectedDatum; | ||||
|  | ||||
|     int selectedId; | ||||
|  | ||||
|     if (selectedDatum.isNotEmpty) { | ||||
|       selectedId = selectedDatum.first.datum.id; | ||||
|       setState(() { | ||||
|         _selectedId = selectedId; | ||||
|       }); | ||||
|     } else { | ||||
|       setState(() { | ||||
|       }); | ||||
|     } | ||||
|   } | ||||
| } | ||||
							
								
								
									
										176
									
								
								lib/entity_widgets/history_chart/simple_state_history_chart.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,176 @@ | ||||
| part of '../../main.dart'; | ||||
|  | ||||
| class SimpleStateHistoryChartWidget extends StatefulWidget { | ||||
|   final rawHistory; | ||||
|  | ||||
|   const SimpleStateHistoryChartWidget({Key key, this.rawHistory}) : super(key: key); | ||||
|  | ||||
|  | ||||
|   @override | ||||
|   State<StatefulWidget> createState() { | ||||
|     return new _SimpleStateHistoryChartWidgetState(); | ||||
|   } | ||||
|  | ||||
| } | ||||
|  | ||||
| class _SimpleStateHistoryChartWidgetState extends State<SimpleStateHistoryChartWidget> { | ||||
|  | ||||
|   int _selectedId = -1; | ||||
|   List<charts.Series<EntityHistoryMoment, DateTime>> _parsedHistory; | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context) { | ||||
|     _parsedHistory = _parseHistory(); | ||||
|     DateTime selectedTimeStart; | ||||
|     DateTime selectedTimeEnd; | ||||
|     String selectedState; | ||||
|     if ((_selectedId > -1) && (_parsedHistory != null) && (_parsedHistory.first.data.length >= (_selectedId + 1))) { | ||||
|       selectedTimeStart = _parsedHistory.first.data[_selectedId].startTime; | ||||
|       selectedTimeEnd = _parsedHistory.first.data[_selectedId].endTime; | ||||
|       selectedState = _parsedHistory.first.data[_selectedId].state; | ||||
|     } | ||||
|     return Column( | ||||
|       mainAxisAlignment: MainAxisAlignment.start, | ||||
|       crossAxisAlignment: CrossAxisAlignment.center, | ||||
|       children: <Widget>[ | ||||
|         HistoryControlWidget( | ||||
|           selectedTimeStart: selectedTimeStart, | ||||
|           selectedTimeEnd: selectedTimeEnd, | ||||
|           selectedStates: [selectedState], | ||||
|           onPrevTap: () => _selectPrev(), | ||||
|           onNextTap: () => _selectNext(), | ||||
|           colorIndexes: [_parsedHistory.first.data[_selectedId].colorId], | ||||
|         ), | ||||
|         SizedBox( | ||||
|           height: 70.0, | ||||
|           child: charts.TimeSeriesChart( | ||||
|             _parsedHistory, | ||||
|             animate: false, | ||||
|             dateTimeFactory: const charts.LocalDateTimeFactory(), | ||||
|             primaryMeasureAxis: charts.NumericAxisSpec( | ||||
|                 renderSpec: charts.NoneRenderSpec() | ||||
|             ), | ||||
|             selectionModels: [ | ||||
|               new charts.SelectionModelConfig( | ||||
|                 type: charts.SelectionModelType.info, | ||||
|                 changedListener: (model) => _onSelectionChanged(model), | ||||
|               ) | ||||
|             ], | ||||
|             customSeriesRenderers: [ | ||||
|               new charts.PointRendererConfig( | ||||
|                 // ID used to link series to this renderer. | ||||
|                   customRendererId: 'startValuePoints'), | ||||
|               new charts.PointRendererConfig( | ||||
|                 // ID used to link series to this renderer. | ||||
|                   customRendererId: 'endValuePoints') | ||||
|             ], | ||||
|           ), | ||||
|         ) | ||||
|       ], | ||||
|     ); | ||||
|   } | ||||
|  | ||||
|   List<charts.Series<EntityHistoryMoment, DateTime>> _parseHistory() { | ||||
|     List<EntityHistoryMoment> data = []; | ||||
|     DateTime now = DateTime.now(); | ||||
|     Map<String, int> cachedStates = {}; | ||||
|     for (var i = 0; i < widget.rawHistory.length; i++) { | ||||
|       var stateData = widget.rawHistory[i]; | ||||
|       DateTime startTime = DateTime.tryParse(stateData["last_updated"])?.toLocal(); | ||||
|       DateTime endTime; | ||||
|       if (i < (widget.rawHistory.length - 1)) { | ||||
|         endTime = DateTime.tryParse(widget.rawHistory[i+1]["last_updated"])?.toLocal(); | ||||
|       } else { | ||||
|         endTime = now; | ||||
|       } | ||||
|       if (cachedStates[stateData["state"]] == null) { | ||||
|         cachedStates.addAll({"${stateData["state"]}": cachedStates.length}); | ||||
|       } | ||||
|       data.add(EntityHistoryMoment( | ||||
|         state: stateData["state"], | ||||
|         startTime: startTime, | ||||
|         endTime: endTime, | ||||
|         id: i, | ||||
|         colorId: cachedStates[stateData["state"]] | ||||
|       )); | ||||
|     } | ||||
|     data.add(EntityHistoryMoment( | ||||
|         state: data.last.state, | ||||
|         startTime: now, | ||||
|         id: widget.rawHistory.length, | ||||
|         colorId: data.last.colorId | ||||
|     )); | ||||
|     if (_selectedId == -1) { | ||||
|       _selectedId = 0; | ||||
|     } | ||||
|     return [ | ||||
|       new charts.Series<EntityHistoryMoment, DateTime>( | ||||
|         id: 'State', | ||||
|         strokeWidthPxFn: (EntityHistoryMoment historyMoment, __) => (historyMoment.id == _selectedId) ? 6.0 : 3.0, | ||||
|         colorFn: (EntityHistoryMoment historyMoment, __) => EntityColor.chartHistoryStateColor(historyMoment.state, historyMoment.colorId), | ||||
|         domainFn: (EntityHistoryMoment historyMoment, _) => historyMoment.startTime, | ||||
|         measureFn: (EntityHistoryMoment historyMoment, _) => 10, | ||||
|         data: data, | ||||
|       ), | ||||
|       new charts.Series<EntityHistoryMoment, DateTime>( | ||||
|         id: 'State', | ||||
|         radiusPxFn: (EntityHistoryMoment historyMoment, __) => (historyMoment.id == _selectedId) ? 5.0 : 3.0, | ||||
|         colorFn: (EntityHistoryMoment historyMoment, __) => EntityColor.chartHistoryStateColor(historyMoment.state, historyMoment.colorId), | ||||
|         domainFn: (EntityHistoryMoment historyMoment, _) => historyMoment.startTime, | ||||
|         measureFn: (EntityHistoryMoment historyMoment, _) => 10, | ||||
|         data: data, | ||||
|       )..setAttribute(charts.rendererIdKey, 'startValuePoints'), | ||||
|       new charts.Series<EntityHistoryMoment, DateTime>( | ||||
|         id: 'State', | ||||
|         radiusPxFn: (EntityHistoryMoment historyMoment, __) => (historyMoment.id == _selectedId) ? 5.0 : 3.0, | ||||
|         colorFn: (EntityHistoryMoment historyMoment, __) => EntityColor.chartHistoryStateColor(historyMoment.state, historyMoment.colorId), | ||||
|         domainFn: (EntityHistoryMoment historyMoment, _) => historyMoment.endTime ?? DateTime.now(), | ||||
|         measureFn: (EntityHistoryMoment historyMoment, _) => 10, | ||||
|         data: data, | ||||
|       )..setAttribute(charts.rendererIdKey, 'endValuePoints') | ||||
|     ]; | ||||
|   } | ||||
|  | ||||
|   void _selectPrev() { | ||||
|     if (_selectedId > 0) { | ||||
|       setState(() { | ||||
|         _selectedId -= 1; | ||||
|       }); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   void _selectNext() { | ||||
|     if (_selectedId < (_parsedHistory.first.data.length - 2)) { | ||||
|       setState(() { | ||||
|         _selectedId += 1; | ||||
|       }); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   void _onSelectionChanged(charts.SelectionModel model) { | ||||
|     final selectedDatum = model.selectedDatum; | ||||
|  | ||||
|     int selectedId; | ||||
|  | ||||
|     if ((selectedDatum.isNotEmpty) &&(selectedDatum.first.datum.endTime != null)) { | ||||
|       selectedId = selectedDatum.first.datum.id; | ||||
|       setState(() { | ||||
|         _selectedId = selectedId; | ||||
|       }); | ||||
|     } else { | ||||
|       setState(() { | ||||
|       }); | ||||
|     } | ||||
|   } | ||||
| } | ||||
|  | ||||
| /* | ||||
| class SimpleEntityStateHistoryMoment { | ||||
|   final DateTime startTime; | ||||
|   final DateTime endTime; | ||||
|   final String state; | ||||
|   final int id; | ||||
|   final int colorId; | ||||
|  | ||||
|   SimpleEntityStateHistoryMoment(this.state, this.startTime, this.endTime,  this.id, this.colorId); | ||||
| }*/ | ||||
							
								
								
									
										19
									
								
								lib/entity_widgets/missed_entity.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,19 @@ | ||||
| part of '../main.dart'; | ||||
|  | ||||
| class MissedEntityWidget extends StatelessWidget { | ||||
|   MissedEntityWidget({ | ||||
|     Key key | ||||
|   }) : super(key: key); | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context) { | ||||
|     final EntityModel entityModel = EntityModel.of(context); | ||||
|     return Container( | ||||
|         child: Padding( | ||||
|           padding: EdgeInsets.all(5.0), | ||||
|           child: Text("Entity not available: ${entityModel.entityWrapper.entity.entityId}"), | ||||
|         ), | ||||
|         color: Colors.amber[100], | ||||
|     ); | ||||
|   } | ||||
| } | ||||
							
								
								
									
										42
									
								
								lib/entity_widgets/model_widgets.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,42 @@ | ||||
| part of '../main.dart'; | ||||
|  | ||||
| class EntityModel extends InheritedWidget { | ||||
|   const EntityModel({ | ||||
|     Key key, | ||||
|     @required this.entityWrapper, | ||||
|     @required this.handleTap, | ||||
|     @required Widget child, | ||||
|   }) : super(key: key, child: child); | ||||
|  | ||||
|   final EntityWrapper entityWrapper; | ||||
|   final bool handleTap; | ||||
|  | ||||
|   static EntityModel of(BuildContext context) { | ||||
|     return context.inheritFromWidgetOfExactType(EntityModel); | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   bool updateShouldNotify(InheritedWidget oldWidget) { | ||||
|     return true; | ||||
|   } | ||||
| } | ||||
|  | ||||
| class HomeAssistantModel extends InheritedWidget { | ||||
|  | ||||
|   const HomeAssistantModel({ | ||||
|     Key key, | ||||
|     @required this.homeAssistant, | ||||
|     @required Widget child, | ||||
|   }) : super(key: key, child: child); | ||||
|  | ||||
|   final HomeAssistant homeAssistant; | ||||
|  | ||||
|   static HomeAssistantModel of(BuildContext context) { | ||||
|     return context.inheritFromWidgetOfExactType(HomeAssistantModel); | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   bool updateShouldNotify(InheritedWidget oldWidget) { | ||||
|     return true; | ||||
|   } | ||||
| } | ||||
							
								
								
									
										52
									
								
								lib/entity_widgets/state/climate_state.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,52 @@ | ||||
| part of '../../main.dart'; | ||||
|  | ||||
| class ClimateStateWidget extends StatelessWidget { | ||||
|   @override | ||||
|   Widget build(BuildContext context) { | ||||
|     final entityModel = EntityModel.of(context); | ||||
|     final ClimateEntity entity = entityModel.entityWrapper.entity; | ||||
|     String targetTemp = "-"; | ||||
|     if ((entity.supportTargetTemperature) && (entity.temperature != null)) { | ||||
|       targetTemp = "${entity.temperature}"; | ||||
|     } else if ((entity.supportTargetTemperatureLow) && | ||||
|         (entity.targetLow != null)) { | ||||
|       targetTemp = "${entity.targetLow}"; | ||||
|       if ((entity.supportTargetTemperatureHigh) && | ||||
|           (entity.targetHigh != null)) { | ||||
|         targetTemp += " - ${entity.targetHigh}"; | ||||
|       } | ||||
|     } | ||||
|     return Padding( | ||||
|         padding: EdgeInsets.fromLTRB( | ||||
|             0.0, 0.0, Sizes.rightWidgetPadding, 0.0), | ||||
|         child: Column( | ||||
|           crossAxisAlignment: CrossAxisAlignment.end, | ||||
|           mainAxisAlignment: MainAxisAlignment.center, | ||||
|           children: <Widget>[ | ||||
|             Row( | ||||
|               children: <Widget>[ | ||||
|                 Text("${entity.state}", | ||||
|                     textAlign: TextAlign.right, | ||||
|                     style: new TextStyle( | ||||
|                       fontWeight: FontWeight.bold, | ||||
|                       fontSize: Sizes.stateFontSize, | ||||
|                     )), | ||||
|                 Text(" $targetTemp", | ||||
|                     textAlign: TextAlign.right, | ||||
|                     style: new TextStyle( | ||||
|                       fontSize: Sizes.stateFontSize, | ||||
|                     )) | ||||
|               ], | ||||
|             ), | ||||
|             entity.attributes["current_temperature"] != null ? | ||||
|             Text("Currently: ${entity.attributes["current_temperature"]}", | ||||
|                 textAlign: TextAlign.right, | ||||
|                 style: new TextStyle( | ||||
|                     fontSize: Sizes.stateFontSize, | ||||
|                     color: Colors.black45) | ||||
|             ) : | ||||
|             Container(height: 0.0,) | ||||
|           ], | ||||
|         )); | ||||
|   } | ||||
| } | ||||
							
								
								
									
										65
									
								
								lib/entity_widgets/state/cover_state.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,65 @@ | ||||
| part of '../../main.dart'; | ||||
|  | ||||
| class CoverStateWidget extends StatelessWidget { | ||||
|   void _open(CoverEntity entity) { | ||||
|     eventBus.fire(new ServiceCallEvent( | ||||
|         entity.domain, "open_cover", entity.entityId, null)); | ||||
|   } | ||||
|  | ||||
|   void _close(CoverEntity entity) { | ||||
|     eventBus.fire(new ServiceCallEvent( | ||||
|         entity.domain, "close_cover", entity.entityId, null)); | ||||
|   } | ||||
|  | ||||
|   void _stop(CoverEntity entity) { | ||||
|     eventBus.fire(new ServiceCallEvent( | ||||
|         entity.domain, "stop_cover", entity.entityId, null)); | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context) { | ||||
|     final entityModel = EntityModel.of(context); | ||||
|     final CoverEntity entity = entityModel.entityWrapper.entity; | ||||
|     List<Widget> buttons = []; | ||||
|     if (entity.supportOpen) { | ||||
|       buttons.add(IconButton( | ||||
|           icon: Icon( | ||||
|             MaterialDesignIcons.getIconDataFromIconName("mdi:arrow-up"), | ||||
|             size: Sizes.iconSize, | ||||
|           ), | ||||
|           onPressed: entity.canBeOpened ? () => _open(entity) : null)); | ||||
|     } else { | ||||
|       buttons.add(Container( | ||||
|         width: Sizes.iconSize + 20.0, | ||||
|       )); | ||||
|     } | ||||
|     if (entity.supportStop) { | ||||
|       buttons.add(IconButton( | ||||
|           icon: Icon( | ||||
|             MaterialDesignIcons.getIconDataFromIconName("mdi:stop"), | ||||
|             size: Sizes.iconSize, | ||||
|           ), | ||||
|           onPressed: () => _stop(entity))); | ||||
|     } else { | ||||
|       buttons.add(Container( | ||||
|         width: Sizes.iconSize + 20.0, | ||||
|       )); | ||||
|     } | ||||
|     if (entity.supportClose) { | ||||
|       buttons.add(IconButton( | ||||
|           icon: Icon( | ||||
|             MaterialDesignIcons.getIconDataFromIconName("mdi:arrow-down"), | ||||
|             size: Sizes.iconSize, | ||||
|           ), | ||||
|           onPressed: entity.canBeClosed ? () => _close(entity) : null)); | ||||
|     } else { | ||||
|       buttons.add(Container( | ||||
|         width: Sizes.iconSize + 20.0, | ||||
|       )); | ||||
|     } | ||||
|  | ||||
|     return Row( | ||||
|       children: buttons, | ||||
|     ); | ||||
|   } | ||||
| } | ||||
							
								
								
									
										75
									
								
								lib/entity_widgets/state/date_time_state.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,75 @@ | ||||
| part of '../../main.dart'; | ||||
|  | ||||
| class DateTimeStateWidget extends StatelessWidget { | ||||
|   @override | ||||
|   Widget build(BuildContext context) { | ||||
|     final entityModel = EntityModel.of(context); | ||||
|     final DateTimeEntity entity = entityModel.entityWrapper.entity; | ||||
|     return Padding( | ||||
|         padding: EdgeInsets.fromLTRB(0.0, 0.0, Sizes.rightWidgetPadding, 0.0), | ||||
|         child: GestureDetector( | ||||
|           child: Text("${entity.formattedState}", | ||||
|               textAlign: TextAlign.right, | ||||
|               style: new TextStyle( | ||||
|                 fontSize: Sizes.stateFontSize, | ||||
|               )), | ||||
|           onTap: () => _handleStateTap(context, entity), | ||||
|         )); | ||||
|   } | ||||
|  | ||||
|   void _handleStateTap(BuildContext context, DateTimeEntity entity) { | ||||
|     if (entity.hasDate) { | ||||
|       _showDatePicker(context, entity).then((date) { | ||||
|         if (date != null) { | ||||
|           if (entity.hasTime) { | ||||
|             _showTimePicker(context, entity).then((time) { | ||||
|               entity.setNewState({ | ||||
|                 "date": "${formatDate(date, [yyyy, '-', mm, '-', dd])}", | ||||
|                 "time": | ||||
|                 "${formatDate(DateTime(1970, 1, 1, time.hour, time.minute), [ | ||||
|                   HH, | ||||
|                   ':', | ||||
|                   nn | ||||
|                 ])}" | ||||
|               }); | ||||
|             }); | ||||
|           } else { | ||||
|             entity.setNewState({ | ||||
|               "date": "${formatDate(date, [yyyy, '-', mm, '-', dd])}" | ||||
|             }); | ||||
|           } | ||||
|         } | ||||
|       }); | ||||
|     } else if (entity.hasTime) { | ||||
|       _showTimePicker(context, entity).then((time) { | ||||
|         if (time != null) { | ||||
|           entity.setNewState({ | ||||
|             "time": | ||||
|             "${formatDate(DateTime(1970, 1, 1, time.hour, time.minute), [ | ||||
|               HH, | ||||
|               ':', | ||||
|               nn | ||||
|             ])}" | ||||
|           }); | ||||
|         } | ||||
|       }); | ||||
|     } else { | ||||
|       Logger.w( "${entity.entityId} has no date and no time"); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   Future _showDatePicker(BuildContext context, DateTimeEntity entity) { | ||||
|     return showDatePicker( | ||||
|         context: context, | ||||
|         initialDate: entity.dateTimeState, | ||||
|         firstDate: DateTime(1970), | ||||
|         lastDate: DateTime(2037) //Unix timestamp will finish at Jan 19, 2038 | ||||
|     ); | ||||
|   } | ||||
|  | ||||
|   Future _showTimePicker(BuildContext context, DateTimeEntity entity) { | ||||
|     return showTimePicker( | ||||
|         context: context, | ||||
|         initialTime: TimeOfDay.fromDateTime(entity.dateTimeState)); | ||||
|   } | ||||
| } | ||||
							
								
								
									
										66
									
								
								lib/entity_widgets/state/lock_state.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,66 @@ | ||||
| part of '../../main.dart'; | ||||
|  | ||||
| class LockStateWidget extends StatelessWidget { | ||||
|  | ||||
|   final bool assumedState; | ||||
|  | ||||
|   const LockStateWidget({Key key, this.assumedState: false}) : super(key: key); | ||||
|  | ||||
|   void _lock(Entity entity) { | ||||
|     eventBus.fire(new ServiceCallEvent("lock", "lock", entity.entityId, null)); | ||||
|   } | ||||
|  | ||||
|   void _unlock(Entity entity) { | ||||
|     eventBus.fire(new ServiceCallEvent("lock", "unlock", entity.entityId, null)); | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context) { | ||||
|     final entityModel = EntityModel.of(context); | ||||
|     final LockEntity entity = entityModel.entityWrapper.entity; | ||||
|     if (assumedState) { | ||||
|       return Row( | ||||
|         mainAxisSize: MainAxisSize.min, | ||||
|         children: <Widget>[ | ||||
|         SizedBox( | ||||
|         height: 34.0, | ||||
|         child: FlatButton( | ||||
|           onPressed: () => _unlock(entity), | ||||
|           child: Text("UNLOCK", | ||||
|               textAlign: TextAlign.right, | ||||
|               style: | ||||
|               new TextStyle(fontSize: Sizes.stateFontSize, color: Colors.blue), | ||||
|             ), | ||||
|           ) | ||||
|         ), | ||||
|         SizedBox( | ||||
|             height: 34.0, | ||||
|             child: FlatButton( | ||||
|               onPressed: () => _lock(entity), | ||||
|               child: Text("LOCK", | ||||
|                 textAlign: TextAlign.right, | ||||
|                 style: | ||||
|                 new TextStyle(fontSize: Sizes.stateFontSize, color: Colors.blue), | ||||
|               ), | ||||
|             ) | ||||
|         ) | ||||
|         ], | ||||
|       ); | ||||
|     } else { | ||||
|       return SizedBox( | ||||
|           height: 34.0, | ||||
|           child: FlatButton( | ||||
|             onPressed: (() { | ||||
|               entity.isLocked ? _unlock(entity) : _lock(entity); | ||||
|             }), | ||||
|             child: Text( | ||||
|               entity.isLocked ? "UNLOCK" : "LOCK", | ||||
|               textAlign: TextAlign.right, | ||||
|               style: | ||||
|               new TextStyle(fontSize: Sizes.stateFontSize, color: Colors.blue), | ||||
|             ), | ||||
|           ) | ||||
|       ); | ||||
|     } | ||||
|   } | ||||
| } | ||||
							
								
								
									
										49
									
								
								lib/entity_widgets/state/select_state.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,49 @@ | ||||
| part of '../../main.dart'; | ||||
|  | ||||
| class SelectStateWidget extends StatefulWidget { | ||||
|  | ||||
|   SelectStateWidget({Key key}) : super(key: key); | ||||
|  | ||||
|   @override | ||||
|   _SelectStateWidgetState createState() => _SelectStateWidgetState(); | ||||
| } | ||||
|  | ||||
| class _SelectStateWidgetState extends State<SelectStateWidget> { | ||||
|  | ||||
|   void setNewState(domain, entityId, newValue) { | ||||
|     eventBus.fire(new ServiceCallEvent(domain, "select_option", entityId, | ||||
|         {"option": "$newValue"})); | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context) { | ||||
|     final entityModel = EntityModel.of(context); | ||||
|     final SelectEntity entity = entityModel.entityWrapper.entity; | ||||
|     Widget ctrl; | ||||
|     if (entity.listOptions.isNotEmpty) { | ||||
|       ctrl = DropdownButton<String>( | ||||
|         value: entity.state, | ||||
|         isExpanded: true, | ||||
|         items: entity.listOptions.map((String value) { | ||||
|           return new DropdownMenuItem<String>( | ||||
|             value: value, | ||||
|             child: new Text(value), | ||||
|           ); | ||||
|         }).toList(), | ||||
|         onChanged: (_) { | ||||
|           setNewState(entity.domain, entity.entityId,_); | ||||
|         }, | ||||
|       ); | ||||
|     } else { | ||||
|       ctrl = Text('---'); | ||||
|     } | ||||
|     return Flexible( | ||||
|       flex: 2, | ||||
|       fit: FlexFit.tight, | ||||
|       //width: Entity.INPUT_WIDTH, | ||||
|       child: ctrl, | ||||
|     ); | ||||
|   } | ||||
|  | ||||
|  | ||||
| } | ||||