Compare commits
	
		
			277 Commits
		
	
	
		
			0.3.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 | 
							
								
								
									
										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)) | keystoreProperties.load(new FileInputStream(keystorePropertiesFile)) | ||||||
|  |  | ||||||
| android { | android { | ||||||
|     compileSdkVersion 27 |     compileSdkVersion 28 | ||||||
|  |  | ||||||
|  |     compileOptions { | ||||||
|  |         sourceCompatibility JavaVersion.VERSION_1_8 | ||||||
|  |         targetCompatibility JavaVersion.VERSION_1_8 | ||||||
|  |     } | ||||||
|  |  | ||||||
|     lintOptions { |     lintOptions { | ||||||
|         disable 'InvalidPackage' |         disable 'InvalidPackage' | ||||||
| @@ -38,7 +43,7 @@ android { | |||||||
|     defaultConfig { |     defaultConfig { | ||||||
|         applicationId "com.keyboardcrumbs.haclient" |         applicationId "com.keyboardcrumbs.haclient" | ||||||
|         minSdkVersion 21 |         minSdkVersion 21 | ||||||
|         targetSdkVersion 27 |         targetSdkVersion 28 | ||||||
|         versionCode flutterVersionCode.toInteger() |         versionCode flutterVersionCode.toInteger() | ||||||
|         versionName flutterVersionName |         versionName flutterVersionName | ||||||
|         testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" |         testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" | ||||||
| @@ -65,7 +70,10 @@ flutter { | |||||||
| } | } | ||||||
|  |  | ||||||
| dependencies { | dependencies { | ||||||
|  |     implementation 'com.google.firebase:firebase-core:16.0.8' | ||||||
|     testImplementation 'junit:junit:4.12' |     testImplementation 'junit:junit:4.12' | ||||||
|     androidTestImplementation 'com.android.support.test:runner:1.0.2' |     androidTestImplementation 'com.android.support.test:runner:1.0.2' | ||||||
|     androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.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 |     <application | ||||||
|         android:name="io.flutter.app.FlutterApplication" |         android:name="io.flutter.app.FlutterApplication" | ||||||
|         android:label="HA Client" |         android:label="HA Client" | ||||||
|         android:icon="@mipmap/ic_launcher"> |         android:icon="@mipmap/ic_launcher" | ||||||
|  |         android:usesCleartextTraffic="true"> | ||||||
|         <activity |         <activity | ||||||
|             android:name=".MainActivity" |             android:name=".MainActivity" | ||||||
|             android:launchMode="singleTop" |             android:launchMode="singleTop" | ||||||
| @@ -30,6 +31,10 @@ | |||||||
|             <meta-data |             <meta-data | ||||||
|                 android:name="io.flutter.app.android.SplashScreenUntilFirstFrame" |                 android:name="io.flutter.app.android.SplashScreenUntilFirstFrame" | ||||||
|                 android:value="true" /> |                 android:value="true" /> | ||||||
|  |             <intent-filter> | ||||||
|  |                 <action android:name="FLUTTER_NOTIFICATION_CLICK" /> | ||||||
|  |                 <category android:name="android.intent.category.DEFAULT" /> | ||||||
|  |             </intent-filter> | ||||||
|             <intent-filter> |             <intent-filter> | ||||||
|                 <action android:name="android.intent.action.MAIN"/> |                 <action android:name="android.intent.action.MAIN"/> | ||||||
|                 <category android:name="android.intent.category.LAUNCHER"/> |                 <category android:name="android.intent.category.LAUNCHER"/> | ||||||
|   | |||||||
| Before Width: | Height: | Size: 2.5 KiB After Width: | Height: | Size: 4.0 KiB | 
| Before Width: | Height: | Size: 1.6 KiB After Width: | Height: | Size: 2.6 KiB | 
| Before Width: | Height: | Size: 3.1 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: 6.2 KiB After Width: | Height: | Size: 11 KiB | 
| @@ -5,7 +5,8 @@ buildscript { | |||||||
|     } |     } | ||||||
|  |  | ||||||
|     dependencies { |     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 | distributionPath=wrapper/dists | ||||||
| zipStoreBase=GRADLE_USER_HOME | zipStoreBase=GRADLE_USER_HOME | ||||||
| zipStorePath=wrapper/dists | 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: 14 KiB After Width: | Height: | Size: 24 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,54 +0,0 @@ | |||||||
| part of 'main.dart'; |  | ||||||
|  |  | ||||||
| class HACard extends StatelessWidget { |  | ||||||
|  |  | ||||||
|   final List<Entity> entities; |  | ||||||
|   final String friendlyName; |  | ||||||
|  |  | ||||||
|   const HACard({ |  | ||||||
|     Key key, |  | ||||||
|     this.entities, |  | ||||||
|     this.friendlyName |  | ||||||
|   }) : super(key: key); |  | ||||||
|  |  | ||||||
|   @override |  | ||||||
|   Widget build(BuildContext context) { |  | ||||||
|     List<Widget> body = []; |  | ||||||
|     body.add(_buildCardHeader()); |  | ||||||
|     body.addAll(_buildCardBody(context)); |  | ||||||
|     return Card( |  | ||||||
|         child: new Column(mainAxisSize: MainAxisSize.min, children: body) |  | ||||||
|     ); |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   Widget _buildCardHeader() { |  | ||||||
|     var result; |  | ||||||
|     if ((friendlyName != null) && (friendlyName.trim().length > 0)) { |  | ||||||
|       result = new ListTile( |  | ||||||
|         //leading: const Icon(Icons.device_hub), |  | ||||||
|         //subtitle: Text(".."), |  | ||||||
|         //trailing: Text("${data["state"]}"), |  | ||||||
|         title: Text("$friendlyName", |  | ||||||
|             textAlign: TextAlign.left, |  | ||||||
|             overflow: TextOverflow.ellipsis, |  | ||||||
|             style: new TextStyle(fontWeight: FontWeight.bold, fontSize: 25.0)), |  | ||||||
|       ); |  | ||||||
|     } else { |  | ||||||
|       result = new Container(width: 0.0, height: 0.0); |  | ||||||
|     } |  | ||||||
|     return result; |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   List<Widget> _buildCardBody(BuildContext context) { |  | ||||||
|     List<Widget> result = []; |  | ||||||
|     entities.forEach((Entity entity) { |  | ||||||
|       result.add( |  | ||||||
|         Padding( |  | ||||||
|           padding: EdgeInsets.fromLTRB(0.0, 10.0, 0.0, 10.0), |  | ||||||
|           child: entity.buildDefaultWidget(context), |  | ||||||
|         )); |  | ||||||
|     }); |  | ||||||
|     return result; |  | ||||||
|   } |  | ||||||
|  |  | ||||||
| } |  | ||||||
							
								
								
									
										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,9 +1,9 @@ | |||||||
| part of 'main.dart'; | part of 'main.dart'; | ||||||
|  |  | ||||||
| class EntityViewPage extends StatefulWidget { | class EntityViewPage extends StatefulWidget { | ||||||
|   EntityViewPage({Key key, @required this.entity, @required this.homeAssistant }) : super(key: key); |   EntityViewPage({Key key, @required this.entityId, @required this.homeAssistant }) : super(key: key); | ||||||
|  |  | ||||||
|   final Entity entity; |   final String entityId; | ||||||
|   final HomeAssistant homeAssistant; |   final HomeAssistant homeAssistant; | ||||||
|  |  | ||||||
|   @override |   @override | ||||||
| @@ -12,31 +12,26 @@ class EntityViewPage extends StatefulWidget { | |||||||
|  |  | ||||||
| class _EntityViewPageState extends State<EntityViewPage> { | class _EntityViewPageState extends State<EntityViewPage> { | ||||||
|   String _title; |   String _title; | ||||||
|  |   StreamSubscription _refreshDataSubscription; | ||||||
|   StreamSubscription _stateSubscription; |   StreamSubscription _stateSubscription; | ||||||
|  |  | ||||||
|   @override |   @override | ||||||
|   void initState() { |   void initState() { | ||||||
|     super.initState(); |     super.initState(); | ||||||
|     if (_stateSubscription != null) _stateSubscription.cancel(); |  | ||||||
|     _stateSubscription = eventBus.on<StateChangedEvent>().listen((event) { |     _stateSubscription = eventBus.on<StateChangedEvent>().listen((event) { | ||||||
|       if (event.entityId == widget.entity.entityId) { |       if (event.entityId == widget.entityId) { | ||||||
|  |         Logger.d("State change event handled by entity page: ${event.entityId}"); | ||||||
|         setState(() {}); |         setState(() {}); | ||||||
|       } |       } | ||||||
|     }); |     }); | ||||||
|  |     _refreshDataSubscription = eventBus.on<RefreshDataFinishedEvent>().listen((event) { | ||||||
|  |       setState(() {}); | ||||||
|  |     }); | ||||||
|     _prepareData(); |     _prepareData(); | ||||||
|     _getHistory(); |  | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   void _prepareData() async { |   void _prepareData() async { | ||||||
|     _title = widget.entity.displayName; |     _title = widget.homeAssistant.entities.get(widget.entityId).displayName; | ||||||
|   } |  | ||||||
|  |  | ||||||
|   void _getHistory() { |  | ||||||
|    /* widget.homeAssistant.getHistory(widget.entity.entityId).then((List history) { |  | ||||||
|       if (history != null) { |  | ||||||
|          |  | ||||||
|       } |  | ||||||
|     });*/ |  | ||||||
|   } |   } | ||||||
|  |  | ||||||
|  |  | ||||||
| @@ -51,9 +46,9 @@ class _EntityViewPageState extends State<EntityViewPage> { | |||||||
|         // the App.build method, and use it to set our appbar title. |         // the App.build method, and use it to set our appbar title. | ||||||
|         title: new Text(_title), |         title: new Text(_title), | ||||||
|       ), |       ), | ||||||
|       body: Padding( |       body: HomeAssistantModel( | ||||||
|           padding: EdgeInsets.all(10.0), |           homeAssistant: widget.homeAssistant, | ||||||
|           child: widget.entity.buildEntityPageWidget(context) |           child: widget.homeAssistant.entities.get(widget.entityId).buildEntityPageWidget(context) | ||||||
|       ), |       ), | ||||||
|     ); |     ); | ||||||
|   } |   } | ||||||
| @@ -61,6 +56,7 @@ class _EntityViewPageState extends State<EntityViewPage> { | |||||||
|   @override |   @override | ||||||
|   void dispose(){ |   void dispose(){ | ||||||
|     if (_stateSubscription != null) _stateSubscription.cancel(); |     if (_stateSubscription != null) _stateSubscription.cancel(); | ||||||
|  |     if (_refreshDataSubscription != null) _refreshDataSubscription.cancel(); | ||||||
|     super.dispose(); |     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)); | ||||||
|  |   } | ||||||
|  | } | ||||||
| @@ -1,18 +1,16 @@ | |||||||
| part of '../main.dart'; | 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 { | class Entity { | ||||||
|   static const STATE_ICONS_COLORS = { |  | ||||||
|     "on": Colors.amber, |  | ||||||
|     "off": Color.fromRGBO(68, 115, 158, 1.0), |  | ||||||
|     "default": Color.fromRGBO(68, 115, 158, 1.0), |  | ||||||
|     "unavailable": Colors.black12, |  | ||||||
|     "unknown": Colors.black12, |  | ||||||
|     "playing": Colors.amber |  | ||||||
|   }; |  | ||||||
|   static const badgeColors = { |  | ||||||
|     "default": Color.fromRGBO(223, 76, 30, 1.0), |  | ||||||
|     "binary_sensor": Color.fromRGBO(3, 155, 229, 1.0) |  | ||||||
|   }; |  | ||||||
|   static List badgeDomains = [ |   static List badgeDomains = [ | ||||||
|     "alarm_control_panel", |     "alarm_control_panel", | ||||||
|     "binary_sensor", |     "binary_sensor", | ||||||
| @@ -23,56 +21,142 @@ class Entity { | |||||||
|     "sensor" |     "sensor" | ||||||
|   ]; |   ]; | ||||||
|  |  | ||||||
|   double rightWidgetPadding = 14.0; |   static Map StateByDeviceClass = { | ||||||
|   double leftWidgetPadding = 8.0; |     "battery.on": "Low", | ||||||
|   double extendedWidgetHeight = 50.0; |     "battery.off": "Normal", | ||||||
|   double widgetHeight = 34.0; |     "cold.on": "Cold", | ||||||
|   double iconSize = 28.0; |     "cold.off": "Normal", | ||||||
|   double stateFontSize = 16.0; |     "connectivity.on": "Connected", | ||||||
|   double nameFontSize = 16.0; |     "connectivity.off": "Diconnected", | ||||||
|   double smallFontSize = 14.0; |     "door.on": "Open", | ||||||
|   double largeFontSize = 24.0; |     "door.off": "Closed", | ||||||
|   double inputWidth = 160.0; |     "garage_door.on": "Open", | ||||||
|   double rowPadding = 10.0; |     "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; |   Map attributes; | ||||||
|   String domain; |   String domain; | ||||||
|   String entityId; |   String entityId; | ||||||
|  |   String entityPicture; | ||||||
|   String state; |   String state; | ||||||
|   String assumedState; |   String displayState; | ||||||
|   DateTime _lastUpdated; |   DateTime _lastUpdated; | ||||||
|  |   int statelessType = 0; | ||||||
|  |  | ||||||
|   List<Entity> childEntities = []; |   List<Entity> childEntities = []; | ||||||
|  |   String deviceClass; | ||||||
|   List<String> attributesToShow = ["all"]; |   EntityHistoryConfig historyConfig = EntityHistoryConfig( | ||||||
|  |     chartType: EntityHistoryWidgetType.simple | ||||||
|  |   ); | ||||||
|  |  | ||||||
|   String get displayName => |   String get displayName => | ||||||
|       attributes["friendly_name"] ?? (attributes["name"] ?? "_"); |       attributes["friendly_name"] ?? (attributes["name"] ?? entityId.split(".")[1].replaceAll("_", " ")); | ||||||
|  |  | ||||||
|   String get deviceClass => attributes["device_class"] ?? null; |  | ||||||
|   bool get isView => |   bool get isView => | ||||||
|       (domain == "group") && |       (domain == "group") && | ||||||
|       (attributes != null ? attributes["view"] ?? false : false); |       (attributes != null ? attributes["view"] ?? false : false); | ||||||
|   bool get isGroup => domain == "group"; |   bool get isGroup => domain == "group"; | ||||||
|   bool get isBadge => Entity.badgeDomains.contains(domain); |   bool get isBadge => Entity.badgeDomains.contains(domain); | ||||||
|   String get icon => attributes["icon"] ?? ""; |   String get icon => attributes["icon"] ?? ""; | ||||||
|   bool get isOn => state == "on"; |   bool get isOn => state == EntityState.on; | ||||||
|   String get entityPicture => attributes["entity_picture"]; |  | ||||||
|   String get unitOfMeasurement => attributes["unit_of_measurement"] ?? ""; |   String get unitOfMeasurement => attributes["unit_of_measurement"] ?? ""; | ||||||
|   List get childEntityIds => attributes["entity_id"] ?? []; |   List get childEntityIds => attributes["entity_id"] ?? []; | ||||||
|   String get lastUpdated => _getLastUpdatedFormatted(); |   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; | ||||||
|  |  | ||||||
|   Entity(Map rawData) { |   String _getEntityPictureUrl(String webHost) { | ||||||
|     update(rawData); |     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; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   void update(Map rawData) { |   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"] ?? {}; |     attributes = rawData["attributes"] ?? {}; | ||||||
|     domain = rawData["entity_id"].split(".")[0]; |     domain = rawData["entity_id"].split(".")[0]; | ||||||
|     entityId = rawData["entity_id"]; |     entityId = rawData["entity_id"]; | ||||||
|  |     deviceClass = attributes["device_class"]; | ||||||
|     state = rawData["state"]; |     state = rawData["state"]; | ||||||
|     assumedState = state; |     displayState = Entity.StateByDeviceClass["$deviceClass.$state"] ?? state; | ||||||
|     _lastUpdated = DateTime.tryParse(rawData["last_updated"]); |     _lastUpdated = DateTime.tryParse(rawData["last_updated"]); | ||||||
|  |     entityPicture = _getEntityPictureUrl(webHost); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   double _getDoubleAttributeValue(String attributeName) { |   double _getDoubleAttributeValue(String attributeName) { | ||||||
| @@ -81,16 +165,34 @@ class Entity { | |||||||
|       return temp1.toDouble(); |       return temp1.toDouble(); | ||||||
|     } else if (temp1 is double) { |     } else if (temp1 is double) { | ||||||
|       return temp1; |       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 { |     } else { | ||||||
|       return null; |       return null; | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   Widget buildDefaultWidget(BuildContext context) { |   Widget buildDefaultWidget(BuildContext context) { | ||||||
|     return EntityModel( |     return DefaultEntityContainer( | ||||||
|       entity: this, |         state: _buildStatePart(context) | ||||||
|       child: DefaultEntityContainer(state: _buildStatePart(context)), |  | ||||||
|       handleTap: true, |  | ||||||
|     ); |     ); | ||||||
|   } |   } | ||||||
|  |  | ||||||
| @@ -111,23 +213,33 @@ class Entity { | |||||||
|  |  | ||||||
|   Widget buildEntityPageWidget(BuildContext context) { |   Widget buildEntityPageWidget(BuildContext context) { | ||||||
|     return EntityModel( |     return EntityModel( | ||||||
|       entity: this, |       entityWrapper: EntityWrapper(entity: this), | ||||||
|       child: EntityPageContainer(children: <Widget>[ |       child: EntityPageContainer(children: <Widget>[ | ||||||
|         DefaultEntityContainer(state: _buildStatePartForPage(context)), |         Padding( | ||||||
|  |           padding: EdgeInsets.only(top: Sizes.rowPadding), | ||||||
|  |           child: DefaultEntityContainer(state: _buildStatePartForPage(context)), | ||||||
|  |         ), | ||||||
|         LastUpdatedWidget(), |         LastUpdatedWidget(), | ||||||
|         Divider(), |         Divider(), | ||||||
|         _buildAdditionalControlsForPage(context), |         _buildAdditionalControlsForPage(context), | ||||||
|         Divider(), |         Divider(), | ||||||
|  |         buildHistoryWidget(), | ||||||
|         EntityAttributesList() |         EntityAttributesList() | ||||||
|       ]), |       ]), | ||||||
|       handleTap: false, |       handleTap: false, | ||||||
|     ); |     ); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|  |   Widget buildHistoryWidget() { | ||||||
|  |     return EntityHistoryWidget( | ||||||
|  |       config: historyConfig, | ||||||
|  |     ); | ||||||
|  |   } | ||||||
|  |  | ||||||
|   Widget buildBadgeWidget(BuildContext context) { |   Widget buildBadgeWidget(BuildContext context) { | ||||||
|     return EntityModel( |     return EntityModel( | ||||||
|       entity: this, |       entityWrapper: EntityWrapper(entity: this), | ||||||
|       child: Badge(), |       child: BadgeWidget(), | ||||||
|       handleTap: true, |       handleTap: true, | ||||||
|     ); |     ); | ||||||
|   } |   } | ||||||
| @@ -168,299 +280,3 @@ class Entity { | |||||||
|     } |     } | ||||||
|   } |   } | ||||||
| } | } | ||||||
|  |  | ||||||
| class SwitchEntity extends Entity { |  | ||||||
|   SwitchEntity(Map rawData) : super(rawData); |  | ||||||
|  |  | ||||||
|   @override |  | ||||||
|   Widget _buildStatePart(BuildContext context) { |  | ||||||
|     return SwitchControlWidget(); |  | ||||||
|   } |  | ||||||
| } |  | ||||||
|  |  | ||||||
| class ButtonEntity extends Entity { |  | ||||||
|   ButtonEntity(Map rawData) : super(rawData); |  | ||||||
|  |  | ||||||
|   @override |  | ||||||
|   Widget _buildStatePart(BuildContext context) { |  | ||||||
|     return ButtonControlWidget(); |  | ||||||
|   } |  | ||||||
| } |  | ||||||
|  |  | ||||||
| class TextEntity extends Entity { |  | ||||||
|   TextEntity(Map rawData) : super(rawData); |  | ||||||
|  |  | ||||||
|   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 TextControlWidget(); |  | ||||||
|   } |  | ||||||
| } |  | ||||||
|  |  | ||||||
| class SunEntity extends Entity { |  | ||||||
|   SunEntity(Map rawData) : super(rawData); |  | ||||||
| } |  | ||||||
|  |  | ||||||
| class SliderEntity extends Entity { |  | ||||||
|   SliderEntity(Map rawData) : super(rawData); |  | ||||||
|  |  | ||||||
|   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; |  | ||||||
|  |  | ||||||
|   @override |  | ||||||
|   Widget _buildStatePart(BuildContext context) { |  | ||||||
|     return Expanded( |  | ||||||
|       //width: 200.0, |  | ||||||
|       child: Row( |  | ||||||
|         children: <Widget>[ |  | ||||||
|           SliderControlWidget( |  | ||||||
|             expanded: true, |  | ||||||
|           ), |  | ||||||
|           SimpleEntityState(), |  | ||||||
|         ], |  | ||||||
|       ), |  | ||||||
|     ); |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   @override |  | ||||||
|   Widget _buildStatePartForPage(BuildContext context) { |  | ||||||
|     return SimpleEntityState(); |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   @override |  | ||||||
|   Widget _buildAdditionalControlsForPage(BuildContext context) { |  | ||||||
|     return SliderControlWidget( |  | ||||||
|       expanded: false, |  | ||||||
|     ); |  | ||||||
|   } |  | ||||||
| } |  | ||||||
|  |  | ||||||
| class ClimateEntity extends Entity { |  | ||||||
|   @override |  | ||||||
|   double widgetHeight = 38.0; |  | ||||||
|  |  | ||||||
|   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; |  | ||||||
|  |  | ||||||
|   bool get supportTargetTemperature => ((attributes["supported_features"] & |  | ||||||
|           ClimateEntity.SUPPORT_TARGET_TEMPERATURE) == |  | ||||||
|       ClimateEntity.SUPPORT_TARGET_TEMPERATURE); |  | ||||||
|   bool get supportTargetTemperatureHigh => ((attributes["supported_features"] & |  | ||||||
|           ClimateEntity.SUPPORT_TARGET_TEMPERATURE_HIGH) == |  | ||||||
|       ClimateEntity.SUPPORT_TARGET_TEMPERATURE_HIGH); |  | ||||||
|   bool get supportTargetTemperatureLow => ((attributes["supported_features"] & |  | ||||||
|           ClimateEntity.SUPPORT_TARGET_TEMPERATURE_LOW) == |  | ||||||
|       ClimateEntity.SUPPORT_TARGET_TEMPERATURE_LOW); |  | ||||||
|   bool get supportTargetHumidity => ((attributes["supported_features"] & |  | ||||||
|           ClimateEntity.SUPPORT_TARGET_HUMIDITY) == |  | ||||||
|       ClimateEntity.SUPPORT_TARGET_HUMIDITY); |  | ||||||
|   bool get supportTargetHumidityHigh => ((attributes["supported_features"] & |  | ||||||
|           ClimateEntity.SUPPORT_TARGET_HUMIDITY_HIGH) == |  | ||||||
|       ClimateEntity.SUPPORT_TARGET_HUMIDITY_HIGH); |  | ||||||
|   bool get supportTargetHumidityLow => ((attributes["supported_features"] & |  | ||||||
|           ClimateEntity.SUPPORT_TARGET_HUMIDITY_LOW) == |  | ||||||
|       ClimateEntity.SUPPORT_TARGET_HUMIDITY_LOW); |  | ||||||
|   bool get supportFanMode => |  | ||||||
|       ((attributes["supported_features"] & ClimateEntity.SUPPORT_FAN_MODE) == |  | ||||||
|           ClimateEntity.SUPPORT_FAN_MODE); |  | ||||||
|   bool get supportOperationMode => ((attributes["supported_features"] & |  | ||||||
|           ClimateEntity.SUPPORT_OPERATION_MODE) == |  | ||||||
|       ClimateEntity.SUPPORT_OPERATION_MODE); |  | ||||||
|   bool get supportHoldMode => |  | ||||||
|       ((attributes["supported_features"] & ClimateEntity.SUPPORT_HOLD_MODE) == |  | ||||||
|           ClimateEntity.SUPPORT_HOLD_MODE); |  | ||||||
|   bool get supportSwingMode => |  | ||||||
|       ((attributes["supported_features"] & ClimateEntity.SUPPORT_SWING_MODE) == |  | ||||||
|           ClimateEntity.SUPPORT_SWING_MODE); |  | ||||||
|   bool get supportAwayMode => |  | ||||||
|       ((attributes["supported_features"] & ClimateEntity.SUPPORT_AWAY_MODE) == |  | ||||||
|           ClimateEntity.SUPPORT_AWAY_MODE); |  | ||||||
|   bool get supportAuxHeat => |  | ||||||
|       ((attributes["supported_features"] & ClimateEntity.SUPPORT_AUX_HEAT) == |  | ||||||
|           ClimateEntity.SUPPORT_AUX_HEAT); |  | ||||||
|   bool get supportOnOff => |  | ||||||
|       ((attributes["supported_features"] & 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'); |  | ||||||
|   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 == "off"; |  | ||||||
|   bool get auxHeat => attributes['aux_heat'] == "on"; |  | ||||||
|  |  | ||||||
|   ClimateEntity(Map rawData) : super(rawData); |  | ||||||
|  |  | ||||||
|   @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; |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
|  |  | ||||||
| } |  | ||||||
|  |  | ||||||
| class SelectEntity extends Entity { |  | ||||||
|   List<String> get listOptions => attributes["options"] != null |  | ||||||
|       ? (attributes["options"] as List).cast<String>() |  | ||||||
|       : []; |  | ||||||
|  |  | ||||||
|   SelectEntity(Map rawData) : super(rawData); |  | ||||||
|  |  | ||||||
|   @override |  | ||||||
|   Widget _buildStatePart(BuildContext context) { |  | ||||||
|     return SelectControlWidget(); |  | ||||||
|   } |  | ||||||
| } |  | ||||||
|  |  | ||||||
| class DateTimeEntity extends Entity { |  | ||||||
|   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(); |  | ||||||
|  |  | ||||||
|   DateTimeEntity(Map rawData) : super(rawData); |  | ||||||
|  |  | ||||||
|   @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)); |  | ||||||
|   } |  | ||||||
| } |  | ||||||
|  |  | ||||||
| class CoverEntity extends Entity { |  | ||||||
|   @override |  | ||||||
|   double widgetHeight = 38.0; |  | ||||||
|  |  | ||||||
|   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; |  | ||||||
|  |  | ||||||
|   bool get supportOpen => ((attributes["supported_features"] & |  | ||||||
|   CoverEntity.SUPPORT_OPEN) == |  | ||||||
|       CoverEntity.SUPPORT_OPEN); |  | ||||||
|   bool get supportClose => ((attributes["supported_features"] & |  | ||||||
|   CoverEntity.SUPPORT_CLOSE) == |  | ||||||
|       CoverEntity.SUPPORT_CLOSE); |  | ||||||
|   bool get supportSetPosition => ((attributes["supported_features"] & |  | ||||||
|   CoverEntity.SUPPORT_SET_POSITION) == |  | ||||||
|       CoverEntity.SUPPORT_SET_POSITION); |  | ||||||
|   bool get supportStop => ((attributes["supported_features"] & |  | ||||||
|   CoverEntity.SUPPORT_STOP) == |  | ||||||
|       CoverEntity.SUPPORT_STOP); |  | ||||||
|  |  | ||||||
|   bool get supportOpenTilt => ((attributes["supported_features"] & |  | ||||||
|   CoverEntity.SUPPORT_OPEN_TILT) == |  | ||||||
|       CoverEntity.SUPPORT_OPEN_TILT); |  | ||||||
|   bool get supportCloseTilt => ((attributes["supported_features"] & |  | ||||||
|   CoverEntity.SUPPORT_CLOSE_TILT) == |  | ||||||
|       CoverEntity.SUPPORT_CLOSE_TILT); |  | ||||||
|   bool get supportStopTilt => ((attributes["supported_features"] & |  | ||||||
|   CoverEntity.SUPPORT_STOP_TILT) == |  | ||||||
|       CoverEntity.SUPPORT_STOP_TILT); |  | ||||||
|   bool get supportSetTiltPosition => ((attributes["supported_features"] & |  | ||||||
|   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 == "closed") || (state == "closing") || (state == "opening")); |  | ||||||
|   bool get canBeClosed => ((state == "open") || (state == "opening")|| (state == "closing")); |  | ||||||
|   bool get canTiltBeOpened => currentPosition < 100; |  | ||||||
|   bool get canTiltBeClosed => currentPosition > 0; |  | ||||||
|  |  | ||||||
|   CoverEntity(Map rawData) : super(rawData); |  | ||||||
|  |  | ||||||
|   @override |  | ||||||
|   Widget _buildStatePart(BuildContext context) { |  | ||||||
|     return CoverEntityControlState(); |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   @override |  | ||||||
|   Widget _buildAdditionalControlsForPage(BuildContext context) { |  | ||||||
|     return CoverControlWidget(); |  | ||||||
|   } |  | ||||||
|  |  | ||||||
| } |  | ||||||
|   | |||||||
							
								
								
									
										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(); | ||||||
|  |   } | ||||||
|  | } | ||||||
| @@ -1,954 +0,0 @@ | |||||||
| part of '../main.dart'; |  | ||||||
|  |  | ||||||
| class SwitchControlWidget extends StatefulWidget { |  | ||||||
|   @override |  | ||||||
|   _SwitchControlWidgetState createState() => _SwitchControlWidgetState(); |  | ||||||
| } |  | ||||||
|  |  | ||||||
| class _SwitchControlWidgetState extends State<SwitchControlWidget> { |  | ||||||
|  |  | ||||||
|   @override |  | ||||||
|   void initState() { |  | ||||||
|     super.initState(); |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   void _setNewState(newValue, Entity entity) { |  | ||||||
|     setState(() { |  | ||||||
|       entity.assumedState = newValue ? 'on' : 'off'; |  | ||||||
|     }); |  | ||||||
|     Timer(Duration(seconds: 2), (){ |  | ||||||
|       setState(() { |  | ||||||
|         entity.assumedState = entity.state; |  | ||||||
|       }); |  | ||||||
|     }); |  | ||||||
|     eventBus.fire(new ServiceCallEvent( |  | ||||||
|         entity.domain, (newValue as bool) ? "turn_on" : "turn_off", entity.entityId, null)); |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   @override |  | ||||||
|   Widget build(BuildContext context) { |  | ||||||
|     final entityModel = EntityModel.of(context); |  | ||||||
|     return Switch( |  | ||||||
|       value: entityModel.entity.assumedState == 'on', |  | ||||||
|       onChanged: ((switchState) { |  | ||||||
|         _setNewState(switchState, entityModel.entity); |  | ||||||
|       }), |  | ||||||
|     ); |  | ||||||
|   } |  | ||||||
| } |  | ||||||
|  |  | ||||||
| class ButtonControlWidget extends StatefulWidget { |  | ||||||
|   @override |  | ||||||
|   _ButtonControlWidgetState createState() => _ButtonControlWidgetState(); |  | ||||||
| } |  | ||||||
|  |  | ||||||
| class _ButtonControlWidgetState extends State<ButtonControlWidget> { |  | ||||||
|  |  | ||||||
|   @override |  | ||||||
|   void initState() { |  | ||||||
|     super.initState(); |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   void _setNewState(Entity entity) { |  | ||||||
|     eventBus.fire(new ServiceCallEvent(entity.domain, "turn_on", entity.entityId, null)); |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   @override |  | ||||||
|   Widget build(BuildContext context) { |  | ||||||
|     final entityModel = EntityModel.of(context); |  | ||||||
|     return FlatButton( |  | ||||||
|       onPressed: (() { |  | ||||||
|         _setNewState(entityModel.entity); |  | ||||||
|       }), |  | ||||||
|       child: Text( |  | ||||||
|         "EXECUTE", |  | ||||||
|         textAlign: TextAlign.right, |  | ||||||
|         style: |  | ||||||
|         new TextStyle(fontSize: entityModel.entity.stateFontSize, color: Colors.blue), |  | ||||||
|       ), |  | ||||||
|     ); |  | ||||||
|   } |  | ||||||
| } |  | ||||||
|  |  | ||||||
| class TextControlWidget extends StatefulWidget { |  | ||||||
|  |  | ||||||
|   TextControlWidget({Key key}) : super(key: key); |  | ||||||
|  |  | ||||||
|   @override |  | ||||||
|   _TextControlWidgetState createState() => _TextControlWidgetState(); |  | ||||||
| } |  | ||||||
|  |  | ||||||
| class _TextControlWidgetState extends State<TextControlWidget> { |  | ||||||
|   String _tmpValue; |  | ||||||
|   String _entityState; |  | ||||||
|   String _entityDomain; |  | ||||||
|   String _entityId; |  | ||||||
|   int _minLength; |  | ||||||
|   int _maxLength; |  | ||||||
|   FocusNode _focusNode = FocusNode(); |  | ||||||
|   bool validValue = false; |  | ||||||
|  |  | ||||||
|   @override |  | ||||||
|   void initState() { |  | ||||||
|     super.initState(); |  | ||||||
|     _focusNode.addListener(_focusListener); |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   void setNewState(newValue, domain, entityId) { |  | ||||||
|     if (validate(newValue, _minLength, _maxLength)) { |  | ||||||
|       eventBus.fire(new ServiceCallEvent(domain, "set_value", entityId, |  | ||||||
|           {"value": "$newValue"})); |  | ||||||
|     } else { |  | ||||||
|       setState(() { |  | ||||||
|         _tmpValue = _entityState; |  | ||||||
|       }); |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   bool validate(newValue, minLength, maxLength) { |  | ||||||
|     if (newValue is String) { |  | ||||||
|       validValue = (newValue.length >= minLength) && |  | ||||||
|           (maxLength == -1 || |  | ||||||
|               (newValue.length <= maxLength)); |  | ||||||
|     } else { |  | ||||||
|       validValue = true; |  | ||||||
|     } |  | ||||||
|     return validValue; |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   void _focusListener() { |  | ||||||
|     if (!_focusNode.hasFocus && (_tmpValue != _entityState)) { |  | ||||||
|       setNewState(_tmpValue, _entityDomain, _entityId); |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   @override |  | ||||||
|   Widget build(BuildContext context) { |  | ||||||
|     final entityModel = EntityModel.of(context); |  | ||||||
|     final TextEntity entity = entityModel.entity; |  | ||||||
|     _entityState = entity.state; |  | ||||||
|     _entityDomain = entity.domain; |  | ||||||
|     _entityId = entity.entityId; |  | ||||||
|     _minLength = entity.valueMinLength; |  | ||||||
|     _maxLength = entity.valueMaxLength; |  | ||||||
|  |  | ||||||
|     if (!_focusNode.hasFocus && (_tmpValue != entity.state)) { |  | ||||||
|       _tmpValue = entity.state; |  | ||||||
|     } |  | ||||||
|     if (entity.isTextField || entity.isPasswordField) { |  | ||||||
|       return Expanded( |  | ||||||
|         //width: Entity.INPUT_WIDTH, |  | ||||||
|         child: TextField( |  | ||||||
|             focusNode: _focusNode, |  | ||||||
|             obscureText: entity.isPasswordField, |  | ||||||
|             controller: new TextEditingController.fromValue( |  | ||||||
|                 new TextEditingValue( |  | ||||||
|                     text: _tmpValue, |  | ||||||
|                     selection: |  | ||||||
|                     new TextSelection.collapsed(offset: _tmpValue.length) |  | ||||||
|                 ) |  | ||||||
|             ), |  | ||||||
|             onChanged: (value) { |  | ||||||
|               _tmpValue = value; |  | ||||||
|             }), |  | ||||||
|       ); |  | ||||||
|     } else { |  | ||||||
|       TheLogger.log("Warning", "Unsupported input mode for ${entity.entityId}"); |  | ||||||
|       return SimpleEntityState(); |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   @override |  | ||||||
|   void dispose() { |  | ||||||
|     _focusNode.removeListener(_focusListener); |  | ||||||
|     _focusNode.dispose(); |  | ||||||
|     super.dispose(); |  | ||||||
|   } |  | ||||||
|  |  | ||||||
| } |  | ||||||
|  |  | ||||||
| class SliderControlWidget extends StatefulWidget { |  | ||||||
|  |  | ||||||
|   final bool expanded; |  | ||||||
|  |  | ||||||
|   SliderControlWidget({Key key, @required this.expanded}) : super(key: key); |  | ||||||
|  |  | ||||||
|   @override |  | ||||||
|   _SliderControlWidgetState createState() => _SliderControlWidgetState(); |  | ||||||
| } |  | ||||||
|  |  | ||||||
| class _SliderControlWidgetState extends State<SliderControlWidget> { |  | ||||||
|   int _multiplier = 1; |  | ||||||
|  |  | ||||||
|   void setNewState(newValue, domain, entityId) { |  | ||||||
|     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.entity; |  | ||||||
|     if (entity.valueStep < 1) { |  | ||||||
|       _multiplier = 10; |  | ||||||
|     } else if (entity.valueStep < 0.1) { |  | ||||||
|       _multiplier = 100; |  | ||||||
|     } |  | ||||||
|     Widget slider = Slider( |  | ||||||
|       min: entity.minValue * _multiplier, |  | ||||||
|       max: entity.maxValue * _multiplier, |  | ||||||
|       value: (entity.doubleState <= entity.maxValue) && |  | ||||||
|           (entity.doubleState >= entity.minValue) |  | ||||||
|           ? entity.doubleState * _multiplier |  | ||||||
|           : entity.minValue * _multiplier, |  | ||||||
|       onChanged: (value) { |  | ||||||
|         setState(() { |  | ||||||
|           entity.state = |  | ||||||
|               (value.roundToDouble() / _multiplier).toString(); |  | ||||||
|         }); |  | ||||||
|         eventBus.fire(new StateChangedEvent(entity.entityId, |  | ||||||
|             (value.roundToDouble() / _multiplier).toString(), true)); |  | ||||||
|  |  | ||||||
|       }, |  | ||||||
|       onChangeEnd: (value) { |  | ||||||
|         setNewState(value.roundToDouble() / _multiplier, entity.domain, entity.entityId); |  | ||||||
|       }, |  | ||||||
|     ); |  | ||||||
|     if (widget.expanded) { |  | ||||||
|       return Expanded( |  | ||||||
|         child: slider, |  | ||||||
|       ); |  | ||||||
|     } else { |  | ||||||
|       return slider; |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
| } |  | ||||||
|  |  | ||||||
| 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; |  | ||||||
|   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, double step) { |  | ||||||
|     _tmpTemperature = ((_tmpTemperature + step) <= entity.maxTemp) ? _tmpTemperature + step : entity.maxTemp; |  | ||||||
|     _setTemperature(entity); |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   void _temperatureDown(ClimateEntity entity, double step) { |  | ||||||
|     _tmpTemperature = ((_tmpTemperature - step) >= entity.minTemp) ? _tmpTemperature - step : entity.minTemp; |  | ||||||
|     _setTemperature(entity); |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   void _targetLowUp(ClimateEntity entity, double step) { |  | ||||||
|     _tmpTargetLow = ((_tmpTargetLow + step) <= entity.maxTemp) ? _tmpTargetLow + step : entity.maxTemp; |  | ||||||
|     _setTargetTemp(entity); |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   void _targetLowDown(ClimateEntity entity, double step) { |  | ||||||
|     _tmpTargetLow = ((_tmpTargetLow - step) >= entity.minTemp) ? _tmpTargetLow - step : entity.minTemp; |  | ||||||
|     _setTargetTemp(entity); |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   void _targetHighUp(ClimateEntity entity, double step) { |  | ||||||
|     _tmpTargetHigh = ((_tmpTargetHigh + step) <= entity.maxTemp) ? _tmpTargetHigh + step : entity.maxTemp; |  | ||||||
|     _setTargetTemp(entity); |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   void _targetHighDown(ClimateEntity entity, double step) { |  | ||||||
|     _tmpTargetHigh = ((_tmpTargetHigh - step) >= entity.minTemp) ? _tmpTargetHigh - step : entity.minTemp; |  | ||||||
|     _setTargetTemp(entity); |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   void _setTemperature(ClimateEntity entity) { |  | ||||||
|     setState(() { |  | ||||||
|       _tmpTemperature = double.parse(_tmpTemperature.toStringAsFixed(1)); |  | ||||||
|       _changedHere = true; |  | ||||||
|       eventBus.fire(new ServiceCallEvent(entity.domain, "set_temperature", entity.entityId,{"temperature": "${_tmpTemperature.toStringAsFixed(1)}"})); |  | ||||||
|       _resetStateTimer(entity); |  | ||||||
|     }); |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   void _setTargetTemp(ClimateEntity entity) { |  | ||||||
|     setState(() { |  | ||||||
|       _tmpTargetLow = double.parse(_tmpTargetLow.toStringAsFixed(1)); |  | ||||||
|       _tmpTargetHigh = double.parse(_tmpTargetHigh.toStringAsFixed(1)); |  | ||||||
|       _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.entity; |  | ||||||
|     if (_changedHere) { |  | ||||||
|       _showPending = (_tmpTemperature != entity.temperature); |  | ||||||
|       _changedHere = false; |  | ||||||
|     } else { |  | ||||||
|       _resetTimer?.cancel(); |  | ||||||
|       _resetVars(entity); |  | ||||||
|     } |  | ||||||
|     return Padding( |  | ||||||
|       padding: EdgeInsets.fromLTRB(entity.leftWidgetPadding, entity.rowPadding, entity.rightWidgetPadding, 0.0), |  | ||||||
|       child: Column( |  | ||||||
|         crossAxisAlignment: CrossAxisAlignment.start, |  | ||||||
|         children: <Widget>[ |  | ||||||
|           _buildOnOffControl(entity), |  | ||||||
|           _buildTemperatureControls(entity), |  | ||||||
|           _buildHumidityControls(entity), |  | ||||||
|           _buildOperationControl(entity), |  | ||||||
|           _buildFanControl(entity), |  | ||||||
|           _buildSwingControl(entity), |  | ||||||
|           _buildAwayModeControl(entity), |  | ||||||
|           _buildAuxHeatControl(entity) |  | ||||||
|         ], |  | ||||||
|       ), |  | ||||||
|     ); |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   Widget _buildAwayModeControl(ClimateEntity entity) { |  | ||||||
|     if (entity.supportAwayMode) { |  | ||||||
|       return Row( |  | ||||||
|         children: <Widget>[ |  | ||||||
|           Expanded( |  | ||||||
|             child: Text( |  | ||||||
|               "Away mode", |  | ||||||
|               style: TextStyle( |  | ||||||
|                   fontSize: entity.stateFontSize |  | ||||||
|               ), |  | ||||||
|             ), |  | ||||||
|           ), |  | ||||||
|           Switch( |  | ||||||
|             onChanged: (value) => _setAwayMode(entity, value), |  | ||||||
|             value: _tmpAwayMode, |  | ||||||
|           ) |  | ||||||
|         ], |  | ||||||
|       ); |  | ||||||
|     } else { |  | ||||||
|       return Container(height: 0.0, width: 0.0,); |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   Widget _buildOnOffControl(ClimateEntity entity) { |  | ||||||
|     if (entity.supportOnOff) { |  | ||||||
|       return Row( |  | ||||||
|         children: <Widget>[ |  | ||||||
|           Expanded( |  | ||||||
|             child: Text( |  | ||||||
|               "On / Off", |  | ||||||
|               style: TextStyle( |  | ||||||
|                   fontSize: entity.stateFontSize |  | ||||||
|               ), |  | ||||||
|             ), |  | ||||||
|           ), |  | ||||||
|           Switch( |  | ||||||
|             onChanged: (value) => _setOnOf(entity, value), |  | ||||||
|             value: !_tmpIsOff, |  | ||||||
|           ) |  | ||||||
|         ], |  | ||||||
|       ); |  | ||||||
|     } else { |  | ||||||
|       return Container(height: 0.0, width: 0.0,); |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   Widget _buildAuxHeatControl(ClimateEntity entity) { |  | ||||||
|     if (entity.supportAuxHeat ) { |  | ||||||
|       return Row( |  | ||||||
|         children: <Widget>[ |  | ||||||
|           Expanded( |  | ||||||
|             child: Text( |  | ||||||
|               "Aux heat", |  | ||||||
|               style: TextStyle( |  | ||||||
|                   fontSize: entity.stateFontSize |  | ||||||
|               ), |  | ||||||
|             ), |  | ||||||
|           ), |  | ||||||
|           Switch( |  | ||||||
|             onChanged: (value) => _setAuxHeat(entity, value), |  | ||||||
|             value: _tmpAuxHeat, |  | ||||||
|           ) |  | ||||||
|         ], |  | ||||||
|       ); |  | ||||||
|     } else { |  | ||||||
|       return Container(height: 0.0, width: 0.0,); |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   Widget _buildOperationControl(ClimateEntity entity) { |  | ||||||
|     if (entity.supportOperationMode) { |  | ||||||
|       return Column( |  | ||||||
|         crossAxisAlignment: CrossAxisAlignment.start, |  | ||||||
|         children: <Widget>[ |  | ||||||
|           Text("Operation", style: TextStyle( |  | ||||||
|               fontSize: entity.stateFontSize |  | ||||||
|           )), |  | ||||||
|           DropdownButton<String>( |  | ||||||
|             value: "$_tmpOperationMode", |  | ||||||
|             iconSize: 30.0, |  | ||||||
|             style: TextStyle( |  | ||||||
|               fontSize: entity.largeFontSize, |  | ||||||
|               color: Colors.black, |  | ||||||
|             ), |  | ||||||
|             items: entity.operationList.map((String value) { |  | ||||||
|               return new DropdownMenuItem<String>( |  | ||||||
|                 value: value, |  | ||||||
|                 child: new Text(value), |  | ||||||
|               ); |  | ||||||
|             }).toList(), |  | ||||||
|             onChanged: (mode) => _setOperationMode(entity, mode), |  | ||||||
|           ), |  | ||||||
|           Container(height: entity.rowPadding,) |  | ||||||
|         ], |  | ||||||
|       ); |  | ||||||
|     } else { |  | ||||||
|       return Container(height: 0.0, width: 0.0); |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   Widget _buildFanControl(ClimateEntity entity) { |  | ||||||
|     if (entity.supportFanMode) { |  | ||||||
|       return Column( |  | ||||||
|         crossAxisAlignment: CrossAxisAlignment.start, |  | ||||||
|         children: <Widget>[ |  | ||||||
|           Text("Fan mode", style: TextStyle( |  | ||||||
|               fontSize: entity.stateFontSize |  | ||||||
|           )), |  | ||||||
|           DropdownButton<String>( |  | ||||||
|             value: "$_tmpFanMode", |  | ||||||
|             iconSize: 30.0, |  | ||||||
|             style: TextStyle( |  | ||||||
|               fontSize: entity.largeFontSize, |  | ||||||
|               color: Colors.black, |  | ||||||
|             ), |  | ||||||
|             items: entity.fanList.map((String value) { |  | ||||||
|               return new DropdownMenuItem<String>( |  | ||||||
|                 value: value, |  | ||||||
|                 child: new Text(value), |  | ||||||
|               ); |  | ||||||
|             }).toList(), |  | ||||||
|             onChanged: (mode) => _setFanMode(entity, mode), |  | ||||||
|           ), |  | ||||||
|           Container(height: entity.rowPadding,) |  | ||||||
|         ], |  | ||||||
|       ); |  | ||||||
|     } else { |  | ||||||
|       return Container(height: 0.0, width: 0.0); |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   Widget _buildSwingControl(ClimateEntity entity) { |  | ||||||
|     if (entity.supportSwingMode) { |  | ||||||
|       return Column( |  | ||||||
|         crossAxisAlignment: CrossAxisAlignment.start, |  | ||||||
|         children: <Widget>[ |  | ||||||
|           Text("Swing mode", style: TextStyle( |  | ||||||
|               fontSize: entity.stateFontSize |  | ||||||
|           )), |  | ||||||
|           DropdownButton<String>( |  | ||||||
|             value: "$_tmpSwingMode", |  | ||||||
|             iconSize: 30.0, |  | ||||||
|             style: TextStyle( |  | ||||||
|               fontSize: entity.largeFontSize, |  | ||||||
|               color: Colors.black, |  | ||||||
|             ), |  | ||||||
|             items: entity.swingList.map((String value) { |  | ||||||
|               return new DropdownMenuItem<String>( |  | ||||||
|                 value: value, |  | ||||||
|                 child: new Text(value), |  | ||||||
|               ); |  | ||||||
|             }).toList(), |  | ||||||
|             onChanged: (mode) => _setSwingMode(entity, mode), |  | ||||||
|           ), |  | ||||||
|           Container(height: entity.rowPadding,) |  | ||||||
|         ], |  | ||||||
|       ); |  | ||||||
|     } else { |  | ||||||
|       return Container(height: 0.0, width: 0.0); |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   Widget _buildTemperatureControls(ClimateEntity entity) { |  | ||||||
|     List<Widget> result = []; |  | ||||||
|     if (entity.supportTargetTemperature) { |  | ||||||
|       result.addAll(<Widget>[ |  | ||||||
|         Text( |  | ||||||
|           "$_tmpTemperature", |  | ||||||
|           style: TextStyle( |  | ||||||
|               fontSize: entity.largeFontSize, |  | ||||||
|               color: _showPending ? Colors.red : Colors.black |  | ||||||
|           ), |  | ||||||
|         ), |  | ||||||
|         Column( |  | ||||||
|           children: <Widget>[ |  | ||||||
|             IconButton( |  | ||||||
|               icon: Icon(MaterialDesignIcons.createIconDataFromIconName('mdi:chevron-up')), |  | ||||||
|               iconSize: 30.0, |  | ||||||
|               onPressed: () => _temperatureUp(entity, 0.1), |  | ||||||
|             ), |  | ||||||
|             IconButton( |  | ||||||
|               icon: Icon(MaterialDesignIcons.createIconDataFromIconName('mdi:chevron-down')), |  | ||||||
|               iconSize: 30.0, |  | ||||||
|               onPressed: () => _temperatureDown(entity, 0.1), |  | ||||||
|             ) |  | ||||||
|           ], |  | ||||||
|         ), |  | ||||||
|         Column( |  | ||||||
|           children: <Widget>[ |  | ||||||
|             IconButton( |  | ||||||
|               icon: Icon(MaterialDesignIcons.createIconDataFromIconName('mdi:chevron-double-up')), |  | ||||||
|               iconSize: 30.0, |  | ||||||
|               onPressed: () => _temperatureUp(entity, 0.5), |  | ||||||
|             ), |  | ||||||
|             IconButton( |  | ||||||
|               icon: Icon(MaterialDesignIcons.createIconDataFromIconName('mdi:chevron-double-down')), |  | ||||||
|               iconSize: 30.0, |  | ||||||
|               onPressed: () => _temperatureDown(entity, 0.5), |  | ||||||
|             ) |  | ||||||
|           ], |  | ||||||
|         ) |  | ||||||
|       ]); |  | ||||||
|     } else if (entity.supportTargetTemperatureHigh && entity.supportTargetTemperatureLow) { |  | ||||||
|       result.addAll(<Widget>[ |  | ||||||
|         Text( |  | ||||||
|           "$_tmpTargetLow", |  | ||||||
|           style: TextStyle( |  | ||||||
|               fontSize: entity.largeFontSize, |  | ||||||
|               color: _showPending ? Colors.red : Colors.black |  | ||||||
|           ), |  | ||||||
|         ), |  | ||||||
|         Column( |  | ||||||
|           children: <Widget>[ |  | ||||||
|             IconButton( |  | ||||||
|               icon: Icon(MaterialDesignIcons.createIconDataFromIconName('mdi:chevron-up')), |  | ||||||
|               iconSize: 30.0, |  | ||||||
|               onPressed: () => _targetLowUp(entity, 0.1), |  | ||||||
|             ), |  | ||||||
|             IconButton( |  | ||||||
|               icon: Icon(MaterialDesignIcons.createIconDataFromIconName('mdi:chevron-down')), |  | ||||||
|               iconSize: 30.0, |  | ||||||
|               onPressed: () => _targetLowDown(entity, 0.1), |  | ||||||
|             ) |  | ||||||
|           ], |  | ||||||
|         ), |  | ||||||
|         Column( |  | ||||||
|           children: <Widget>[ |  | ||||||
|             IconButton( |  | ||||||
|               icon: Icon(MaterialDesignIcons.createIconDataFromIconName('mdi:chevron-double-up')), |  | ||||||
|               iconSize: 30.0, |  | ||||||
|               onPressed: () => _targetLowUp(entity, 0.5), |  | ||||||
|             ), |  | ||||||
|             IconButton( |  | ||||||
|               icon: Icon(MaterialDesignIcons.createIconDataFromIconName('mdi:chevron-double-down')), |  | ||||||
|               iconSize: 30.0, |  | ||||||
|               onPressed: () => _targetLowDown(entity, 0.5), |  | ||||||
|             ) |  | ||||||
|           ], |  | ||||||
|         ), |  | ||||||
|         Expanded( |  | ||||||
|           child: Container(height: 10.0), |  | ||||||
|         ), |  | ||||||
|         Text( |  | ||||||
|           "$_tmpTargetHigh", |  | ||||||
|           style: TextStyle( |  | ||||||
|               fontSize: entity.largeFontSize, |  | ||||||
|               color: _showPending ? Colors.red : Colors.black |  | ||||||
|           ), |  | ||||||
|         ), |  | ||||||
|         Column( |  | ||||||
|           children: <Widget>[ |  | ||||||
|             IconButton( |  | ||||||
|               icon: Icon(MaterialDesignIcons.createIconDataFromIconName('mdi:chevron-up')), |  | ||||||
|               iconSize: 30.0, |  | ||||||
|               onPressed: () => _targetHighUp(entity, 0.1), |  | ||||||
|             ), |  | ||||||
|             IconButton( |  | ||||||
|               icon: Icon(MaterialDesignIcons.createIconDataFromIconName('mdi:chevron-down')), |  | ||||||
|               iconSize: 30.0, |  | ||||||
|               onPressed: () => _targetHighDown(entity, 0.1), |  | ||||||
|             ) |  | ||||||
|           ], |  | ||||||
|         ), |  | ||||||
|         Column( |  | ||||||
|           children: <Widget>[ |  | ||||||
|             IconButton( |  | ||||||
|               icon: Icon(MaterialDesignIcons.createIconDataFromIconName('mdi:chevron-double-up')), |  | ||||||
|               iconSize: 30.0, |  | ||||||
|               onPressed: () => _targetHighUp(entity, 0.5), |  | ||||||
|             ), |  | ||||||
|             IconButton( |  | ||||||
|               icon: Icon(MaterialDesignIcons.createIconDataFromIconName('mdi:chevron-double-down')), |  | ||||||
|               iconSize: 30.0, |  | ||||||
|               onPressed: () => _targetHighDown(entity, 0.5), |  | ||||||
|             ) |  | ||||||
|           ], |  | ||||||
|         ) |  | ||||||
|       ]); |  | ||||||
|     } else if (entity.supportTargetTemperatureHigh || entity.supportTargetTemperatureLow) { |  | ||||||
|       result.add(Text("Unsupported temperature control. Please, report an issue.")); |  | ||||||
|     } |  | ||||||
|     if (result.isNotEmpty) { |  | ||||||
|       return Column( |  | ||||||
|         crossAxisAlignment: CrossAxisAlignment.start, |  | ||||||
|         children: <Widget>[ |  | ||||||
|           Text("Target temperature", style: TextStyle( |  | ||||||
|               fontSize: entity.stateFontSize |  | ||||||
|           )), |  | ||||||
|           Row( |  | ||||||
|             crossAxisAlignment: CrossAxisAlignment.center, |  | ||||||
|             children: result, |  | ||||||
|           ) |  | ||||||
|         ], |  | ||||||
|       ); |  | ||||||
|     } else { |  | ||||||
|       return Container(height: 0.0, width: 0.0,); |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   Widget _buildHumidityControls(ClimateEntity entity) { |  | ||||||
|     List<Widget> result = []; |  | ||||||
|     if (entity.supportTargetHumidity) { |  | ||||||
|       result.addAll(<Widget>[ |  | ||||||
|         Text( |  | ||||||
|           "$_tmpTargetHumidity%", |  | ||||||
|           style: TextStyle(fontSize: entity.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, entity.rowPadding, 0.0, entity.rowPadding), |  | ||||||
|             child: Text("Target humidity", style: TextStyle( |  | ||||||
|                 fontSize: entity.stateFontSize |  | ||||||
|             )), |  | ||||||
|           ), |  | ||||||
|           Row( |  | ||||||
|             crossAxisAlignment: CrossAxisAlignment.center, |  | ||||||
|             children: result, |  | ||||||
|           ), |  | ||||||
|           Container( |  | ||||||
|             height: entity.rowPadding, |  | ||||||
|           ) |  | ||||||
|         ], |  | ||||||
|       ); |  | ||||||
|     } else { |  | ||||||
|       return Container( |  | ||||||
|         width: 0.0, |  | ||||||
|         height: 0.0, |  | ||||||
|       ); |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|  |  | ||||||
|   @override |  | ||||||
|   void dispose() { |  | ||||||
|     _resetTimer?.cancel(); |  | ||||||
|     super.dispose(); |  | ||||||
|   } |  | ||||||
|  |  | ||||||
| } |  | ||||||
|  |  | ||||||
| class SelectControlWidget extends StatefulWidget { |  | ||||||
|  |  | ||||||
|   SelectControlWidget({Key key}) : super(key: key); |  | ||||||
|  |  | ||||||
|   @override |  | ||||||
|   _SelectControlWidgetState createState() => _SelectControlWidgetState(); |  | ||||||
| } |  | ||||||
|  |  | ||||||
| class _SelectControlWidgetState extends State<SelectControlWidget> { |  | ||||||
|  |  | ||||||
|   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.entity; |  | ||||||
|     Widget ctrl; |  | ||||||
|     if (entity.listOptions.isNotEmpty) { |  | ||||||
|       ctrl = DropdownButton<String>( |  | ||||||
|         value: entity.state, |  | ||||||
|         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 Expanded( |  | ||||||
|       //width: Entity.INPUT_WIDTH, |  | ||||||
|       child: ctrl, |  | ||||||
|     ); |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|  |  | ||||||
| } |  | ||||||
|  |  | ||||||
| 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.entity; |  | ||||||
|     if (_changedHere) { |  | ||||||
|       _changedHere = false; |  | ||||||
|     } else { |  | ||||||
|       _resetVars(entity); |  | ||||||
|     } |  | ||||||
|     return Padding( |  | ||||||
|       padding: EdgeInsets.fromLTRB(entity.leftWidgetPadding, entity.rowPadding, entity.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, entity.rowPadding, 0.0, entity.rowPadding), |  | ||||||
|             child: Text("Position", style: TextStyle( |  | ||||||
|                 fontSize: entity.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: entity.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( |  | ||||||
|         CoverEntityTiltControlState() |  | ||||||
|       ); |  | ||||||
|     } |  | ||||||
|     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: entity.rowPadding,) |  | ||||||
|       ]); |  | ||||||
|     } |  | ||||||
|     if (controls.isNotEmpty) { |  | ||||||
|       controls.insert(0, Padding( |  | ||||||
|         padding: EdgeInsets.fromLTRB( |  | ||||||
|             0.0, entity.rowPadding, 0.0, entity.rowPadding), |  | ||||||
|         child: Text("Tilt position", style: TextStyle( |  | ||||||
|             fontSize: entity.stateFontSize |  | ||||||
|         )), |  | ||||||
|       )); |  | ||||||
|       return Column( |  | ||||||
|         crossAxisAlignment: CrossAxisAlignment.start, |  | ||||||
|         children: controls, |  | ||||||
|       ); |  | ||||||
|     } else { |  | ||||||
|       return Container(width: 0.0, height: 0.0); |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
|  |  | ||||||
| } |  | ||||||
| @@ -1,569 +0,0 @@ | |||||||
| part of '../main.dart'; |  | ||||||
|  |  | ||||||
| class EntityWidgetsSizes { |  | ||||||
|  |  | ||||||
| } |  | ||||||
|  |  | ||||||
| class EntityModel extends InheritedWidget { |  | ||||||
|  |  | ||||||
|   const EntityModel({ |  | ||||||
|     Key key, |  | ||||||
|     @required this.entity, |  | ||||||
|     @required this.handleTap, |  | ||||||
|     @required Widget child, |  | ||||||
|   }) : super(key: key, child: child); |  | ||||||
|  |  | ||||||
|   final Entity entity; |  | ||||||
|   final bool handleTap; |  | ||||||
|  |  | ||||||
|   static EntityModel of(BuildContext context) { |  | ||||||
|     return context.inheritFromWidgetOfExactType(EntityModel); |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   @override |  | ||||||
|   bool updateShouldNotify(InheritedWidget oldWidget) { |  | ||||||
|     return true; |  | ||||||
|   } |  | ||||||
|  |  | ||||||
| } |  | ||||||
|  |  | ||||||
| class DefaultEntityContainer extends StatelessWidget { |  | ||||||
|  |  | ||||||
|   DefaultEntityContainer({ |  | ||||||
|     Key key, |  | ||||||
|     @required this.state, |  | ||||||
|   }) : super(key: key); |  | ||||||
|  |  | ||||||
|   final Widget state; |  | ||||||
|  |  | ||||||
|   @override |  | ||||||
|   Widget build(BuildContext context) { |  | ||||||
|     final entityModel = EntityModel.of(context); |  | ||||||
|     return SizedBox( |  | ||||||
|       height: entityModel.entity.widgetHeight, |  | ||||||
|       child: Row( |  | ||||||
|         children: <Widget>[ |  | ||||||
|           EntityIcon(), |  | ||||||
|           Expanded( |  | ||||||
|             child: EntityName(), |  | ||||||
|           ), |  | ||||||
|           state |  | ||||||
|         ], |  | ||||||
|       ), |  | ||||||
|     ); |  | ||||||
|   } |  | ||||||
|  |  | ||||||
| } |  | ||||||
|  |  | ||||||
| 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, |  | ||||||
|     ); |  | ||||||
|   } |  | ||||||
|  |  | ||||||
| } |  | ||||||
|  |  | ||||||
| class SimpleEntityState extends StatelessWidget { |  | ||||||
|  |  | ||||||
|   @override |  | ||||||
|   Widget build(BuildContext context) { |  | ||||||
|     final entityModel = EntityModel.of(context); |  | ||||||
|     return Padding( |  | ||||||
|         padding: |  | ||||||
|         EdgeInsets.fromLTRB(0.0, 0.0, entityModel.entity.rightWidgetPadding, 0.0), |  | ||||||
|         child: GestureDetector( |  | ||||||
|           child: Text( |  | ||||||
|               "${entityModel.entity.state}${entityModel.entity.unitOfMeasurement}", |  | ||||||
|               textAlign: TextAlign.right, |  | ||||||
|               style: new TextStyle( |  | ||||||
|                 fontSize: entityModel.entity.stateFontSize, |  | ||||||
|               )), |  | ||||||
|           onTap: () => entityModel.handleTap ? eventBus.fire(new ShowEntityPageEvent(entityModel.entity)) : null, |  | ||||||
|         ) |  | ||||||
|     ); |  | ||||||
|   } |  | ||||||
|  |  | ||||||
| } |  | ||||||
|  |  | ||||||
| class EntityName extends StatelessWidget { |  | ||||||
|  |  | ||||||
|   @override |  | ||||||
|   Widget build(BuildContext context) { |  | ||||||
|     final entityModel = EntityModel.of(context); |  | ||||||
|     return GestureDetector( |  | ||||||
|       child: Padding( |  | ||||||
|         padding: EdgeInsets.only(right: 10.0), |  | ||||||
|         child: Text( |  | ||||||
|           "${entityModel.entity.displayName}", |  | ||||||
|           overflow: TextOverflow.ellipsis, |  | ||||||
|           softWrap: false, |  | ||||||
|           style: TextStyle(fontSize: entityModel.entity.nameFontSize), |  | ||||||
|         ), |  | ||||||
|       ), |  | ||||||
|       onTap: () => entityModel.handleTap ? eventBus.fire(new ShowEntityPageEvent(entityModel.entity)) : null, |  | ||||||
|     ); |  | ||||||
|   } |  | ||||||
|  |  | ||||||
| } |  | ||||||
|  |  | ||||||
| class EntityIcon extends StatelessWidget { |  | ||||||
|  |  | ||||||
|   @override |  | ||||||
|   Widget build(BuildContext context) { |  | ||||||
|     final entityModel = EntityModel.of(context); |  | ||||||
|     return GestureDetector( |  | ||||||
|       child: Padding( |  | ||||||
|         padding: EdgeInsets.fromLTRB(entityModel.entity.leftWidgetPadding, 0.0, 12.0, 0.0), |  | ||||||
|         //TODO: move createIconWidgetFromEntityData into this widget |  | ||||||
|         child: MaterialDesignIcons.createIconWidgetFromEntityData( |  | ||||||
|             entityModel.entity, |  | ||||||
|             entityModel.entity.iconSize, |  | ||||||
|             Entity.STATE_ICONS_COLORS[entityModel.entity.state] ?? Entity.STATE_ICONS_COLORS["default"]), |  | ||||||
|       ), |  | ||||||
|       onTap: () => entityModel.handleTap ? eventBus.fire(new ShowEntityPageEvent(entityModel.entity)) : null, |  | ||||||
|     ); |  | ||||||
|   } |  | ||||||
|  |  | ||||||
| } |  | ||||||
|  |  | ||||||
| class LastUpdatedWidget extends StatelessWidget { |  | ||||||
|  |  | ||||||
|   @override |  | ||||||
|   Widget build(BuildContext context) { |  | ||||||
|     final entityModel = EntityModel.of(context); |  | ||||||
|     return Padding( |  | ||||||
|       padding: EdgeInsets.fromLTRB( |  | ||||||
|           entityModel.entity.leftWidgetPadding, 0.0, 0.0, 0.0), |  | ||||||
|       child: Text( |  | ||||||
|         '${entityModel.entity.lastUpdated}', |  | ||||||
|         textAlign: TextAlign.left, |  | ||||||
|         style: |  | ||||||
|         TextStyle(fontSize: entityModel.entity.smallFontSize, color: Colors.black26), |  | ||||||
|       ), |  | ||||||
|     ); |  | ||||||
|   } |  | ||||||
|  |  | ||||||
| } |  | ||||||
|  |  | ||||||
| class EntityAttributesList extends StatelessWidget { |  | ||||||
|  |  | ||||||
|   EntityAttributesList({Key key}) : super(key: key); |  | ||||||
|  |  | ||||||
|   @override |  | ||||||
|   Widget build(BuildContext context) { |  | ||||||
|     final entityModel = EntityModel.of(context); |  | ||||||
|     List<Widget> attrs = []; |  | ||||||
|     if ((entityModel.entity.attributesToShow == null) || (entityModel.entity.attributesToShow.contains("all"))) { |  | ||||||
|       entityModel.entity.attributes.forEach((name, value){ |  | ||||||
|         attrs.add( |  | ||||||
|             _buildSingleAttribute(entityModel.entity, "$name", "$value") |  | ||||||
|         ); |  | ||||||
|       }); |  | ||||||
|     } else { |  | ||||||
|       entityModel.entity.attributesToShow.forEach((String attr) { |  | ||||||
|         String attrValue = entityModel.entity.getAttribute("$attr"); |  | ||||||
|         if (attrValue != null) { |  | ||||||
|           attrs.add( |  | ||||||
|               _buildSingleAttribute(entityModel.entity, "$attr", "$attrValue") |  | ||||||
|           ); |  | ||||||
|         } |  | ||||||
|       }); |  | ||||||
|     } |  | ||||||
|     return Column( |  | ||||||
|       children: attrs, |  | ||||||
|       crossAxisAlignment: CrossAxisAlignment.start, |  | ||||||
|       mainAxisSize: MainAxisSize.min, |  | ||||||
|     ); |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   Widget _buildSingleAttribute(Entity entity, String name, String value) { |  | ||||||
|     return Row( |  | ||||||
|       children: <Widget>[ |  | ||||||
|         Expanded( |  | ||||||
|           child: Padding( |  | ||||||
|             padding: EdgeInsets.fromLTRB(entity.leftWidgetPadding, entity.rowPadding, 0.0, 0.0), |  | ||||||
|             child: Text( |  | ||||||
|               "$name", |  | ||||||
|               textAlign: TextAlign.left, |  | ||||||
|             ), |  | ||||||
|           ), |  | ||||||
|         ), |  | ||||||
|         Expanded( |  | ||||||
|           child: Padding( |  | ||||||
|             padding: EdgeInsets.fromLTRB(0.0, entity.rowPadding, entity.rightWidgetPadding, 0.0), |  | ||||||
|             child: Text( |  | ||||||
|               "$value", |  | ||||||
|               textAlign: TextAlign.right, |  | ||||||
|             ), |  | ||||||
|           ), |  | ||||||
|         ) |  | ||||||
|       ], |  | ||||||
|     ); |  | ||||||
|   } |  | ||||||
| } |  | ||||||
|  |  | ||||||
| class Badge extends StatelessWidget { |  | ||||||
|  |  | ||||||
|   @override |  | ||||||
|   Widget build(BuildContext context) { |  | ||||||
|     final entityModel = EntityModel.of(context); |  | ||||||
|     double iconSize = 26.0; |  | ||||||
|     Widget badgeIcon; |  | ||||||
|     String onBadgeTextValue; |  | ||||||
|     Color iconColor = Entity.badgeColors[entityModel.entity.domain] ?? Entity.badgeColors["default"]; |  | ||||||
|     switch (entityModel.entity.domain) { |  | ||||||
|       case "sun": { |  | ||||||
|         badgeIcon = entityModel.entity.state == "below_horizon" ? |  | ||||||
|         Icon( |  | ||||||
|           MaterialDesignIcons.createIconDataFromIconCode(0xf0dc), |  | ||||||
|           size: iconSize, |  | ||||||
|         ) : |  | ||||||
|         Icon( |  | ||||||
|           MaterialDesignIcons.createIconDataFromIconCode(0xf5a8), |  | ||||||
|           size: iconSize, |  | ||||||
|         ); |  | ||||||
|         break; |  | ||||||
|       } |  | ||||||
|       case "sensor": { |  | ||||||
|         onBadgeTextValue = entityModel.entity.unitOfMeasurement; |  | ||||||
|         badgeIcon = Center( |  | ||||||
|           child: Text( |  | ||||||
|             "${entityModel.entity.state}", |  | ||||||
|             overflow: TextOverflow.fade, |  | ||||||
|             softWrap: false, |  | ||||||
|             textAlign: TextAlign.center, |  | ||||||
|             style: TextStyle(fontSize: 17.0), |  | ||||||
|           ), |  | ||||||
|         ); |  | ||||||
|         break; |  | ||||||
|       } |  | ||||||
|       case "device_tracker": { |  | ||||||
|         badgeIcon = MaterialDesignIcons.createIconWidgetFromEntityData(entityModel.entity, iconSize,Colors.black); |  | ||||||
|         onBadgeTextValue = entityModel.entity.state; |  | ||||||
|         break; |  | ||||||
|       } |  | ||||||
|       default: { |  | ||||||
|         badgeIcon = MaterialDesignIcons.createIconWidgetFromEntityData(entityModel.entity, iconSize,Colors.black); |  | ||||||
|       } |  | ||||||
|     } |  | ||||||
|     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.entity.displayName}", |  | ||||||
|               textAlign: TextAlign.center, |  | ||||||
|               style: TextStyle(fontSize: 12.0), |  | ||||||
|               softWrap: true, |  | ||||||
|               maxLines: 3, |  | ||||||
|               overflow: TextOverflow.ellipsis, |  | ||||||
|             ), |  | ||||||
|           ), |  | ||||||
|         ], |  | ||||||
|       ), |  | ||||||
|       onTap: () => eventBus.fire(new ShowEntityPageEvent(entityModel.entity)) |  | ||||||
|     ); |  | ||||||
|   } |  | ||||||
|  |  | ||||||
| } |  | ||||||
|  |  | ||||||
| class ClimateStateWidget extends StatelessWidget { |  | ||||||
|   @override |  | ||||||
|   Widget build(BuildContext context) { |  | ||||||
|     final entityModel = EntityModel.of(context); |  | ||||||
|     final ClimateEntity entity = entityModel.entity; |  | ||||||
|     return Padding( |  | ||||||
|         padding: |  | ||||||
|         EdgeInsets.fromLTRB(0.0, 0.0, entityModel.entity.rightWidgetPadding, 0.0), |  | ||||||
|         child: GestureDetector( |  | ||||||
|           child: Column( |  | ||||||
|             crossAxisAlignment: CrossAxisAlignment.end, |  | ||||||
|             children: <Widget>[ |  | ||||||
|               Row( |  | ||||||
|                 children: <Widget>[ |  | ||||||
|                   Text( |  | ||||||
|                       "${entity.state}", |  | ||||||
|                       textAlign: TextAlign.right, |  | ||||||
|                       style: new TextStyle( |  | ||||||
|                         fontWeight: FontWeight.bold, |  | ||||||
|                         fontSize: entityModel.entity.stateFontSize, |  | ||||||
|                       )), |  | ||||||
|                   Text( |  | ||||||
|                       entity.supportTargetTemperature ? " ${entity.temperature}" : " ${entity.targetLow} - ${entity.targetHigh}", |  | ||||||
|                       textAlign: TextAlign.right, |  | ||||||
|                       style: new TextStyle( |  | ||||||
|                         fontSize: entityModel.entity.stateFontSize, |  | ||||||
|                       )) |  | ||||||
|                 ], |  | ||||||
|               ), |  | ||||||
|               Text( |  | ||||||
|                   "Currently: ${entity.attributes["current_temperature"]}", |  | ||||||
|                   textAlign: TextAlign.right, |  | ||||||
|                   style: new TextStyle( |  | ||||||
|                       fontSize: entityModel.entity.stateFontSize, |  | ||||||
|                       color: Colors.black45 |  | ||||||
|                   )) |  | ||||||
|             ], |  | ||||||
|           ), |  | ||||||
|           onTap: () => entityModel.handleTap ? eventBus.fire(new ShowEntityPageEvent(entity)) : null, |  | ||||||
|         ) |  | ||||||
|     ); |  | ||||||
|   } |  | ||||||
|  |  | ||||||
| } |  | ||||||
|  |  | ||||||
| class DateTimeStateWidget extends StatelessWidget { |  | ||||||
|  |  | ||||||
|   @override |  | ||||||
|   Widget build(BuildContext context) { |  | ||||||
|     final entityModel = EntityModel.of(context); |  | ||||||
|     final DateTimeEntity entity = entityModel.entity; |  | ||||||
|     return Padding( |  | ||||||
|         padding: |  | ||||||
|         EdgeInsets.fromLTRB(0.0, 0.0, entity.rightWidgetPadding, 0.0), |  | ||||||
|         child: GestureDetector( |  | ||||||
|           child: Text( |  | ||||||
|               "${entity.formattedState}", |  | ||||||
|               textAlign: TextAlign.right, |  | ||||||
|               style: new TextStyle( |  | ||||||
|                 fontSize: entity.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 { |  | ||||||
|       TheLogger.log("Warning", "${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) |  | ||||||
|     ); |  | ||||||
|   } |  | ||||||
|  |  | ||||||
| } |  | ||||||
|  |  | ||||||
| class CoverEntityControlState 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.entity; |  | ||||||
|     List<Widget> buttons = []; |  | ||||||
|     if (entity.supportOpen) { |  | ||||||
|       buttons.add( |  | ||||||
|         IconButton( |  | ||||||
|             icon: Icon( |  | ||||||
|                 MaterialDesignIcons.createIconDataFromIconName("mdi:arrow-up"), |  | ||||||
|                 size: entity.iconSize, |  | ||||||
|             ), |  | ||||||
|             onPressed: entity.canBeOpened ? () =>_open(entity) : null |  | ||||||
|         ) |  | ||||||
|       ); |  | ||||||
|     } else { |  | ||||||
|       buttons.add(Container(width: entity.iconSize+20.0,)); |  | ||||||
|     } |  | ||||||
|     if (entity.supportStop) { |  | ||||||
|       buttons.add( |  | ||||||
|           IconButton( |  | ||||||
|               icon: Icon( |  | ||||||
|                   MaterialDesignIcons.createIconDataFromIconName("mdi:stop"), |  | ||||||
|                   size: entity.iconSize, |  | ||||||
|               ), |  | ||||||
|               onPressed: () => _stop(entity) |  | ||||||
|           ) |  | ||||||
|       ); |  | ||||||
|     } else { |  | ||||||
|       buttons.add(Container(width: entity.iconSize+20.0,)); |  | ||||||
|     } |  | ||||||
|     if (entity.supportClose) { |  | ||||||
|       buttons.add( |  | ||||||
|           IconButton( |  | ||||||
|               icon: Icon( |  | ||||||
|                   MaterialDesignIcons.createIconDataFromIconName("mdi:arrow-down"), |  | ||||||
|                   size: entity.iconSize, |  | ||||||
|               ), |  | ||||||
|               onPressed: entity.canBeClosed ? () => _close(entity) : null |  | ||||||
|           ) |  | ||||||
|       ); |  | ||||||
|     } else { |  | ||||||
|       buttons.add(Container(width: entity.iconSize+20.0,)); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     return Row( |  | ||||||
|       children: buttons, |  | ||||||
|     ); |  | ||||||
|   } |  | ||||||
|  |  | ||||||
| } |  | ||||||
|  |  | ||||||
| class CoverEntityTiltControlState 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.entity; |  | ||||||
|     List<Widget> buttons = []; |  | ||||||
|     if (entity.supportOpenTilt) { |  | ||||||
|       buttons.add( |  | ||||||
|           IconButton( |  | ||||||
|               icon: Icon( |  | ||||||
|                 MaterialDesignIcons.createIconDataFromIconName("mdi:arrow-top-right"), |  | ||||||
|                 size: entity.iconSize, |  | ||||||
|               ), |  | ||||||
|               onPressed: entity.canTiltBeOpened ? () =>_open(entity) : null |  | ||||||
|           ) |  | ||||||
|       ); |  | ||||||
|     } else { |  | ||||||
|       buttons.add(Container(width: entity.iconSize+20.0,)); |  | ||||||
|     } |  | ||||||
|     if (entity.supportStopTilt) { |  | ||||||
|       buttons.add( |  | ||||||
|           IconButton( |  | ||||||
|               icon: Icon( |  | ||||||
|                 MaterialDesignIcons.createIconDataFromIconName("mdi:stop"), |  | ||||||
|                 size: entity.iconSize, |  | ||||||
|               ), |  | ||||||
|               onPressed: () => _stop(entity) |  | ||||||
|           ) |  | ||||||
|       ); |  | ||||||
|     } else { |  | ||||||
|       buttons.add(Container(width: entity.iconSize+20.0,)); |  | ||||||
|     } |  | ||||||
|     if (entity.supportCloseTilt) { |  | ||||||
|       buttons.add( |  | ||||||
|           IconButton( |  | ||||||
|               icon: Icon( |  | ||||||
|                 MaterialDesignIcons.createIconDataFromIconName("mdi:arrow-bottom-left"), |  | ||||||
|                 size: entity.iconSize, |  | ||||||
|               ), |  | ||||||
|               onPressed: entity.canTiltBeClosed ? () => _close(entity) : null |  | ||||||
|           ) |  | ||||||
|       ); |  | ||||||
|     } else { |  | ||||||
|       buttons.add(Container(width: entity.iconSize+20.0,)); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     return Row( |  | ||||||
|       children: buttons, |  | ||||||
|     ); |  | ||||||
|   } |  | ||||||
|  |  | ||||||
| } |  | ||||||
							
								
								
									
										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,95 +2,136 @@ part of 'main.dart'; | |||||||
|  |  | ||||||
| class EntityCollection { | class EntityCollection { | ||||||
|  |  | ||||||
|   Map<String, Entity> _entities; |   final homeAssistantWebHost; | ||||||
|   List<String> viewList; |  | ||||||
|  |  | ||||||
|   bool get isEmpty => _entities.isEmpty; |   Map<String, Entity> _allEntities; | ||||||
|  |   //Map<String, Entity> views; | ||||||
|  |  | ||||||
|   EntityCollection() { |   bool get isEmpty => _allEntities.isEmpty; | ||||||
|     _entities = {}; |   List<Entity> get viewEntities => _allEntities.values.where((entity) => entity.isView).toList(); | ||||||
|     viewList = []; |  | ||||||
|  |   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) { |   void parse(List rawData) { | ||||||
|     _entities.clear(); |     _allEntities.clear(); | ||||||
|     viewList.clear(); |     //views.clear(); | ||||||
|  |  | ||||||
|     TheLogger.log("Debug","Parsing ${rawData.length} Home Assistant entities"); |     Logger.d("Parsing ${rawData.length} Home Assistant entities"); | ||||||
|     rawData.forEach((rawEntityData) { |     rawData.forEach((rawEntityData) { | ||||||
|       Entity newEntity = addFromRaw(rawEntityData); |       addFromRaw(rawEntityData); | ||||||
|  |  | ||||||
|       if (newEntity.isView) { |  | ||||||
|         viewList.add(newEntity.entityId); |  | ||||||
|       } |  | ||||||
|     }); |     }); | ||||||
|  |     _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) { |   Entity _createEntityInstance(rawEntityData) { | ||||||
|     switch (rawEntityData["entity_id"].split(".")[0]) { |     switch (rawEntityData["entity_id"].split(".")[0]) { | ||||||
|       case 'sun': { |       case 'sun': { | ||||||
|         return SunEntity(rawEntityData); |         return SunEntity(rawEntityData, homeAssistantWebHost); | ||||||
|       } |       } | ||||||
|       case "automation": |       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 "input_boolean": | ||||||
|       case "switch": |       case "switch": { | ||||||
|  |         return SwitchEntity(rawEntityData, homeAssistantWebHost); | ||||||
|  |       } | ||||||
|       case "light": { |       case "light": { | ||||||
|       return SwitchEntity(rawEntityData); |         return LightEntity(rawEntityData, homeAssistantWebHost); | ||||||
|  |       } | ||||||
|  |       case "group": { | ||||||
|  |         return GroupEntity(rawEntityData, homeAssistantWebHost); | ||||||
|       } |       } | ||||||
|       case "script": |       case "script": | ||||||
|       case "scene": { |       case "scene": { | ||||||
|       return ButtonEntity(rawEntityData); |         return ButtonEntity(rawEntityData, homeAssistantWebHost); | ||||||
|       } |       } | ||||||
|       case "input_datetime": { |       case "input_datetime": { | ||||||
|         return DateTimeEntity(rawEntityData); |         return DateTimeEntity(rawEntityData, homeAssistantWebHost); | ||||||
|       } |       } | ||||||
|       case "input_select": { |       case "input_select": { | ||||||
|         return SelectEntity(rawEntityData); |         return SelectEntity(rawEntityData, homeAssistantWebHost); | ||||||
|       } |       } | ||||||
|       case "input_number": { |       case "input_number": { | ||||||
|         return SliderEntity(rawEntityData); |         return SliderEntity(rawEntityData, homeAssistantWebHost); | ||||||
|       } |       } | ||||||
|       case "input_text": { |       case "input_text": { | ||||||
|         return TextEntity(rawEntityData); |         return TextEntity(rawEntityData, homeAssistantWebHost); | ||||||
|       } |       } | ||||||
|       case "climate": { |       case "climate": { | ||||||
|         return ClimateEntity(rawEntityData); |         return ClimateEntity(rawEntityData, homeAssistantWebHost); | ||||||
|       } |       } | ||||||
|       case "cover": { |       case "cover": { | ||||||
|         return CoverEntity(rawEntityData); |         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: { |       default: { | ||||||
|         return Entity(rawEntityData); |         return Entity(rawEntityData, homeAssistantWebHost); | ||||||
|       } |       } | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   void updateState(Map rawStateData) { |   bool updateState(Map rawStateData) { | ||||||
|     if (isExist(rawStateData["entity_id"])) { |     if (isExist(rawStateData["entity_id"])) { | ||||||
|       updateFromRaw(rawStateData["new_state"] ?? rawStateData["old_state"]); |       updateFromRaw(rawStateData["new_state"] ?? rawStateData["old_state"]); | ||||||
|  |       return false; | ||||||
|     } else { |     } else { | ||||||
|       addFromRaw(rawStateData["new_state"] ?? rawStateData["old_state"]); |       addFromRaw(rawStateData["new_state"] ?? rawStateData["old_state"]); | ||||||
|  |       return true; | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   void add(Entity entity) { |   void add(Entity entity) { | ||||||
|     _entities[entity.entityId] = entity; |     _allEntities[entity.entityId] = entity; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   Entity addFromRaw(Map rawEntityData) { |   void addFromRaw(Map rawEntityData) { | ||||||
|     Entity entity = _createEntityInstance(rawEntityData); |     Entity entity = _createEntityInstance(rawEntityData); | ||||||
|     _entities[entity.entityId] = entity; |     _allEntities[entity.entityId] = entity; | ||||||
|     return entity; |  | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   void updateFromRaw(Map rawEntityData) { |   void updateFromRaw(Map rawEntityData) { | ||||||
|     get("${rawEntityData["entity_id"]}")?.update(rawEntityData); |     get("${rawEntityData["entity_id"]}")?.update(rawEntityData, homeAssistantWebHost); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   Entity get(String entityId) { |   Entity get(String entityId) { | ||||||
|     return _entities[entityId]; |     return _allEntities[entityId]; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   List<Entity> getAll(List ids) { |   List<Entity> getAll(List ids) { | ||||||
| @@ -105,34 +146,35 @@ class EntityCollection { | |||||||
|   } |   } | ||||||
|  |  | ||||||
|   bool isExist(String entityId) { |   bool isExist(String entityId) { | ||||||
|     return _entities[entityId] != null; |     return _allEntities[entityId] != null; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   Map<String,List<String>> getDefaultViewTopLevelEntities() { |   List<Entity> filterEntitiesForDefaultView() { | ||||||
|     Map<String,List<String>> result = {"userGroups": [], "notGroupedEntities": []}; |     List<Entity> result = []; | ||||||
|     List<String> entities = []; |     List<Entity> groups = []; | ||||||
|     _entities.forEach((id, entity){ |     List<Entity> nonGroupEntities = []; | ||||||
|       if ((id.indexOf("group.") == 0) && (id.indexOf(".all_") == -1) && (!entity.isView)) { |     _allEntities.forEach((id, entity){ | ||||||
|         result["userGroups"].add(id); |       if (entity.isGroup && (entity.attributes['auto'] == null || (entity.attributes['auto'] && !entity.isHidden)) && (!entity.isView)) { | ||||||
|  |         groups.add(entity); | ||||||
|       } |       } | ||||||
|       if (!entity.isGroup) { |       if (!entity.isGroup) { | ||||||
|         entities.add(id); |         nonGroupEntities.add(entity); | ||||||
|       } |       } | ||||||
|     }); |     }); | ||||||
|  |  | ||||||
|     entities.forEach((entiyId) { |     nonGroupEntities.forEach((entity) { | ||||||
|       bool foundInGroup = false; |       bool foundInGroup = false; | ||||||
|       result["userGroups"].forEach((userGroupId) { |       groups.forEach((groupEntity) { | ||||||
|         if (_entities[userGroupId].childEntityIds.contains(entiyId)) { |         if (groupEntity.childEntityIds.contains(entity.entityId)) { | ||||||
|           foundInGroup = true; |           foundInGroup = true; | ||||||
|         } |         } | ||||||
|       }); |       }); | ||||||
|       if (!foundInGroup) { |       if (!foundInGroup) { | ||||||
|         result["notGroupedEntities"].add(entiyId); |         result.add(entity); | ||||||
|       } |       } | ||||||
|     }); |     }); | ||||||
|  |     result.insertAll(0, groups); | ||||||
|  |  | ||||||
|     return result; |     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, | ||||||
|  |     ); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |  | ||||||
|  | } | ||||||
							
								
								
									
										53
									
								
								lib/entity_widgets/state/simple_state.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,53 @@ | |||||||
|  | part of '../../main.dart'; | ||||||
|  |  | ||||||
|  | class SimpleEntityState extends StatelessWidget { | ||||||
|  |  | ||||||
|  |   final bool expanded; | ||||||
|  |   final TextAlign textAlign; | ||||||
|  |   final EdgeInsetsGeometry padding; | ||||||
|  |   final int maxLines; | ||||||
|  |   final String customValue; | ||||||
|  |  | ||||||
|  |   const SimpleEntityState({Key key, this.maxLines: 10, this.expanded: true, this.textAlign: TextAlign.right, this.padding: const EdgeInsets.fromLTRB(0.0, 0.0, Sizes.rightWidgetPadding, 0.0), this.customValue}) : super(key: key); | ||||||
|  |  | ||||||
|  |   @override | ||||||
|  |   Widget build(BuildContext context) { | ||||||
|  |     final entityModel = EntityModel.of(context); | ||||||
|  |     String state; | ||||||
|  |     if (customValue == null) { | ||||||
|  |       state = entityModel.entityWrapper.entity.displayState ?? ""; | ||||||
|  |       state = state.replaceAll("\n", "").replaceAll("\t", " ").trim(); | ||||||
|  |     } else { | ||||||
|  |       state = customValue; | ||||||
|  |     } | ||||||
|  |     TextStyle textStyle =  TextStyle( | ||||||
|  |       fontSize: Sizes.stateFontSize, | ||||||
|  |     ); | ||||||
|  |     if (entityModel.entityWrapper.entity.statelessType == StatelessEntityType.CALL_SERVICE) { | ||||||
|  |       textStyle = textStyle.apply(color: Colors.blue); | ||||||
|  |     } | ||||||
|  |     while (state.contains("  ")){ | ||||||
|  |       state = state.replaceAll("  ", " "); | ||||||
|  |     } | ||||||
|  |     Widget result = Padding( | ||||||
|  |       padding: padding, | ||||||
|  |       child: Text( | ||||||
|  |         "$state ${entityModel.entityWrapper.entity.unitOfMeasurement}", | ||||||
|  |         textAlign: textAlign, | ||||||
|  |         maxLines: maxLines, | ||||||
|  |         overflow: TextOverflow.ellipsis, | ||||||
|  |         softWrap: true, | ||||||
|  |         style: textStyle | ||||||
|  |       ) | ||||||
|  |     ); | ||||||
|  |     if (expanded) { | ||||||
|  |       return Flexible( | ||||||
|  |         fit: FlexFit.tight, | ||||||
|  |         flex: 2, | ||||||
|  |         child: result, | ||||||
|  |       ); | ||||||
|  |     } else { | ||||||
|  |       return result; | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | } | ||||||
							
								
								
									
										89
									
								
								lib/entity_widgets/state/switch_state.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,89 @@ | |||||||
|  | part of '../../main.dart'; | ||||||
|  |  | ||||||
|  | class SwitchStateWidget extends StatefulWidget { | ||||||
|  |  | ||||||
|  |   final String domainForService; | ||||||
|  |  | ||||||
|  |   const SwitchStateWidget({Key key, this.domainForService}) : super(key: key); | ||||||
|  |  | ||||||
|  |   @override | ||||||
|  |   _SwitchStateWidgetState createState() => _SwitchStateWidgetState(); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | class _SwitchStateWidgetState extends State<SwitchStateWidget> { | ||||||
|  |  | ||||||
|  |   String newState; | ||||||
|  |   bool updatedHere = false; | ||||||
|  |  | ||||||
|  |   @override | ||||||
|  |   void initState() { | ||||||
|  |     super.initState(); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   void _setNewState(newValue, Entity entity) { | ||||||
|  |     setState(() { | ||||||
|  |       newState = newValue ? EntityState.on : EntityState.off; | ||||||
|  |       updatedHere = true; | ||||||
|  |     }); | ||||||
|  |     Timer(Duration(seconds: 2), (){ | ||||||
|  |       setState(() { | ||||||
|  |         newState = entity.state; | ||||||
|  |         updatedHere = true; | ||||||
|  |         //TheLogger.debug("Timer@!!"); | ||||||
|  |       }); | ||||||
|  |     }); | ||||||
|  |     String domain; | ||||||
|  |     if (widget.domainForService != null) { | ||||||
|  |       domain = widget.domainForService; | ||||||
|  |     } else { | ||||||
|  |       domain = entity.domain; | ||||||
|  |     } | ||||||
|  |     eventBus.fire(new ServiceCallEvent( | ||||||
|  |         domain, (newValue as bool) ? "turn_on" : "turn_off", entity.entityId, null)); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   @override | ||||||
|  |   Widget build(BuildContext context) { | ||||||
|  |     final entityModel = EntityModel.of(context); | ||||||
|  |     final entity = entityModel.entityWrapper.entity; | ||||||
|  |     if (!updatedHere) { | ||||||
|  |       newState = entity.state; | ||||||
|  |     } else { | ||||||
|  |       updatedHere = false; | ||||||
|  |     } | ||||||
|  |     if (entity.state == EntityState.unavailable || entity.state == EntityState.unknown) { | ||||||
|  |       return SimpleEntityState(); | ||||||
|  |     } else if ((entity.attributes["assumed_state"] == null) || (entity.attributes["assumed_state"] == false)) { | ||||||
|  |       return SizedBox( | ||||||
|  |         height: 32.0, | ||||||
|  |         child: Switch( | ||||||
|  |           value: newState == EntityState.on, | ||||||
|  |           onChanged: ((switchState) { | ||||||
|  |             _setNewState(switchState, entity); | ||||||
|  |           }), | ||||||
|  |         ) | ||||||
|  |       ); | ||||||
|  |     } else { | ||||||
|  |       return SizedBox( | ||||||
|  |         height: 32.0, | ||||||
|  |         child: Row( | ||||||
|  |           crossAxisAlignment: CrossAxisAlignment.start, | ||||||
|  |           children: <Widget>[ | ||||||
|  |             IconButton( | ||||||
|  |               onPressed: () => _setNewState(false, entity), | ||||||
|  |               icon: Icon(MaterialDesignIcons.getIconDataFromIconName("mdi:flash-off")), | ||||||
|  |               color: newState == EntityState.on ? Colors.black : Colors.blue, | ||||||
|  |               iconSize: Sizes.iconSize, | ||||||
|  |             ), | ||||||
|  |             IconButton( | ||||||
|  |                 onPressed: () => _setNewState(true, entity), | ||||||
|  |                 icon: Icon(MaterialDesignIcons.getIconDataFromIconName("mdi:flash")), | ||||||
|  |                 color: newState == EntityState.on ? Colors.blue : Colors.black, | ||||||
|  |                 iconSize: Sizes.iconSize | ||||||
|  |             ) | ||||||
|  |           ], | ||||||
|  |         ), | ||||||
|  |       ); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | } | ||||||
							
								
								
									
										100
									
								
								lib/entity_widgets/state/text_input_state.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,100 @@ | |||||||
|  | part of '../../main.dart'; | ||||||
|  |  | ||||||
|  | class TextInputStateWidget extends StatefulWidget { | ||||||
|  |  | ||||||
|  |   TextInputStateWidget({Key key}) : super(key: key); | ||||||
|  |  | ||||||
|  |   @override | ||||||
|  |   _TextInputStateWidgetState createState() => _TextInputStateWidgetState(); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | class _TextInputStateWidgetState extends State<TextInputStateWidget> { | ||||||
|  |   String _tmpValue; | ||||||
|  |   String _entityState; | ||||||
|  |   String _entityDomain; | ||||||
|  |   String _entityId; | ||||||
|  |   int _minLength; | ||||||
|  |   int _maxLength; | ||||||
|  |   FocusNode _focusNode = FocusNode(); | ||||||
|  |   bool validValue = false; | ||||||
|  |  | ||||||
|  |   @override | ||||||
|  |   void initState() { | ||||||
|  |     super.initState(); | ||||||
|  |     _focusNode.addListener(_focusListener); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   void setNewState(newValue, domain, entityId) { | ||||||
|  |     if (validate(newValue, _minLength, _maxLength)) { | ||||||
|  |       eventBus.fire(new ServiceCallEvent(domain, "set_value", entityId, | ||||||
|  |           {"value": "$newValue"})); | ||||||
|  |     } else { | ||||||
|  |       setState(() { | ||||||
|  |         _tmpValue = _entityState; | ||||||
|  |       }); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   bool validate(newValue, minLength, maxLength) { | ||||||
|  |     if (newValue is String) { | ||||||
|  |       validValue = (newValue.length >= minLength) && | ||||||
|  |           (maxLength == -1 || | ||||||
|  |               (newValue.length <= maxLength)); | ||||||
|  |     } else { | ||||||
|  |       validValue = true; | ||||||
|  |     } | ||||||
|  |     return validValue; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   void _focusListener() { | ||||||
|  |     if (!_focusNode.hasFocus && (_tmpValue != _entityState)) { | ||||||
|  |       setNewState(_tmpValue, _entityDomain, _entityId); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   @override | ||||||
|  |   Widget build(BuildContext context) { | ||||||
|  |     final entityModel = EntityModel.of(context); | ||||||
|  |     final TextEntity entity = entityModel.entityWrapper.entity; | ||||||
|  |     _entityState = entity.state; | ||||||
|  |     _entityDomain = entity.domain; | ||||||
|  |     _entityId = entity.entityId; | ||||||
|  |     _minLength = entity.valueMinLength; | ||||||
|  |     _maxLength = entity.valueMaxLength; | ||||||
|  |  | ||||||
|  |     if (!_focusNode.hasFocus && (_tmpValue != entity.state)) { | ||||||
|  |       _tmpValue = entity.state; | ||||||
|  |     } | ||||||
|  |     if (entity.isTextField || entity.isPasswordField) { | ||||||
|  |       return Flexible( | ||||||
|  |         fit: FlexFit.tight, | ||||||
|  |         flex: 2, | ||||||
|  |         //width: Entity.INPUT_WIDTH, | ||||||
|  |         child: TextField( | ||||||
|  |             focusNode: _focusNode, | ||||||
|  |             obscureText: entity.isPasswordField, | ||||||
|  |             controller: new TextEditingController.fromValue( | ||||||
|  |                 new TextEditingValue( | ||||||
|  |                     text: _tmpValue, | ||||||
|  |                     selection: | ||||||
|  |                     new TextSelection.collapsed(offset: _tmpValue.length) | ||||||
|  |                 ) | ||||||
|  |             ), | ||||||
|  |             onChanged: (value) { | ||||||
|  |               _tmpValue = value; | ||||||
|  |             }), | ||||||
|  |       ); | ||||||
|  |     } else { | ||||||
|  |       Logger.w( "Unsupported input mode for ${entity.entityId}"); | ||||||
|  |       return SimpleEntityState(); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   @override | ||||||
|  |   void dispose() { | ||||||
|  |     _focusNode.removeListener(_focusListener); | ||||||
|  |     _focusNode.dispose(); | ||||||
|  |     super.dispose(); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  | } | ||||||
							
								
								
									
										65
									
								
								lib/entity_widgets/state/timer_state.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,65 @@ | |||||||
|  | part of '../../main.dart'; | ||||||
|  |  | ||||||
|  | class TimerState extends StatefulWidget  { | ||||||
|  |   //final bool expanded; | ||||||
|  |   //final TextAlign textAlign; | ||||||
|  |   //final EdgeInsetsGeometry padding; | ||||||
|  |   //final int maxLines; | ||||||
|  |  | ||||||
|  |   const TimerState({Key key}) : super(key: key); | ||||||
|  |  | ||||||
|  |   @override | ||||||
|  |   _TimerStateState createState() => _TimerStateState(); | ||||||
|  |  | ||||||
|  | } | ||||||
|  |  | ||||||
|  | class _TimerStateState extends State<TimerState> { | ||||||
|  |  | ||||||
|  |   Timer timer; | ||||||
|  |   Duration remaining = Duration(seconds: 0); | ||||||
|  |  | ||||||
|  |   void checkState(TimerEntity entity) { | ||||||
|  |     if (entity.state == EntityState.active) { | ||||||
|  |       //Logger.d("Timer is active"); | ||||||
|  |       if (timer == null || !timer.isActive) { | ||||||
|  |         timer = Timer.periodic(Duration(seconds: 1), (timer) { | ||||||
|  |           setState(() { | ||||||
|  |             try { | ||||||
|  |               int passed = DateTime | ||||||
|  |                   .now() | ||||||
|  |                   .difference(entity._lastUpdated) | ||||||
|  |                   .inSeconds; | ||||||
|  |               remaining = Duration(seconds: entity.duration.inSeconds - passed); | ||||||
|  |             } catch (e) { | ||||||
|  |               Logger.e("Error calculating ${entity.entityId} remaining time: ${e.toString()}"); | ||||||
|  |               remaining = Duration(seconds: 0); | ||||||
|  |             } | ||||||
|  |           }); | ||||||
|  |         }); | ||||||
|  |       } | ||||||
|  |     } else { | ||||||
|  |       timer?.cancel(); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   @override | ||||||
|  |   Widget build(BuildContext context) { | ||||||
|  |     EntityModel model = EntityModel.of(context); | ||||||
|  |     TimerEntity entity = model.entityWrapper.entity; | ||||||
|  |     checkState(entity); | ||||||
|  |     if (entity.state != EntityState.active) { | ||||||
|  |       return SimpleEntityState(); | ||||||
|  |     } else { | ||||||
|  |       return SimpleEntityState( | ||||||
|  |         customValue: "${remaining.toString().split('.')[0]}", | ||||||
|  |       ); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   @override | ||||||
|  |   void dispose() { | ||||||
|  |     timer?.cancel(); | ||||||
|  |     super.dispose(); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  | } | ||||||
| @@ -1,410 +1,360 @@ | |||||||
| part of 'main.dart'; | part of 'main.dart'; | ||||||
|  |  | ||||||
| class HomeAssistant { | class HomeAssistant { | ||||||
|   String _webSocketAPIEndpoint; |  | ||||||
|   String _password; |  | ||||||
|   String _authType; |  | ||||||
|  |  | ||||||
|   IOWebSocketChannel _hassioChannel; |   final Connection connection = Connection(); | ||||||
|   SendMessageQueue _messageQueue; |  | ||||||
|  |  | ||||||
|   int _currentMessageId = 0; |   bool _useLovelace = false; | ||||||
|   int _statesMessageId = 0; |   //bool isSettingsLoaded = false; | ||||||
|   int _servicesMessageId = 0; |  | ||||||
|   int _subscriptionMessageId = 0; |  | ||||||
|   int _configMessageId = 0; |  | ||||||
|   int _userInfoMessageId = 0; |  | ||||||
|   EntityCollection _entities; |   EntityCollection entities; | ||||||
|   ViewBuilder _viewBuilder; |   HomeAssistantUI ui; | ||||||
|   Map _instanceConfig = {}; |   Map _instanceConfig = {}; | ||||||
|   String _userName; |   String _userName; | ||||||
|  |   String hostname; | ||||||
|  |   HSVColor savedColor; | ||||||
|  |  | ||||||
|   Completer _fetchCompleter; |   Map _rawLovelaceData; | ||||||
|   Completer _statesCompleter; |  | ||||||
|   Completer _servicesCompleter; |  | ||||||
|   Completer _configCompleter; |  | ||||||
|   Completer _connectionCompleter; |  | ||||||
|   Completer _userInfoCompleter; |  | ||||||
|   Timer _connectionTimer; |  | ||||||
|   Timer _fetchTimer; |  | ||||||
|   bool autoReconnect = false; |  | ||||||
|  |  | ||||||
|   StreamSubscription _socketSubscription; |   List<Panel> panels = []; | ||||||
|  |  | ||||||
|   int messageExpirationTime = 30; //seconds |  | ||||||
|   Duration fetchTimeout = Duration(seconds: 30); |   Duration fetchTimeout = Duration(seconds: 30); | ||||||
|   Duration connectTimeout = Duration(seconds: 15); |  | ||||||
|  |  | ||||||
|   String get locationName => _instanceConfig["location_name"] ?? ""; |   String get locationName { | ||||||
|  |     if (_useLovelace) { | ||||||
|  |       return ui?.title ?? ""; | ||||||
|  |     } else { | ||||||
|  |       return _instanceConfig["location_name"] ?? ""; | ||||||
|  |     } | ||||||
|  |   } | ||||||
|   String get userName => _userName ?? locationName; |   String get userName => _userName ?? locationName; | ||||||
|   String get userAvatarText => userName.length > 0 ? userName[0] : ""; |   String get userAvatarText => userName.length > 0 ? userName[0] : ""; | ||||||
|   int get viewsCount => _entities.viewList.length ?? 0; |   bool get isNoEntities => entities == null || entities.isEmpty; | ||||||
|  |   bool get isNoViews => ui == null || ui.isEmpty; | ||||||
|  |   //int get viewsCount => entities.views.length ?? 0; | ||||||
|  |  | ||||||
|   EntityCollection get entities => _entities; |   HomeAssistant(); | ||||||
|  |  | ||||||
|   HomeAssistant() { |   Completer _connectCompleter; | ||||||
|     _entities = EntityCollection(); |  | ||||||
|     _messageQueue = SendMessageQueue(messageExpirationTime); |   Future init() { | ||||||
|  |     if (_connectCompleter != null && !_connectCompleter.isCompleted) { | ||||||
|  |       Logger.w("Previous connection pending..."); | ||||||
|  |       return _connectCompleter.future; | ||||||
|  |     } | ||||||
|  |     Logger.d("init..."); | ||||||
|  |     _connectCompleter = Completer(); | ||||||
|  |     connection.init(_handleEntityStateChange).then((_) { | ||||||
|  |       SharedPreferences.getInstance().then((prefs) { | ||||||
|  |         if (entities == null) entities = EntityCollection(connection.httpWebHost); | ||||||
|  |         _useLovelace = prefs.getBool('use-lovelace') ?? true; | ||||||
|  |         _connectCompleter.complete(); | ||||||
|  |       }).catchError((e) => _connectCompleter.completeError(e)); | ||||||
|  |     }).catchError((e) => _connectCompleter.completeError(e)); | ||||||
|  |     return _connectCompleter.future; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   void updateConnectionSettings(String url, String password, String authType) { |   Completer _fetchCompleter; | ||||||
|     _webSocketAPIEndpoint = url; |  | ||||||
|     _password = password; |  | ||||||
|     _authType = authType; |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   Future fetch() { |   Future fetch() { | ||||||
|     if ((_fetchCompleter != null) && (!_fetchCompleter.isCompleted)) { |     if (_fetchCompleter != null && !_fetchCompleter.isCompleted) { | ||||||
|       TheLogger.log("Warning","Previous fetch is not complited"); |       Logger.w("Previous data fetch is not completed yet"); | ||||||
|     } else { |  | ||||||
|       _fetchCompleter = new Completer(); |  | ||||||
|       _fetchTimer = Timer(fetchTimeout, () { |  | ||||||
|         TheLogger.log("Error", "Data fetching timeout"); |  | ||||||
|         disconnect().then((_) { |  | ||||||
|           _completeFetching({ |  | ||||||
|             "errorCode": 9, |  | ||||||
|             "errorMessage": "Couldn't get data from server" |  | ||||||
|           }); |  | ||||||
|         }); |  | ||||||
|       }); |  | ||||||
|       _connection().then((r) { |  | ||||||
|         _getData(); |  | ||||||
|       }).catchError((e) { |  | ||||||
|         _completeFetching(e); |  | ||||||
|       }); |  | ||||||
|     } |  | ||||||
|       return _fetchCompleter.future; |       return _fetchCompleter.future; | ||||||
|     } |     } | ||||||
|  |     _fetchCompleter = Completer(); | ||||||
|   disconnect() async { |  | ||||||
|     if ((_hassioChannel != null) && (_hassioChannel.closeCode == null) && (_hassioChannel.sink != null)) { |  | ||||||
|       await _hassioChannel.sink.close().timeout(Duration(seconds: 3), |  | ||||||
|         onTimeout: () => TheLogger.log("Debug", "Socket sink closed") |  | ||||||
|       ); |  | ||||||
|       await _socketSubscription.cancel(); |  | ||||||
|       _hassioChannel = null; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   Future _connection() { |  | ||||||
|     if ((_connectionCompleter != null) && (!_connectionCompleter.isCompleted)) { |  | ||||||
|       TheLogger.log("Debug","Previous connection is not complited"); |  | ||||||
|     } else { |  | ||||||
|       if ((_hassioChannel == null) || (_hassioChannel.closeCode != null)) { |  | ||||||
|         _connectionCompleter = new Completer(); |  | ||||||
|         autoReconnect = false; |  | ||||||
|         disconnect().then((_){ |  | ||||||
|           TheLogger.log("Debug", "Socket connecting..."); |  | ||||||
|           _connectionTimer = Timer(connectTimeout, () { |  | ||||||
|             TheLogger.log("Error", "Socket connection timeout"); |  | ||||||
|             _handleSocketError(null); |  | ||||||
|           }); |  | ||||||
|           if (_socketSubscription != null) { |  | ||||||
|             _socketSubscription.cancel(); |  | ||||||
|           } |  | ||||||
|           _hassioChannel = IOWebSocketChannel.connect( |  | ||||||
|               _webSocketAPIEndpoint, pingInterval: Duration(seconds: 30)); |  | ||||||
|           _socketSubscription = _hassioChannel.stream.listen( |  | ||||||
|                   (message) => _handleMessage(message), |  | ||||||
|               cancelOnError: true, |  | ||||||
|               onDone: () => _handleSocketClose(), |  | ||||||
|               onError: (e) => _handleSocketError(e) |  | ||||||
|           ); |  | ||||||
|         }); |  | ||||||
|       } else { |  | ||||||
|         _completeConnecting(null); |  | ||||||
|       } |  | ||||||
|     } |  | ||||||
|     return _connectionCompleter.future; |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   void _handleSocketClose() { |  | ||||||
|     TheLogger.log("Debug","Socket disconnected. Automatic reconnect is $autoReconnect"); |  | ||||||
|     if (autoReconnect) { |  | ||||||
|       _reconnect(); |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   void _handleSocketError(e) { |  | ||||||
|     TheLogger.log("Error","Socket stream Error: $e"); |  | ||||||
|     TheLogger.log("Debug","Automatic reconnect is $autoReconnect"); |  | ||||||
|     if (autoReconnect) { |  | ||||||
|       _reconnect(); |  | ||||||
|     } else { |  | ||||||
|       disconnect().then((_) { |  | ||||||
|         _completeConnecting({ |  | ||||||
|           "errorCode": 1, |  | ||||||
|           "errorMessage": "Couldn't connect to Home Assistant. Check network connection or connection settings." |  | ||||||
|         }); |  | ||||||
|       }); |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   void _reconnect() { |  | ||||||
|     disconnect().then((_) { |  | ||||||
|       _connection().catchError((e){ |  | ||||||
|         _completeConnecting(e); |  | ||||||
|       }); |  | ||||||
|     }); |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   _getData() async { |  | ||||||
|     List<Future> futures = []; |     List<Future> futures = []; | ||||||
|     futures.add(_getStates()); |     futures.add(_getStates()); | ||||||
|  |     if (_useLovelace) { | ||||||
|  |       futures.add(_getLovelace()); | ||||||
|  |     } | ||||||
|     futures.add(_getConfig()); |     futures.add(_getConfig()); | ||||||
|     futures.add(_getServices()); |     futures.add(_getServices()); | ||||||
|     futures.add(_getUserInfo()); |     futures.add(_getUserInfo()); | ||||||
|     try { |     futures.add(_getPanels()); | ||||||
|       await Future.wait(futures); |     Future.wait(futures).then((_) { | ||||||
|       _completeFetching(null); |       _createUI(); | ||||||
|     } catch (error) { |  | ||||||
|       _completeFetching(error); |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   void _completeFetching(error) { |  | ||||||
|     _fetchTimer.cancel(); |  | ||||||
|     _completeConnecting(error); |  | ||||||
|     if (!_fetchCompleter.isCompleted) { |  | ||||||
|       if (error != null) { |  | ||||||
|         _fetchCompleter.completeError(error); |  | ||||||
|       } else { |  | ||||||
|         autoReconnect = true; |  | ||||||
|         TheLogger.log("Debug", "Fetch complete successful"); |  | ||||||
|       _fetchCompleter.complete(); |       _fetchCompleter.complete(); | ||||||
|       } |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   void _completeConnecting(error) { |  | ||||||
|     _connectionTimer.cancel(); |  | ||||||
|     if (!_connectionCompleter.isCompleted) { |  | ||||||
|       if (error != null) { |  | ||||||
|         _connectionCompleter.completeError(error); |  | ||||||
|       } else { |  | ||||||
|         _connectionCompleter.complete(); |  | ||||||
|       } |  | ||||||
|     } else if (error != null) { |  | ||||||
|       eventBus.fire(ShowErrorEvent(error["errorMessage"], error["errorCode"])); |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   _handleMessage(String message) { |  | ||||||
|     var data = json.decode(message); |  | ||||||
|     TheLogger.log("Debug","[Received] => ${data['type']}"); |  | ||||||
|     if (data["type"] == "auth_required") { |  | ||||||
|       _sendAuthMessageRaw('{"type": "auth","$_authType": "$_password"}'); |  | ||||||
|     } else if (data["type"] == "auth_ok") { |  | ||||||
|       _completeConnecting(null); |  | ||||||
|       _sendSubscribe(); |  | ||||||
|     } else if (data["type"] == "auth_invalid") { |  | ||||||
|       _completeConnecting({"errorCode": 6, "errorMessage": "${data["message"]}"}); |  | ||||||
|     } else if (data["type"] == "result") { |  | ||||||
|       if (data["id"] == _configMessageId) { |  | ||||||
|         _parseConfig(data); |  | ||||||
|       } else if (data["id"] == _statesMessageId) { |  | ||||||
|         _parseEntities(data); |  | ||||||
|       } else if (data["id"] == _servicesMessageId) { |  | ||||||
|         _parseServices(data); |  | ||||||
|       } else if (data["id"] == _userInfoMessageId) { |  | ||||||
|         _parseUserInfo(data); |  | ||||||
|       } else if (data["id"] == _currentMessageId) { |  | ||||||
|         TheLogger.log("Debug","Request id:$_currentMessageId was successful"); |  | ||||||
|       } |  | ||||||
|     } else if (data["type"] == "event") { |  | ||||||
|       if ((data["event"] != null) && (data["event"]["event_type"] == "state_changed")) { |  | ||||||
|         _handleEntityStateChange(data["event"]["data"]); |  | ||||||
|       } else if (data["event"] != null) { |  | ||||||
|         TheLogger.log("Warning","Unhandled event type: ${data["event"]["event_type"]}"); |  | ||||||
|       } else { |  | ||||||
|         TheLogger.log("Error","Event is null: $message"); |  | ||||||
|       } |  | ||||||
|     } else { |  | ||||||
|       TheLogger.log("Warning","Unknown message type: $message"); |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   void _sendSubscribe() { |  | ||||||
|     _incrementMessageId(); |  | ||||||
|     _subscriptionMessageId = _currentMessageId; |  | ||||||
|     _sendMessageRaw('{"id": $_subscriptionMessageId, "type": "subscribe_events", "event_type": "state_changed"}', false); |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   Future _getConfig() { |  | ||||||
|     _configCompleter = new Completer(); |  | ||||||
|     _incrementMessageId(); |  | ||||||
|     _configMessageId = _currentMessageId; |  | ||||||
|     _sendMessageRaw('{"id": $_configMessageId, "type": "get_config"}', false); |  | ||||||
|  |  | ||||||
|     return _configCompleter.future; |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   Future _getStates() { |  | ||||||
|     _statesCompleter = new Completer(); |  | ||||||
|     _incrementMessageId(); |  | ||||||
|     _statesMessageId = _currentMessageId; |  | ||||||
|     _sendMessageRaw('{"id": $_statesMessageId, "type": "get_states"}', false); |  | ||||||
|  |  | ||||||
|     return _statesCompleter.future; |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   Future _getUserInfo() { |  | ||||||
|     _userInfoCompleter = new Completer(); |  | ||||||
|     _incrementMessageId(); |  | ||||||
|     _userInfoMessageId = _currentMessageId; |  | ||||||
|     _sendMessageRaw('{"id": $_userInfoMessageId, "type": "auth/current_user"}', false); |  | ||||||
|  |  | ||||||
|     return _userInfoCompleter.future; |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   Future _getServices() { |  | ||||||
|     _servicesCompleter = new Completer(); |  | ||||||
|     _incrementMessageId(); |  | ||||||
|     _servicesMessageId = _currentMessageId; |  | ||||||
|     _sendMessageRaw('{"id": $_servicesMessageId, "type": "get_services"}', false); |  | ||||||
|  |  | ||||||
|     return _servicesCompleter.future; |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   _incrementMessageId() { |  | ||||||
|     _currentMessageId += 1; |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   void _sendAuthMessageRaw(String message) { |  | ||||||
|     TheLogger.log("Debug", "[Sending] ==> auth request"); |  | ||||||
|     _hassioChannel.sink.add(message); |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   _sendMessageRaw(String message, bool queued) { |  | ||||||
|     var sendCompleter = Completer(); |  | ||||||
|     if (queued) _messageQueue.add(message); |  | ||||||
|     _connection().then((r) { |  | ||||||
|       _messageQueue.getActualMessages().forEach((message){ |  | ||||||
|         TheLogger.log("Debug", "[Sending queued] ==> $message"); |  | ||||||
|         _hassioChannel.sink.add(message); |  | ||||||
|       }); |  | ||||||
|       if (!queued) { |  | ||||||
|         TheLogger.log("Debug", "[Sending] ==> $message"); |  | ||||||
|         _hassioChannel.sink.add(message); |  | ||||||
|       } |  | ||||||
|       sendCompleter.complete(); |  | ||||||
|     }).catchError((e) { |     }).catchError((e) { | ||||||
|       sendCompleter.completeError(e); |       _fetchCompleter.completeError(e); | ||||||
|     }); |     }); | ||||||
|     return sendCompleter.future; |     return _fetchCompleter.future; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   Future callService(String domain, String service, String entityId, Map<String, dynamic> additionalParams) { |   Future logout() async { | ||||||
|     _incrementMessageId(); |     Logger.d("Logging out..."); | ||||||
|     String message = '{"id": $_currentMessageId, "type": "call_service", "domain": "$domain", "service": "$service", "service_data": {"entity_id": "$entityId"'; |     await connection.logout().then((_) { | ||||||
|     if (additionalParams != null) { |       ui?.clear(); | ||||||
|       additionalParams.forEach((name, value){ |       entities?.clear(); | ||||||
|         if ((value is double) || (value is int)) { |  | ||||||
|           message += ', "$name" : $value'; |  | ||||||
|         } else { |  | ||||||
|           message += ', "$name" : "$value"'; |  | ||||||
|         } |  | ||||||
|     }); |     }); | ||||||
|   } |   } | ||||||
|     message += '}}'; |  | ||||||
|     return _sendMessageRaw(message, true); |   Future _getConfig() async { | ||||||
|  |     await connection.sendSocketMessage(type: "get_config").then((data) { | ||||||
|  |       _instanceConfig = Map.from(data); | ||||||
|  |     }).catchError((e) { | ||||||
|  |       throw {"errorCode": 1, "errorMessage": "Error getting config: $e"}; | ||||||
|  |     }); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   Future _getStates() async { | ||||||
|  |     await connection.sendSocketMessage(type: "get_states").then( | ||||||
|  |             (data) => entities.parse(data) | ||||||
|  |     ).catchError((e) { | ||||||
|  |       throw {"errorCode": 1, "errorMessage": "Error getting states: $e"}; | ||||||
|  |     }); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   Future _getLovelace() async { | ||||||
|  |     await connection.sendSocketMessage(type: "lovelace/config").then((data) => _rawLovelaceData = data).catchError((e) { | ||||||
|  |       throw {"errorCode": 1, "errorMessage": "Error getting lovelace config: $e"}; | ||||||
|  |     }); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   Future _getUserInfo() async { | ||||||
|  |     _userName = null; | ||||||
|  |     await connection.sendSocketMessage(type: "auth/current_user").then((data) => _userName = data["name"]).catchError((e) { | ||||||
|  |       Logger.w("Can't get user info: ${e}"); | ||||||
|  |     }); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   Future _getServices() async { | ||||||
|  |     await connection.sendSocketMessage(type: "get_services").then((data) => Logger.d("Services received")).catchError((e) { | ||||||
|  |       Logger.w("Can't get services: ${e}"); | ||||||
|  |     }); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   Future _getPanels() async { | ||||||
|  |     panels.clear(); | ||||||
|  |     await connection.sendSocketMessage(type: "get_panels").then((data) { | ||||||
|  |       data.forEach((k,v) { | ||||||
|  |         String title = v['title'] == null ? "${k[0].toUpperCase()}${k.substring(1)}" : "${v['title'][0].toUpperCase()}${v['title'].substring(1)}"; | ||||||
|  |         panels.add(Panel( | ||||||
|  |             id: k, | ||||||
|  |             type: v["component_name"], | ||||||
|  |             title: title, | ||||||
|  |             urlPath: v["url_path"], | ||||||
|  |             config: v["config"], | ||||||
|  |             icon: v["icon"] | ||||||
|  |         ) | ||||||
|  |         ); | ||||||
|  |       }); | ||||||
|  |     }).catchError((e) { | ||||||
|  |       throw {"errorCode": 1, "errorMessage": "Error getting panels list: $e"}; | ||||||
|  |     }); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   void _handleEntityStateChange(Map eventData) { |   void _handleEntityStateChange(Map eventData) { | ||||||
|     //TheLogger.log("Debug", "New state for ${eventData['entity_id']}"); |     //TheLogger.debug( "New state for ${eventData['entity_id']}"); | ||||||
|     Map data = Map.from(eventData); |     Map data = Map.from(eventData); | ||||||
|     _entities.updateState(data); |     eventBus.fire(new StateChangedEvent( | ||||||
|     eventBus.fire(new StateChangedEvent(data["entity_id"], null, false)); |       entityId: data["entity_id"], | ||||||
|  |       needToRebuildUI: entities.updateState(data) | ||||||
|  |     )); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   void _parseConfig(Map data) { |   void _parseLovelace() { | ||||||
|     if (data["success"] == true) { |       Logger.d("--Title: ${_rawLovelaceData["title"]}"); | ||||||
|       _instanceConfig = Map.from(data["result"]); |       ui.title = _rawLovelaceData["title"]; | ||||||
|       _configCompleter.complete(); |       int viewCounter = 0; | ||||||
|     } else { |       Logger.d("--Views count: ${_rawLovelaceData['views'].length}"); | ||||||
|       _configCompleter.completeError({"errorCode": 2, "errorMessage": data["error"]["message"]}); |       _rawLovelaceData["views"].forEach((rawView){ | ||||||
|  |         Logger.d("----view id: ${rawView['id']}"); | ||||||
|  |         HAView view = HAView( | ||||||
|  |             count: viewCounter, | ||||||
|  |             id: "${rawView['id']}", | ||||||
|  |             name: rawView['title'], | ||||||
|  |             iconName: rawView['icon'] | ||||||
|  |         ); | ||||||
|  |  | ||||||
|  |         if (rawView['badges'] != null && rawView['badges'] is List) { | ||||||
|  |           rawView['badges'].forEach((entity) { | ||||||
|  |             if (entities.isExist(entity)) { | ||||||
|  |               Entity e = entities.get(entity); | ||||||
|  |               view.badges.add(e); | ||||||
|             } |             } | ||||||
|  |           }); | ||||||
|         } |         } | ||||||
|  |  | ||||||
|   void _parseUserInfo(Map data) { |         view.cards.addAll(_createLovelaceCards(rawView["cards"] ?? [])); | ||||||
|     if (data["success"] == true) { |         ui.views.add( | ||||||
|       _userName = data["result"]["name"]; |             view | ||||||
|     } else { |         ); | ||||||
|       _userName = null; |         viewCounter += 1; | ||||||
|     } |       }); | ||||||
|     _userInfoCompleter.complete(); |  | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   void _parseServices(response) { |   List<HACard> _createLovelaceCards(List rawCards) { | ||||||
|     _servicesCompleter.complete(); |     List<HACard> result = []; | ||||||
|     /*if (response["success"] == false) { |     rawCards.forEach((rawCard){ | ||||||
|       _servicesCompleter.completeError({"errorCode": 4, "errorMessage": response["error"]["message"]}); |  | ||||||
|       return; |  | ||||||
|     } |  | ||||||
|       try { |       try { | ||||||
|       Map data = response["result"]; |         bool isThereCardOptionsInside = rawCard["card"] != null; | ||||||
|       Map result = {}; |         HACard card = HACard( | ||||||
|       TheLogger.log("Debug","Parsing ${data.length} Home Assistant service domains"); |             id: "card", | ||||||
|       data.forEach((domain, services) { |             name: isThereCardOptionsInside ? rawCard["card"]["title"] ?? | ||||||
|         result[domain] = Map.from(services); |                 rawCard["card"]["name"] : rawCard["title"] ?? rawCard["name"], | ||||||
|         services.forEach((serviceName, serviceData) { |             type: isThereCardOptionsInside | ||||||
|           if (_entitiesData.isExist("$domain.$serviceName")) { |                 ? rawCard["card"]['type'] | ||||||
|             result[domain].remove(serviceName); |                 : rawCard['type'], | ||||||
|  |             columnsCount: isThereCardOptionsInside | ||||||
|  |                 ? rawCard["card"]['columns'] ?? 4 | ||||||
|  |                 : rawCard['columns'] ?? 4, | ||||||
|  |             showName: isThereCardOptionsInside ? rawCard["card"]['show_name'] ?? | ||||||
|  |                 true : rawCard['show_name'] ?? true, | ||||||
|  |             showState: isThereCardOptionsInside | ||||||
|  |                 ? rawCard["card"]['show_state'] ?? true | ||||||
|  |                 : rawCard['show_state'] ?? true, | ||||||
|  |             showEmpty: rawCard['show_empty'] ?? true, | ||||||
|  |             stateFilter: rawCard['state_filter'] ?? [], | ||||||
|  |             states: rawCard['states'], | ||||||
|  |             content: rawCard['content'] | ||||||
|  |         ); | ||||||
|  |         if (rawCard["cards"] != null) { | ||||||
|  |           card.childCards = _createLovelaceCards(rawCard["cards"]); | ||||||
|         } |         } | ||||||
|         }); |         rawCard["entities"]?.forEach((rawEntity) { | ||||||
|       }); |           if (rawEntity is String) { | ||||||
|       _servicesData = result; |             if (entities.isExist(rawEntity)) { | ||||||
|       _servicesCompleter.complete(); |               card.entities.add(EntityWrapper(entity: entities.get(rawEntity))); | ||||||
|     } catch (e) { |  | ||||||
|       TheLogger.log("Error","Error parsing services. But they are not used :-)"); |  | ||||||
|       _servicesCompleter.complete(); |  | ||||||
|     }*/ |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   void _parseEntities(response) async { |  | ||||||
|     if (response["success"] == false) { |  | ||||||
|       _statesCompleter.completeError({"errorCode": 3, "errorMessage": response["error"]["message"]}); |  | ||||||
|       return; |  | ||||||
|     } |  | ||||||
|     _entities.parse(response["result"]); |  | ||||||
|     _viewBuilder = ViewBuilder(entityCollection: _entities); |  | ||||||
|     _statesCompleter.complete(); |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   Widget buildViews(BuildContext context) { |  | ||||||
|     return _viewBuilder.buildWidget(context); |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   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]); |  | ||||||
|     TheLogger.log("Debug", "$startTime"); |  | ||||||
|     String url = "$homeAssistantWebHost/api/history/period/$startTime?&filter_entity_id=$entityId&skip_initial_state"; |  | ||||||
|     TheLogger.log("Debug", "$url"); |  | ||||||
|     http.Response historyResponse; |  | ||||||
|     if (_authType == "access_token") { |  | ||||||
|       historyResponse = await http.get(url, headers: { |  | ||||||
|         "authorization": "Bearer $_password", |  | ||||||
|         "Content-Type": "application/json" |  | ||||||
|       }); |  | ||||||
|             } else { |             } else { | ||||||
|       historyResponse = await http.get(url, headers: { |               card.entities.add(EntityWrapper(entity: Entity.missed(rawEntity))); | ||||||
|         "X-HA-Access": "$_password", |             } | ||||||
|         "Content-Type": "application/json" |           } else { | ||||||
|  |             if (rawEntity["type"] == "divider") { | ||||||
|  |               card.entities.add(EntityWrapper(entity: Entity.divider())); | ||||||
|  |             } else if (rawEntity["type"] == "section") { | ||||||
|  |               card.entities.add(EntityWrapper(entity: Entity.section(rawEntity["label"] ?? ""))); | ||||||
|  |             } else if (rawEntity["type"] == "call-service") { | ||||||
|  |               Map uiActionData = { | ||||||
|  |                 "tap_action": { | ||||||
|  |                   "action": EntityUIAction.callService, | ||||||
|  |                   "service": rawEntity["service"], | ||||||
|  |                   "service_data": rawEntity["service_data"] | ||||||
|  |                 }, | ||||||
|  |                 "hold_action": EntityUIAction.none | ||||||
|  |               }; | ||||||
|  |               card.entities.add(EntityWrapper( | ||||||
|  |                   entity: Entity.callService( | ||||||
|  |                     icon: rawEntity["icon"], | ||||||
|  |                     name: rawEntity["name"], | ||||||
|  |                     service: rawEntity["service"], | ||||||
|  |                     actionName: rawEntity["action_name"] | ||||||
|  |                   ), | ||||||
|  |                 uiAction: EntityUIAction(rawEntityData: uiActionData) | ||||||
|  |               ) | ||||||
|  |               ); | ||||||
|  |             } else if (rawEntity["type"] == "weblink") { | ||||||
|  |               Map uiActionData = { | ||||||
|  |                 "tap_action": { | ||||||
|  |                   "action": EntityUIAction.navigate, | ||||||
|  |                   "service": rawEntity["url"] | ||||||
|  |                 }, | ||||||
|  |                 "hold_action": EntityUIAction.none | ||||||
|  |               }; | ||||||
|  |               card.entities.add(EntityWrapper( | ||||||
|  |                   entity: Entity.weblink( | ||||||
|  |                       icon: rawEntity["icon"], | ||||||
|  |                       name: rawEntity["name"], | ||||||
|  |                       url: rawEntity["url"] | ||||||
|  |                   ), | ||||||
|  |                   uiAction: EntityUIAction(rawEntityData: uiActionData) | ||||||
|  |               ) | ||||||
|  |               ); | ||||||
|  |             } else if (entities.isExist(rawEntity["entity"])) { | ||||||
|  |               Entity e = entities.get(rawEntity["entity"]); | ||||||
|  |               card.entities.add( | ||||||
|  |                   EntityWrapper( | ||||||
|  |                       entity: e, | ||||||
|  |                       displayName: rawEntity["name"], | ||||||
|  |                       icon: rawEntity["icon"], | ||||||
|  |                       uiAction: EntityUIAction(rawEntityData: rawEntity) | ||||||
|  |                   ) | ||||||
|  |               ); | ||||||
|  |             } else { | ||||||
|  |               card.entities.add(EntityWrapper(entity: Entity.missed(rawEntity["entity"]))); | ||||||
|  |             } | ||||||
|  |           } | ||||||
|         }); |         }); | ||||||
|  |         if (rawCard["entity"] != null) { | ||||||
|  |           var en = rawCard["entity"]; | ||||||
|  |           if (en is String) { | ||||||
|  |             if (entities.isExist(en)) { | ||||||
|  |               Entity e = entities.get(en); | ||||||
|  |               card.linkedEntityWrapper = EntityWrapper( | ||||||
|  |                   entity: e, | ||||||
|  |                   icon: rawCard["icon"], | ||||||
|  |                   displayName: rawCard["name"], | ||||||
|  |                   uiAction: EntityUIAction(rawEntityData: rawCard) | ||||||
|  |               ); | ||||||
|  |             } else { | ||||||
|  |               card.linkedEntityWrapper = EntityWrapper(entity: Entity.missed(en)); | ||||||
|             } |             } | ||||||
|     var _history = json.decode(historyResponse.body); |           } else { | ||||||
|     if (_history is Map) { |             if (entities.isExist(en["entity"])) { | ||||||
|       return null; |               Entity e = entities.get(en["entity"]); | ||||||
|     } else if (_history is List) { |               card.linkedEntityWrapper = EntityWrapper( | ||||||
|       TheLogger.log("Debug", "${_history[0].toString()}"); |                   entity: e, | ||||||
|       return _history; |                   icon: en["icon"], | ||||||
|  |                   displayName: en["name"], | ||||||
|  |                   uiAction: EntityUIAction(rawEntityData: rawCard) | ||||||
|  |               ); | ||||||
|  |             } else { | ||||||
|  |               card.linkedEntityWrapper = EntityWrapper(entity: Entity.missed(en["entity"])); | ||||||
|             } |             } | ||||||
|           } |           } | ||||||
|         } |         } | ||||||
|  |         result.add(card); | ||||||
|  |       } catch (e) { | ||||||
|  |           Logger.e("There was an error parsing card: ${e.toString()}"); | ||||||
|  |       } | ||||||
|  |     }); | ||||||
|  |     return result; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   void _createUI() { | ||||||
|  |     ui = HomeAssistantUI(); | ||||||
|  |     if ((_useLovelace) && (_rawLovelaceData != null)) { | ||||||
|  |       Logger.d("Creating Lovelace UI"); | ||||||
|  |       _parseLovelace(); | ||||||
|  |     } else { | ||||||
|  |       Logger.d("Creating group-based UI"); | ||||||
|  |       int viewCounter = 0; | ||||||
|  |       if (!entities.hasDefaultView) { | ||||||
|  |         HAView view = HAView( | ||||||
|  |             count: viewCounter, | ||||||
|  |             id: "group.default_view", | ||||||
|  |             name: "Home", | ||||||
|  |             childEntities: entities.filterEntitiesForDefaultView() | ||||||
|  |         ); | ||||||
|  |         ui.views.add( | ||||||
|  |             view | ||||||
|  |         ); | ||||||
|  |         viewCounter += 1; | ||||||
|  |       } | ||||||
|  |       entities.viewEntities.forEach((viewEntity) { | ||||||
|  |         HAView view = HAView( | ||||||
|  |             count: viewCounter, | ||||||
|  |             id: viewEntity.entityId, | ||||||
|  |             name: viewEntity.displayName, | ||||||
|  |             childEntities: viewEntity.childEntities | ||||||
|  |         ); | ||||||
|  |         view.linkedEntity = viewEntity; | ||||||
|  |         ui.views.add( | ||||||
|  |             view | ||||||
|  |         ); | ||||||
|  |         viewCounter += 1; | ||||||
|  |       }); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   Widget buildViews(BuildContext context, TabController tabController) { | ||||||
|  |     return ui.build(context, tabController); | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | /* | ||||||
| class SendMessageQueue { | class SendMessageQueue { | ||||||
|   int _messageTimeout; |   int _messageTimeout; | ||||||
|   List<HAMessage> _queue = []; |   List<HAMessage> _queue = []; | ||||||
| @@ -443,4 +393,4 @@ class HAMessage { | |||||||
|   bool isExpired() { |   bool isExpired() { | ||||||
|     return DateTime.now().difference(_timeStamp).inSeconds > _messageTimeout; |     return DateTime.now().difference(_timeStamp).inSeconds > _messageTimeout; | ||||||
|   } |   } | ||||||
| } | }*/ | ||||||
|   | |||||||
| @@ -19,7 +19,7 @@ class _LogViewPageState extends State<LogViewPage> { | |||||||
|   } |   } | ||||||
|  |  | ||||||
|   _loadLog() async { |   _loadLog() async { | ||||||
|     _logData = TheLogger.getLog(); |     _logData = Logger.getLog(); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   @override |   @override | ||||||
| @@ -38,20 +38,11 @@ class _LogViewPageState extends State<LogViewPage> { | |||||||
|             onPressed: () { |             onPressed: () { | ||||||
|               Clipboard.setData(new ClipboardData(text: _logData)); |               Clipboard.setData(new ClipboardData(text: _logData)); | ||||||
|             }, |             }, | ||||||
|           ), |           ) | ||||||
|           IconButton( |  | ||||||
|             icon: Icon(MaterialDesignIcons.createIconDataFromIconName("mdi:github-circle")), |  | ||||||
|             onPressed: () { |  | ||||||
|               String body = "```\n$_logData```"; |  | ||||||
|               String encodedBody = "${Uri.encodeFull(body)}"; |  | ||||||
|               HAUtils.launchURL("https://github.com/estevez-dev/ha_client_pub/issues/new?body=$encodedBody"); |  | ||||||
|             }, |  | ||||||
|           ), |  | ||||||
|         ], |         ], | ||||||
|       ), |       ), | ||||||
|       body: TextField( |       body: TextField( | ||||||
|         maxLines: null, |         maxLines: null, | ||||||
|  |  | ||||||
|         controller: TextEditingController( |         controller: TextEditingController( | ||||||
|             text: _logData |             text: _logData | ||||||
|         ), |         ), | ||||||
|   | |||||||
							
								
								
									
										708
									
								
								lib/main.dart
									
									
									
									
									
								
							
							
						
						| @@ -1,43 +1,116 @@ | |||||||
| import 'dart:convert'; | import 'dart:convert'; | ||||||
| import 'dart:async'; | import 'dart:async'; | ||||||
|  | import 'dart:typed_data'; | ||||||
| import 'package:flutter/rendering.dart'; | import 'package:flutter/rendering.dart'; | ||||||
| import 'package:flutter/material.dart'; | import 'package:flutter/material.dart'; | ||||||
| import 'package:shared_preferences/shared_preferences.dart'; | import 'package:shared_preferences/shared_preferences.dart'; | ||||||
| import 'package:web_socket_channel/io.dart'; | import 'package:web_socket_channel/io.dart'; | ||||||
| import 'package:progress_indicators/progress_indicators.dart'; |  | ||||||
| import 'package:event_bus/event_bus.dart'; | import 'package:event_bus/event_bus.dart'; | ||||||
| import 'package:flutter/widgets.dart'; | import 'package:flutter/widgets.dart'; | ||||||
| import 'package:cached_network_image/cached_network_image.dart'; | import 'package:cached_network_image/cached_network_image.dart'; | ||||||
| import 'package:url_launcher/url_launcher.dart'; | import 'package:url_launcher/url_launcher.dart' as urlLauncher; | ||||||
| import 'package:flutter/services.dart'; | import 'package:flutter/services.dart'; | ||||||
| import 'package:date_format/date_format.dart'; | import 'package:date_format/date_format.dart'; | ||||||
| import 'package:http/http.dart' as http; | import 'package:http/http.dart' as http; | ||||||
|  | import 'package:charts_flutter/flutter.dart' as charts; | ||||||
|  | import 'package:progress_indicators/progress_indicators.dart'; | ||||||
|  | import 'package:flutter_markdown/flutter_markdown.dart'; | ||||||
|  | import 'package:flutter_svg/flutter_svg.dart'; | ||||||
|  | import 'package:flutter_custom_tabs/flutter_custom_tabs.dart'; | ||||||
|  | import 'package:firebase_messaging/firebase_messaging.dart'; | ||||||
|  | import 'package:flutter_webview_plugin/flutter_webview_plugin.dart'; | ||||||
|  | import 'package:flutter_secure_storage/flutter_secure_storage.dart'; | ||||||
|  |  | ||||||
|  | part 'entity_class/const.dart'; | ||||||
| part 'entity_class/entity.class.dart'; | part 'entity_class/entity.class.dart'; | ||||||
| part 'entity_class/stateless_widgets.dart'; | part 'entity_class/entity_wrapper.class.dart'; | ||||||
| part 'entity_class/stateful_widgets.dart'; | part 'entity_class/timer_entity.dart'; | ||||||
|  | part 'entity_class/switch_entity.class.dart'; | ||||||
|  | part 'entity_class/button_entity.class.dart'; | ||||||
|  | part 'entity_class/text_entity.class.dart'; | ||||||
|  | part 'entity_class/climate_entity.class.dart'; | ||||||
|  | part 'entity_class/cover_entity.class.dart'; | ||||||
|  | part 'entity_class/date_time_entity.class.dart'; | ||||||
|  | part 'entity_class/light_entity.class.dart'; | ||||||
|  | part 'entity_class/select_entity.class.dart'; | ||||||
|  | part 'entity_class/other_entity.class.dart'; | ||||||
|  | part 'entity_class/slider_entity.dart'; | ||||||
|  | part 'entity_class/media_player_entity.class.dart'; | ||||||
|  | part 'entity_class/lock_entity.class.dart'; | ||||||
|  | part 'entity_class/group_entity.class.dart'; | ||||||
|  | part 'entity_class/fan_entity.class.dart'; | ||||||
|  | part 'entity_class/automation_entity.dart'; | ||||||
|  | part 'entity_class/camera_entity.class.dart'; | ||||||
|  | part 'entity_class/alarm_control_panel.class.dart'; | ||||||
|  | part 'entity_widgets/common/badge.dart'; | ||||||
|  | part 'entity_widgets/model_widgets.dart'; | ||||||
|  | part 'entity_widgets/default_entity_container.dart'; | ||||||
|  | part 'entity_widgets/missed_entity.dart'; | ||||||
|  | part 'entity_widgets/glance_entity_container.dart'; | ||||||
|  | part 'entity_widgets/button_entity_container.dart'; | ||||||
|  | part 'entity_widgets/common/entity_attributes_list.dart'; | ||||||
|  | part 'entity_widgets/entity_icon.dart'; | ||||||
|  | part 'entity_widgets/entity_name.dart'; | ||||||
|  | part 'entity_widgets/common/last_updated.dart'; | ||||||
|  | part 'entity_widgets/common/mode_swicth.dart'; | ||||||
|  | part 'entity_widgets/common/mode_selector.dart'; | ||||||
|  | part 'entity_widgets/common/universal_slider.dart'; | ||||||
|  | part 'entity_widgets/common/flat_service_button.dart'; | ||||||
|  | part 'entity_widgets/common/light_color_picker.dart'; | ||||||
|  | part 'entity_widgets/common/camera_stream_view.dart'; | ||||||
|  | part 'entity_widgets/entity_colors.class.dart'; | ||||||
|  | part 'entity_widgets/entity_page_container.dart'; | ||||||
|  | part 'entity_widgets/history_chart/entity_history.dart'; | ||||||
|  | part 'entity_widgets/history_chart/simple_state_history_chart.dart'; | ||||||
|  | part 'entity_widgets/history_chart/numeric_state_history_chart.dart'; | ||||||
|  | part 'entity_widgets/history_chart/combined_history_chart.dart'; | ||||||
|  | part 'entity_widgets/history_chart/history_control_widget.dart'; | ||||||
|  | part 'entity_widgets/history_chart/entity_history_moment.dart'; | ||||||
|  | part 'entity_widgets/state/switch_state.dart'; | ||||||
|  | part 'entity_widgets/controls/slider_controls.dart'; | ||||||
|  | part 'entity_widgets/state/text_input_state.dart'; | ||||||
|  | part 'entity_widgets/state/select_state.dart'; | ||||||
|  | part 'entity_widgets/state/simple_state.dart'; | ||||||
|  | part 'entity_widgets/state/timer_state.dart'; | ||||||
|  | part 'entity_widgets/state/climate_state.dart'; | ||||||
|  | part 'entity_widgets/state/cover_state.dart'; | ||||||
|  | part 'entity_widgets/state/date_time_state.dart'; | ||||||
|  | part 'entity_widgets/state/lock_state.dart'; | ||||||
|  | part 'entity_widgets/controls/climate_controls.dart'; | ||||||
|  | part 'entity_widgets/controls/cover_controls.dart'; | ||||||
|  | part 'entity_widgets/controls/light_controls.dart'; | ||||||
|  | part 'entity_widgets/controls/media_player_widgets.dart'; | ||||||
|  | part 'entity_widgets/controls/fan_controls.dart'; | ||||||
|  | part 'entity_widgets/controls/alarm_control_panel_controls.dart'; | ||||||
| part 'settings.page.dart'; | part 'settings.page.dart'; | ||||||
|  | part 'panel.page.dart'; | ||||||
| part 'home_assistant.class.dart'; | part 'home_assistant.class.dart'; | ||||||
| part 'log.page.dart'; | part 'log.page.dart'; | ||||||
| part 'entity.page.dart'; | part 'entity.page.dart'; | ||||||
| part 'utils.class.dart'; | part 'utils.class.dart'; | ||||||
| part 'mdi.class.dart'; | part 'mdi.class.dart'; | ||||||
| part 'entity_collection.class.dart'; | part 'entity_collection.class.dart'; | ||||||
| part 'view_builder.class.dart'; | part 'auth_manager.class.dart'; | ||||||
| part 'view_class.dart'; | part 'connection.class.dart'; | ||||||
| part 'card_class.dart'; | part 'ui_class/ui.dart'; | ||||||
|  | part 'ui_class/view.class.dart'; | ||||||
|  | part 'ui_class/card.class.dart'; | ||||||
|  | part 'ui_class/sizes_class.dart'; | ||||||
|  | part 'ui_class/panel_class.dart'; | ||||||
|  | part 'ui_widgets/view.dart'; | ||||||
|  | part 'ui_widgets/card_widget.dart'; | ||||||
|  | part 'ui_widgets/card_header_widget.dart'; | ||||||
|  | part 'ui_widgets/config_panel_widget.dart'; | ||||||
|  |  | ||||||
|  |  | ||||||
| EventBus eventBus = new EventBus(); | EventBus eventBus = new EventBus(); | ||||||
| const String appName = "HA Client"; | const String appName = "HA Client"; | ||||||
| const appVersion = "0.3.0.38"; | const appVersion = "0.6.0-alpha1"; | ||||||
|  |  | ||||||
| String homeAssistantWebHost; |  | ||||||
|  |  | ||||||
| void main() { | void main() { | ||||||
|   FlutterError.onError = (errorDetails) { |   FlutterError.onError = (errorDetails) { | ||||||
|     TheLogger.log("Error", "${errorDetails.exception}"); |     Logger.e( "${errorDetails.exception}"); | ||||||
|     if (TheLogger.isInDebugMode) { |     if (Logger.isInDebugMode) { | ||||||
|       FlutterError.dumpErrorToConsole(errorDetails); |       FlutterError.dumpErrorToConsole(errorDetails); | ||||||
|     } |     } | ||||||
|   }; |   }; | ||||||
| @@ -45,14 +118,17 @@ void main() { | |||||||
|   runZoned(() { |   runZoned(() { | ||||||
|     runApp(new HAClientApp()); |     runApp(new HAClientApp()); | ||||||
|   }, onError: (error, stack) { |   }, onError: (error, stack) { | ||||||
|     TheLogger.log("Global error", "$error"); |     Logger.e("$error"); | ||||||
|     if (TheLogger.isInDebugMode) { |     Logger.e("$stack"); | ||||||
|  |     if (Logger.isInDebugMode) { | ||||||
|       debugPrint("$stack"); |       debugPrint("$stack"); | ||||||
|     } |     } | ||||||
|   }); |   }); | ||||||
| } | } | ||||||
|  |  | ||||||
| class HAClientApp extends StatelessWidget { | class HAClientApp extends StatelessWidget { | ||||||
|  |  | ||||||
|  |   final HomeAssistant homeAssistant = HomeAssistant(); | ||||||
|   // This widget is the root of your application. |   // This widget is the root of your application. | ||||||
|   @override |   @override | ||||||
|   Widget build(BuildContext context) { |   Widget build(BuildContext context) { | ||||||
| @@ -63,8 +139,9 @@ class HAClientApp extends StatelessWidget { | |||||||
|       ), |       ), | ||||||
|       initialRoute: "/", |       initialRoute: "/", | ||||||
|       routes: { |       routes: { | ||||||
|         "/": (context) => MainPage(title: 'HA Client'), |         "/": (context) => MainPage(title: 'HA Client', homeAssistant: homeAssistant,), | ||||||
|         "/connection-settings": (context) => ConnectionSettingsPage(title: "Connection Settings"), |         "/connection-settings": (context) => ConnectionSettingsPage(title: "Settings"), | ||||||
|  |         "/configuration": (context) => PanelPage(title: "Configuration"), | ||||||
|         "/log-view": (context) => LogViewPage(title: "Log") |         "/log-view": (context) => LogViewPage(title: "Log") | ||||||
|       }, |       }, | ||||||
|     ); |     ); | ||||||
| @@ -72,99 +149,101 @@ class HAClientApp extends StatelessWidget { | |||||||
| } | } | ||||||
|  |  | ||||||
| class MainPage extends StatefulWidget { | class MainPage extends StatefulWidget { | ||||||
|   MainPage({Key key, this.title}) : super(key: key); |   MainPage({Key key, this.title, this.homeAssistant}) : super(key: key); | ||||||
|  |  | ||||||
|   final String title; |   final String title; | ||||||
|  |   final HomeAssistant homeAssistant; | ||||||
|  |  | ||||||
|   @override |   @override | ||||||
|   _MainPageState createState() => new _MainPageState(); |   _MainPageState createState() => new _MainPageState(); | ||||||
| } | } | ||||||
|  |  | ||||||
| class _MainPageState extends State<MainPage> with WidgetsBindingObserver { | class _MainPageState extends State<MainPage> with WidgetsBindingObserver, TickerProviderStateMixin { | ||||||
|   HomeAssistant _homeAssistant; |  | ||||||
|   EntityCollection _entities; |  | ||||||
|   //Map _instanceConfig; |  | ||||||
|   String _webSocketApiEndpoint; |  | ||||||
|   String _password; |  | ||||||
|   String _authType; |  | ||||||
|   int _uiViewsCount = 0; |  | ||||||
|   String _instanceHost; |  | ||||||
|   StreamSubscription _stateSubscription; |   StreamSubscription _stateSubscription; | ||||||
|   StreamSubscription _settingsSubscription; |   StreamSubscription _settingsSubscription; | ||||||
|   StreamSubscription _serviceCallSubscription; |   StreamSubscription _serviceCallSubscription; | ||||||
|   StreamSubscription _showEntityPageSubscription; |   StreamSubscription _showEntityPageSubscription; | ||||||
|   StreamSubscription _refreshDataSubscription; |  | ||||||
|   StreamSubscription _showErrorSubscription; |   StreamSubscription _showErrorSubscription; | ||||||
|   int _isLoading = 1; |   StreamSubscription _startAuthSubscription; | ||||||
|   bool _settingsLoaded = false; |   StreamSubscription _reloadUISubscription; | ||||||
|   bool _accountMenuExpanded = false; |   int _previousViewCount; | ||||||
|  |   //final FirebaseMessaging _firebaseMessaging = FirebaseMessaging(); | ||||||
|  |  | ||||||
|   @override |   @override | ||||||
|   void initState() { |   void initState() { | ||||||
|     super.initState(); |     super.initState(); | ||||||
|     _settingsLoaded = false; |     //widget.homeAssistant = HomeAssistant(); | ||||||
|  |     //_settingsLoaded = false; | ||||||
|     WidgetsBinding.instance.addObserver(this); |     WidgetsBinding.instance.addObserver(this); | ||||||
|  |  | ||||||
|     _homeAssistant = HomeAssistant(); |  | ||||||
|  |  | ||||||
|     _settingsSubscription = eventBus.on<SettingsChangedEvent>().listen((event) { |     _settingsSubscription = eventBus.on<SettingsChangedEvent>().listen((event) { | ||||||
|       TheLogger.log("Debug","Settings change event: reconnect=${event.reconnect}"); |       Logger.d("Settings change event: reconnect=${event.reconnect}"); | ||||||
|       if (event.reconnect) { |       if (event.reconnect) { | ||||||
|         _homeAssistant.disconnect().then((_){ |         _reLoad(); | ||||||
|           _initialLoad(); |  | ||||||
|         }); |  | ||||||
|       } |       } | ||||||
|     }); |     }); | ||||||
|  |  | ||||||
|     _initialLoad(); |     _initialLoad(); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   void _initialLoad() { |   void _initialLoad() { | ||||||
|     _loadConnectionSettings().then((_){ |     _showInfoBottomBar(progress: true,); | ||||||
|     _subscribe(); |     _subscribe(); | ||||||
|       _refreshData(); |     widget.homeAssistant.init().then((_){ | ||||||
|     }, onError: (_) { |       _fetchData(); | ||||||
|       setState(() { |     }, onError: (e) { | ||||||
|         _isLoading = 2; |       _setErrorState(e); | ||||||
|     }); |     }); | ||||||
|       _showErrorSnackBar(message: _, errorCode: 5); |   } | ||||||
|  |  | ||||||
|  |   void _reLoad() { | ||||||
|  |     _hideBottomBar(); | ||||||
|  |     _showInfoBottomBar(progress: true,); | ||||||
|  |     widget.homeAssistant.init().then((_){ | ||||||
|  |       _fetchData(); | ||||||
|  |     }, onError: (e) { | ||||||
|  |       _setErrorState(e); | ||||||
|     }); |     }); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|  |   _fetchData() async { | ||||||
|  |     await widget.homeAssistant.fetch().then((_) { | ||||||
|  |       _hideBottomBar(); | ||||||
|  |       int currentViewCount = widget.homeAssistant.ui?.views?.length ?? 0; | ||||||
|  |       if (_previousViewCount != currentViewCount) { | ||||||
|  |         Logger.d("Views count changed ($_previousViewCount->$currentViewCount). Creating new tabs controller."); | ||||||
|  |         _viewsTabController = TabController(vsync: this, length: currentViewCount); | ||||||
|  |         _previousViewCount = currentViewCount; | ||||||
|  |       } | ||||||
|  |     }).catchError((e) { | ||||||
|  |       _setErrorState(e); | ||||||
|  |     }); | ||||||
|  |     eventBus.fire(RefreshDataFinishedEvent()); | ||||||
|  |   } | ||||||
|  |  | ||||||
|   @override |   @override | ||||||
|   void didChangeAppLifecycleState(AppLifecycleState state) { |   void didChangeAppLifecycleState(AppLifecycleState state) { | ||||||
|     TheLogger.log("Debug","$state"); |     Logger.d("$state"); | ||||||
|     if (state == AppLifecycleState.resumed && _settingsLoaded) { |     if (state == AppLifecycleState.resumed) { | ||||||
|       _refreshData(); |       _reLoad(); | ||||||
|     } |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   _loadConnectionSettings() async { |  | ||||||
|     SharedPreferences prefs = await SharedPreferences.getInstance(); |  | ||||||
|     String domain = prefs.getString('hassio-domain'); |  | ||||||
|     String port = prefs.getString('hassio-port'); |  | ||||||
|     _instanceHost = "$domain:$port"; |  | ||||||
|     _webSocketApiEndpoint = "${prefs.getString('hassio-protocol')}://$domain:$port/api/websocket"; |  | ||||||
|     homeAssistantWebHost = "${prefs.getString('hassio-res-protocol')}://$domain:$port"; |  | ||||||
|     _password = prefs.getString('hassio-password'); |  | ||||||
|     _authType = prefs.getString('hassio-auth-type'); |  | ||||||
|     if ((domain == null) || (port == null) || (_password == null) || |  | ||||||
|         (domain.length == 0) || (port.length == 0) || (_password.length == 0)) { |  | ||||||
|       throw("Check connection settings"); |  | ||||||
|     } else { |  | ||||||
|       _settingsLoaded = true; |  | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   _subscribe() { |   _subscribe() { | ||||||
|     if (_stateSubscription == null) { |     if (_stateSubscription == null) { | ||||||
|       _stateSubscription = eventBus.on<StateChangedEvent>().listen((event) { |       _stateSubscription = eventBus.on<StateChangedEvent>().listen((event) { | ||||||
|         setState(() { |         if (event.needToRebuildUI) { | ||||||
|           if (event.localChange) { |           Logger.d("New entity. Need to rebuild UI"); | ||||||
|             _entities |           _reLoad(); | ||||||
|                 .get(event.entityId) |         } else { | ||||||
|                 .state = event.newState; |           setState(() {}); | ||||||
|         } |         } | ||||||
|       }); |       }); | ||||||
|  |     } | ||||||
|  |     if (_reloadUISubscription == null) { | ||||||
|  |       _reloadUISubscription = eventBus.on<ReloadUIEvent>().listen((event){ | ||||||
|  |         _reLoad(); | ||||||
|       }); |       }); | ||||||
|     } |     } | ||||||
|     if (_serviceCallSubscription == null) { |     if (_serviceCallSubscription == null) { | ||||||
| @@ -178,134 +257,121 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver { | |||||||
|     if (_showEntityPageSubscription == null) { |     if (_showEntityPageSubscription == null) { | ||||||
|       _showEntityPageSubscription = |       _showEntityPageSubscription = | ||||||
|           eventBus.on<ShowEntityPageEvent>().listen((event) { |           eventBus.on<ShowEntityPageEvent>().listen((event) { | ||||||
|             _showEntityPage(event.entity); |             _showEntityPage(event.entity.entityId); | ||||||
|           }); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     if (_refreshDataSubscription == null) { |  | ||||||
|       _refreshDataSubscription = eventBus.on<RefreshDataEvent>().listen((event){ |  | ||||||
|         _refreshData(); |  | ||||||
|           }); |           }); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     if (_showErrorSubscription == null) { |     if (_showErrorSubscription == null) { | ||||||
|       _showErrorSubscription = eventBus.on<ShowErrorEvent>().listen((event){ |       _showErrorSubscription = eventBus.on<ShowErrorEvent>().listen((event){ | ||||||
|         _showErrorSnackBar(message: event.text, errorCode: event.errorCode); |         _showErrorBottomBar(message: event.text, errorCode: event.errorCode); | ||||||
|       }); |       }); | ||||||
|     } |     } | ||||||
|   } |  | ||||||
|  |  | ||||||
|   _refreshData() async { |     if (_startAuthSubscription == null) { | ||||||
|     _homeAssistant.updateConnectionSettings(_webSocketApiEndpoint, _password, _authType); |       _startAuthSubscription = eventBus.on<StartAuthEvent>().listen((event){ | ||||||
|     setState(() { |         _showOAuth(); | ||||||
|       _hideErrorSnackBar(); |  | ||||||
|       _isLoading = 1; |  | ||||||
|       }); |       }); | ||||||
|     await _homeAssistant.fetch().then((result) { |     } | ||||||
|       setState(() { |  | ||||||
|         //_instanceConfig = _homeAssistant.instanceConfig; |  | ||||||
|         _entities = _homeAssistant.entities; |  | ||||||
|         _uiViewsCount = _homeAssistant.viewsCount; |     /*_firebaseMessaging.getToken().then((String token) { | ||||||
|         _isLoading = 0; |       //Logger.d("FCM token: $token"); | ||||||
|  |       widget.homeAssistant.sendHTTPPost( | ||||||
|  |           endPoint: '/api/notify.fcm-android', | ||||||
|  |           jsonData:  '{"token": "$token"}' | ||||||
|  |       ); | ||||||
|     }); |     }); | ||||||
|     }).catchError((e) { |     _firebaseMessaging.configure( | ||||||
|       _setErrorState(e); |         onLaunch: (data) { | ||||||
|     }); |           Logger.d("Notification [onLaunch]: $data"); | ||||||
|     eventBus.fire(RefreshDataFinishedEvent()); |         }, | ||||||
|  |         onMessage: (data) { | ||||||
|  |           Logger.d("Notification [onMessage]: $data"); | ||||||
|  |         }, | ||||||
|  |         onResume: (data) { | ||||||
|  |           Logger.d("Notification [onResume]: $data"); | ||||||
|  |         } | ||||||
|  |     );*/ | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   void _showOAuth() { | ||||||
|  |     Navigator.push( | ||||||
|  |         context, | ||||||
|  |         MaterialPageRoute( | ||||||
|  |           builder: (context) => WebviewScaffold( | ||||||
|  |             url: "${widget.homeAssistant.connection.oauthUrl}", | ||||||
|  |             appBar: new AppBar( | ||||||
|  |               leading: IconButton( | ||||||
|  |                   icon: Icon(Icons.help), | ||||||
|  |                   onPressed: () => HAUtils.launchURLInCustomTab(context, "http://ha-client.homemade.systems/docs#authentication") | ||||||
|  |               ), | ||||||
|  |               title: new Text("Login to your Home Assistant"), | ||||||
|  |             ), | ||||||
|  |           ), | ||||||
|  |         ) | ||||||
|  |     ); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   _setErrorState(e) { |   _setErrorState(e) { | ||||||
|     setState(() { |     if (e is Error) { | ||||||
|       _isLoading = 2; |       Logger.e(e.toString()); | ||||||
|     }); |       Logger.e("${e.stackTrace}"); | ||||||
|     _showErrorSnackBar( |       _showErrorBottomBar( | ||||||
|  |           message: "Unknown error", | ||||||
|  |           errorCode: 13 | ||||||
|  |       ); | ||||||
|  |     } else { | ||||||
|  |       _showErrorBottomBar( | ||||||
|           message: e != null ? e["errorMessage"] ?? "$e" : "Unknown error", |           message: e != null ? e["errorMessage"] ?? "$e" : "Unknown error", | ||||||
|           errorCode: e["errorCode"] != null ? e["errorCode"] : 99 |           errorCode: e["errorCode"] != null ? e["errorCode"] : 99 | ||||||
|       ); |       ); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|   void _callService(String domain, String service, String entityId, Map<String, dynamic> additionalParams) { |  | ||||||
|     _homeAssistant.callService(domain, service, entityId, additionalParams).catchError((e) => _setErrorState(e)); |  | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   void _showEntityPage(Entity entity) { |   void _callService(String domain, String service, String entityId, Map additionalParams) { | ||||||
|  |     _showInfoBottomBar( | ||||||
|  |       message: "Calling $domain.$service", | ||||||
|  |       duration: Duration(seconds: 3) | ||||||
|  |     ); | ||||||
|  |     widget.homeAssistant.connection.callService(domain: domain, service: service, entityId: entityId, additionalServiceData: additionalParams).catchError((e) => _setErrorState(e)); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   void _showEntityPage(String entityId) { | ||||||
|     Navigator.push( |     Navigator.push( | ||||||
|         context, |         context, | ||||||
|         MaterialPageRoute( |         MaterialPageRoute( | ||||||
|           builder: (context) => EntityViewPage(entity: entity, homeAssistant: _homeAssistant), |           builder: (context) => EntityViewPage(entityId: entityId, homeAssistant: widget.homeAssistant), | ||||||
|         ) |         ) | ||||||
|     ); |     ); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   List<Tab> buildUIViewTabs() { |   List<Tab> buildUIViewTabs() { | ||||||
|     //TODO move somewhere to ViewBuilder |  | ||||||
|     List<Tab> result = []; |     List<Tab> result = []; | ||||||
|     if (!_entities.isEmpty) { |  | ||||||
|       if (!_entities.hasDefaultView) { |       if (widget.homeAssistant.ui.views.isNotEmpty) { | ||||||
|         result.add( |         widget.homeAssistant.ui.views.forEach((HAView view) { | ||||||
|             Tab( |           result.add(view.buildTab()); | ||||||
|                 icon: |  | ||||||
|                   Icon( |  | ||||||
|                     MaterialDesignIcons.createIconDataFromIconName("mdi:home-assistant"), |  | ||||||
|                     size: 24.0, |  | ||||||
|                   ) |  | ||||||
|             ) |  | ||||||
|         ); |  | ||||||
|       } |  | ||||||
|       _entities.viewList.forEach((viewId) { |  | ||||||
|         result.add( |  | ||||||
|             Tab( |  | ||||||
|                 icon: MaterialDesignIcons.createIconWidgetFromEntityData(_entities.get(viewId), 24.0, null) ?? |  | ||||||
|                     Icon( |  | ||||||
|                       MaterialDesignIcons.createIconDataFromIconName("mdi:home-assistant"), |  | ||||||
|                       size: 24.0, |  | ||||||
|                     ) |  | ||||||
|             ) |  | ||||||
|         ); |  | ||||||
|         }); |         }); | ||||||
|       } |       } | ||||||
|     return result; |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   Widget _buildAppTitle() { |     return result; | ||||||
|     Row titleRow = Row( |  | ||||||
|       children: [Text(_homeAssistant != null ? _homeAssistant.locationName : "")], |  | ||||||
|     ); |  | ||||||
|     if (_isLoading == 1) { |  | ||||||
|       titleRow.children.add(Padding( |  | ||||||
|         child: JumpingDotsProgressIndicator( |  | ||||||
|           fontSize: 26.0, |  | ||||||
|           color: Colors.white, |  | ||||||
|         ), |  | ||||||
|         padding: const EdgeInsets.fromLTRB(5.0, 0.0, 0.0, 30.0), |  | ||||||
|       )); |  | ||||||
|     } else if (_isLoading == 2) { |  | ||||||
|       titleRow.children.add(Padding( |  | ||||||
|         child: Icon( |  | ||||||
|             Icons.error_outline, |  | ||||||
|             size: 20.0, |  | ||||||
|           color: Colors.red, |  | ||||||
|         ), |  | ||||||
|         padding: const EdgeInsets.fromLTRB(5.0, 0.0, 0.0, 0.0), |  | ||||||
|       )); |  | ||||||
|     } |  | ||||||
|     return titleRow; |  | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   Drawer _buildAppDrawer() { |   Drawer _buildAppDrawer() { | ||||||
|     List<Widget> menuItems = []; |     List<Widget> menuItems = []; | ||||||
|     menuItems.add( |     menuItems.add( | ||||||
|         UserAccountsDrawerHeader( |         UserAccountsDrawerHeader( | ||||||
|           accountName: Text(_homeAssistant.userName), |           accountName: Text(widget.homeAssistant.userName), | ||||||
|           accountEmail: Text(_instanceHost ?? "Not configured"), |           accountEmail: Text(widget.homeAssistant.hostname ?? "Not configured"), | ||||||
|           onDetailsPressed: () { |           /*onDetailsPressed: () { | ||||||
|             setState(() { |             setState(() { | ||||||
|               _accountMenuExpanded = !_accountMenuExpanded; |               _accountMenuExpanded = !_accountMenuExpanded; | ||||||
|             }); |             }); | ||||||
|           }, |           },*/ | ||||||
|           currentAccountPicture: CircleAvatar( |           currentAccountPicture: CircleAvatar( | ||||||
|             child: Text( |             child: Text( | ||||||
|               _homeAssistant.userAvatarText, |               widget.homeAssistant.userAvatarText, | ||||||
|               style: TextStyle( |               style: TextStyle( | ||||||
|                   fontSize: 32.0 |                   fontSize: 32.0 | ||||||
|               ), |               ), | ||||||
| @@ -313,20 +379,40 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver { | |||||||
|           ), |           ), | ||||||
|         ) |         ) | ||||||
|     ); |     ); | ||||||
|     if (_accountMenuExpanded) { |       if (widget.homeAssistant.panels.isNotEmpty) { | ||||||
|  |         widget.homeAssistant.panels.forEach((Panel panel) { | ||||||
|  |           if (!panel.isHidden) { | ||||||
|  |             menuItems.add( | ||||||
|  |                 new ListTile( | ||||||
|  |                     leading: Icon(MaterialDesignIcons.getIconDataFromIconName(panel.icon)), | ||||||
|  |                     title: Text("${panel.title}"), | ||||||
|  |                     onTap: () => panel.handleOpen(context) | ||||||
|  |                 ) | ||||||
|  |             ); | ||||||
|  |           } | ||||||
|  |         }); | ||||||
|  |       } | ||||||
|  |       //TODO check for loaded | ||||||
|  |       menuItems.add( | ||||||
|  |           new ListTile( | ||||||
|  |             leading: Icon(MaterialDesignIcons.getIconDataFromIconName("mdi:home-assistant")), | ||||||
|  |             title: Text("Open Web UI"), | ||||||
|  |             onTap: () => HAUtils.launchURL(widget.homeAssistant.connection.httpWebHost), | ||||||
|  |           ) | ||||||
|  |       ); | ||||||
|       menuItems.addAll([ |       menuItems.addAll([ | ||||||
|  |         Divider(), | ||||||
|         ListTile( |         ListTile( | ||||||
|           leading: Icon(Icons.settings), |           leading: Icon(MaterialDesignIcons.getIconDataFromIconName("mdi:login-variant")), | ||||||
|           title: Text("Connection settings"), |           title: Text("Connection settings"), | ||||||
|           onTap: () { |           onTap: () { | ||||||
|             Navigator.of(context).pop(); |             Navigator.of(context).pop(); | ||||||
|             Navigator.of(context).pushNamed('/connection-settings'); |             Navigator.of(context).pushNamed('/connection-settings', arguments: {"homeAssistant", widget.homeAssistant}); | ||||||
|           }, |           }, | ||||||
|         ), |         ) | ||||||
|         Divider(), |  | ||||||
|       ]); |       ]); | ||||||
|     } else { |  | ||||||
|       menuItems.addAll([ |       menuItems.addAll([ | ||||||
|  |         Divider(), | ||||||
|         new ListTile( |         new ListTile( | ||||||
|           leading: Icon(Icons.insert_drive_file), |           leading: Icon(Icons.insert_drive_file), | ||||||
|           title: Text("Log"), |           title: Text("Log"), | ||||||
| @@ -336,21 +422,42 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver { | |||||||
|           }, |           }, | ||||||
|         ), |         ), | ||||||
|         new ListTile( |         new ListTile( | ||||||
|           leading: Icon(MaterialDesignIcons.createIconDataFromIconName("mdi:github-circle")), |           leading: Icon(MaterialDesignIcons.getIconDataFromIconName("mdi:github-circle")), | ||||||
|           title: Text("Report an issue"), |           title: Text("Report an issue"), | ||||||
|           onTap: () { |           onTap: () { | ||||||
|             Navigator.of(context).pop(); |             Navigator.of(context).pop(); | ||||||
|             HAUtils.launchURL("https://github.com/estevez-dev/ha_client_pub/issues/new"); |             HAUtils.launchURL("https://github.com/estevez-dev/ha_client/issues/new"); | ||||||
|           }, |           }, | ||||||
|         ), |         ), | ||||||
|         Divider(), |         Divider(), | ||||||
|  |         new ListTile( | ||||||
|  |           leading: Icon(MaterialDesignIcons.getIconDataFromIconName("mdi:discord")), | ||||||
|  |           title: Text("Join Discord server"), | ||||||
|  |           onTap: () { | ||||||
|  |             Navigator.of(context).pop(); | ||||||
|  |             HAUtils.launchURL("https://discord.gg/AUzEvwn"); | ||||||
|  |           }, | ||||||
|  |         ), | ||||||
|         new AboutListTile( |         new AboutListTile( | ||||||
|  |           aboutBoxChildren: <Widget>[ | ||||||
|  |             GestureDetector( | ||||||
|  |               onTap: () { | ||||||
|  |                 Navigator.of(context).pop(); | ||||||
|  |                 HAUtils.launchURL("http://ha-client.homemade.systems/"); | ||||||
|  |               }, | ||||||
|  |               child: Text( | ||||||
|  |                 "ha-client.homemade.systems", | ||||||
|  |                 style: TextStyle( | ||||||
|  |                   color: Colors.blue, | ||||||
|  |                   decoration: TextDecoration.underline | ||||||
|  |                 ), | ||||||
|  |               ), | ||||||
|  |             ) | ||||||
|  |           ], | ||||||
|           applicationName: appName, |           applicationName: appName, | ||||||
|           applicationVersion: appVersion, |           applicationVersion: appVersion | ||||||
|           applicationLegalese: "Keyboard Crumbs | www.keyboardcrumbs.io", |  | ||||||
|         ) |         ) | ||||||
|       ]); |       ]); | ||||||
|     } |  | ||||||
|     return new Drawer( |     return new Drawer( | ||||||
|       child: ListView( |       child: ListView( | ||||||
|         children: menuItems, |         children: menuItems, | ||||||
| @@ -358,22 +465,52 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver { | |||||||
|     ); |     ); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   void _hideErrorSnackBar() { |   void _hideBottomBar() { | ||||||
|     _scaffoldKey?.currentState?.hideCurrentSnackBar(); |     //_scaffoldKey?.currentState?.hideCurrentSnackBar(); | ||||||
|  |     setState(() { | ||||||
|  |       _showBottomBar = false; | ||||||
|  |     }); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   void _showErrorSnackBar({Key key, @required String message, @required int errorCode}) { |   Widget _bottomBarAction; | ||||||
|       SnackBarAction action; |   bool _showBottomBar = false; | ||||||
|  |   String _bottomBarText; | ||||||
|  |   bool _bottomBarProgress; | ||||||
|  |   Color _bottomBarColor; | ||||||
|  |   Timer _bottomBarTimer; | ||||||
|  |  | ||||||
|  |   void _showInfoBottomBar({String message, bool progress: false, Duration duration}) { | ||||||
|  |     _bottomBarTimer?.cancel(); | ||||||
|  |     _bottomBarAction = Container(height: 0.0, width: 0.0,); | ||||||
|  |     _bottomBarColor = Colors.grey.shade50; | ||||||
|  |     setState(() { | ||||||
|  |       _bottomBarText = message; | ||||||
|  |       _bottomBarProgress = progress; | ||||||
|  |       _showBottomBar = true; | ||||||
|  |     }); | ||||||
|  |     if (duration != null) { | ||||||
|  |       _bottomBarTimer = Timer(duration, () { | ||||||
|  |         _hideBottomBar(); | ||||||
|  |       }); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   void _showErrorBottomBar({Key key, @required String message, @required int errorCode}) { | ||||||
|  |     TextStyle textStyle = TextStyle( | ||||||
|  |       color: Colors.blue, | ||||||
|  |       fontSize: Sizes.nameFontSize | ||||||
|  |     ); | ||||||
|  |     _bottomBarColor = Colors.red.shade100; | ||||||
|       switch (errorCode) { |       switch (errorCode) { | ||||||
|         case 9: |         case 9: | ||||||
|         case 11: |         case 11: | ||||||
|         case 7: |         case 7: | ||||||
|         case 1: { |         case 1: { | ||||||
|             action = SnackBarAction( |         _bottomBarAction = FlatButton( | ||||||
|                 label: "Retry", |                 child: Text("Retry", style: textStyle), | ||||||
|                 onPressed: () { |                 onPressed: () { | ||||||
|                   _scaffoldKey?.currentState?.hideCurrentSnackBar(); |                   //_scaffoldKey?.currentState?.hideCurrentSnackBar(); | ||||||
|                   _refreshData(); |                   _reLoad(); | ||||||
|                 }, |                 }, | ||||||
|             ); |             ); | ||||||
|             break; |             break; | ||||||
| @@ -381,122 +518,249 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver { | |||||||
|  |  | ||||||
|         case 5: { |         case 5: { | ||||||
|           message = "Check connection settings"; |           message = "Check connection settings"; | ||||||
|           action = SnackBarAction( |           _bottomBarAction = FlatButton( | ||||||
|             label: "Open", |               child: Text("Open", style: textStyle), | ||||||
|             onPressed: () { |             onPressed: () { | ||||||
|               _scaffoldKey?.currentState?.hideCurrentSnackBar(); |               //_scaffoldKey?.currentState?.hideCurrentSnackBar(); | ||||||
|               Navigator.pushNamed(context, '/connection-settings'); |               Navigator.pushNamed(context, '/connection-settings'); | ||||||
|             }, |             }, | ||||||
|           ); |           ); | ||||||
|           break; |           break; | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         case 6: { |         case 60: { | ||||||
|           action = SnackBarAction( |           _bottomBarAction = FlatButton( | ||||||
|             label: "Settings", |               child: Text("Login", style: textStyle), | ||||||
|             onPressed: () { |             onPressed: () { | ||||||
|               _scaffoldKey?.currentState?.hideCurrentSnackBar(); |               _reLoad(); | ||||||
|               Navigator.pushNamed(context, '/connection-settings'); |             }, | ||||||
|  |           ); | ||||||
|  |           break; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         case 63: | ||||||
|  |         case 61: { | ||||||
|  |           _bottomBarAction = FlatButton( | ||||||
|  |             child: Text("Try again", style: textStyle), | ||||||
|  |             onPressed: () { | ||||||
|  |               _reLoad(); | ||||||
|  |             }, | ||||||
|  |           ); | ||||||
|  |           break; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         case 62: { | ||||||
|  |           _bottomBarAction = FlatButton( | ||||||
|  |             child: Text("Login again", style: textStyle), | ||||||
|  |             onPressed: () { | ||||||
|  |               _reLoad(); | ||||||
|             }, |             }, | ||||||
|           ); |           ); | ||||||
|           break; |           break; | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         case 10: { |         case 10: { | ||||||
|           action = SnackBarAction( |           _bottomBarAction = FlatButton( | ||||||
|             label: "Refresh", |               child: Text("Refresh", style: textStyle), | ||||||
|             onPressed: () { |             onPressed: () { | ||||||
|               _scaffoldKey?.currentState?.hideCurrentSnackBar(); |               //_scaffoldKey?.currentState?.hideCurrentSnackBar(); | ||||||
|               _refreshData(); |               _reLoad(); | ||||||
|             }, |             }, | ||||||
|           ); |           ); | ||||||
|           break; |           break; | ||||||
|         } |         } | ||||||
|  |  | ||||||
|  |         case 82: | ||||||
|  |         case 81: | ||||||
|         case 8: { |         case 8: { | ||||||
|           action = SnackBarAction( |           _bottomBarAction = FlatButton( | ||||||
|             label: "Reconnect", |               child: Text("Reconnect", style: textStyle), | ||||||
|             onPressed: () { |             onPressed: () { | ||||||
|               _scaffoldKey?.currentState?.hideCurrentSnackBar(); |               _reLoad(); | ||||||
|               _refreshData(); |  | ||||||
|             }, |             }, | ||||||
|           ); |           ); | ||||||
|           break; |           break; | ||||||
|         } |         } | ||||||
|  |  | ||||||
|  |         default: { | ||||||
|  |           _bottomBarAction = Container(width: 0.0, height: 0.0,); | ||||||
|  |           break; | ||||||
|         } |         } | ||||||
|       _scaffoldKey.currentState.hideCurrentSnackBar(); |       } | ||||||
|       _scaffoldKey.currentState.showSnackBar( |       setState(() { | ||||||
|         SnackBar( |         _bottomBarProgress = false; | ||||||
|           content: Text("$message (code: $errorCode)"), |         _bottomBarText = "$message"; | ||||||
|           action: action, |         _showBottomBar = true; | ||||||
|           duration: Duration(hours: 1), |       }); | ||||||
|         ) |  | ||||||
|       ); |  | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   final GlobalKey<ScaffoldState> _scaffoldKey = new GlobalKey<ScaffoldState>(); |   final GlobalKey<ScaffoldState> _scaffoldKey = new GlobalKey<ScaffoldState>(); | ||||||
|  |  | ||||||
|   Scaffold _buildScaffold(bool empty) { |   Widget _buildScaffoldBody(bool empty) { | ||||||
|     return Scaffold( |     List<PopupMenuItem<String>> popupMenuItems = []; | ||||||
|       key: _scaffoldKey, |     popupMenuItems.add(PopupMenuItem<String>( | ||||||
|       appBar: AppBar( |       child: new Text("Reload"), | ||||||
|         title: _buildAppTitle(), |       value: "reload", | ||||||
|  |     )); | ||||||
|  |     if (widget.homeAssistant.connection.isAuthenticated) { | ||||||
|  |       popupMenuItems.add( | ||||||
|  |           PopupMenuItem<String>( | ||||||
|  |             child: new Text("Logout"), | ||||||
|  |             value: "logout", | ||||||
|  |           )); | ||||||
|  |     } | ||||||
|  |     return NestedScrollView( | ||||||
|  |       headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) { | ||||||
|  |         return <Widget>[ | ||||||
|  |           SliverAppBar( | ||||||
|  |             floating: true, | ||||||
|  |             pinned: true, | ||||||
|  |             primary: true, | ||||||
|  |             title: Text(widget.homeAssistant.locationName ?? ""), | ||||||
|  |             actions: <Widget>[ | ||||||
|  |               IconButton( | ||||||
|  |                 icon: Icon(MaterialDesignIcons.getIconDataFromIconName( | ||||||
|  |                     "mdi:dots-vertical"), color: Colors.white,), | ||||||
|  |                 onPressed: () { | ||||||
|  |                   showMenu( | ||||||
|  |                     position: RelativeRect.fromLTRB(MediaQuery.of(context).size.width, 70.0, 0.0, 0.0), | ||||||
|  |                     context: context, | ||||||
|  |                     items: popupMenuItems | ||||||
|  |                   ).then((String val) { | ||||||
|  |                     if (val == "reload") { | ||||||
|  |                       _reLoad(); | ||||||
|  |                     } else if (val == "logout") { | ||||||
|  |                       widget.homeAssistant.logout().then((_) { | ||||||
|  |                         _reLoad(); | ||||||
|  |                       }); | ||||||
|  |                     } | ||||||
|  |                   }); | ||||||
|  |                 } | ||||||
|  |               ) | ||||||
|  |             ], | ||||||
|             leading: IconButton( |             leading: IconButton( | ||||||
|               icon: Icon(Icons.menu), |               icon: Icon(Icons.menu), | ||||||
|               onPressed: () { |               onPressed: () { | ||||||
|                 _scaffoldKey.currentState.openDrawer(); |                 _scaffoldKey.currentState.openDrawer(); | ||||||
|             setState(() { |  | ||||||
|               _accountMenuExpanded = false; |  | ||||||
|             }); |  | ||||||
|               }, |               }, | ||||||
|             ), |             ), | ||||||
|             bottom: empty ? null : TabBar( |             bottom: empty ? null : TabBar( | ||||||
|  |               controller: _viewsTabController, | ||||||
|               tabs: buildUIViewTabs(), |               tabs: buildUIViewTabs(), | ||||||
|               isScrollable: true, |               isScrollable: true, | ||||||
|             ), |             ), | ||||||
|           ), |           ), | ||||||
|       drawer: _buildAppDrawer(), |  | ||||||
|  |         ]; | ||||||
|  |       }, | ||||||
|       body: empty ? |       body: empty ? | ||||||
|       Center( |       Center( | ||||||
|         child: Column( |         child: Column( | ||||||
|             mainAxisAlignment: MainAxisAlignment.center, |             mainAxisAlignment: MainAxisAlignment.center, | ||||||
|             children: [ |             children: [ | ||||||
|               Icon( |               Icon( | ||||||
|                   MaterialDesignIcons.createIconDataFromIconName("mdi:home-assistant"), |                 MaterialDesignIcons.getIconDataFromIconName("mdi:border-none-variant"), | ||||||
|                 size: 100.0, |                 size: 100.0, | ||||||
|                   color: _isLoading == 2 ? Colors.redAccent : Colors.blue, |                 color: Colors.black26, | ||||||
|               ), |               ), | ||||||
|             ] |             ] | ||||||
|         ), |         ), | ||||||
|       ) |       ) | ||||||
|           : |           : | ||||||
|         _homeAssistant.buildViews(context) |       widget.homeAssistant.buildViews(context, _viewsTabController), | ||||||
|     ); |     ); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|  |   TabController _viewsTabController; | ||||||
|  |  | ||||||
|   @override |   @override | ||||||
|   Widget build(BuildContext context) { |   Widget build(BuildContext context) { | ||||||
|  |     Widget bottomBar; | ||||||
|  |     if (_showBottomBar) { | ||||||
|  |       List<Widget> bottomBarChildren = []; | ||||||
|  |       if (_bottomBarText != null) { | ||||||
|  |         bottomBarChildren.add( | ||||||
|  |           Padding( | ||||||
|  |             padding: EdgeInsets.fromLTRB( | ||||||
|  |                 Sizes.leftWidgetPadding, Sizes.rowPadding, 0.0, | ||||||
|  |                 Sizes.rowPadding), | ||||||
|  |             child: Text( | ||||||
|  |               "$_bottomBarText", | ||||||
|  |               textAlign: TextAlign.left, | ||||||
|  |               softWrap: true, | ||||||
|  |             ), | ||||||
|  |           ) | ||||||
|  |  | ||||||
|  |         ); | ||||||
|  |       } | ||||||
|  |       if (_bottomBarProgress) { | ||||||
|  |         bottomBarChildren.add( | ||||||
|  |           CollectionScaleTransition( | ||||||
|  |             children: <Widget>[ | ||||||
|  |               Icon(Icons.stop, size: 10.0, color: EntityColor.stateColor(EntityState.on),), | ||||||
|  |               Icon(Icons.stop, size: 10.0, color: EntityColor.stateColor(EntityState.unavailable),), | ||||||
|  |               Icon(Icons.stop, size: 10.0, color: EntityColor.stateColor(EntityState.off),), | ||||||
|  |             ], | ||||||
|  |           ), | ||||||
|  |         ); | ||||||
|  |       } | ||||||
|  |       if (bottomBarChildren.isNotEmpty) { | ||||||
|  |         bottomBar = Container( | ||||||
|  |           color: _bottomBarColor, | ||||||
|  |           child: Row( | ||||||
|  |             mainAxisSize: MainAxisSize.max, | ||||||
|  |             children: <Widget>[ | ||||||
|  |               Expanded( | ||||||
|  |                 child: Column( | ||||||
|  |                   crossAxisAlignment: _bottomBarProgress ? CrossAxisAlignment.center : CrossAxisAlignment.start, | ||||||
|  |                   mainAxisSize: MainAxisSize.min, | ||||||
|  |                   children: bottomBarChildren, | ||||||
|  |                 ), | ||||||
|  |               ), | ||||||
|  |               _bottomBarAction | ||||||
|  |             ], | ||||||
|  |           ), | ||||||
|  |         ); | ||||||
|  |       } | ||||||
|  |     } | ||||||
|     // This method is rerun every time setState is called. |     // This method is rerun every time setState is called. | ||||||
|     if (_entities == null) { |     if (widget.homeAssistant.isNoViews) { | ||||||
|       return _buildScaffold(true); |       return Scaffold( | ||||||
|  |         key: _scaffoldKey, | ||||||
|  |         primary: false, | ||||||
|  |         drawer: _buildAppDrawer(), | ||||||
|  |         bottomNavigationBar: bottomBar, | ||||||
|  |         body: _buildScaffoldBody(true) | ||||||
|  |       ); | ||||||
|     } else { |     } else { | ||||||
|       return DefaultTabController( |       return Scaffold( | ||||||
|           length: _uiViewsCount, |         key: _scaffoldKey, | ||||||
|           child: _buildScaffold(false) |         drawer: _buildAppDrawer(), | ||||||
|  |         primary: false, | ||||||
|  |         bottomNavigationBar: bottomBar, | ||||||
|  |         body: HomeAssistantModel( | ||||||
|  |           child: _buildScaffoldBody(false), | ||||||
|  |           homeAssistant: widget.homeAssistant | ||||||
|  |         ), | ||||||
|       ); |       ); | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   @override |   @override | ||||||
|   void dispose() { |   void dispose() { | ||||||
|  |     final flutterWebviewPlugin = new FlutterWebviewPlugin(); | ||||||
|  |     flutterWebviewPlugin.dispose(); | ||||||
|     WidgetsBinding.instance.removeObserver(this); |     WidgetsBinding.instance.removeObserver(this); | ||||||
|     if (_stateSubscription != null) _stateSubscription.cancel(); |     _viewsTabController?.dispose(); | ||||||
|     if (_settingsSubscription != null) _settingsSubscription.cancel(); |     _stateSubscription?.cancel(); | ||||||
|     if (_serviceCallSubscription != null) _serviceCallSubscription.cancel(); |     _settingsSubscription?.cancel(); | ||||||
|     if (_showEntityPageSubscription != null) _showEntityPageSubscription.cancel(); |     _serviceCallSubscription?.cancel(); | ||||||
|     if (_refreshDataSubscription != null) _refreshDataSubscription.cancel(); |     _showEntityPageSubscription?.cancel(); | ||||||
|     if (_showErrorSubscription != null) _showErrorSubscription.cancel(); |     _showErrorSubscription?.cancel(); | ||||||
|     _homeAssistant.disconnect(); |     _startAuthSubscription?.cancel(); | ||||||
|  |     _reloadUISubscription?.cancel(); | ||||||
|  |     //TODO disconnect | ||||||
|  |     //widget.homeAssistant?.disconnect(); | ||||||
|     super.dispose(); |     super.dispose(); | ||||||
|   } |   } | ||||||
| } | } | ||||||
|   | |||||||
							
								
								
									
										40
									
								
								lib/panel.page.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,40 @@ | |||||||
|  | part of 'main.dart'; | ||||||
|  |  | ||||||
|  | class PanelPage extends StatefulWidget { | ||||||
|  |   PanelPage({Key key, this.title, this.panel}) : super(key: key); | ||||||
|  |  | ||||||
|  |   final String title; | ||||||
|  |   final Panel panel; | ||||||
|  |  | ||||||
|  |   @override | ||||||
|  |   _PanelPageState createState() => new _PanelPageState(); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | class _PanelPageState extends State<PanelPage> { | ||||||
|  |  | ||||||
|  |   List<ConfigurationItem> _items; | ||||||
|  |  | ||||||
|  |   @override | ||||||
|  |   void initState() { | ||||||
|  |     super.initState(); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   @override | ||||||
|  |   Widget build(BuildContext context) { | ||||||
|  |  | ||||||
|  |     return new Scaffold( | ||||||
|  |       appBar: new AppBar( | ||||||
|  |         leading: IconButton(icon: Icon(Icons.arrow_back), onPressed: (){ | ||||||
|  |           Navigator.pop(context); | ||||||
|  |         }), | ||||||
|  |         title: new Text(widget.title), | ||||||
|  |       ), | ||||||
|  |       body: widget.panel.getWidget(), | ||||||
|  |     ); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   @override | ||||||
|  |   void dispose() { | ||||||
|  |     super.dispose(); | ||||||
|  |   } | ||||||
|  | } | ||||||
| @@ -14,27 +14,18 @@ class _ConnectionSettingsPageState extends State<ConnectionSettingsPage> { | |||||||
|   String _newHassioDomain = ""; |   String _newHassioDomain = ""; | ||||||
|   String _hassioPort = ""; |   String _hassioPort = ""; | ||||||
|   String _newHassioPort = ""; |   String _newHassioPort = ""; | ||||||
|   String _hassioPassword = ""; |  | ||||||
|   String _newHassioPassword = ""; |  | ||||||
|   String _socketProtocol = "wss"; |   String _socketProtocol = "wss"; | ||||||
|   String _newSocketProtocol = "wss"; |   String _newSocketProtocol = "wss"; | ||||||
|   String _authType = "access_token"; |   bool _useLovelace = true; | ||||||
|   String _newAuthType = "access_token"; |   bool _newUseLovelace = true; | ||||||
|   bool _edited = false; |  | ||||||
|   FocusNode _domainFocusNode; |   String oauthUrl; | ||||||
|   FocusNode _portFocusNode; |  | ||||||
|   FocusNode _passwordFocusNode; |  | ||||||
|  |  | ||||||
|   @override |   @override | ||||||
|   void initState() { |   void initState() { | ||||||
|     super.initState(); |     super.initState(); | ||||||
|     _domainFocusNode = FocusNode(); |  | ||||||
|     _portFocusNode = FocusNode(); |  | ||||||
|     _passwordFocusNode = FocusNode(); |  | ||||||
|     _domainFocusNode.addListener(_checkConfigChanged); |  | ||||||
|     _portFocusNode.addListener(_checkConfigChanged); |  | ||||||
|     _passwordFocusNode.addListener(_checkConfigChanged); |  | ||||||
|     _loadSettings(); |     _loadSettings(); | ||||||
|  |  | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   _loadSettings() async { |   _loadSettings() async { | ||||||
| @@ -43,20 +34,22 @@ class _ConnectionSettingsPageState extends State<ConnectionSettingsPage> { | |||||||
|     setState(() { |     setState(() { | ||||||
|       _hassioDomain = _newHassioDomain = prefs.getString("hassio-domain")?? ""; |       _hassioDomain = _newHassioDomain = prefs.getString("hassio-domain")?? ""; | ||||||
|       _hassioPort = _newHassioPort = prefs.getString("hassio-port") ?? ""; |       _hassioPort = _newHassioPort = prefs.getString("hassio-port") ?? ""; | ||||||
|       _hassioPassword = _newHassioPassword = prefs.getString("hassio-password") ?? ""; |  | ||||||
|       _socketProtocol = _newSocketProtocol = prefs.getString("hassio-protocol") ?? 'wss'; |       _socketProtocol = _newSocketProtocol = prefs.getString("hassio-protocol") ?? 'wss'; | ||||||
|       _authType = _newAuthType = prefs.getString("hassio-auth-type") ?? 'access_token'; |       try { | ||||||
|  |         _useLovelace = _newUseLovelace = prefs.getBool("use-lovelace") ?? true; | ||||||
|  |       } catch (e) { | ||||||
|  |         _useLovelace = _newUseLovelace = true; | ||||||
|  |       } | ||||||
|     }); |     }); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   void _checkConfigChanged() { |   bool _checkConfigChanged() { | ||||||
|     setState(() { |     return ( | ||||||
|       _edited = ((_newHassioPassword != _hassioPassword) || |  | ||||||
|       (_newHassioPort != _hassioPort) || |       (_newHassioPort != _hassioPort) || | ||||||
|       (_newHassioDomain != _hassioDomain) || |       (_newHassioDomain != _hassioDomain) || | ||||||
|       (_newSocketProtocol != _socketProtocol) || |       (_newSocketProtocol != _socketProtocol) || | ||||||
|           (_newAuthType != _authType)); |       (_newUseLovelace != _useLovelace)); | ||||||
|     }); |  | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   _saveSettings() async { |   _saveSettings() async { | ||||||
| @@ -66,10 +59,9 @@ class _ConnectionSettingsPageState extends State<ConnectionSettingsPage> { | |||||||
|     SharedPreferences prefs = await SharedPreferences.getInstance(); |     SharedPreferences prefs = await SharedPreferences.getInstance(); | ||||||
|     prefs.setString("hassio-domain", _newHassioDomain); |     prefs.setString("hassio-domain", _newHassioDomain); | ||||||
|     prefs.setString("hassio-port", _newHassioPort); |     prefs.setString("hassio-port", _newHassioPort); | ||||||
|     prefs.setString("hassio-password", _newHassioPassword); |  | ||||||
|     prefs.setString("hassio-protocol", _newSocketProtocol); |     prefs.setString("hassio-protocol", _newSocketProtocol); | ||||||
|     prefs.setString("hassio-res-protocol", _newSocketProtocol == "wss" ? "https" : "http"); |     prefs.setString("hassio-res-protocol", _newSocketProtocol == "wss" ? "https" : "http"); | ||||||
|     prefs.setString("hassio-auth-type", _newAuthType); |     prefs.setBool("use-lovelace", _newUseLovelace); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   @override |   @override | ||||||
| @@ -83,26 +75,41 @@ class _ConnectionSettingsPageState extends State<ConnectionSettingsPage> { | |||||||
|         actions: <Widget>[ |         actions: <Widget>[ | ||||||
|           IconButton( |           IconButton( | ||||||
|             icon: Icon(Icons.check), |             icon: Icon(Icons.check), | ||||||
|             onPressed: _edited ? (){ |             onPressed: (){ | ||||||
|  |               if (_checkConfigChanged()) { | ||||||
|  |                 Logger.d("Settings changed. Saving..."); | ||||||
|                 _saveSettings().then((r) { |                 _saveSettings().then((r) { | ||||||
|                   Navigator.pop(context); |                   Navigator.pop(context); | ||||||
|                   eventBus.fire(SettingsChangedEvent(true)); |                   eventBus.fire(SettingsChangedEvent(true)); | ||||||
|                 }); |                 }); | ||||||
|             } : null |               } else { | ||||||
|  |                 Logger.d("Settings was not changed"); | ||||||
|  |                 Navigator.pop(context); | ||||||
|  |               } | ||||||
|  |             } | ||||||
|           ) |           ) | ||||||
|         ], |         ], | ||||||
|       ), |       ), | ||||||
|       body: ListView( |       body: ListView( | ||||||
|  |         scrollDirection: Axis.vertical, | ||||||
|         padding: const EdgeInsets.all(20.0), |         padding: const EdgeInsets.all(20.0), | ||||||
|         children: <Widget>[ |         children: <Widget>[ | ||||||
|  |           Text( | ||||||
|  |               "Connection settings", | ||||||
|  |               style: TextStyle( | ||||||
|  |                 color: Colors.black45, | ||||||
|  |                 fontSize: 20.0 | ||||||
|  |               ), | ||||||
|  |           ), | ||||||
|           new Row( |           new Row( | ||||||
|             children: [ |             children: [ | ||||||
|               Text("Use ssl (HTTPS)"), |               Text("Use ssl (HTTPS)"), | ||||||
|               Switch( |               Switch( | ||||||
|                 value: (_newSocketProtocol == "wss"), |                 value: (_newSocketProtocol == "wss"), | ||||||
|                 onChanged: (value) { |                 onChanged: (value) { | ||||||
|  |                   setState(() { | ||||||
|                     _newSocketProtocol = value ? "wss" : "ws"; |                     _newSocketProtocol = value ? "wss" : "ws"; | ||||||
|                   _checkConfigChanged(); |                   }); | ||||||
|                 }, |                 }, | ||||||
|               ) |               ) | ||||||
|             ], |             ], | ||||||
| @@ -120,9 +127,7 @@ class _ConnectionSettingsPageState extends State<ConnectionSettingsPage> { | |||||||
|             ), |             ), | ||||||
|             onChanged: (value) { |             onChanged: (value) { | ||||||
|               _newHassioDomain = value; |               _newHassioDomain = value; | ||||||
|             }, |             } | ||||||
|             focusNode: _domainFocusNode, |  | ||||||
|             onEditingComplete: _checkConfigChanged, |  | ||||||
|           ), |           ), | ||||||
|           new TextField( |           new TextField( | ||||||
|             decoration: InputDecoration( |             decoration: InputDecoration( | ||||||
| @@ -137,42 +142,35 @@ class _ConnectionSettingsPageState extends State<ConnectionSettingsPage> { | |||||||
|             ), |             ), | ||||||
|             onChanged: (value) { |             onChanged: (value) { | ||||||
|               _newHassioPort = value; |               _newHassioPort = value; | ||||||
|               //_saveSettings(); |             } | ||||||
|             }, |           ), | ||||||
|             focusNode: _portFocusNode, |           new Text( | ||||||
|             onEditingComplete: _checkConfigChanged, |             "Try ports 80 and 443 if default is not working and you don't know why.", | ||||||
|  |             style: TextStyle(color: Colors.grey), | ||||||
|  |           ), | ||||||
|  |           Padding( | ||||||
|  |             padding: EdgeInsets.only(top: 20.0), | ||||||
|  |             child: Text( | ||||||
|  |               "UI", | ||||||
|  |               style: TextStyle( | ||||||
|  |                   color: Colors.black45, | ||||||
|  |                   fontSize: 20.0 | ||||||
|  |               ), | ||||||
|  |             ), | ||||||
|           ), |           ), | ||||||
|           new Row( |           new Row( | ||||||
|             children: [ |             children: [ | ||||||
|               Text("Login with access token (HA >= 0.78.0)"), |               Text("Use Lovelace UI"), | ||||||
|               Switch( |               Switch( | ||||||
|                 value: (_newAuthType == "access_token"), |                 value: _newUseLovelace, | ||||||
|                 onChanged: (value) { |                 onChanged: (value) { | ||||||
|                   _newAuthType = value ? "access_token" : "api_password"; |                   setState(() { | ||||||
|                   _checkConfigChanged(); |                     _newUseLovelace = value; | ||||||
|                   //_saveSettings(); |                   }); | ||||||
|                 }, |                 }, | ||||||
|               ) |               ) | ||||||
|             ], |             ], | ||||||
|           ), |           ), | ||||||
|           new TextField( |  | ||||||
|             decoration: InputDecoration( |  | ||||||
|                 labelText: _authType == "access_token" ? "Access token" : "API password" |  | ||||||
|             ), |  | ||||||
|             controller: new TextEditingController.fromValue( |  | ||||||
|                 new TextEditingValue( |  | ||||||
|                     text: _newHassioPassword, |  | ||||||
|                     selection: |  | ||||||
|                     new TextSelection.collapsed(offset: _newHassioPassword.length) |  | ||||||
|                 ) |  | ||||||
|             ), |  | ||||||
|             onChanged: (value) { |  | ||||||
|               _newHassioPassword = value; |  | ||||||
|               //_saveSettings(); |  | ||||||
|             }, |  | ||||||
|             focusNode: _passwordFocusNode, |  | ||||||
|             onEditingComplete: _checkConfigChanged, |  | ||||||
|           ) |  | ||||||
|         ], |         ], | ||||||
|       ), |       ), | ||||||
|     ); |     ); | ||||||
| @@ -180,12 +178,6 @@ class _ConnectionSettingsPageState extends State<ConnectionSettingsPage> { | |||||||
|  |  | ||||||
|   @override |   @override | ||||||
|   void dispose() { |   void dispose() { | ||||||
|     _domainFocusNode.removeListener(_checkConfigChanged); |  | ||||||
|     _portFocusNode.removeListener(_checkConfigChanged); |  | ||||||
|     _passwordFocusNode.removeListener(_checkConfigChanged); |  | ||||||
|     _domainFocusNode.dispose(); |  | ||||||
|     _portFocusNode.dispose(); |  | ||||||
|     _passwordFocusNode.dispose(); |  | ||||||
|     super.dispose(); |     super.dispose(); | ||||||
|   } |   } | ||||||
| } | } | ||||||
|   | |||||||
							
								
								
									
										50
									
								
								lib/ui_class/card.class.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,50 @@ | |||||||
|  | part of '../main.dart'; | ||||||
|  |  | ||||||
|  | class HACard { | ||||||
|  |   List<EntityWrapper> entities = []; | ||||||
|  |   List<HACard> childCards = []; | ||||||
|  |   EntityWrapper linkedEntityWrapper; | ||||||
|  |   String name; | ||||||
|  |   String id; | ||||||
|  |   String type; | ||||||
|  |   bool showName; | ||||||
|  |   bool showState; | ||||||
|  |   bool showEmpty; | ||||||
|  |   int columnsCount; | ||||||
|  |   List stateFilter; | ||||||
|  |   List states; | ||||||
|  |   String content; | ||||||
|  |  | ||||||
|  |   HACard({ | ||||||
|  |     this.name, | ||||||
|  |     this.id, | ||||||
|  |     this.linkedEntityWrapper, | ||||||
|  |     this.columnsCount: 4, | ||||||
|  |     this.showName: true, | ||||||
|  |     this.showState: true, | ||||||
|  |     this.stateFilter: const [], | ||||||
|  |     this.showEmpty: true, | ||||||
|  |     this.content, | ||||||
|  |     this.states, | ||||||
|  |     @required this.type | ||||||
|  |   }); | ||||||
|  |  | ||||||
|  |   List<EntityWrapper> getEntitiesToShow() { | ||||||
|  |     return entities.where((entityWrapper) { | ||||||
|  |       if (entityWrapper.entity.isHidden) { | ||||||
|  |         return false; | ||||||
|  |       } | ||||||
|  |       if (stateFilter.isNotEmpty) { | ||||||
|  |         return stateFilter.contains(entityWrapper.entity.state); | ||||||
|  |       } | ||||||
|  |       return true; | ||||||
|  |     }).toList(); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   Widget build(BuildContext context) { | ||||||
|  |     return CardWidget( | ||||||
|  |       card: this, | ||||||
|  |     ); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  | } | ||||||
							
								
								
									
										58
									
								
								lib/ui_class/panel_class.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,58 @@ | |||||||
|  | part of '../main.dart'; | ||||||
|  |  | ||||||
|  | class Panel { | ||||||
|  |  | ||||||
|  |   static const iconsByComponent = { | ||||||
|  |     "config": "mdi:settings", | ||||||
|  |     "history": "mdi:poll-box", | ||||||
|  |     "map": "mdi:tooltip-account", | ||||||
|  |     "logbook": "mdi:format-list-bulleted-type", | ||||||
|  |     "custom": "mdi:home-assistant" | ||||||
|  |   }; | ||||||
|  |  | ||||||
|  |   final String id; | ||||||
|  |   final String type; | ||||||
|  |   final String title; | ||||||
|  |   final String urlPath; | ||||||
|  |   final Map config; | ||||||
|  |   String icon; | ||||||
|  |   bool isHidden = true; | ||||||
|  |  | ||||||
|  |   Panel({this.id, this.type, this.title, this.urlPath, this.icon, this.config}) { | ||||||
|  |     if (icon == null || !icon.startsWith("mdi:")) { | ||||||
|  |       icon = Panel.iconsByComponent[type]; | ||||||
|  |     } | ||||||
|  |     isHidden = (type != "iframe" && type != "config"); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   void handleOpen(BuildContext context) { | ||||||
|  |     if (type == "iframe") { | ||||||
|  |       Logger.d("Launching custom tab with ${config["url"]}"); | ||||||
|  |       HAUtils.launchURLInCustomTab(context, config["url"]); | ||||||
|  |     } else if (type == "config") { | ||||||
|  |       Navigator.of(context).push( | ||||||
|  |           MaterialPageRoute( | ||||||
|  |             builder: (context) => PanelPage(title: "$title", panel: this), | ||||||
|  |           ) | ||||||
|  |       ); | ||||||
|  |     } else { | ||||||
|  |       HomeAssistantModel haModel = HomeAssistantModel.of(context); | ||||||
|  |       String url = "${haModel.homeAssistant.connection.httpWebHost}/$urlPath"; | ||||||
|  |       Logger.d("Launching custom tab with $url"); | ||||||
|  |       HAUtils.launchURLInCustomTab(context, url); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   Widget getWidget() { | ||||||
|  |     switch (type) { | ||||||
|  |       case "config": { | ||||||
|  |         return ConfigPanelWidget(); | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       default: { | ||||||
|  |         return Text("Unsupported panel component: $type"); | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  | } | ||||||
							
								
								
									
										16
									
								
								lib/ui_class/sizes_class.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,16 @@ | |||||||
|  | part of '../main.dart'; | ||||||
|  |  | ||||||
|  | class Sizes { | ||||||
|  |   static const rightWidgetPadding = 16.0; | ||||||
|  |   static const leftWidgetPadding = 16.0; | ||||||
|  |   static const buttonPadding = 4.0; | ||||||
|  |   static const extendedWidgetHeight = 50.0; | ||||||
|  |   static const iconSize = 28.0; | ||||||
|  |   static const largeIconSize = 46.0; | ||||||
|  |   static const stateFontSize = 15.0; | ||||||
|  |   static const nameFontSize = 15.0; | ||||||
|  |   static const smallFontSize = 14.0; | ||||||
|  |   static const largeFontSize = 24.0; | ||||||
|  |   static const inputWidth = 160.0; | ||||||
|  |   static const rowPadding = 10.0; | ||||||
|  | } | ||||||
							
								
								
									
										34
									
								
								lib/ui_class/ui.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,34 @@ | |||||||
|  | part of '../main.dart'; | ||||||
|  |  | ||||||
|  | class HomeAssistantUI { | ||||||
|  |   List<HAView> views; | ||||||
|  |   String title; | ||||||
|  |  | ||||||
|  |   bool get isEmpty => views == null || views.isEmpty; | ||||||
|  |  | ||||||
|  |   HomeAssistantUI() { | ||||||
|  |     views = []; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   Widget build(BuildContext context, TabController tabController) { | ||||||
|  |     return TabBarView( | ||||||
|  |       controller: tabController, | ||||||
|  |       children: _buildViews(context) | ||||||
|  |     ); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   List<Widget> _buildViews(BuildContext context) { | ||||||
|  |     List<Widget> result = []; | ||||||
|  |     views.forEach((view) { | ||||||
|  |       result.add( | ||||||
|  |         view.build(context) | ||||||
|  |       ); | ||||||
|  |     }); | ||||||
|  |     return result; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   void clear() { | ||||||
|  |     views.clear(); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  | } | ||||||
							
								
								
									
										114
									
								
								lib/ui_class/view.class.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,114 @@ | |||||||
|  | part of '../main.dart'; | ||||||
|  |  | ||||||
|  | class HAView { | ||||||
|  |   List<HACard> cards = []; | ||||||
|  |   List<Entity> badges = []; | ||||||
|  |   Entity linkedEntity; | ||||||
|  |   String name; | ||||||
|  |   String id; | ||||||
|  |   String iconName; | ||||||
|  |   int count; | ||||||
|  |  | ||||||
|  |   HAView({ | ||||||
|  |     this.name, | ||||||
|  |     this.id, | ||||||
|  |     this.count, | ||||||
|  |     this.iconName, | ||||||
|  |     List<Entity> childEntities | ||||||
|  |   }) { | ||||||
|  |     if (childEntities != null) { | ||||||
|  |       _fillView(childEntities); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   void _fillView(List<Entity> childEntities) { | ||||||
|  |     List<HACard> autoGeneratedCards = []; | ||||||
|  |     badges.addAll(childEntities.where((entity){ return entity.isBadge;})); | ||||||
|  |     childEntities.where((entity){ return entity.domain == "media_player";}).forEach((e){ | ||||||
|  |       HACard card = HACard( | ||||||
|  |           name: e.displayName, | ||||||
|  |           id: e.entityId, | ||||||
|  |           linkedEntityWrapper: EntityWrapper(entity: e), | ||||||
|  |           type: CardType.mediaControl | ||||||
|  |       ); | ||||||
|  |       cards.add(card); | ||||||
|  |     }); | ||||||
|  |     childEntities.where((e){return (!e.isBadge && e.domain != "media_player");}).forEach((entity) { | ||||||
|  |       if (!entity.isGroup) { | ||||||
|  |         String groupIdToAdd = "${entity.domain}.${entity.domain}$count"; | ||||||
|  |         if (autoGeneratedCards.every((HACard card) => card.id != groupIdToAdd )) { | ||||||
|  |           HACard card = HACard( | ||||||
|  |               id: groupIdToAdd, | ||||||
|  |               name: entity.domain, | ||||||
|  |               type: CardType.entities | ||||||
|  |           ); | ||||||
|  |           card.entities.add(EntityWrapper(entity: entity)); | ||||||
|  |           autoGeneratedCards.add(card); | ||||||
|  |         } else { | ||||||
|  |           autoGeneratedCards.firstWhere((card) => card.id == groupIdToAdd).entities.add(EntityWrapper(entity: entity)); | ||||||
|  |         } | ||||||
|  |       } else { | ||||||
|  |         HACard card = HACard( | ||||||
|  |             name: entity.displayName, | ||||||
|  |             id: entity.entityId, | ||||||
|  |             linkedEntityWrapper: EntityWrapper(entity: entity), | ||||||
|  |             type: CardType.entities | ||||||
|  |         ); | ||||||
|  |         card.entities.addAll(entity.childEntities.where((entity) {return entity.domain != "media_player";}).map((e) {return EntityWrapper(entity: e);})); | ||||||
|  |         entity.childEntities.where((entity) {return entity.domain == "media_player";}).forEach((entity){ | ||||||
|  |           HACard mediaCard = HACard( | ||||||
|  |               name: entity.displayName, | ||||||
|  |               id: entity.entityId, | ||||||
|  |               linkedEntityWrapper: EntityWrapper(entity: entity), | ||||||
|  |               type: CardType.mediaControl | ||||||
|  |           ); | ||||||
|  |           cards.add(mediaCard); | ||||||
|  |         }); | ||||||
|  |         cards.add(card); | ||||||
|  |       } | ||||||
|  |     }); | ||||||
|  |     cards.addAll(autoGeneratedCards); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   Widget buildTab() { | ||||||
|  |     if (linkedEntity == null) { | ||||||
|  |       if (iconName != null) { | ||||||
|  |         return | ||||||
|  |           Tab( | ||||||
|  |               icon: | ||||||
|  |               Icon( | ||||||
|  |                 MaterialDesignIcons.getIconDataFromIconName( | ||||||
|  |                     iconName ?? "mdi:home-assistant"), | ||||||
|  |                 size: 24.0, | ||||||
|  |               ) | ||||||
|  |           ); | ||||||
|  |       } else { | ||||||
|  |         return | ||||||
|  |           Tab( | ||||||
|  |               text: name.toUpperCase(), | ||||||
|  |           ); | ||||||
|  |       } | ||||||
|  |     } else { | ||||||
|  |       if (linkedEntity.icon != null && linkedEntity.icon.length > 0) { | ||||||
|  |         return Tab( | ||||||
|  |           icon: Icon( | ||||||
|  |               MaterialDesignIcons.getIconDataFromIconName( | ||||||
|  |                   linkedEntity.icon), | ||||||
|  |               size: 24.0, | ||||||
|  |             ) | ||||||
|  |         ); | ||||||
|  |       } else { | ||||||
|  |         return Tab( | ||||||
|  |             text: linkedEntity.displayName.toUpperCase(), | ||||||
|  |         ); | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   Widget build(BuildContext context) { | ||||||
|  |     return ViewWidget( | ||||||
|  |       view: this, | ||||||
|  |     ); | ||||||
|  |   } | ||||||
|  | } | ||||||