Compare commits
455 Commits
Author | SHA1 | Date | |
---|---|---|---|
753df3c724 | |||
dc62a08da3 | |||
56a333a852 | |||
c5922368de | |||
8c2316a51a | |||
e2e6c015de | |||
0a6ff4586d | |||
fc228d85ae | |||
61823cb43b | |||
127e0b8182 | |||
38c37fa212 | |||
dfaf2a2924 | |||
c90c40c046 | |||
d2049b726a | |||
6508f109f7 | |||
37e63637a7 | |||
6650c5c145 | |||
9160dbf7f2 | |||
243fcd7c49 | |||
c114bcfb35 | |||
83defb08f1 | |||
57ebdbbe85 | |||
c6aceed623 | |||
ba4c88ec5d | |||
ee1685e981 | |||
996fbf7bba | |||
56cd8963d7 | |||
5759aad0cb | |||
02717332f7 | |||
8d1b159f56 | |||
fb335e1100 | |||
5f0bc83d67 | |||
6a8cee2cc2 | |||
0d2f1cf9aa | |||
8efeb3da8a | |||
620aa3b8d8 | |||
ab5bf3b807 | |||
6663bcad72 | |||
113cd29f74 | |||
f2fdfb0a32 | |||
691e48a36b | |||
2036cc117f | |||
389d28a1e1 | |||
27e6198d83 | |||
de762a4878 | |||
e8efefe25d | |||
21f3e8985a | |||
622543d405 | |||
abdc0fc1c8 | |||
1ecb839042 | |||
cece4d1e16 | |||
623634cb6e | |||
f9c37f5084 | |||
3e12f4f8a4 | |||
b07ff6fe71 | |||
5a3b57c28e | |||
e858eee83b | |||
73f00d3bd7 | |||
eea59cf11b | |||
61b459ed8a | |||
dca8c309aa | |||
be53500104 | |||
bc1a791608 | |||
b112ff980a | |||
7beab9ae93 | |||
8c0d1f90a3 | |||
05c05ba768 | |||
67e885e76a | |||
594bce0b8d | |||
7f6569e0db | |||
1c829c8364 | |||
7ca4b02e6d | |||
fadfefd836 | |||
37155901ef | |||
fbbb96409d | |||
5126c54914 | |||
916d0b7e3c | |||
0815840a9c | |||
bc237796b2 | |||
7f44800f64 | |||
85ac746e9d | |||
8215175098 | |||
39ee8b1799 | |||
c76d3d68c8 | |||
cde257922b | |||
be0c9d3372 | |||
66cd7ea307 | |||
b704ce6984 | |||
247c856a41 | |||
9afaebfa12 | |||
929abea5d3 | |||
13102a6b04 | |||
57c3083f9f | |||
5c31ddd00f | |||
8f55be187d | |||
1fe82d8b0d | |||
cbc56a8105 | |||
b63cddfa46 | |||
91db82f730 | |||
0c4d1b78ff | |||
5af2fd0562 | |||
2375543ebf | |||
de187f3ed5 | |||
9266ffacf3 | |||
3c0ca5d16d | |||
caabf25260 | |||
0af2afbb80 | |||
12d226509d | |||
3417c38426 | |||
c7fc5afbb8 | |||
11f565a9dc | |||
53240faac3 | |||
95d4878785 | |||
ef15026203 | |||
ad6355503b | |||
491c2b0dc0 | |||
5b99ade088 | |||
e1d9d9f304 | |||
209ccd4f7f | |||
5a8a207f2e | |||
19c85d9c16 | |||
a916ddfa50 | |||
8c1ad9c7f9 | |||
93af1eca7e | |||
cabf836fa3 | |||
15b3d31a6f | |||
9b98689012 | |||
84ebd0c33c | |||
ccd7774931 | |||
b2773635f5 | |||
8b046b7313 | |||
885a516676 | |||
921b0e09b0 | |||
277c67fc6f | |||
2a01ff8a03 | |||
b246b7bc1d | |||
e1868b9a14 | |||
125f3ac16c | |||
be502b5668 | |||
6f33fdca9f | |||
a7cda2a35e | |||
102b10ade0 | |||
4e96b9adbb | |||
b9581d3762 | |||
7c010359c3 | |||
4a75243994 | |||
d29d7e5b3b | |||
5ebd25e0d1 | |||
b7d5a53e86 | |||
20d3498bfd | |||
67d7bb45f5 | |||
6a03105d01 | |||
5ae580ecf1 | |||
0efef33e53 | |||
ccb88884a7 | |||
d70ba0a55a | |||
5140840d3a | |||
14759fd3c9 | |||
fed35be517 | |||
db77cc43aa | |||
b2269cc96d | |||
8b28bb2e9e | |||
fb456878bc | |||
8b961ebd69 | |||
9bd3a41cf5 | |||
491ae55a2a | |||
e1d2981782 | |||
74572168ae | |||
92d0b5c055 | |||
3504d3276c | |||
736b38b64c | |||
cb118b599a | |||
a08a056cff | |||
0ef2ebfe31 | |||
4f4ac3b574 | |||
7064cb0e30 | |||
91a99e17e0 | |||
2e9b7d20b9 | |||
b8aa808de4 | |||
2cfa92a42b | |||
146efef72d | |||
8c9804e16f | |||
a4736bfb5a | |||
15c54df629 | |||
32ffef21e9 | |||
848d3cb510 | |||
8a4caeebba | |||
aa923f0fba | |||
4d8f50ddd5 | |||
fe06b21a6c | |||
efed7fb1b5 | |||
df2cbb7d13 | |||
03edaa9ca2 | |||
1a7457abf9 | |||
00889b13e0 | |||
0615073ec4 | |||
eb7d17d147 | |||
24f80feeee | |||
4b6dda5a9c | |||
4099fa0c83 | |||
76057e8797 | |||
538d3603dc | |||
bc0e72ca52 | |||
f25a47beb2 | |||
cc3c6b0087 | |||
6cf80c0bfd | |||
8ce9bdb7a5 | |||
31e50150b1 | |||
e359150d97 | |||
93680c981c | |||
e06b66c523 | |||
3dea844e1e | |||
62b1af30e0 | |||
e006c4e403 | |||
983573388e | |||
bdd1dc7e17 | |||
9c1970ee14 | |||
d0e0bf3571 | |||
b399357517 | |||
0290cd3a32 | |||
d8a1d03179 | |||
216fad3cb9 | |||
fead6ea348 | |||
8814687be6 | |||
71c0e2caa0 | |||
1531c41542 | |||
bc90d013e8 | |||
2adfaca0c4 | |||
6cc1a37d9d | |||
4bb616b327 | |||
38219618ba | |||
6774b53758 | |||
29a94c882f | |||
5897fa3a99 | |||
7af92c2dc9 | |||
1094177a42 | |||
5e814e8109 | |||
24c7675fa4 | |||
dc3ca38c78 | |||
96b528e055 | |||
3858036631 | |||
19d42ceeb3 | |||
a2836a3603 | |||
2a45758a6d | |||
dc1bf4d878 | |||
e82ba60c4e | |||
09199d30e8 | |||
724d32dbe2 | |||
949c8ee44e | |||
1a446d34c7 | |||
22a5847285 | |||
1c8f770f10 | |||
be5ea55f6b | |||
c65ade9827 | |||
d3c1422b9e | |||
b6ac9f985f | |||
a59de4b6dc | |||
f507d5df0c | |||
f77e46de37 | |||
cda17b1217 | |||
be560769ef | |||
3815800e32 | |||
a3226311a2 | |||
79669243c2 | |||
fdc81f6ea4 | |||
7fe44459e7 | |||
a8500d44e1 | |||
b4d4c5abec | |||
c19a3f272a | |||
b264534858 | |||
ab53f77f9e | |||
c73956720c | |||
051041e794 | |||
5c83be9fee | |||
4bece42693 | |||
4ae107fe4c | |||
9523ed2562 | |||
9c403480e2 | |||
20b1b90e39 | |||
5633e30448 | |||
4492fb9f0c | |||
36410752e4 | |||
0219f7bfbb | |||
5f3c77f4b9 | |||
a36c7a9ca3 | |||
56ce6dfeeb | |||
67c214454f | |||
73398378c4 | |||
215871ce9e | |||
fd8ea6befd | |||
809a1a1c8c | |||
fc8f2f200f | |||
f41c9f9197 | |||
cdf55ce68b | |||
12088d9516 | |||
a0235ee385 | |||
67fbdb13c6 | |||
c5960de0be | |||
da15e880ec | |||
efbe33f4e3 | |||
af84c99a2d | |||
438449cad8 | |||
d9ca55c3b7 | |||
f248268984 | |||
8ee096595c | |||
a8e79c289b | |||
2cd8533882 | |||
0a21d9c690 | |||
e77bb533b1 | |||
96f1211395 | |||
1e4cb03470 | |||
ab67b557ca | |||
82c9bd26d1 | |||
1bd04abd37 | |||
c5942d22b3 | |||
37ad5e81cf | |||
26187e6233 | |||
b8f6fda8d3 | |||
62b4e99810 | |||
25bf10a64e | |||
874410964d | |||
57c30917b3 | |||
87f89b63e1 | |||
3190b45db3 | |||
f5434e26e5 | |||
86b6ad6bba | |||
8a9641fbed | |||
5142391da2 | |||
01090dc3b1 | |||
0a7bbb5a38 | |||
c347eee9f0 | |||
90f197ba54 | |||
e09917c687 | |||
a69da832cb | |||
c1708fd980 | |||
c85a9bbe27 | |||
d9790dedbb | |||
30e4eaa023 | |||
54e00c3403 | |||
0e3474bbcb | |||
efd06ca547 | |||
69fd37d4fe | |||
4a49372410 | |||
478f58e2d8 | |||
a87aff67ac | |||
644f5e7fc6 | |||
3cddac3dc6 | |||
ab30c64eab | |||
6d79487219 | |||
9f7444eae0 | |||
788d682f2f | |||
66f84952f0 | |||
5d95c3702d | |||
1f0bd8059b | |||
a7830df628 | |||
790446d592 | |||
bb17885b4a | |||
04d8681656 | |||
71c4ac7fed | |||
3f7e21e97e | |||
e24c47b041 | |||
73b32b30a8 | |||
5b6155057c | |||
ff4185effe | |||
b2da9fc04d | |||
f281fab744 | |||
3b99f4feeb | |||
efab8b60b1 | |||
0e96406573 | |||
ed8757c08d | |||
813770329c | |||
1853bd466e | |||
07258477b3 | |||
a3adb72cf8 | |||
e25162f7b5 | |||
d30c9d574b | |||
efa5a1958c | |||
37f20fae5a | |||
91db34badb | |||
c20200b609 | |||
fcd4ac7292 | |||
e16338c3f2 | |||
6e038b0685 | |||
052cd3894e | |||
809c7d6355 | |||
9edfec7dff | |||
df56f6ceda | |||
5e834b0645 | |||
8fb0d61a84 | |||
54979b583b | |||
4e955e98d8 | |||
88cfcb4382 | |||
5338e45ddc | |||
24d071e2f8 | |||
988cd4a72f | |||
d1ea916781 | |||
ce9f25b86c | |||
f29762c931 | |||
30e4496ef1 | |||
7f9dc5dd3a | |||
0f6babc243 | |||
6a43e04b31 | |||
36fa5a50c4 | |||
9ad6d92ccd | |||
fafa8f43f4 | |||
9b490d33d5 | |||
33f9a1075e | |||
b83006e2c3 | |||
ba09c36bd2 | |||
c71ee568b0 | |||
75041f5c23 | |||
14da471774 | |||
369b44f1c8 | |||
8284bb6e76 | |||
9b3b4dfbbc | |||
5ca4424933 | |||
a308aa29a4 | |||
9e80b0eaaf | |||
85379cf491 | |||
758376a891 | |||
2ebba364e3 | |||
6e604440c0 | |||
c23034688e | |||
69f45b52cf | |||
ffc053fbe6 | |||
b5f9ecf601 | |||
948d1d4e23 | |||
136297c18b | |||
164800951d | |||
84d283de2b | |||
2fa35d771a | |||
326cd073b9 | |||
e99c3f5742 | |||
16a9392fa6 | |||
5bf063969b | |||
c19a0511a6 | |||
a4ac40b366 | |||
ce69f044fb | |||
70b6469bd1 | |||
253316fb1f | |||
ec71200ab0 | |||
bc1f4eab2e | |||
4085006446 | |||
b7fb821abe | |||
284e7ba451 | |||
17a3bd8d35 | |||
c2b88c8a12 | |||
c975af4c79 | |||
debf1b71f1 | |||
4725953b32 | |||
e7ca1209e2 | |||
f9afa663f5 | |||
5068cbbcf4 | |||
043d3a9905 | |||
77c5f80c13 |
2
.gitignore
vendored
@ -11,3 +11,5 @@ build/
|
|||||||
.idea/
|
.idea/
|
||||||
|
|
||||||
key.properties
|
key.properties
|
||||||
|
premium_features_manager.class.dart
|
||||||
|
pubspec.lock
|
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.
|
10
README.md
@ -1,3 +1,9 @@
|
|||||||
# Android client for Home Assistant
|
# 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.
|
||||||
|
|
||||||
|
Download the app from [Google Play](https://play.google.com/apps/testing/com.keyboardcrumbs.haclient)
|
||||||
|
|
||||||
|
Discuss it in [Home Assistant community](https://community.home-assistant.io/t/alpha-testing-ha-client-native-android-client-for-home-assistant/69912) or on [Discord server](https://discord.gg/AUzEvwn)
|
||||||
|
@ -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"
|
||||||
|
}
|
@ -6,6 +6,11 @@
|
|||||||
to allow setting breakpoints, to provide hot reload, etc.
|
to allow setting breakpoints, to provide hot reload, etc.
|
||||||
-->
|
-->
|
||||||
<uses-permission android:name="android.permission.INTERNET"/>
|
<uses-permission android:name="android.permission.INTERNET"/>
|
||||||
|
<uses-permission android:name="android.permission.VIBRATE" />
|
||||||
|
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
|
||||||
|
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
|
||||||
|
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
|
||||||
|
<uses-permission android:name="android.permission.WAKE_LOCK"/>
|
||||||
|
|
||||||
<!-- io.flutter.app.FlutterApplication is an android.app.Application that
|
<!-- io.flutter.app.FlutterApplication is an android.app.Application that
|
||||||
calls FlutterMain.startInitialization(this); in its onCreate method.
|
calls FlutterMain.startInitialization(this); in its onCreate method.
|
||||||
@ -13,9 +18,15 @@
|
|||||||
additional functionality it is fine to subclass or reimplement
|
additional functionality it is fine to subclass or reimplement
|
||||||
FlutterApplication and put your custom class here. -->
|
FlutterApplication and put your custom class here. -->
|
||||||
<application
|
<application
|
||||||
android:name="io.flutter.app.FlutterApplication"
|
android:name=".Application"
|
||||||
android:label="HA Client"
|
android:label="HA Client"
|
||||||
android:icon="@mipmap/ic_launcher">
|
android:icon="@mipmap/ic_launcher"
|
||||||
|
android:usesCleartextTraffic="true">
|
||||||
|
|
||||||
|
<meta-data
|
||||||
|
android:name="com.google.firebase.messaging.default_notification_channel_id"
|
||||||
|
android:value="ha_notify" />
|
||||||
|
|
||||||
<activity
|
<activity
|
||||||
android:name=".MainActivity"
|
android:name=".MainActivity"
|
||||||
android:launchMode="singleTop"
|
android:launchMode="singleTop"
|
||||||
@ -26,14 +37,33 @@
|
|||||||
<!-- This keeps the window background of the activity showing
|
<!-- This keeps the window background of the activity showing
|
||||||
until Flutter renders its first frame. It can be removed if
|
until Flutter renders its first frame. It can be removed if
|
||||||
there is no splash screen (such as the default splash screen
|
there is no splash screen (such as the default splash screen
|
||||||
defined in @style/LaunchTheme). -->
|
defined in @style/LaunchTheme).
|
||||||
<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"/>
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</activity>
|
</activity>
|
||||||
|
|
||||||
|
<service
|
||||||
|
android:name="io.flutter.plugins.androidalarmmanager.AlarmService"
|
||||||
|
android:permission="android.permission.BIND_JOB_SERVICE"
|
||||||
|
android:exported="false"/>
|
||||||
|
<receiver
|
||||||
|
android:name="io.flutter.plugins.androidalarmmanager.AlarmBroadcastReceiver"
|
||||||
|
android:exported="false"/>
|
||||||
|
<receiver
|
||||||
|
android:name="io.flutter.plugins.androidalarmmanager.RebootBroadcastReceiver"
|
||||||
|
android:enabled="false">
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.intent.action.BOOT_COMPLETED"></action>
|
||||||
|
</intent-filter>
|
||||||
|
</receiver>
|
||||||
</application>
|
</application>
|
||||||
</manifest>
|
</manifest>
|
@ -0,0 +1,18 @@
|
|||||||
|
package com.keyboardcrumbs.hassclient;
|
||||||
|
|
||||||
|
import io.flutter.app.FlutterApplication;
|
||||||
|
import io.flutter.plugin.common.PluginRegistry;
|
||||||
|
import io.flutter.plugin.common.PluginRegistry.PluginRegistrantCallback;
|
||||||
|
import io.flutter.plugins.GeneratedPluginRegistrant;
|
||||||
|
|
||||||
|
public class Application extends FlutterApplication implements PluginRegistrantCallback {
|
||||||
|
@Override
|
||||||
|
public void onCreate() {
|
||||||
|
super.onCreate();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void registerWith(PluginRegistry registry) {
|
||||||
|
GeneratedPluginRegistrant.registerWith(registry);
|
||||||
|
}
|
||||||
|
}
|
@ -3,8 +3,9 @@ package com.keyboardcrumbs.hassclient;
|
|||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import io.flutter.app.FlutterActivity;
|
import io.flutter.app.FlutterActivity;
|
||||||
import io.flutter.plugins.GeneratedPluginRegistrant;
|
import io.flutter.plugins.GeneratedPluginRegistrant;
|
||||||
|
import io.flutter.plugins.share.FlutterShareReceiverActivity;
|
||||||
|
|
||||||
public class MainActivity extends FlutterActivity {
|
public class MainActivity extends FlutterShareReceiverActivity {
|
||||||
@Override
|
@Override
|
||||||
protected void onCreate(Bundle savedInstanceState) {
|
protected void onCreate(Bundle savedInstanceState) {
|
||||||
super.onCreate(savedInstanceState);
|
super.onCreate(savedInstanceState);
|
||||||
|
BIN
android/app/src/main/res/drawable/mini_icon.png
Normal file
After Width: | Height: | Size: 612 B |
Before Width: | Height: | Size: 2.2 KiB After Width: | Height: | Size: 4.0 KiB |
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 2.6 KiB |
Before Width: | Height: | Size: 3.0 KiB After Width: | Height: | Size: 5.4 KiB |
Before Width: | Height: | Size: 4.5 KiB After Width: | Height: | Size: 8.0 KiB |
Before Width: | Height: | Size: 5.6 KiB After Width: | Height: | Size: 11 KiB |
@ -5,7 +5,8 @@ buildscript {
|
|||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
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
1232
android/hs_err_pid766.log
Normal file
16
assets/js/externalAuth.js
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
window.externalApp = {};
|
||||||
|
window.externalApp.getExternalAuth = function(options) {
|
||||||
|
console.log("Starting external auth");
|
||||||
|
var options = JSON.parse(options);
|
||||||
|
if (options && options.callback) {
|
||||||
|
var responseData = {
|
||||||
|
access_token: "[token]",
|
||||||
|
expires_in: 1800
|
||||||
|
};
|
||||||
|
console.log("Waiting for callback to be added");
|
||||||
|
setTimeout(function(){
|
||||||
|
console.log("Calling a callback");
|
||||||
|
window[options.callback](true, responseData);
|
||||||
|
}, 500);
|
||||||
|
}
|
||||||
|
};
|
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 |
41
flutter_01.log
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
Flutter crash report; please file at https://github.com/flutter/flutter/issues.
|
||||||
|
|
||||||
|
## command
|
||||||
|
|
||||||
|
flutter --no-color run --machine --track-widget-creation --device-id=89AY052S4 lib/main.dart
|
||||||
|
|
||||||
|
## exception
|
||||||
|
|
||||||
|
_InternalLinkedHashMap<String, dynamic>: {code: 105, message: Isolate must be runnable, data: {request: {method: _reloadSources, params: {pause: true, rootLibUri: file:///data/user/0/com.keyboardcrumbs.haclient/code_cache/ha_clientSYJJZI/ha_client/lib/main.dart.incremental.dill, packagesUri: file:///data/user/0/com.keyboardcrumbs.haclient/code_cache/ha_clientSYJJZI/ha_client/.packages, isolateId: isolates/68989666}}, details: Isolate must be runnable before this request is made.}}
|
||||||
|
|
||||||
|
```
|
||||||
|
null```
|
||||||
|
|
||||||
|
## flutter doctor
|
||||||
|
|
||||||
|
```
|
||||||
|
[✓] Flutter (Channel stable, v1.7.8+hotfix.4, on Linux, locale en_US.UTF-8)
|
||||||
|
• Flutter version 1.7.8+hotfix.4 at /home/estevez/sdk/flutter
|
||||||
|
• Framework revision 20e59316b8 (6 weeks ago), 2019-07-18 20:04:33 -0700
|
||||||
|
• Engine revision fee001c93f
|
||||||
|
• Dart version 2.4.0
|
||||||
|
|
||||||
|
[✓] Android toolchain - develop for Android devices (Android SDK version 29.0.2)
|
||||||
|
• Android SDK at /home/estevez/Android/Sdk
|
||||||
|
• Android NDK location not configured (optional; useful for native profiling support)
|
||||||
|
• Platform android-29, build-tools 29.0.2
|
||||||
|
• Java binary at: /home/estevez/bin/android-studio/jre/bin/java
|
||||||
|
• Java version OpenJDK Runtime Environment (build 1.8.0_202-release-1483-b49-5587405)
|
||||||
|
• All Android licenses accepted.
|
||||||
|
|
||||||
|
[✓] Android Studio (version 3.5)
|
||||||
|
• Android Studio at /home/estevez/bin/android-studio
|
||||||
|
• Flutter plugin version 38.2.3
|
||||||
|
• Dart plugin version 191.8423
|
||||||
|
• Java version OpenJDK Runtime Environment (build 1.8.0_202-release-1483-b49-5587405)
|
||||||
|
|
||||||
|
[✓] Connected device (1 available)
|
||||||
|
• Pixel 3 XL • 89AY052S4 • android-arm64 • Android 9 (API 28)
|
||||||
|
|
||||||
|
• No issues found!
|
||||||
|
```
|
41
flutter_02.log
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
Flutter crash report; please file at https://github.com/flutter/flutter/issues.
|
||||||
|
|
||||||
|
## command
|
||||||
|
|
||||||
|
flutter --no-color run --machine --track-widget-creation --device-id=89AY052S4 lib/main.dart
|
||||||
|
|
||||||
|
## exception
|
||||||
|
|
||||||
|
_InternalLinkedHashMap<String, dynamic>: {code: 105, message: Isolate must be runnable, data: {request: {method: _reloadSources, params: {pause: true, rootLibUri: file:///data/user/0/com.keyboardcrumbs.haclient/code_cache/ha_clientWYMXDL/ha_client/lib/main.dart.incremental.dill, packagesUri: file:///data/user/0/com.keyboardcrumbs.haclient/code_cache/ha_clientWYMXDL/ha_client/.packages, isolateId: isolates/289688365}}, details: Isolate must be runnable before this request is made.}}
|
||||||
|
|
||||||
|
```
|
||||||
|
null```
|
||||||
|
|
||||||
|
## flutter doctor
|
||||||
|
|
||||||
|
```
|
||||||
|
[✓] Flutter (Channel stable, v1.7.8+hotfix.4, on Linux, locale en_US.UTF-8)
|
||||||
|
• Flutter version 1.7.8+hotfix.4 at /home/estevez/sdk/flutter
|
||||||
|
• Framework revision 20e59316b8 (6 weeks ago), 2019-07-18 20:04:33 -0700
|
||||||
|
• Engine revision fee001c93f
|
||||||
|
• Dart version 2.4.0
|
||||||
|
|
||||||
|
[✓] Android toolchain - develop for Android devices (Android SDK version 29.0.2)
|
||||||
|
• Android SDK at /home/estevez/Android/Sdk
|
||||||
|
• Android NDK location not configured (optional; useful for native profiling support)
|
||||||
|
• Platform android-29, build-tools 29.0.2
|
||||||
|
• Java binary at: /home/estevez/bin/android-studio/jre/bin/java
|
||||||
|
• Java version OpenJDK Runtime Environment (build 1.8.0_202-release-1483-b49-5587405)
|
||||||
|
• All Android licenses accepted.
|
||||||
|
|
||||||
|
[✓] Android Studio (version 3.5)
|
||||||
|
• Android Studio at /home/estevez/bin/android-studio
|
||||||
|
• Flutter plugin version 38.2.3
|
||||||
|
• Dart plugin version 191.8423
|
||||||
|
• Java version OpenJDK Runtime Environment (build 1.8.0_202-release-1483-b49-5587405)
|
||||||
|
|
||||||
|
[✓] Connected device (1 available)
|
||||||
|
• Pixel 3 XL • 89AY052S4 • android-arm64 • Android 9 (API 28)
|
||||||
|
|
||||||
|
• No issues found!
|
||||||
|
```
|
41
flutter_03.log
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
Flutter crash report; please file at https://github.com/flutter/flutter/issues.
|
||||||
|
|
||||||
|
## command
|
||||||
|
|
||||||
|
flutter --no-color run --machine --track-widget-creation --device-id=89AY052S4 lib/main.dart
|
||||||
|
|
||||||
|
## exception
|
||||||
|
|
||||||
|
_InternalLinkedHashMap<String, dynamic>: {code: 105, message: Isolate must be runnable, data: {request: {method: _reloadSources, params: {pause: true, rootLibUri: file:///data/user/0/com.keyboardcrumbs.haclient/code_cache/ha_clientLNSJAH/ha_client/lib/main.dart.incremental.dill, packagesUri: file:///data/user/0/com.keyboardcrumbs.haclient/code_cache/ha_clientLNSJAH/ha_client/.packages, isolateId: isolates/866521062}}, details: Isolate must be runnable before this request is made.}}
|
||||||
|
|
||||||
|
```
|
||||||
|
null```
|
||||||
|
|
||||||
|
## flutter doctor
|
||||||
|
|
||||||
|
```
|
||||||
|
[✓] Flutter (Channel stable, v1.7.8+hotfix.4, on Linux, locale en_US.UTF-8)
|
||||||
|
• Flutter version 1.7.8+hotfix.4 at /home/estevez/sdk/flutter
|
||||||
|
• Framework revision 20e59316b8 (6 weeks ago), 2019-07-18 20:04:33 -0700
|
||||||
|
• Engine revision fee001c93f
|
||||||
|
• Dart version 2.4.0
|
||||||
|
|
||||||
|
[✓] Android toolchain - develop for Android devices (Android SDK version 29.0.2)
|
||||||
|
• Android SDK at /home/estevez/Android/Sdk
|
||||||
|
• Android NDK location not configured (optional; useful for native profiling support)
|
||||||
|
• Platform android-29, build-tools 29.0.2
|
||||||
|
• Java binary at: /home/estevez/bin/android-studio/jre/bin/java
|
||||||
|
• Java version OpenJDK Runtime Environment (build 1.8.0_202-release-1483-b49-5587405)
|
||||||
|
• All Android licenses accepted.
|
||||||
|
|
||||||
|
[✓] Android Studio (version 3.5)
|
||||||
|
• Android Studio at /home/estevez/bin/android-studio
|
||||||
|
• Flutter plugin version 38.2.3
|
||||||
|
• Dart plugin version 191.8423
|
||||||
|
• Java version OpenJDK Runtime Environment (build 1.8.0_202-release-1483-b49-5587405)
|
||||||
|
|
||||||
|
[✓] Connected device (1 available)
|
||||||
|
• Pixel 3 XL • 89AY052S4 • android-arm64 • Android 9 (API 28)
|
||||||
|
|
||||||
|
• No issues found!
|
||||||
|
```
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 24 KiB |
Before Width: | Height: | Size: 43 KiB After Width: | Height: | Size: 48 KiB |
Before Width: | Height: | Size: 711 B After Width: | Height: | Size: 715 B |
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 1.4 KiB |
Before Width: | Height: | Size: 2.2 KiB After Width: | Height: | Size: 2.0 KiB |
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 1.0 KiB |
Before Width: | Height: | Size: 2.1 KiB After Width: | Height: | Size: 2.0 KiB |
Before Width: | Height: | Size: 3.0 KiB After Width: | Height: | Size: 2.9 KiB |
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 1.4 KiB |
Before Width: | Height: | Size: 2.6 KiB After Width: | Height: | Size: 2.7 KiB |
Before Width: | Height: | Size: 3.8 KiB After Width: | Height: | Size: 3.9 KiB |
Before Width: | Height: | Size: 3.8 KiB After Width: | Height: | Size: 3.9 KiB |
Before Width: | Height: | Size: 5.9 KiB After Width: | Height: | Size: 5.7 KiB |
Before Width: | Height: | Size: 2.7 KiB After Width: | Height: | Size: 2.5 KiB |
Before Width: | Height: | Size: 4.7 KiB After Width: | Height: | Size: 4.9 KiB |
Before Width: | Height: | Size: 5.5 KiB After Width: | Height: | Size: 5.3 KiB |
@ -1,9 +0,0 @@
|
|||||||
part of 'main.dart';
|
|
||||||
|
|
||||||
class Badge {
|
|
||||||
String _entityId;
|
|
||||||
|
|
||||||
Badge(String groupId) {
|
|
||||||
_entityId = groupId;
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,25 +0,0 @@
|
|||||||
part of 'main.dart';
|
|
||||||
|
|
||||||
class HACard {
|
|
||||||
String _entityId;
|
|
||||||
List _entities;
|
|
||||||
String _friendlyName;
|
|
||||||
|
|
||||||
List get entities => _entities;
|
|
||||||
String get friendlyName => _friendlyName;
|
|
||||||
|
|
||||||
HACard(String groupId, String friendlyName) {
|
|
||||||
_entityId = groupId;
|
|
||||||
_entities = [];
|
|
||||||
_friendlyName = friendlyName;
|
|
||||||
}
|
|
||||||
|
|
||||||
void addEntity(String entityId) {
|
|
||||||
_entities.add(entityId);
|
|
||||||
}
|
|
||||||
|
|
||||||
void addEntities(List entities) {
|
|
||||||
_entities.addAll(entities);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
64
lib/cards/card.class.dart
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
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;
|
||||||
|
List conditions;
|
||||||
|
String content;
|
||||||
|
String unit;
|
||||||
|
int min;
|
||||||
|
int max;
|
||||||
|
Map severity;
|
||||||
|
|
||||||
|
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,
|
||||||
|
this.conditions: const [],
|
||||||
|
this.unit,
|
||||||
|
this.min,
|
||||||
|
this.max,
|
||||||
|
this.severity,
|
||||||
|
@required this.type
|
||||||
|
}) {
|
||||||
|
if (this.columnsCount <= 0) {
|
||||||
|
this.columnsCount = 4;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
363
lib/cards/card_widget.dart
Normal file
@ -0,0 +1,363 @@
|
|||||||
|
part of '../main.dart';
|
||||||
|
|
||||||
|
class CardWidget extends StatelessWidget {
|
||||||
|
|
||||||
|
final HACard card;
|
||||||
|
|
||||||
|
const CardWidget({
|
||||||
|
Key key,
|
||||||
|
this.card
|
||||||
|
}) : super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
if (card.linkedEntityWrapper!= null) {
|
||||||
|
if (card.linkedEntityWrapper.entity.isHidden) {
|
||||||
|
return Container(width: 0.0, height: 0.0,);
|
||||||
|
}
|
||||||
|
if (card.linkedEntityWrapper.entity.statelessType == StatelessEntityType.MISSED) {
|
||||||
|
return EntityModel(
|
||||||
|
entityWrapper: card.linkedEntityWrapper,
|
||||||
|
child: MissedEntityWidget(),
|
||||||
|
handleTap: false,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (card.conditions.isNotEmpty) {
|
||||||
|
bool showCardByConditions = true;
|
||||||
|
for (var condition in card.conditions) {
|
||||||
|
Entity conditionEntity = HomeAssistant().entities.get(condition['entity']);
|
||||||
|
if (conditionEntity != null &&
|
||||||
|
((condition['state'] != null && conditionEntity.state != condition['state']) ||
|
||||||
|
(condition['state_not'] != null && conditionEntity.state == condition['state_not']))
|
||||||
|
) {
|
||||||
|
showCardByConditions = false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!showCardByConditions) {
|
||||||
|
return Container(width: 0.0, height: 0.0,);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (card.type) {
|
||||||
|
|
||||||
|
case CardType.ENTITIES: {
|
||||||
|
return _buildEntitiesCard(context);
|
||||||
|
}
|
||||||
|
|
||||||
|
case CardType.GLANCE: {
|
||||||
|
return _buildGlanceCard(context);
|
||||||
|
}
|
||||||
|
|
||||||
|
case CardType.MEDIA_CONTROL: {
|
||||||
|
return _buildMediaControlsCard(context);
|
||||||
|
}
|
||||||
|
|
||||||
|
case CardType.ENTITY_BUTTON: {
|
||||||
|
return _buildEntityButtonCard(context);
|
||||||
|
}
|
||||||
|
|
||||||
|
case CardType.GAUGE: {
|
||||||
|
return _buildGaugeCard(context);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* case CardType.LIGHT: {
|
||||||
|
return _buildLightCard(context);
|
||||||
|
}*/
|
||||||
|
|
||||||
|
case CardType.MARKDOWN: {
|
||||||
|
return _buildMarkdownCard(context);
|
||||||
|
}
|
||||||
|
|
||||||
|
case CardType.ALARM_PANEL: {
|
||||||
|
return _buildAlarmPanelCard(context);
|
||||||
|
}
|
||||||
|
|
||||||
|
case CardType.HORIZONTAL_STACK: {
|
||||||
|
if (card.childCards.isNotEmpty) {
|
||||||
|
List<Widget> children = [];
|
||||||
|
card.childCards.forEach((card) {
|
||||||
|
if (card.getEntitiesToShow().isNotEmpty || card.showEmpty) {
|
||||||
|
children.add(
|
||||||
|
Flexible(
|
||||||
|
fit: FlexFit.tight,
|
||||||
|
child: card.build(context),
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return Row(
|
||||||
|
mainAxisSize: MainAxisSize.max,
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: children,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return Container(height: 0.0, width: 0.0,);
|
||||||
|
}
|
||||||
|
|
||||||
|
case CardType.VERTICAL_STACK: {
|
||||||
|
if (card.childCards.isNotEmpty) {
|
||||||
|
List<Widget> children = [];
|
||||||
|
card.childCards.forEach((card) {
|
||||||
|
children.add(
|
||||||
|
card.build(context)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
return Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
mainAxisAlignment: MainAxisAlignment.start,
|
||||||
|
children: children,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return Container(height: 0.0, width: 0.0,);
|
||||||
|
}
|
||||||
|
|
||||||
|
default: {
|
||||||
|
if ((card.linkedEntityWrapper == null) && (card.entities.isNotEmpty)) {
|
||||||
|
return _buildEntitiesCard(context);
|
||||||
|
} else {
|
||||||
|
return _buildUnsupportedCard(context);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildEntitiesCard(BuildContext context) {
|
||||||
|
List<EntityWrapper> entitiesToShow = card.getEntitiesToShow();
|
||||||
|
if (entitiesToShow.isEmpty && !card.showEmpty) {
|
||||||
|
return Container(height: 0.0, width: 0.0,);
|
||||||
|
}
|
||||||
|
List<Widget> body = [];
|
||||||
|
body.add(CardHeader(name: card.name));
|
||||||
|
entitiesToShow.forEach((EntityWrapper entity) {
|
||||||
|
if (!entity.entity.isHidden) {
|
||||||
|
body.add(
|
||||||
|
Padding(
|
||||||
|
padding: EdgeInsets.fromLTRB(0.0, 4.0, 0.0, 4.0),
|
||||||
|
child: EntityModel(
|
||||||
|
entityWrapper: entity,
|
||||||
|
handleTap: true,
|
||||||
|
child: entity.entity.buildDefaultWidget(context)
|
||||||
|
),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return Card(
|
||||||
|
child: Padding(
|
||||||
|
padding: EdgeInsets.only(right: Sizes.rightWidgetPadding, left: Sizes.leftWidgetPadding),
|
||||||
|
child: Column(mainAxisSize: MainAxisSize.min, children: body),
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildMarkdownCard(BuildContext context) {
|
||||||
|
if (card.content == null) {
|
||||||
|
return Container(height: 0.0, width: 0.0,);
|
||||||
|
}
|
||||||
|
List<Widget> body = [];
|
||||||
|
body.add(CardHeader(name: card.name));
|
||||||
|
body.add(MarkdownBody(data: card.content));
|
||||||
|
return Card(
|
||||||
|
child: Padding(
|
||||||
|
padding: EdgeInsets.fromLTRB(Sizes.leftWidgetPadding, Sizes.rowPadding, Sizes.rightWidgetPadding, Sizes.rowPadding),
|
||||||
|
child: new Column(mainAxisSize: MainAxisSize.min, children: body),
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildAlarmPanelCard(BuildContext context) {
|
||||||
|
List<Widget> body = [];
|
||||||
|
body.add(CardHeader(
|
||||||
|
name: card.name ?? "",
|
||||||
|
subtitle: Text("${card.linkedEntityWrapper.entity.displayState}",
|
||||||
|
style: TextStyle(
|
||||||
|
color: Colors.grey
|
||||||
|
),
|
||||||
|
),
|
||||||
|
trailing: Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
mainAxisAlignment: MainAxisAlignment.end,
|
||||||
|
children: [
|
||||||
|
EntityIcon(
|
||||||
|
size: 50.0,
|
||||||
|
),
|
||||||
|
Container(
|
||||||
|
width: 26.0,
|
||||||
|
child: IconButton(
|
||||||
|
padding: EdgeInsets.all(0.0),
|
||||||
|
alignment: Alignment.centerRight,
|
||||||
|
icon: Icon(MaterialDesignIcons.getIconDataFromIconName(
|
||||||
|
"mdi:dots-vertical")),
|
||||||
|
onPressed: () => eventBus.fire(new ShowEntityPageEvent(card.linkedEntityWrapper.entity))
|
||||||
|
)
|
||||||
|
)
|
||||||
|
]
|
||||||
|
),
|
||||||
|
));
|
||||||
|
body.add(
|
||||||
|
AlarmControlPanelControlsWidget(
|
||||||
|
extended: true,
|
||||||
|
states: card.states,
|
||||||
|
)
|
||||||
|
);
|
||||||
|
return Card(
|
||||||
|
child: EntityModel(
|
||||||
|
entityWrapper: card.linkedEntityWrapper,
|
||||||
|
handleTap: null,
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: body
|
||||||
|
)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildGlanceCard(BuildContext context) {
|
||||||
|
List<EntityWrapper> entitiesToShow = card.getEntitiesToShow();
|
||||||
|
if (entitiesToShow.isEmpty && !card.showEmpty) {
|
||||||
|
return Container(height: 0.0, width: 0.0,);
|
||||||
|
}
|
||||||
|
List<Widget> rows = [];
|
||||||
|
rows.add(CardHeader(name: card.name));
|
||||||
|
|
||||||
|
int columnsCount = entitiesToShow.length >= card.columnsCount ? card.columnsCount : entitiesToShow.length;
|
||||||
|
|
||||||
|
rows.add(
|
||||||
|
Padding(
|
||||||
|
padding: EdgeInsets.only(bottom: Sizes.rowPadding, top: Sizes.rowPadding),
|
||||||
|
child: FractionallySizedBox(
|
||||||
|
widthFactor: 1,
|
||||||
|
child: LayoutBuilder(
|
||||||
|
builder: (BuildContext context, BoxConstraints constraints) {
|
||||||
|
List<Widget> buttons = [];
|
||||||
|
double buttonWidth = constraints.maxWidth / columnsCount;
|
||||||
|
entitiesToShow.forEach((EntityWrapper entity) {
|
||||||
|
buttons.add(
|
||||||
|
SizedBox(
|
||||||
|
width: buttonWidth,
|
||||||
|
child: EntityModel(
|
||||||
|
entityWrapper: entity,
|
||||||
|
child: GlanceCardEntityContainer(
|
||||||
|
showName: card.showName,
|
||||||
|
showState: card.showState,
|
||||||
|
),
|
||||||
|
handleTap: true
|
||||||
|
),
|
||||||
|
)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
return Wrap(
|
||||||
|
//spacing: 5.0,
|
||||||
|
//alignment: WrapAlignment.spaceEvenly,
|
||||||
|
runSpacing: Sizes.doubleRowPadding,
|
||||||
|
children: buttons,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
return Card(
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: rows
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildMediaControlsCard(BuildContext context) {
|
||||||
|
return Card(
|
||||||
|
child: EntityModel(
|
||||||
|
entityWrapper: card.linkedEntityWrapper,
|
||||||
|
handleTap: null,
|
||||||
|
child: MediaPlayerWidget()
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildEntityButtonCard(BuildContext context) {
|
||||||
|
card.linkedEntityWrapper.displayName = card.name?.toUpperCase() ??
|
||||||
|
card.linkedEntityWrapper.displayName.toUpperCase();
|
||||||
|
return Card(
|
||||||
|
child: EntityModel(
|
||||||
|
entityWrapper: card.linkedEntityWrapper,
|
||||||
|
child: EntityButtonCardBody(),
|
||||||
|
handleTap: true
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildGaugeCard(BuildContext context) {
|
||||||
|
card.linkedEntityWrapper.displayName = card.name ??
|
||||||
|
card.linkedEntityWrapper.displayName;
|
||||||
|
card.linkedEntityWrapper.unitOfMeasurement = card.unit ??
|
||||||
|
card.linkedEntityWrapper.unitOfMeasurement;
|
||||||
|
return Card(
|
||||||
|
child: EntityModel(
|
||||||
|
entityWrapper: card.linkedEntityWrapper,
|
||||||
|
child: GaugeCardBody(
|
||||||
|
min: card.min,
|
||||||
|
max: card.max,
|
||||||
|
severity: card.severity,
|
||||||
|
),
|
||||||
|
handleTap: true
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildLightCard(BuildContext context) {
|
||||||
|
card.linkedEntityWrapper.displayName = card.name ??
|
||||||
|
card.linkedEntityWrapper.displayName;
|
||||||
|
return Card(
|
||||||
|
child: EntityModel(
|
||||||
|
entityWrapper: card.linkedEntityWrapper,
|
||||||
|
child: LightCardBody(
|
||||||
|
min: card.min,
|
||||||
|
max: card.max,
|
||||||
|
severity: card.severity,
|
||||||
|
),
|
||||||
|
handleTap: true
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildUnsupportedCard(BuildContext context) {
|
||||||
|
List<Widget> body = [];
|
||||||
|
body.add(CardHeader(name: card.name ?? ""));
|
||||||
|
List<Widget> result = [];
|
||||||
|
if (card.linkedEntityWrapper != null) {
|
||||||
|
result.addAll(<Widget>[
|
||||||
|
Padding(
|
||||||
|
padding: EdgeInsets.fromLTRB(0.0, Sizes.rowPadding, 0.0, Sizes.rowPadding),
|
||||||
|
child: EntityModel(
|
||||||
|
entityWrapper: card.linkedEntityWrapper,
|
||||||
|
handleTap: true,
|
||||||
|
child: card.linkedEntityWrapper.entity.buildDefaultWidget(context)
|
||||||
|
),
|
||||||
|
)
|
||||||
|
]);
|
||||||
|
} else {
|
||||||
|
result.addAll(<Widget>[
|
||||||
|
Padding(
|
||||||
|
padding: EdgeInsets.fromLTRB(Sizes.leftWidgetPadding, Sizes.rowPadding, Sizes.rightWidgetPadding, Sizes.rowPadding),
|
||||||
|
child: Text("'${card.type}' card is not supported yet"),
|
||||||
|
),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
body.addAll(result);
|
||||||
|
return Card(
|
||||||
|
child: new Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: body
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
29
lib/cards/widgets/card_header.widget.dart
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
part of '../../main.dart';
|
||||||
|
|
||||||
|
class CardHeader extends StatelessWidget {
|
||||||
|
|
||||||
|
final String name;
|
||||||
|
final Widget trailing;
|
||||||
|
final Widget subtitle;
|
||||||
|
|
||||||
|
const CardHeader({Key key, this.name, this.trailing, this.subtitle}) : super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
var result;
|
||||||
|
if ((name != null) && (name.trim().length > 0)) {
|
||||||
|
result = new ListTile(
|
||||||
|
trailing: trailing,
|
||||||
|
subtitle: subtitle,
|
||||||
|
title: Text("$name",
|
||||||
|
textAlign: TextAlign.left,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
style: new TextStyle(fontWeight: FontWeight.bold, fontSize: Sizes.largeFontSize)),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
result = new Container(width: 0.0, height: 0.0);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
51
lib/cards/widgets/entity_button_card_body.widget.dart
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
part of '../../main.dart';
|
||||||
|
|
||||||
|
class EntityButtonCardBody extends StatelessWidget {
|
||||||
|
|
||||||
|
EntityButtonCardBody({
|
||||||
|
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: FractionallySizedBox(
|
||||||
|
widthFactor: 1,
|
||||||
|
child: Column(
|
||||||
|
children: <Widget>[
|
||||||
|
LayoutBuilder(
|
||||||
|
builder: (BuildContext context, BoxConstraints constraints) {
|
||||||
|
return EntityIcon(
|
||||||
|
padding: EdgeInsets.fromLTRB(2.0, 6.0, 2.0, 2.0),
|
||||||
|
size: constraints.maxWidth / 2.5,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
),
|
||||||
|
_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,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
153
lib/cards/widgets/gauge_card_body.dart
Normal file
@ -0,0 +1,153 @@
|
|||||||
|
part of '../../main.dart';
|
||||||
|
|
||||||
|
class GaugeCardBody extends StatefulWidget {
|
||||||
|
|
||||||
|
final int min;
|
||||||
|
final int max;
|
||||||
|
final Map severity;
|
||||||
|
|
||||||
|
GaugeCardBody({Key key, this.min, this.max, this.severity}) : super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
_GaugeCardBodyState createState() => _GaugeCardBodyState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _GaugeCardBodyState extends State<GaugeCardBody> {
|
||||||
|
|
||||||
|
List<charts.Series> seriesList;
|
||||||
|
|
||||||
|
List<charts.Series<GaugeSegment, String>> _createData(double value) {
|
||||||
|
double fixedValue;
|
||||||
|
if (value > widget.max) {
|
||||||
|
fixedValue = widget.max.toDouble();
|
||||||
|
} else if (value < widget.min) {
|
||||||
|
fixedValue = widget.min.toDouble();
|
||||||
|
} else {
|
||||||
|
fixedValue = value;
|
||||||
|
}
|
||||||
|
double toShow = ((fixedValue - widget.min) / (widget.max - widget.min)) * 100;
|
||||||
|
Color mainColor;
|
||||||
|
if (widget.severity != null) {
|
||||||
|
if (widget.severity["red"] is int && fixedValue >= widget.severity["red"]) {
|
||||||
|
mainColor = Colors.red;
|
||||||
|
} else if (widget.severity["yellow"] is int && fixedValue >= widget.severity["yellow"]) {
|
||||||
|
mainColor = Colors.amber;
|
||||||
|
} else {
|
||||||
|
mainColor = Colors.green;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
mainColor = Colors.green;
|
||||||
|
}
|
||||||
|
final data = [
|
||||||
|
GaugeSegment('Main', toShow, mainColor),
|
||||||
|
GaugeSegment('Rest', 100 - toShow, Colors.black45),
|
||||||
|
];
|
||||||
|
|
||||||
|
return [
|
||||||
|
charts.Series<GaugeSegment, String>(
|
||||||
|
id: 'Segments',
|
||||||
|
domainFn: (GaugeSegment segment, _) => segment.segment,
|
||||||
|
measureFn: (GaugeSegment segment, _) => segment.value,
|
||||||
|
colorFn: (GaugeSegment segment, _) => segment.color,
|
||||||
|
// Set a label accessor to control the text of the arc label.
|
||||||
|
labelAccessorFn: (GaugeSegment segment, _) =>
|
||||||
|
segment.segment == 'Main' ? '${segment.value}' : null,
|
||||||
|
data: data,
|
||||||
|
)
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
EntityWrapper entityWrapper = EntityModel.of(context).entityWrapper;
|
||||||
|
|
||||||
|
return InkWell(
|
||||||
|
onTap: () => entityWrapper.handleTap(),
|
||||||
|
onLongPress: () => entityWrapper.handleHold(),
|
||||||
|
child: AspectRatio(
|
||||||
|
aspectRatio: 1.5,
|
||||||
|
child: Stack(
|
||||||
|
fit: StackFit.expand,
|
||||||
|
overflow: Overflow.clip,
|
||||||
|
children: [
|
||||||
|
LayoutBuilder(
|
||||||
|
builder: (BuildContext context, BoxConstraints constraints) {
|
||||||
|
double verticalOffset;
|
||||||
|
if(constraints.maxWidth > 150.0) {
|
||||||
|
verticalOffset = 0.2;
|
||||||
|
} else if (constraints.maxWidth > 100.0) {
|
||||||
|
verticalOffset = 0.3;
|
||||||
|
} else {
|
||||||
|
verticalOffset = 0.3;
|
||||||
|
}
|
||||||
|
return FractionallySizedBox(
|
||||||
|
heightFactor: 2,
|
||||||
|
widthFactor: 1,
|
||||||
|
alignment: FractionalOffset(0,verticalOffset),
|
||||||
|
child: charts.PieChart(
|
||||||
|
_createData(entityWrapper.entity.doubleState),
|
||||||
|
animate: false,
|
||||||
|
defaultRenderer: charts.ArcRendererConfig(
|
||||||
|
arcRatio: 0.4,
|
||||||
|
startAngle: pi,
|
||||||
|
arcLength: pi,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
),
|
||||||
|
Align(
|
||||||
|
alignment: Alignment.bottomCenter,
|
||||||
|
child: LayoutBuilder(
|
||||||
|
builder: (BuildContext context, BoxConstraints constraints) {
|
||||||
|
double fontSize = constraints.maxHeight / 7;
|
||||||
|
return Padding(
|
||||||
|
padding: EdgeInsets.only(bottom: 2*fontSize),
|
||||||
|
child: SimpleEntityState(
|
||||||
|
//textAlign: TextAlign.center,
|
||||||
|
expanded: false,
|
||||||
|
maxLines: 1,
|
||||||
|
bold: true,
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
padding: EdgeInsets.all(0.0),
|
||||||
|
fontSize: fontSize,
|
||||||
|
//padding: EdgeInsets.only(top: Sizes.rowPadding),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Align(
|
||||||
|
alignment: Alignment.bottomCenter,
|
||||||
|
child: LayoutBuilder(
|
||||||
|
builder: (BuildContext context, BoxConstraints constraints) {
|
||||||
|
double fontSize = constraints.maxHeight / 7;
|
||||||
|
return Padding(
|
||||||
|
padding: EdgeInsets.only(bottom: fontSize),
|
||||||
|
child: EntityName(
|
||||||
|
fontSize: fontSize,
|
||||||
|
maxLines: 1,
|
||||||
|
padding: EdgeInsets.all(0.0),
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
textOverflow: TextOverflow.ellipsis,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
),
|
||||||
|
)
|
||||||
|
]
|
||||||
|
)
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class GaugeSegment {
|
||||||
|
final String segment;
|
||||||
|
final double value;
|
||||||
|
final charts.Color color;
|
||||||
|
|
||||||
|
GaugeSegment(this.segment, this.value, Color color)
|
||||||
|
: this.color = charts.Color(
|
||||||
|
r: color.red, g: color.green, b: color.blue, a: color.alpha);
|
||||||
|
}
|
85
lib/cards/widgets/glance_card_entity_container.dart
Normal file
@ -0,0 +1,85 @@
|
|||||||
|
part of '../../main.dart';
|
||||||
|
|
||||||
|
class GlanceCardEntityContainer extends StatelessWidget {
|
||||||
|
|
||||||
|
final bool showName;
|
||||||
|
final bool showState;
|
||||||
|
final bool nameInTheBottom;
|
||||||
|
final double iconSize;
|
||||||
|
final double nameFontSize;
|
||||||
|
final bool wordsWrapInName;
|
||||||
|
|
||||||
|
GlanceCardEntityContainer({
|
||||||
|
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: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
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),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
90
lib/cards/widgets/light_card_body.dart
Normal file
@ -0,0 +1,90 @@
|
|||||||
|
part of '../../main.dart';
|
||||||
|
|
||||||
|
class LightCardBody extends StatefulWidget {
|
||||||
|
|
||||||
|
final int min;
|
||||||
|
final int max;
|
||||||
|
final Map severity;
|
||||||
|
|
||||||
|
LightCardBody({Key key, this.min, this.max, this.severity}) : super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
_LightCardBodyState createState() => _LightCardBodyState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _LightCardBodyState extends State<LightCardBody> {
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
EntityWrapper entityWrapper = EntityModel.of(context).entityWrapper;
|
||||||
|
LightEntity entity = entityWrapper.entity;
|
||||||
|
Logger.d("Light brightness: ${entity.brightness}");
|
||||||
|
|
||||||
|
return FractionallySizedBox(
|
||||||
|
widthFactor: 0.5,
|
||||||
|
child: Container(
|
||||||
|
//color: Colors.redAccent,
|
||||||
|
child: SingleCircularSlider(
|
||||||
|
255,
|
||||||
|
entity.brightness ?? 0,
|
||||||
|
baseColor: Colors.white,
|
||||||
|
handlerColor: Colors.blue[200],
|
||||||
|
selectionColor: Colors.blue[100],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
return InkWell(
|
||||||
|
onTap: () => entityWrapper.handleTap(),
|
||||||
|
onLongPress: () => entityWrapper.handleHold(),
|
||||||
|
child: AspectRatio(
|
||||||
|
aspectRatio: 1.5,
|
||||||
|
child: Stack(
|
||||||
|
fit: StackFit.expand,
|
||||||
|
overflow: Overflow.clip,
|
||||||
|
children: [
|
||||||
|
Align(
|
||||||
|
alignment: Alignment.bottomCenter,
|
||||||
|
child: LayoutBuilder(
|
||||||
|
builder: (BuildContext context, BoxConstraints constraints) {
|
||||||
|
double fontSize = constraints.maxHeight / 7;
|
||||||
|
return Padding(
|
||||||
|
padding: EdgeInsets.only(bottom: 2*fontSize),
|
||||||
|
child: SimpleEntityState(
|
||||||
|
//textAlign: TextAlign.center,
|
||||||
|
expanded: false,
|
||||||
|
maxLines: 1,
|
||||||
|
bold: true,
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
padding: EdgeInsets.all(0.0),
|
||||||
|
fontSize: fontSize,
|
||||||
|
//padding: EdgeInsets.only(top: Sizes.rowPadding),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Align(
|
||||||
|
alignment: Alignment.bottomCenter,
|
||||||
|
child: LayoutBuilder(
|
||||||
|
builder: (BuildContext context, BoxConstraints constraints) {
|
||||||
|
double fontSize = constraints.maxHeight / 7;
|
||||||
|
return Padding(
|
||||||
|
padding: EdgeInsets.only(bottom: fontSize),
|
||||||
|
child: EntityName(
|
||||||
|
fontSize: fontSize,
|
||||||
|
maxLines: 1,
|
||||||
|
padding: EdgeInsets.all(0.0),
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
textOverflow: TextOverflow.ellipsis,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
),
|
||||||
|
)
|
||||||
|
]
|
||||||
|
)
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
116
lib/const.dart
Normal file
@ -0,0 +1,116 @@
|
|||||||
|
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 HORIZONTAL_STACK = "horizontal-stack";
|
||||||
|
static const VERTICAL_STACK = "vertical-stack";
|
||||||
|
static const ENTITIES = "entities";
|
||||||
|
static const GLANCE = "glance";
|
||||||
|
static const MEDIA_CONTROL = "media-control";
|
||||||
|
static const WEATHER_FORECAST = "weather-forecast";
|
||||||
|
static const THERMOSTAT = "thermostat";
|
||||||
|
static const SENSOR = "sensor";
|
||||||
|
static const PLANT_STATUS = "plant-status";
|
||||||
|
static const PICTURE_ENTITY = "picture-entity";
|
||||||
|
static const PICTURE_ELEMENTS = "picture-elements";
|
||||||
|
static const PICTURE = "picture";
|
||||||
|
static const MAP = "map";
|
||||||
|
static const IFRAME = "iframe";
|
||||||
|
static const GAUGE = "gauge";
|
||||||
|
static const ENTITY_BUTTON = "entity-button";
|
||||||
|
static const CONDITIONAL = "conditional";
|
||||||
|
static const ALARM_PANEL = "alarm-panel";
|
||||||
|
static const MARKDOWN = "markdown";
|
||||||
|
static const LIGHT = "light";
|
||||||
|
}
|
||||||
|
|
||||||
|
class Sizes {
|
||||||
|
static const rightWidgetPadding = 10.0;
|
||||||
|
static const leftWidgetPadding = 10.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;
|
||||||
|
static const doubleRowPadding = rowPadding*2;
|
||||||
|
}
|
@ -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,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -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
|
||||||
|
]
|
||||||
|
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
27
lib/entities/automation/automation_entity.class.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/entities/button/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/entities/camera/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();
|
||||||
|
}
|
||||||
|
}
|
114
lib/entities/climate/climate_entity.class.dart
Normal file
@ -0,0 +1,114 @@
|
|||||||
|
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_RANGE = 2;
|
||||||
|
static const SUPPORT_TARGET_HUMIDITY = 4;
|
||||||
|
static const SUPPORT_FAN_MODE = 8;
|
||||||
|
static const SUPPORT_PRESET_MODE = 16;
|
||||||
|
static const SUPPORT_SWING_MODE = 32;
|
||||||
|
static const SUPPORT_AUX_HEAT = 64;
|
||||||
|
|
||||||
|
|
||||||
|
//static const SUPPORT_OPERATION_MODE = 16;
|
||||||
|
//static const SUPPORT_HOLD_MODE = 256;
|
||||||
|
//static const SUPPORT_AWAY_MODE = 1024;
|
||||||
|
//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 supportTargetTemperatureRange => ((supportedFeatures &
|
||||||
|
ClimateEntity.SUPPORT_TARGET_TEMPERATURE_RANGE) ==
|
||||||
|
ClimateEntity.SUPPORT_TARGET_TEMPERATURE_RANGE);
|
||||||
|
bool get supportTargetHumidity => ((supportedFeatures &
|
||||||
|
ClimateEntity.SUPPORT_TARGET_HUMIDITY) ==
|
||||||
|
ClimateEntity.SUPPORT_TARGET_HUMIDITY);
|
||||||
|
bool get supportFanMode =>
|
||||||
|
((supportedFeatures & ClimateEntity.SUPPORT_FAN_MODE) ==
|
||||||
|
ClimateEntity.SUPPORT_FAN_MODE);
|
||||||
|
bool get supportSwingMode =>
|
||||||
|
((supportedFeatures & ClimateEntity.SUPPORT_SWING_MODE) ==
|
||||||
|
ClimateEntity.SUPPORT_SWING_MODE);
|
||||||
|
bool get supportPresetMode =>
|
||||||
|
((supportedFeatures & ClimateEntity.SUPPORT_PRESET_MODE) ==
|
||||||
|
ClimateEntity.SUPPORT_PRESET_MODE);
|
||||||
|
bool get supportAuxHeat =>
|
||||||
|
((supportedFeatures & ClimateEntity.SUPPORT_AUX_HEAT) ==
|
||||||
|
ClimateEntity.SUPPORT_AUX_HEAT);
|
||||||
|
|
||||||
|
List<String> get hvacModes => attributes["hvac_modes"] != null
|
||||||
|
? (attributes["hvac_modes"] as List).cast<String>()
|
||||||
|
: null;
|
||||||
|
List<String> get fanModes => attributes["fan_modes"] != null
|
||||||
|
? (attributes["fan_modes"] as List).cast<String>()
|
||||||
|
: null;
|
||||||
|
List<String> get presetModes => attributes["preset_modes"] != null
|
||||||
|
? (attributes["preset_modes"] as List).cast<String>()
|
||||||
|
: null;
|
||||||
|
List<String> get swingModes => attributes["swing_modes"] != null
|
||||||
|
? (attributes["swing_modes"] as List).cast<String>()
|
||||||
|
: null;
|
||||||
|
double get temperature => _getDoubleAttributeValue('temperature');
|
||||||
|
double get currentTemperature => _getDoubleAttributeValue('current_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 hvacAction => attributes['hvac_action'];
|
||||||
|
String get fanMode => attributes['fan_mode'];
|
||||||
|
String get presetMode => attributes['preset_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 (supportTargetTemperatureRange) {
|
||||||
|
historyConfig.numericAttributesToShow.add("target_temp_high");
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
411
lib/entities/climate/widgets/climate_controls.dart
Normal file
@ -0,0 +1,411 @@
|
|||||||
|
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 _tmpHVACMode;
|
||||||
|
String _tmpFanMode;
|
||||||
|
String _tmpSwingMode;
|
||||||
|
String _tmpPresetMode;
|
||||||
|
//bool _tmpIsOff = false;
|
||||||
|
bool _tmpAuxHeat = false;
|
||||||
|
|
||||||
|
void _resetVars(ClimateEntity entity) {
|
||||||
|
_tmpTemperature = entity.temperature;
|
||||||
|
_tmpTargetHigh = entity.targetHigh;
|
||||||
|
_tmpTargetLow = entity.targetLow;
|
||||||
|
_tmpHVACMode = entity.state;
|
||||||
|
_tmpFanMode = entity.fanMode;
|
||||||
|
_tmpSwingMode = entity.swingMode;
|
||||||
|
_tmpPresetMode = entity.presetMode;
|
||||||
|
//_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 _setHVACMode(ClimateEntity entity, value) {
|
||||||
|
setState(() {
|
||||||
|
_tmpHVACMode = value;
|
||||||
|
_changedHere = true;
|
||||||
|
eventBus.fire(new ServiceCallEvent(entity.domain, "set_hvac_mode", entity.entityId,{"hvac_mode": "$_tmpHVACMode"}));
|
||||||
|
_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 _setPresetMode(ClimateEntity entity, value) {
|
||||||
|
setState(() {
|
||||||
|
_tmpPresetMode = value;
|
||||||
|
_changedHere = true;
|
||||||
|
eventBus.fire(new ServiceCallEvent(entity.domain, "set_preset_mode", entity.entityId,{"preset_mode": "$_tmpPresetMode"}));
|
||||||
|
_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),
|
||||||
|
_buildPresetModeControl(entity),
|
||||||
|
_buildAuxHeatControl(entity)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildPresetModeControl(ClimateEntity entity) {
|
||||||
|
if (entity.supportPresetMode) {
|
||||||
|
return ModeSelectorWidget(
|
||||||
|
options: entity.presetModes,
|
||||||
|
onChange: (mode) => _setPresetMode(entity, mode),
|
||||||
|
caption: "Preset",
|
||||||
|
value: _tmpPresetMode,
|
||||||
|
);
|
||||||
|
} 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.hvacModes != null) {
|
||||||
|
return ModeSelectorWidget(
|
||||||
|
onChange: (mode) => _setHVACMode(entity, mode),
|
||||||
|
options: entity.hvacModes,
|
||||||
|
caption: "Operation",
|
||||||
|
value: _tmpHVACMode,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return Container(height: 0.0, width: 0.0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildFanControl(ClimateEntity entity) {
|
||||||
|
if (entity.supportFanMode) {
|
||||||
|
return ModeSelectorWidget(
|
||||||
|
options: entity.fanModes,
|
||||||
|
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.swingModes,
|
||||||
|
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.supportTargetTemperatureRange) && (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.supportTargetTemperatureRange) && (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();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
58
lib/entities/climate/widgets/climate_state.widget.dart
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
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.supportTargetTemperatureRange) &&
|
||||||
|
(entity.targetLow != null) &&
|
||||||
|
(entity.targetHigh != null)) {
|
||||||
|
targetTemp = "${entity.targetLow} - ${entity.targetHigh}";
|
||||||
|
}
|
||||||
|
String displayState = '';
|
||||||
|
if (entity.hvacAction != null) {
|
||||||
|
displayState = "${entity.hvacAction} (${entity.displayState})";
|
||||||
|
} else {
|
||||||
|
displayState = "${entity.displayState}";
|
||||||
|
}
|
||||||
|
if (entity.presetMode != null) {
|
||||||
|
displayState += " - ${entity.presetMode}";
|
||||||
|
}
|
||||||
|
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("$displayState",
|
||||||
|
textAlign: TextAlign.right,
|
||||||
|
style: new TextStyle(
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
fontSize: Sizes.stateFontSize,
|
||||||
|
)),
|
||||||
|
Text(" $targetTemp",
|
||||||
|
textAlign: TextAlign.right,
|
||||||
|
style: new TextStyle(
|
||||||
|
fontSize: Sizes.stateFontSize,
|
||||||
|
))
|
||||||
|
],
|
||||||
|
),
|
||||||
|
entity.currentTemperature != null ?
|
||||||
|
Text("Currently: ${entity.currentTemperature}",
|
||||||
|
textAlign: TextAlign.right,
|
||||||
|
style: new TextStyle(
|
||||||
|
fontSize: Sizes.stateFontSize,
|
||||||
|
color: Colors.black45)
|
||||||
|
) :
|
||||||
|
Container(height: 0.0,)
|
||||||
|
],
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
50
lib/entities/climate/widgets/temperature_control_widget.dart
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
part of '../../../main.dart';
|
||||||
|
|
||||||
|
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(),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
)
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
60
lib/entities/cover/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();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
200
lib/entities/cover/widgets/cover_controls.widget.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,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
65
lib/entities/cover/widgets/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,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
42
lib/entities/date_time/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));
|
||||||
|
}
|
||||||
|
}
|
75
lib/entities/date_time/widgets/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));
|
||||||
|
}
|
||||||
|
}
|
282
lib/entities/entity.class.dart
Normal file
@ -0,0 +1,282 @@
|
|||||||
|
part of '../main.dart';
|
||||||
|
|
||||||
|
class StatelessEntityType {
|
||||||
|
static const NONE = 0;
|
||||||
|
static const MISSED = 1;
|
||||||
|
static const DIVIDER = 2;
|
||||||
|
static const SECTION = 3;
|
||||||
|
static const CALL_SERVICE = 4;
|
||||||
|
static const WEBLINK = 5;
|
||||||
|
}
|
||||||
|
|
||||||
|
class Entity {
|
||||||
|
|
||||||
|
static List badgeDomains = [
|
||||||
|
"alarm_control_panel",
|
||||||
|
"binary_sensor",
|
||||||
|
"device_tracker",
|
||||||
|
"updater",
|
||||||
|
"sun",
|
||||||
|
"timer",
|
||||||
|
"sensor"
|
||||||
|
];
|
||||||
|
|
||||||
|
static Map StateByDeviceClass = {
|
||||||
|
"battery.on": "Low",
|
||||||
|
"battery.off": "Normal",
|
||||||
|
"cold.on": "Cold",
|
||||||
|
"cold.off": "Normal",
|
||||||
|
"connectivity.on": "Connected",
|
||||||
|
"connectivity.off": "Disconnected",
|
||||||
|
"door.on": "Open",
|
||||||
|
"door.off": "Closed",
|
||||||
|
"garage_door.on": "Open",
|
||||||
|
"garage_door.off": "Closed",
|
||||||
|
"gas.on": "Detected",
|
||||||
|
"gas.off": "Clear",
|
||||||
|
"heat.on": "Hot",
|
||||||
|
"heat.off": "Normal",
|
||||||
|
"light.on": "Detected",
|
||||||
|
"lignt.off": "No light",
|
||||||
|
"lock.on": "Unlocked",
|
||||||
|
"lock.off": "Locked",
|
||||||
|
"moisture.on": "Wet",
|
||||||
|
"moisture.off": "Dry",
|
||||||
|
"motion.on": "Detected",
|
||||||
|
"motion.off": "Clear",
|
||||||
|
"moving.on": "Moving",
|
||||||
|
"moving.off": "Stopped",
|
||||||
|
"occupancy.on": "Occupied",
|
||||||
|
"occupancy.off": "Clear",
|
||||||
|
"opening.on": "Open",
|
||||||
|
"opening.off": "Closed",
|
||||||
|
"plug.on": "Plugged in",
|
||||||
|
"plug.off": "Unplugged",
|
||||||
|
"power.on": "Powered",
|
||||||
|
"power.off": "No power",
|
||||||
|
"presence.on": "Home",
|
||||||
|
"presence.off": "Away",
|
||||||
|
"problem.on": "Problem",
|
||||||
|
"problem.off": "OK",
|
||||||
|
"safety.on": "Unsafe",
|
||||||
|
"safety.off": "Safe",
|
||||||
|
"smoke.on": "Detected",
|
||||||
|
"smoke.off": "Clear",
|
||||||
|
"sound.on": "Detected",
|
||||||
|
"sound.off": "Clear",
|
||||||
|
"vibration.on": "Detected",
|
||||||
|
"vibration.off": "Clear",
|
||||||
|
"window.on": "Open",
|
||||||
|
"window.off": "Closed"
|
||||||
|
};
|
||||||
|
|
||||||
|
Map attributes;
|
||||||
|
String domain;
|
||||||
|
String entityId;
|
||||||
|
String entityPicture;
|
||||||
|
String state;
|
||||||
|
String displayState;
|
||||||
|
DateTime _lastUpdated;
|
||||||
|
int statelessType = 0;
|
||||||
|
|
||||||
|
List<Entity> childEntities = [];
|
||||||
|
String deviceClass;
|
||||||
|
EntityHistoryConfig historyConfig = EntityHistoryConfig(
|
||||||
|
chartType: EntityHistoryWidgetType.simple
|
||||||
|
);
|
||||||
|
|
||||||
|
String get displayName =>
|
||||||
|
attributes["friendly_name"] ?? (attributes["name"] ?? entityId.split(".")[1].replaceAll("_", " "));
|
||||||
|
|
||||||
|
bool get isView =>
|
||||||
|
(domain == "group") &&
|
||||||
|
(attributes != null ? attributes["view"] ?? false : false);
|
||||||
|
bool get isGroup => domain == "group";
|
||||||
|
bool get isBadge => Entity.badgeDomains.contains(domain);
|
||||||
|
String get icon => attributes["icon"] ?? "";
|
||||||
|
bool get isOn => state == EntityState.on;
|
||||||
|
String get unitOfMeasurement => attributes["unit_of_measurement"] ?? "";
|
||||||
|
List get childEntityIds => attributes["entity_id"] ?? [];
|
||||||
|
String get lastUpdated => _getLastUpdatedFormatted();
|
||||||
|
bool get isHidden => attributes["hidden"] ?? false;
|
||||||
|
double get doubleState => double.tryParse(state) ?? 0.0;
|
||||||
|
int get supportedFeatures => attributes["supported_features"] ?? 0;
|
||||||
|
|
||||||
|
String _getEntityPictureUrl(String webHost) {
|
||||||
|
String result = attributes["entity_picture"];
|
||||||
|
if (result == null) return result;
|
||||||
|
if (!result.startsWith("http")) {
|
||||||
|
if (result.startsWith("/")) {
|
||||||
|
result = "$webHost$result";
|
||||||
|
} else {
|
||||||
|
result = "$webHost/$result";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
Entity(Map rawData, String webHost) {
|
||||||
|
update(rawData, webHost);
|
||||||
|
}
|
||||||
|
|
||||||
|
Entity.missed(String entityId) {
|
||||||
|
statelessType = StatelessEntityType.MISSED;
|
||||||
|
attributes = {"hidden": false};
|
||||||
|
this.entityId = entityId;
|
||||||
|
}
|
||||||
|
|
||||||
|
Entity.divider() {
|
||||||
|
statelessType = StatelessEntityType.DIVIDER;
|
||||||
|
attributes = {"hidden": false};
|
||||||
|
}
|
||||||
|
|
||||||
|
Entity.section(String label) {
|
||||||
|
statelessType = StatelessEntityType.SECTION;
|
||||||
|
attributes = {"hidden": false, "friendly_name": "$label"};
|
||||||
|
}
|
||||||
|
|
||||||
|
Entity.callService({String icon, String name, String service, String actionName}) {
|
||||||
|
statelessType = StatelessEntityType.CALL_SERVICE;
|
||||||
|
entityId = service;
|
||||||
|
displayState = actionName?.toUpperCase() ?? "RUN";
|
||||||
|
attributes = {"hidden": false, "friendly_name": "$name", "icon": "$icon"};
|
||||||
|
}
|
||||||
|
|
||||||
|
Entity.weblink({String url, String name, String icon}) {
|
||||||
|
statelessType = StatelessEntityType.WEBLINK;
|
||||||
|
entityId = "custom.custom"; //TODO wtf??
|
||||||
|
attributes = {"hidden": false, "friendly_name": "${name ?? url}", "icon": "${icon ?? 'mdi:link'}"};
|
||||||
|
}
|
||||||
|
|
||||||
|
void update(Map rawData, String webHost) {
|
||||||
|
attributes = rawData["attributes"] ?? {};
|
||||||
|
domain = rawData["entity_id"].split(".")[0];
|
||||||
|
entityId = rawData["entity_id"];
|
||||||
|
deviceClass = attributes["device_class"];
|
||||||
|
state = rawData["state"];
|
||||||
|
displayState = Entity.StateByDeviceClass["$deviceClass.$state"] ?? (state.toLowerCase() == 'unknown' ? '-' : state);
|
||||||
|
_lastUpdated = DateTime.tryParse(rawData["last_updated"]);
|
||||||
|
entityPicture = _getEntityPictureUrl(webHost);
|
||||||
|
}
|
||||||
|
|
||||||
|
double _getDoubleAttributeValue(String attributeName) {
|
||||||
|
var temp1 = attributes["$attributeName"];
|
||||||
|
if (temp1 is int) {
|
||||||
|
return temp1.toDouble();
|
||||||
|
} else if (temp1 is double) {
|
||||||
|
return temp1;
|
||||||
|
} else {
|
||||||
|
return double.tryParse("$temp1");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
int _getIntAttributeValue(String attributeName) {
|
||||||
|
var temp1 = attributes["$attributeName"];
|
||||||
|
if (temp1 is int) {
|
||||||
|
return temp1;
|
||||||
|
} else if (temp1 is double) {
|
||||||
|
return temp1.round();
|
||||||
|
} else {
|
||||||
|
return int.tryParse("$temp1");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
List<String> getStringListAttributeValue(String attribute) {
|
||||||
|
if (attributes["$attribute"] != null) {
|
||||||
|
List<String> result = (attributes["$attribute"] as List).cast<String>();
|
||||||
|
return result;
|
||||||
|
} else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget buildDefaultWidget(BuildContext context) {
|
||||||
|
return DefaultEntityContainer(
|
||||||
|
state: _buildStatePart(context)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildStatePart(BuildContext context) {
|
||||||
|
return SimpleEntityState();
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildStatePartForPage(BuildContext context) {
|
||||||
|
return _buildStatePart(context);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildAdditionalControlsForPage(BuildContext context) {
|
||||||
|
return Container(
|
||||||
|
width: 0.0,
|
||||||
|
height: 0.0,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget buildEntityPageWidget(BuildContext context) {
|
||||||
|
return EntityModel(
|
||||||
|
entityWrapper: EntityWrapper(entity: this),
|
||||||
|
child: EntityPageContainer(children: <Widget>[
|
||||||
|
Padding(
|
||||||
|
padding: EdgeInsets.only(top: Sizes.rowPadding, left: Sizes.leftWidgetPadding),
|
||||||
|
child: DefaultEntityContainer(state: _buildStatePartForPage(context)),
|
||||||
|
),
|
||||||
|
LastUpdatedWidget(),
|
||||||
|
Divider(),
|
||||||
|
_buildAdditionalControlsForPage(context),
|
||||||
|
Divider(),
|
||||||
|
buildHistoryWidget(),
|
||||||
|
EntityAttributesList()
|
||||||
|
]),
|
||||||
|
handleTap: false,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget buildHistoryWidget() {
|
||||||
|
return EntityHistoryWidget(
|
||||||
|
config: historyConfig,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget buildBadgeWidget(BuildContext context) {
|
||||||
|
return EntityModel(
|
||||||
|
entityWrapper: EntityWrapper(entity: this),
|
||||||
|
child: BadgeWidget(),
|
||||||
|
handleTap: true,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
String getAttribute(String attributeName) {
|
||||||
|
if (attributes != null) {
|
||||||
|
return attributes["$attributeName"];
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
String _getLastUpdatedFormatted() {
|
||||||
|
if (_lastUpdated == null) {
|
||||||
|
return "-";
|
||||||
|
} else {
|
||||||
|
DateTime now = DateTime.now();
|
||||||
|
Duration d = now.difference(_lastUpdated);
|
||||||
|
String text;
|
||||||
|
int v;
|
||||||
|
if (d.inDays == 0) {
|
||||||
|
if (d.inHours == 0) {
|
||||||
|
if (d.inMinutes == 0) {
|
||||||
|
text = "seconds ago";
|
||||||
|
v = d.inSeconds;
|
||||||
|
} else {
|
||||||
|
text = "minutes ago";
|
||||||
|
v = d.inMinutes;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
text = "hours ago";
|
||||||
|
v = d.inHours;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
text = "days ago";
|
||||||
|
v = d.inDays;
|
||||||
|
}
|
||||||
|
return "$v $text";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
116
lib/entities/entity_wrapper.class.dart
Normal file
@ -0,0 +1,116 @@
|
|||||||
|
part of '../main.dart';
|
||||||
|
|
||||||
|
class EntityWrapper {
|
||||||
|
|
||||||
|
String displayName;
|
||||||
|
String icon;
|
||||||
|
String unitOfMeasurement;
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
unitOfMeasurement = entity.unitOfMeasurement;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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 {
|
||||||
|
Launcher.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 {
|
||||||
|
Launcher.launchURL(uiAction.holdService);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
default: {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
32
lib/entities/fan/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();
|
||||||
|
}
|
||||||
|
}
|
123
lib/entities/fan/widgets/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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
44
lib/entities/group/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/entities/light/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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
218
lib/entities/light/widgets/light_controls.dart
Normal file
@ -0,0 +1,218 @@
|
|||||||
|
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 ?? 1;
|
||||||
|
_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;
|
||||||
|
eventBus.fire(new ServiceCallEvent(
|
||||||
|
entity.domain, "turn_on", entity.entityId,
|
||||||
|
{"brightness": _tmpBrightness}));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
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: 1.0,
|
||||||
|
max: 255.0,
|
||||||
|
onChangeEnd: (value) => _setBrightness(entity, value),
|
||||||
|
value: _tmpBrightness == null ? 1.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 = 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(() {
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
21
lib/entities/lock/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,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
66
lib/entities/lock/widgets/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),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
83
lib/entities/media_player/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();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
466
lib/entities/media_player/widgets/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();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
14
lib/entities/select/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();
|
||||||
|
}
|
||||||
|
}
|
49
lib/entities/select/widgets/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,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
13
lib/entities/sensor/sensor_entity.class.dart
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
part of '../../main.dart';
|
||||||
|
|
||||||
|
class SensorEntity extends Entity {
|
||||||
|
|
||||||
|
@override
|
||||||
|
EntityHistoryConfig historyConfig = EntityHistoryConfig(
|
||||||
|
chartType: EntityHistoryWidgetType.numericState,
|
||||||
|
numericState: true
|
||||||
|
);
|
||||||
|
|
||||||
|
SensorEntity(Map rawData, String webHost) : super(rawData, webHost);
|
||||||
|
|
||||||
|
}
|
44
lib/entities/slider/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();
|
||||||
|
}
|
||||||
|
}
|
70
lib/entities/slider/widgets/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
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
5
lib/entities/sun/sun_entity.class.dart
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
part of '../../main.dart';
|
||||||
|
|
||||||
|
class SunEntity extends Entity {
|
||||||
|
SunEntity(Map rawData, String webHost) : super(rawData, webHost);
|
||||||
|
}
|
10
lib/entities/switch/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();
|
||||||
|
}
|
||||||
|
}
|
89
lib/entities/switch/widget/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
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
16
lib/entities/text/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();
|
||||||
|
}
|
||||||
|
}
|
94
lib/entities/text/widgets/text_input_state.dart
Normal file
@ -0,0 +1,94 @@
|
|||||||
|
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: TextEditingController.fromValue(TextEditingValue(text: _tmpValue)),
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
45
lib/entities/timer/timer_entity.class.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();
|
||||||
|
}
|
||||||
|
}
|
65
lib/entities/timer/widgets/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,24 +0,0 @@
|
|||||||
part of '../main.dart';
|
|
||||||
|
|
||||||
class _ButtonEntityWidgetState extends _EntityWidgetState {
|
|
||||||
|
|
||||||
@override
|
|
||||||
void setNewState(newValue) {
|
|
||||||
eventBus.fire(new ServiceCallEvent(widget.entity.domain, "turn_on", widget.entity.entityId, null));
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget _buildActionWidget(bool inCard, BuildContext context) {
|
|
||||||
return FlatButton(
|
|
||||||
onPressed: (() {
|
|
||||||
setNewState(null);
|
|
||||||
}),
|
|
||||||
child: Text(
|
|
||||||
"EXECUTE",
|
|
||||||
textAlign: TextAlign.right,
|
|
||||||
style:
|
|
||||||
new TextStyle(fontSize: Entity.STATE_FONT_SIZE, color: Colors.blue),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,92 +0,0 @@
|
|||||||
part of '../main.dart';
|
|
||||||
|
|
||||||
class _DateTimeEntityWidgetState extends _EntityWidgetState {
|
|
||||||
bool get hasDate => widget.entity._attributes["has_date"] ?? false;
|
|
||||||
bool get hasTime => widget.entity._attributes["has_time"] ?? false;
|
|
||||||
int get year => widget.entity._attributes["year"] ?? 1970;
|
|
||||||
int get month => widget.entity._attributes["month"] ?? 1;
|
|
||||||
int get day => widget.entity._attributes["day"] ?? 1;
|
|
||||||
int get hour => widget.entity._attributes["hour"] ?? 0;
|
|
||||||
int get minute => widget.entity._attributes["minute"] ?? 0;
|
|
||||||
int get second => widget.entity._attributes["second"] ?? 0;
|
|
||||||
String get formattedState => _getFormattedState();
|
|
||||||
DateTime get dateTimeState => _getDateTimeState();
|
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void setNewState(newValue) {
|
|
||||||
eventBus.fire(new ServiceCallEvent(widget.entity.domain, "set_datetime", widget.entity.entityId,
|
|
||||||
newValue));
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget _buildActionWidget(bool inCard, BuildContext context) {
|
|
||||||
return Padding(
|
|
||||||
padding:
|
|
||||||
EdgeInsets.fromLTRB(0.0, 0.0, Entity.RIGHT_WIDGET_PADDING, 0.0),
|
|
||||||
child: GestureDetector(
|
|
||||||
child: Text(
|
|
||||||
"$formattedState",
|
|
||||||
textAlign: TextAlign.right,
|
|
||||||
style: new TextStyle(
|
|
||||||
fontSize: Entity.STATE_FONT_SIZE,
|
|
||||||
)),
|
|
||||||
onTap: () => _handleStateTap(context),
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
void _handleStateTap(BuildContext context) {
|
|
||||||
if (hasDate) {
|
|
||||||
_showDatePicker(context).then((date) {
|
|
||||||
if (date != null) {
|
|
||||||
if (hasTime) {
|
|
||||||
_showTimePicker(context).then((time){
|
|
||||||
setNewState({"date": "${formatDate(date, [yyyy, '-', mm, '-', dd])}", "time": "${formatDate(DateTime(1970, 1, 1, time.hour, time.minute), [HH, ':', nn])}"});
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
setNewState({"date": "${formatDate(date, [yyyy, '-', mm, '-', dd])}"});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} else if (hasTime) {
|
|
||||||
_showTimePicker(context).then((time){
|
|
||||||
if (time != null) {
|
|
||||||
setNewState({"time": "${formatDate(DateTime(1970, 1, 1, time.hour, time.minute), [HH, ':', nn])}"});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
TheLogger.log("Warning", "${widget.entity.entityId} has no date and no time");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Future _showDatePicker(BuildContext context) {
|
|
||||||
return showDatePicker(
|
|
||||||
context: context,
|
|
||||||
initialDate: dateTimeState,
|
|
||||||
firstDate: DateTime(1970),
|
|
||||||
lastDate: DateTime(2037) //Unix timestamp will finish at Jan 19, 2038
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Future _showTimePicker(BuildContext context) {
|
|
||||||
return showTimePicker(
|
|
||||||
context: context,
|
|
||||||
initialTime: TimeOfDay.fromDateTime(dateTimeState)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,242 +0,0 @@
|
|||||||
part of '../main.dart';
|
|
||||||
|
|
||||||
class Entity {
|
|
||||||
static const STATE_ICONS_COLORS = {
|
|
||||||
"on": Colors.amber,
|
|
||||||
"off": Color.fromRGBO(68, 115, 158, 1.0),
|
|
||||||
"unavailable": Colors.black12,
|
|
||||||
"unknown": Colors.black12,
|
|
||||||
"playing": Colors.amber
|
|
||||||
};
|
|
||||||
static const RIGHT_WIDGET_PADDING = 14.0;
|
|
||||||
static const LEFT_WIDGET_PADDING = 8.0;
|
|
||||||
static const EXTENDED_WIDGET_HEIGHT = 50.0;
|
|
||||||
static const WIDGET_HEIGHT = 34.0;
|
|
||||||
static const ICON_SIZE = 28.0;
|
|
||||||
static const STATE_FONT_SIZE = 16.0;
|
|
||||||
static const NAME_FONT_SIZE = 16.0;
|
|
||||||
static const SMALL_FONT_SIZE = 14.0;
|
|
||||||
static const INPUT_WIDTH = 160.0;
|
|
||||||
|
|
||||||
Map _attributes;
|
|
||||||
String _domain;
|
|
||||||
String _entityId;
|
|
||||||
String _state;
|
|
||||||
String assumedState;
|
|
||||||
DateTime _lastUpdated;
|
|
||||||
|
|
||||||
String get displayName =>
|
|
||||||
_attributes["friendly_name"] ?? (_attributes["name"] ?? "_");
|
|
||||||
String get domain => _domain;
|
|
||||||
String get entityId => _entityId;
|
|
||||||
String get state => _state;
|
|
||||||
set state(value) => _state = value;
|
|
||||||
|
|
||||||
String get deviceClass => _attributes["device_class"] ?? null;
|
|
||||||
bool get isView =>
|
|
||||||
(_domain == "group") &&
|
|
||||||
(_attributes != null ? _attributes["view"] ?? false : false);
|
|
||||||
bool get isGroup => _domain == "group";
|
|
||||||
String get icon => _attributes["icon"] ?? "";
|
|
||||||
bool get isOn => state == "on";
|
|
||||||
String get entityPicture => _attributes["entity_picture"];
|
|
||||||
String get unitOfMeasurement => _attributes["unit_of_measurement"] ?? "";
|
|
||||||
List get childEntities => _attributes["entity_id"] ?? [];
|
|
||||||
String get lastUpdated => _getLastUpdatedFormatted();
|
|
||||||
|
|
||||||
Entity(Map rawData) {
|
|
||||||
update(rawData);
|
|
||||||
}
|
|
||||||
|
|
||||||
void update(Map rawData) {
|
|
||||||
_attributes = rawData["attributes"] ?? {};
|
|
||||||
_domain = rawData["entity_id"].split(".")[0];
|
|
||||||
_entityId = rawData["entity_id"];
|
|
||||||
_state = rawData["state"];
|
|
||||||
assumedState = _state;
|
|
||||||
_lastUpdated = DateTime.tryParse(rawData["last_updated"]);
|
|
||||||
}
|
|
||||||
|
|
||||||
EntityWidget buildWidget(BuildContext context, bool inCard) {
|
|
||||||
return EntityWidget(
|
|
||||||
entity: this,
|
|
||||||
inCard: inCard,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
String _getLastUpdatedFormatted() {
|
|
||||||
if (_lastUpdated == null) {
|
|
||||||
return "-";
|
|
||||||
} else {
|
|
||||||
DateTime now = DateTime.now();
|
|
||||||
Duration d = now.difference(_lastUpdated);
|
|
||||||
String text;
|
|
||||||
int v;
|
|
||||||
if (d.inDays == 0) {
|
|
||||||
if (d.inHours == 0) {
|
|
||||||
if (d.inMinutes == 0) {
|
|
||||||
text = "seconds ago";
|
|
||||||
v = d.inSeconds;
|
|
||||||
} else {
|
|
||||||
text = "minutes ago";
|
|
||||||
v = d.inMinutes;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
text = "hours ago";
|
|
||||||
v = d.inHours;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
text = "days ago";
|
|
||||||
v = d.inDays;
|
|
||||||
}
|
|
||||||
return "$v $text";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
class EntityWidget extends StatefulWidget {
|
|
||||||
EntityWidget({Key key, this.entity, this.inCard}) : super(key: key);
|
|
||||||
|
|
||||||
final Entity entity;
|
|
||||||
final bool inCard;
|
|
||||||
|
|
||||||
@override
|
|
||||||
_EntityWidgetState createState() {
|
|
||||||
switch (entity.domain) {
|
|
||||||
case "automation":
|
|
||||||
case "input_boolean":
|
|
||||||
case "switch":
|
|
||||||
case "light": {
|
|
||||||
return _SwitchEntityWidgetState();
|
|
||||||
}
|
|
||||||
|
|
||||||
case "script":
|
|
||||||
case "scene": {
|
|
||||||
return _ButtonEntityWidgetState();
|
|
||||||
}
|
|
||||||
|
|
||||||
case "input_datetime": {
|
|
||||||
return _DateTimeEntityWidgetState();
|
|
||||||
}
|
|
||||||
|
|
||||||
case "input_select": {
|
|
||||||
return _SelectEntityWidgetState();
|
|
||||||
}
|
|
||||||
|
|
||||||
case "input_number": {
|
|
||||||
return _SliderEntityWidgetState();
|
|
||||||
}
|
|
||||||
|
|
||||||
case "input_text": {
|
|
||||||
return _TextEntityWidgetState();
|
|
||||||
}
|
|
||||||
|
|
||||||
default: {
|
|
||||||
return _EntityWidgetState();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class _EntityWidgetState extends State<EntityWidget> {
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
if (widget.inCard) {
|
|
||||||
return _buildMainWidget(context);
|
|
||||||
} else {
|
|
||||||
return ListView(
|
|
||||||
children: <Widget>[
|
|
||||||
_buildMainWidget(context),
|
|
||||||
_buildLastUpdatedWidget()
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildMainWidget(BuildContext context) {
|
|
||||||
return SizedBox(
|
|
||||||
height: Entity.WIDGET_HEIGHT,
|
|
||||||
child: Row(
|
|
||||||
children: <Widget>[
|
|
||||||
GestureDetector(
|
|
||||||
child: _buildIconWidget(),
|
|
||||||
onTap: widget.inCard ? openEntityPage : null,
|
|
||||||
),
|
|
||||||
Expanded(
|
|
||||||
child: GestureDetector(
|
|
||||||
child: _buildNameWidget(),
|
|
||||||
onTap: widget.inCard ? openEntityPage : null,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
_buildActionWidget(widget.inCard, context)
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
void openEntityPage() {
|
|
||||||
eventBus.fire(new ShowEntityPageEvent(widget.entity));
|
|
||||||
}
|
|
||||||
|
|
||||||
void setNewState(newState) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget buildAdditionalWidget() {
|
|
||||||
return _buildLastUpdatedWidget();
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildIconWidget() {
|
|
||||||
return Padding(
|
|
||||||
padding: EdgeInsets.fromLTRB(Entity.LEFT_WIDGET_PADDING, 0.0, 12.0, 0.0),
|
|
||||||
child: MaterialDesignIcons.createIconWidgetFromEntityData(
|
|
||||||
widget.entity,
|
|
||||||
Entity.ICON_SIZE,
|
|
||||||
Entity.STATE_ICONS_COLORS[widget.entity.state] ?? Colors.blueGrey),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildLastUpdatedWidget() {
|
|
||||||
return Padding(
|
|
||||||
padding: EdgeInsets.fromLTRB(
|
|
||||||
Entity.LEFT_WIDGET_PADDING, Entity.SMALL_FONT_SIZE, 0.0, 0.0),
|
|
||||||
child: Text(
|
|
||||||
'${widget.entity.lastUpdated}',
|
|
||||||
textAlign: TextAlign.left,
|
|
||||||
style:
|
|
||||||
TextStyle(fontSize: Entity.SMALL_FONT_SIZE, color: Colors.black26),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildNameWidget() {
|
|
||||||
return Padding(
|
|
||||||
padding: EdgeInsets.only(right: 10.0),
|
|
||||||
child: Text(
|
|
||||||
"${widget.entity.displayName}",
|
|
||||||
overflow: TextOverflow.ellipsis,
|
|
||||||
softWrap: false,
|
|
||||||
style: TextStyle(fontSize: Entity.NAME_FONT_SIZE),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildActionWidget(bool inCard, BuildContext context) {
|
|
||||||
return Padding(
|
|
||||||
padding:
|
|
||||||
EdgeInsets.fromLTRB(0.0, 0.0, Entity.RIGHT_WIDGET_PADDING, 0.0),
|
|
||||||
child: GestureDetector(
|
|
||||||
child: Text(
|
|
||||||
"${widget.entity.state}${widget.entity.unitOfMeasurement}",
|
|
||||||
textAlign: TextAlign.right,
|
|
||||||
style: new TextStyle(
|
|
||||||
fontSize: Entity.STATE_FONT_SIZE,
|
|
||||||
)),
|
|
||||||
onTap: openEntityPage,
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,36 +0,0 @@
|
|||||||
part of '../main.dart';
|
|
||||||
|
|
||||||
class _SelectEntityWidgetState extends _EntityWidgetState {
|
|
||||||
List<String> _listOptions = [];
|
|
||||||
|
|
||||||
@override
|
|
||||||
void setNewState(newValue) {
|
|
||||||
eventBus.fire(new ServiceCallEvent(widget.entity.domain, "select_option", widget.entity.entityId,
|
|
||||||
{"option": "$newValue"}));
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget _buildActionWidget(bool inCard, BuildContext context) {
|
|
||||||
_listOptions.clear();
|
|
||||||
if (widget.entity._attributes["options"] != null) {
|
|
||||||
widget.entity._attributes["options"].forEach((value){
|
|
||||||
_listOptions.add(value.toString());
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return Expanded(
|
|
||||||
//width: Entity.INPUT_WIDTH,
|
|
||||||
child: DropdownButton<String>(
|
|
||||||
value: widget.entity.state,
|
|
||||||
items: this._listOptions.map((String value) {
|
|
||||||
return new DropdownMenuItem<String>(
|
|
||||||
value: value,
|
|
||||||
child: new Text(value),
|
|
||||||
);
|
|
||||||
}).toList(),
|
|
||||||
onChanged: (_) {
|
|
||||||
setNewState(_);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,65 +0,0 @@
|
|||||||
part of '../main.dart';
|
|
||||||
|
|
||||||
class _SliderEntityWidgetState extends _EntityWidgetState {
|
|
||||||
int _multiplier = 1;
|
|
||||||
|
|
||||||
double get minValue => widget.entity._attributes["min"] ?? 0.0;
|
|
||||||
double get maxValue => widget.entity._attributes["max"] ?? 100.0;
|
|
||||||
double get valueStep => widget.entity._attributes["step"] ?? 1.0;
|
|
||||||
double get doubleState => double.tryParse(widget.entity.state) ?? 0.0;
|
|
||||||
|
|
||||||
@override
|
|
||||||
void initState() {
|
|
||||||
super.initState();
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void setNewState(newValue) {
|
|
||||||
eventBus.fire(new ServiceCallEvent(widget.entity.domain, "set_value", widget.entity.entityId,
|
|
||||||
{"value": "${newValue.toString()}"}));
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget _buildActionWidget(bool inCard, BuildContext context) {
|
|
||||||
if (valueStep < 1) {
|
|
||||||
_multiplier = 10;
|
|
||||||
} else if (valueStep < 0.1) {
|
|
||||||
_multiplier = 100;
|
|
||||||
}
|
|
||||||
return Expanded(
|
|
||||||
//width: 200.0,
|
|
||||||
child: Row(
|
|
||||||
children: <Widget>[
|
|
||||||
Expanded(
|
|
||||||
child: Slider(
|
|
||||||
min: this.minValue * _multiplier,
|
|
||||||
max: this.maxValue * _multiplier,
|
|
||||||
value: (doubleState <= this.maxValue) &&
|
|
||||||
(doubleState >= this.minValue)
|
|
||||||
? doubleState * _multiplier
|
|
||||||
: this.minValue * _multiplier,
|
|
||||||
onChanged: (value) {
|
|
||||||
setState(() {
|
|
||||||
widget.entity.state = (value.roundToDouble() / _multiplier).toString();
|
|
||||||
});
|
|
||||||
/*eventBus.fire(new StateChangedEvent(widget.entity.entityId,
|
|
||||||
(value.roundToDouble() / _multiplier).toString(), true));*/
|
|
||||||
},
|
|
||||||
onChangeEnd: (value) {
|
|
||||||
setNewState(value.roundToDouble() / _multiplier);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Padding(
|
|
||||||
padding: EdgeInsets.only(right: Entity.RIGHT_WIDGET_PADDING),
|
|
||||||
child: Text("${widget.entity.state}${widget.entity.unitOfMeasurement}",
|
|
||||||
textAlign: TextAlign.right,
|
|
||||||
style: new TextStyle(
|
|
||||||
fontSize: Entity.STATE_FONT_SIZE,
|
|
||||||
)),
|
|
||||||
)
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,33 +0,0 @@
|
|||||||
part of '../main.dart';
|
|
||||||
|
|
||||||
class _SwitchEntityWidgetState extends _EntityWidgetState {
|
|
||||||
|
|
||||||
@override
|
|
||||||
void initState() {
|
|
||||||
super.initState();
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void setNewState(newValue) {
|
|
||||||
setState(() {
|
|
||||||
widget.entity.assumedState = newValue ? 'on' : 'off';
|
|
||||||
});
|
|
||||||
Timer(Duration(seconds: 2), (){
|
|
||||||
setState(() {
|
|
||||||
widget.entity.assumedState = widget.entity.state;
|
|
||||||
});
|
|
||||||
});
|
|
||||||
eventBus.fire(new ServiceCallEvent(
|
|
||||||
widget.entity.domain, (newValue as bool) ? "turn_on" : "turn_off", widget.entity.entityId, null));
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget _buildActionWidget(bool inCard, BuildContext context) {
|
|
||||||
return Switch(
|
|
||||||
value: widget.entity.assumedState == 'on',
|
|
||||||
onChanged: ((switchState) {
|
|
||||||
setNewState(switchState);
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,83 +0,0 @@
|
|||||||
part of '../main.dart';
|
|
||||||
|
|
||||||
class _TextEntityWidgetState extends _EntityWidgetState {
|
|
||||||
String _tmpValue;
|
|
||||||
FocusNode _focusNode = FocusNode();
|
|
||||||
bool validValue = false;
|
|
||||||
|
|
||||||
int get valueMinLength => widget.entity._attributes["min"] ?? -1;
|
|
||||||
int get valueMaxLength => widget.entity._attributes["max"] ?? -1;
|
|
||||||
String get valuePattern => widget.entity._attributes["pattern"] ?? null;
|
|
||||||
bool get isTextField => widget.entity._attributes["mode"] == "text";
|
|
||||||
bool get isPasswordField => widget.entity._attributes["mode"] == "password";
|
|
||||||
|
|
||||||
@override
|
|
||||||
void initState() {
|
|
||||||
super.initState();
|
|
||||||
_focusNode.addListener(_focusListener);
|
|
||||||
_tmpValue = widget.entity.state;
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void setNewState(newValue) {
|
|
||||||
if (validate(newValue)) {
|
|
||||||
eventBus.fire(new ServiceCallEvent(widget.entity.domain, "set_value", widget.entity.entityId,
|
|
||||||
{"value": "$newValue"}));
|
|
||||||
} else {
|
|
||||||
setState(() {
|
|
||||||
_tmpValue = widget.entity.state;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
bool validate(newValue) {
|
|
||||||
if (newValue is String) {
|
|
||||||
validValue = (newValue.length >= this.valueMinLength) &&
|
|
||||||
(this.valueMaxLength == -1 ||
|
|
||||||
(newValue.length <= this.valueMaxLength));
|
|
||||||
} else {
|
|
||||||
validValue = true;
|
|
||||||
}
|
|
||||||
return validValue;
|
|
||||||
}
|
|
||||||
|
|
||||||
void _focusListener() {
|
|
||||||
if (!_focusNode.hasFocus && (_tmpValue != widget.entity.state)) {
|
|
||||||
setNewState(_tmpValue);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget _buildActionWidget(bool inCard, BuildContext context) {
|
|
||||||
if (!_focusNode.hasFocus && (_tmpValue != widget.entity.state)) {
|
|
||||||
_tmpValue = widget.entity.state;
|
|
||||||
}
|
|
||||||
if (this.isTextField || this.isPasswordField) {
|
|
||||||
return Expanded(
|
|
||||||
//width: Entity.INPUT_WIDTH,
|
|
||||||
child: TextField(
|
|
||||||
focusNode: _focusNode,
|
|
||||||
obscureText: this.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 ${widget.entity.entityId}");
|
|
||||||
return super._buildActionWidget(inCard, context);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void dispose() {
|
|
||||||
_focusNode.removeListener(_focusListener);
|
|
||||||
_focusNode.dispose();
|
|
||||||
super.dispose();
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -2,90 +2,190 @@ part of 'main.dart';
|
|||||||
|
|
||||||
class EntityCollection {
|
class EntityCollection {
|
||||||
|
|
||||||
Map<String, Entity> _entities;
|
final homeAssistantWebHost;
|
||||||
List<String> viewList;
|
|
||||||
|
|
||||||
EntityCollection() {
|
Map<String, Entity> _allEntities;
|
||||||
_entities = {};
|
//Map<String, Entity> views;
|
||||||
viewList = [];
|
|
||||||
|
bool get isEmpty => _allEntities.isEmpty;
|
||||||
|
List<Entity> get viewEntities => _allEntities.values.where((entity) => entity.isView).toList();
|
||||||
|
|
||||||
|
EntityCollection(this.homeAssistantWebHost) {
|
||||||
|
_allEntities = {};
|
||||||
|
//views = {};
|
||||||
}
|
}
|
||||||
|
|
||||||
bool get hasDefaultView => _entities["group.default_view"] != null;
|
bool get hasDefaultView => _allEntities.keys.contains("group.default_view");
|
||||||
|
|
||||||
void parse(List rawData) {
|
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) {
|
_allEntities.forEach((entityId, entity){
|
||||||
viewList.add(newEntity.entityId);
|
if ((entity.isGroup) && (entity.childEntityIds != null)) {
|
||||||
|
entity.childEntities = getAll(entity.childEntityIds);
|
||||||
}
|
}
|
||||||
|
/*if (entity.isView) {
|
||||||
|
views[entityId] = entity;
|
||||||
|
}*/
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
Entity _createEntityInstance(rawEntityData) {
|
void clear() {
|
||||||
return Entity(rawEntityData);
|
_allEntities.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
void updateState(Map rawStateData) {
|
Entity _createEntityInstance(rawEntityData) {
|
||||||
|
switch (rawEntityData["entity_id"].split(".")[0]) {
|
||||||
|
case 'sun': {
|
||||||
|
return SunEntity(rawEntityData, homeAssistantWebHost);
|
||||||
|
}
|
||||||
|
case "media_player": {
|
||||||
|
return MediaPlayerEntity(rawEntityData, homeAssistantWebHost);
|
||||||
|
}
|
||||||
|
case 'sensor': {
|
||||||
|
return SensorEntity(rawEntityData, homeAssistantWebHost);
|
||||||
|
}
|
||||||
|
case 'lock': {
|
||||||
|
return LockEntity(rawEntityData, homeAssistantWebHost);
|
||||||
|
}
|
||||||
|
case "automation": {
|
||||||
|
return AutomationEntity(rawEntityData, homeAssistantWebHost);
|
||||||
|
}
|
||||||
|
|
||||||
|
case "input_boolean":
|
||||||
|
case "switch": {
|
||||||
|
return SwitchEntity(rawEntityData, homeAssistantWebHost);
|
||||||
|
}
|
||||||
|
case "light": {
|
||||||
|
return LightEntity(rawEntityData, homeAssistantWebHost);
|
||||||
|
}
|
||||||
|
case "group": {
|
||||||
|
return GroupEntity(rawEntityData, homeAssistantWebHost);
|
||||||
|
}
|
||||||
|
case "script":
|
||||||
|
case "scene": {
|
||||||
|
return ButtonEntity(rawEntityData, homeAssistantWebHost);
|
||||||
|
}
|
||||||
|
case "input_datetime": {
|
||||||
|
return DateTimeEntity(rawEntityData, homeAssistantWebHost);
|
||||||
|
}
|
||||||
|
case "input_select": {
|
||||||
|
return SelectEntity(rawEntityData, homeAssistantWebHost);
|
||||||
|
}
|
||||||
|
case "input_number": {
|
||||||
|
return SliderEntity(rawEntityData, homeAssistantWebHost);
|
||||||
|
}
|
||||||
|
case "input_text": {
|
||||||
|
return TextEntity(rawEntityData, homeAssistantWebHost);
|
||||||
|
}
|
||||||
|
case "climate": {
|
||||||
|
return ClimateEntity(rawEntityData, homeAssistantWebHost);
|
||||||
|
}
|
||||||
|
case "cover": {
|
||||||
|
return CoverEntity(rawEntityData, homeAssistantWebHost);
|
||||||
|
}
|
||||||
|
case "fan": {
|
||||||
|
return FanEntity(rawEntityData, homeAssistantWebHost);
|
||||||
|
}
|
||||||
|
case "camera": {
|
||||||
|
return CameraEntity(rawEntityData, homeAssistantWebHost);
|
||||||
|
}
|
||||||
|
case "alarm_control_panel": {
|
||||||
|
return AlarmControlPanelEntity(rawEntityData, homeAssistantWebHost);
|
||||||
|
}
|
||||||
|
case "timer": {
|
||||||
|
return TimerEntity(rawEntityData, homeAssistantWebHost);
|
||||||
|
}
|
||||||
|
default: {
|
||||||
|
return Entity(rawEntityData, homeAssistantWebHost);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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) {
|
||||||
//TODO pass entity in this function and call update from it
|
get("${rawEntityData["entity_id"]}")?.update(rawEntityData, homeAssistantWebHost);
|
||||||
_entities[rawEntityData["entity_id"]].update(rawEntityData);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Entity get(String entityId) {
|
Entity get(String entityId) {
|
||||||
return _entities[entityId];
|
return _allEntities[entityId];
|
||||||
|
}
|
||||||
|
|
||||||
|
List<Entity> getAll(List ids) {
|
||||||
|
List<Entity> result = [];
|
||||||
|
ids.forEach((id){
|
||||||
|
Entity en = get(id);
|
||||||
|
if (en != null) {
|
||||||
|
result.add(en);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool isExist(String entityId) {
|
bool isExist(String entityId) {
|
||||||
return _entities[entityId] != null;
|
return _allEntities[entityId] != null;
|
||||||
}
|
}
|
||||||
|
|
||||||
Map<String,List<String>> getDefaultViewTopLevelEntities() {
|
List<Entity> getByDomains(List<String> domains) {
|
||||||
Map<String,List<String>> result = {"userGroups": [], "notGroupedEntities": []};
|
List<Entity> result = [];
|
||||||
List<String> entities = [];
|
_allEntities.forEach((id, entity) {
|
||||||
_entities.forEach((id, entity){
|
if (domains.contains(entity.domain)) {
|
||||||
if ((id.indexOf("group.") == 0) && (id.indexOf(".all_") == -1) && (!entity.isView)) {
|
Logger.d("getByDomain: ${entity.isHidden}");
|
||||||
result["userGroups"].add(id);
|
result.add(entity);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
List<Entity> filterEntitiesForDefaultView() {
|
||||||
|
List<Entity> result = [];
|
||||||
|
List<Entity> groups = [];
|
||||||
|
List<Entity> nonGroupEntities = [];
|
||||||
|
_allEntities.forEach((id, entity){
|
||||||
|
if (entity.isGroup && (entity.attributes['auto'] == null || (entity.attributes['auto'] && !entity.isHidden)) && (!entity.isView)) {
|
||||||
|
groups.add(entity);
|
||||||
}
|
}
|
||||||
if (!entity.isGroup) {
|
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].childEntities.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;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|