Compare commits
254 Commits
0.1.1-alph
...
0.3.11
Author | SHA1 | Date | |
---|---|---|---|
a8500d44e1 | |||
b4d4c5abec | |||
c19a3f272a | |||
b264534858 | |||
ab53f77f9e | |||
c73956720c | |||
051041e794 | |||
5c83be9fee | |||
4bece42693 | |||
4ae107fe4c | |||
9523ed2562 | |||
9c403480e2 | |||
20b1b90e39 | |||
5633e30448 | |||
4492fb9f0c | |||
36410752e4 | |||
0219f7bfbb | |||
5f3c77f4b9 | |||
a36c7a9ca3 | |||
56ce6dfeeb | |||
67c214454f | |||
73398378c4 | |||
215871ce9e | |||
fd8ea6befd | |||
809a1a1c8c | |||
fc8f2f200f | |||
f41c9f9197 | |||
cdf55ce68b | |||
12088d9516 | |||
a0235ee385 | |||
67fbdb13c6 | |||
c5960de0be | |||
da15e880ec | |||
efbe33f4e3 | |||
af84c99a2d | |||
438449cad8 | |||
d9ca55c3b7 | |||
f248268984 | |||
8ee096595c | |||
a8e79c289b | |||
2cd8533882 | |||
0a21d9c690 | |||
e77bb533b1 | |||
96f1211395 | |||
1e4cb03470 | |||
ab67b557ca | |||
82c9bd26d1 | |||
1bd04abd37 | |||
c5942d22b3 | |||
37ad5e81cf | |||
26187e6233 | |||
b8f6fda8d3 | |||
62b4e99810 | |||
25bf10a64e | |||
874410964d | |||
57c30917b3 | |||
87f89b63e1 | |||
3190b45db3 | |||
f5434e26e5 | |||
86b6ad6bba | |||
8a9641fbed | |||
5142391da2 | |||
01090dc3b1 | |||
0a7bbb5a38 | |||
c347eee9f0 | |||
90f197ba54 | |||
e09917c687 | |||
a69da832cb | |||
c1708fd980 | |||
c85a9bbe27 | |||
d9790dedbb | |||
30e4eaa023 | |||
54e00c3403 | |||
0e3474bbcb | |||
efd06ca547 | |||
69fd37d4fe | |||
4a49372410 | |||
478f58e2d8 | |||
a87aff67ac | |||
644f5e7fc6 | |||
3cddac3dc6 | |||
ab30c64eab | |||
6d79487219 | |||
9f7444eae0 | |||
788d682f2f | |||
66f84952f0 | |||
5d95c3702d | |||
1f0bd8059b | |||
a7830df628 | |||
790446d592 | |||
bb17885b4a | |||
04d8681656 | |||
71c4ac7fed | |||
3f7e21e97e | |||
e24c47b041 | |||
73b32b30a8 | |||
5b6155057c | |||
ff4185effe | |||
b2da9fc04d | |||
f281fab744 | |||
3b99f4feeb | |||
efab8b60b1 | |||
0e96406573 | |||
ed8757c08d | |||
813770329c | |||
1853bd466e | |||
07258477b3 | |||
a3adb72cf8 | |||
e25162f7b5 | |||
d30c9d574b | |||
efa5a1958c | |||
37f20fae5a | |||
91db34badb | |||
c20200b609 | |||
fcd4ac7292 | |||
e16338c3f2 | |||
6e038b0685 | |||
052cd3894e | |||
809c7d6355 | |||
9edfec7dff | |||
df56f6ceda | |||
5e834b0645 | |||
8fb0d61a84 | |||
54979b583b | |||
4e955e98d8 | |||
88cfcb4382 | |||
5338e45ddc | |||
24d071e2f8 | |||
988cd4a72f | |||
d1ea916781 | |||
ce9f25b86c | |||
f29762c931 | |||
30e4496ef1 | |||
7f9dc5dd3a | |||
0f6babc243 | |||
6a43e04b31 | |||
36fa5a50c4 | |||
9ad6d92ccd | |||
fafa8f43f4 | |||
9b490d33d5 | |||
33f9a1075e | |||
b83006e2c3 | |||
ba09c36bd2 | |||
c71ee568b0 | |||
75041f5c23 | |||
14da471774 | |||
369b44f1c8 | |||
8284bb6e76 | |||
9b3b4dfbbc | |||
5ca4424933 | |||
a308aa29a4 | |||
9e80b0eaaf | |||
85379cf491 | |||
758376a891 | |||
2ebba364e3 | |||
6e604440c0 | |||
c23034688e | |||
69f45b52cf | |||
ffc053fbe6 | |||
b5f9ecf601 | |||
948d1d4e23 | |||
136297c18b | |||
164800951d | |||
84d283de2b | |||
2fa35d771a | |||
326cd073b9 | |||
e99c3f5742 | |||
16a9392fa6 | |||
5bf063969b | |||
c19a0511a6 | |||
a4ac40b366 | |||
ce69f044fb | |||
70b6469bd1 | |||
253316fb1f | |||
ec71200ab0 | |||
bc1f4eab2e | |||
4085006446 | |||
b7fb821abe | |||
284e7ba451 | |||
17a3bd8d35 | |||
c2b88c8a12 | |||
c975af4c79 | |||
debf1b71f1 | |||
4725953b32 | |||
e7ca1209e2 | |||
f9afa663f5 | |||
5068cbbcf4 | |||
043d3a9905 | |||
77c5f80c13 | |||
e0d35d07dc | |||
285447a5b7 | |||
ed3e4ba272 | |||
908563063a | |||
7f2611b410 | |||
648750655c | |||
8a0d5581d9 | |||
98d716109b | |||
ebb2f2b4e5 | |||
d910e4dd43 | |||
95d80fbbfc | |||
41297150c2 | |||
b14b248f2f | |||
13fc1bff27 | |||
eee8f21e76 | |||
8ce3560d8d | |||
9e97bac85b | |||
4a0b447f00 | |||
bc4969dae8 | |||
5025b3d384 | |||
0d7e7eb6f7 | |||
062392b38c | |||
acd468ae75 | |||
60f216df13 | |||
9de8a659d3 | |||
7dd8f65af7 | |||
9e83a3e447 | |||
2f135169a9 | |||
76d2750ad6 | |||
571778fbd4 | |||
b89b5dfb98 | |||
a196b0d8d4 | |||
95f7c14296 | |||
2fcd27d240 | |||
6834f2ca34 | |||
c0a9b89d40 | |||
067ccfde02 | |||
4b4fc338f6 | |||
08c07e8398 | |||
df04d000b2 | |||
d0d1ab2740 | |||
af3a5bc611 | |||
b935a0e372 | |||
49444ab3df | |||
098a556279 | |||
375ae36884 | |||
0b42019ef3 | |||
516d38a8a9 | |||
fb886a4622 | |||
662b44d443 | |||
f9c48e6cc7 | |||
88d6e1008f | |||
4540fadf1e | |||
bd13d3693d | |||
5db9d6005f | |||
7e4f744598 | |||
772b569da5 | |||
0e11c1a146 | |||
60793dbf89 | |||
2b622cff04 | |||
94bcc30421 | |||
94f43ded6f | |||
7f7be8aa78 | |||
c0e0059487 | |||
23d3d1839f |
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.
|
14
README.md
@ -1,3 +1,13 @@
|
|||||||
# 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.
|
Home Assistant Android client on Dart with Flutter.
|
||||||
|
|
||||||
|
Visit [www.keyboardcrumbs.io](http://www.keyboardcrumbs.io/ha-client) for more info.
|
||||||
|
|
||||||
|
Join [Google Group](https://groups.google.com/d/forum/ha-client-alpha-testing) to become an alpha tester
|
||||||
|
|
||||||
|
Download the app from [Google Play](https://play.google.com/apps/testing/com.keyboardcrumbs.haclient) after joining the group
|
||||||
|
|
||||||
|
Discuss it in [Home Assistant community](https://community.home-assistant.io/t/alpha-testing-ha-client-native-android-client-for-home-assistant/69912) or in [Discord](https://discord.gg/NSaQEQ8)
|
||||||
|
@ -39,8 +39,8 @@ android {
|
|||||||
applicationId "com.keyboardcrumbs.haclient"
|
applicationId "com.keyboardcrumbs.haclient"
|
||||||
minSdkVersion 21
|
minSdkVersion 21
|
||||||
targetSdkVersion 27
|
targetSdkVersion 27
|
||||||
versionCode 19
|
versionCode flutterVersionCode.toInteger()
|
||||||
versionName "0.1.1-alpha"
|
versionName flutterVersionName
|
||||||
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
|
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Before Width: | Height: | Size: 2.2 KiB After Width: | Height: | Size: 2.5 KiB |
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 1.6 KiB |
Before Width: | Height: | Size: 3.0 KiB After Width: | Height: | Size: 3.1 KiB |
Before Width: | Height: | Size: 4.5 KiB After Width: | Height: | Size: 4.5 KiB |
Before Width: | Height: | Size: 5.6 KiB After Width: | Height: | Size: 6.2 KiB |
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 |
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 14 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 |
65
lib/entity.page.dart
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
part of 'main.dart';
|
||||||
|
|
||||||
|
class EntityViewPage extends StatefulWidget {
|
||||||
|
EntityViewPage({Key key, @required this.entityId, @required this.homeAssistant }) : super(key: key);
|
||||||
|
|
||||||
|
final String entityId;
|
||||||
|
final HomeAssistant homeAssistant;
|
||||||
|
|
||||||
|
@override
|
||||||
|
_EntityViewPageState createState() => new _EntityViewPageState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _EntityViewPageState extends State<EntityViewPage> {
|
||||||
|
String _title;
|
||||||
|
StreamSubscription _refreshDataSubscription;
|
||||||
|
StreamSubscription _stateSubscription;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_stateSubscription = eventBus.on<StateChangedEvent>().listen((event) {
|
||||||
|
TheLogger.debug("State change event handled by entity page: ${event.entityId}");
|
||||||
|
if (event.entityId == widget.entityId) {
|
||||||
|
setState(() {});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
_refreshDataSubscription = eventBus.on<RefreshDataFinishedEvent>().listen((event) {
|
||||||
|
setState(() {});
|
||||||
|
});
|
||||||
|
_prepareData();
|
||||||
|
}
|
||||||
|
|
||||||
|
void _prepareData() async {
|
||||||
|
_title = widget.homeAssistant.entities.get(widget.entityId).displayName;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return new Scaffold(
|
||||||
|
appBar: new AppBar(
|
||||||
|
leading: IconButton(icon: Icon(Icons.arrow_back), onPressed: (){
|
||||||
|
Navigator.pop(context);
|
||||||
|
}),
|
||||||
|
// Here we take the value from the MyHomePage object that was created by
|
||||||
|
// the App.build method, and use it to set our appbar title.
|
||||||
|
title: new Text(_title),
|
||||||
|
),
|
||||||
|
body: Padding(
|
||||||
|
padding: EdgeInsets.all(10.0),
|
||||||
|
child: HomeAssistantModel(
|
||||||
|
homeAssistant: widget.homeAssistant,
|
||||||
|
child: widget.homeAssistant.entities.get(widget.entityId).buildEntityPageWidget(context)
|
||||||
|
)
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose(){
|
||||||
|
if (_stateSubscription != null) _stateSubscription.cancel();
|
||||||
|
if (_refreshDataSubscription != null) _refreshDataSubscription.cancel();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
}
|
10
lib/entity_class/button_entity.class.dart
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
part of '../main.dart';
|
||||||
|
|
||||||
|
class ButtonEntity extends Entity {
|
||||||
|
ButtonEntity(Map rawData) : super(rawData);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget _buildStatePart(BuildContext context) {
|
||||||
|
return ButtonStateWidget();
|
||||||
|
}
|
||||||
|
}
|
128
lib/entity_class/climate_entity.class.dart
Normal file
@ -0,0 +1,128 @@
|
|||||||
|
part of '../main.dart';
|
||||||
|
|
||||||
|
class ClimateEntity extends Entity {
|
||||||
|
|
||||||
|
@override
|
||||||
|
EntityHistoryConfig historyConfig = EntityHistoryConfig(
|
||||||
|
chartType: EntityHistoryWidgetType.numericAttributes,
|
||||||
|
numericState: false,
|
||||||
|
numericAttributesToShow: ["current_temperature"]
|
||||||
|
);
|
||||||
|
|
||||||
|
static const SUPPORT_TARGET_TEMPERATURE = 1;
|
||||||
|
static const SUPPORT_TARGET_TEMPERATURE_HIGH = 2;
|
||||||
|
static const SUPPORT_TARGET_TEMPERATURE_LOW = 4;
|
||||||
|
static const SUPPORT_TARGET_HUMIDITY = 8;
|
||||||
|
static const SUPPORT_TARGET_HUMIDITY_HIGH = 16;
|
||||||
|
static const SUPPORT_TARGET_HUMIDITY_LOW = 32;
|
||||||
|
static const SUPPORT_FAN_MODE = 64;
|
||||||
|
static const SUPPORT_OPERATION_MODE = 128;
|
||||||
|
static const SUPPORT_HOLD_MODE = 256;
|
||||||
|
static const SUPPORT_SWING_MODE = 512;
|
||||||
|
static const SUPPORT_AWAY_MODE = 1024;
|
||||||
|
static const SUPPORT_AUX_HEAT = 2048;
|
||||||
|
static const SUPPORT_ON_OFF = 4096;
|
||||||
|
|
||||||
|
bool get supportTargetTemperature => ((attributes["supported_features"] &
|
||||||
|
ClimateEntity.SUPPORT_TARGET_TEMPERATURE) ==
|
||||||
|
ClimateEntity.SUPPORT_TARGET_TEMPERATURE);
|
||||||
|
bool get supportTargetTemperatureHigh => ((attributes["supported_features"] &
|
||||||
|
ClimateEntity.SUPPORT_TARGET_TEMPERATURE_HIGH) ==
|
||||||
|
ClimateEntity.SUPPORT_TARGET_TEMPERATURE_HIGH);
|
||||||
|
bool get supportTargetTemperatureLow => ((attributes["supported_features"] &
|
||||||
|
ClimateEntity.SUPPORT_TARGET_TEMPERATURE_LOW) ==
|
||||||
|
ClimateEntity.SUPPORT_TARGET_TEMPERATURE_LOW);
|
||||||
|
bool get supportTargetHumidity => ((attributes["supported_features"] &
|
||||||
|
ClimateEntity.SUPPORT_TARGET_HUMIDITY) ==
|
||||||
|
ClimateEntity.SUPPORT_TARGET_HUMIDITY);
|
||||||
|
bool get supportTargetHumidityHigh => ((attributes["supported_features"] &
|
||||||
|
ClimateEntity.SUPPORT_TARGET_HUMIDITY_HIGH) ==
|
||||||
|
ClimateEntity.SUPPORT_TARGET_HUMIDITY_HIGH);
|
||||||
|
bool get supportTargetHumidityLow => ((attributes["supported_features"] &
|
||||||
|
ClimateEntity.SUPPORT_TARGET_HUMIDITY_LOW) ==
|
||||||
|
ClimateEntity.SUPPORT_TARGET_HUMIDITY_LOW);
|
||||||
|
bool get supportFanMode =>
|
||||||
|
((attributes["supported_features"] & ClimateEntity.SUPPORT_FAN_MODE) ==
|
||||||
|
ClimateEntity.SUPPORT_FAN_MODE);
|
||||||
|
bool get supportOperationMode => ((attributes["supported_features"] &
|
||||||
|
ClimateEntity.SUPPORT_OPERATION_MODE) ==
|
||||||
|
ClimateEntity.SUPPORT_OPERATION_MODE);
|
||||||
|
bool get supportHoldMode =>
|
||||||
|
((attributes["supported_features"] & ClimateEntity.SUPPORT_HOLD_MODE) ==
|
||||||
|
ClimateEntity.SUPPORT_HOLD_MODE);
|
||||||
|
bool get supportSwingMode =>
|
||||||
|
((attributes["supported_features"] & ClimateEntity.SUPPORT_SWING_MODE) ==
|
||||||
|
ClimateEntity.SUPPORT_SWING_MODE);
|
||||||
|
bool get supportAwayMode =>
|
||||||
|
((attributes["supported_features"] & ClimateEntity.SUPPORT_AWAY_MODE) ==
|
||||||
|
ClimateEntity.SUPPORT_AWAY_MODE);
|
||||||
|
bool get supportAuxHeat =>
|
||||||
|
((attributes["supported_features"] & ClimateEntity.SUPPORT_AUX_HEAT) ==
|
||||||
|
ClimateEntity.SUPPORT_AUX_HEAT);
|
||||||
|
bool get supportOnOff =>
|
||||||
|
((attributes["supported_features"] & ClimateEntity.SUPPORT_ON_OFF) ==
|
||||||
|
ClimateEntity.SUPPORT_ON_OFF);
|
||||||
|
|
||||||
|
List<String> get operationList => attributes["operation_list"] != null
|
||||||
|
? (attributes["operation_list"] as List).cast<String>()
|
||||||
|
: null;
|
||||||
|
List<String> get fanList => attributes["fan_list"] != null
|
||||||
|
? (attributes["fan_list"] as List).cast<String>()
|
||||||
|
: null;
|
||||||
|
List<String> get swingList => attributes["swing_list"] != null
|
||||||
|
? (attributes["swing_list"] as List).cast<String>()
|
||||||
|
: null;
|
||||||
|
double get temperature => _getDoubleAttributeValue('temperature');
|
||||||
|
double get targetHigh => _getDoubleAttributeValue('target_temp_high');
|
||||||
|
double get targetLow => _getDoubleAttributeValue('target_temp_low');
|
||||||
|
double get maxTemp => _getDoubleAttributeValue('max_temp') ?? 100.0;
|
||||||
|
double get minTemp => _getDoubleAttributeValue('min_temp') ?? -100.0;
|
||||||
|
double get targetHumidity => _getDoubleAttributeValue('humidity');
|
||||||
|
double get maxHumidity => _getDoubleAttributeValue('max_humidity');
|
||||||
|
double get minHumidity => _getDoubleAttributeValue('min_humidity');
|
||||||
|
String get operationMode => attributes['operation_mode'];
|
||||||
|
String get fanMode => attributes['fan_mode'];
|
||||||
|
String get swingMode => attributes['swing_mode'];
|
||||||
|
bool get awayMode => attributes['away_mode'] == "on";
|
||||||
|
bool get isOff => state == EntityState.off;
|
||||||
|
bool get auxHeat => attributes['aux_heat'] == "on";
|
||||||
|
|
||||||
|
ClimateEntity(Map rawData) : super(rawData);
|
||||||
|
|
||||||
|
@override
|
||||||
|
void update(Map rawData) {
|
||||||
|
super.update(rawData);
|
||||||
|
if (supportTargetTemperature) {
|
||||||
|
historyConfig.numericAttributesToShow.add("temperature");
|
||||||
|
}
|
||||||
|
if (supportTargetTemperatureHigh) {
|
||||||
|
historyConfig.numericAttributesToShow.add("target_temp_high");
|
||||||
|
}
|
||||||
|
if (supportTargetTemperatureLow) {
|
||||||
|
historyConfig.numericAttributesToShow.add("target_temp_low");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget _buildStatePart(BuildContext context) {
|
||||||
|
return ClimateStateWidget();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget _buildAdditionalControlsForPage(BuildContext context) {
|
||||||
|
return ClimateControlWidget();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
double _getDoubleAttributeValue(String attributeName) {
|
||||||
|
var temp1 = attributes["$attributeName"];
|
||||||
|
if (temp1 is int) {
|
||||||
|
return temp1.toDouble();
|
||||||
|
} else if (temp1 is double) {
|
||||||
|
return temp1;
|
||||||
|
} else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
59
lib/entity_class/const.dart
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
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';
|
||||||
|
}
|
||||||
|
|
||||||
|
class EntityTapAction {
|
||||||
|
static const moreInfo = 'more-info';
|
||||||
|
static const toggle = 'toggle';
|
||||||
|
static const callService = 'call-service';
|
||||||
|
static const none = 'none';
|
||||||
|
}
|
||||||
|
|
||||||
|
class CardType {
|
||||||
|
static const horizontalStack = "horizontal-stack";
|
||||||
|
static const verticalStack = "vertical-stack";
|
||||||
|
static const entities = "entities";
|
||||||
|
static const glance = "glance";
|
||||||
|
static const mediaControl = "media-control";
|
||||||
|
static const weatherForecast = "weather-forecast";
|
||||||
|
static const thermostat = "thermostat";
|
||||||
|
static const sensor = "sensor";
|
||||||
|
static const plantStatus = "plant-status";
|
||||||
|
static const pictureEntity = "picture-entity";
|
||||||
|
static const pictureElements = "picture-elements";
|
||||||
|
static const picture = "picture";
|
||||||
|
static const map = "map";
|
||||||
|
static const iframe = "iframe";
|
||||||
|
static const gauge = "gauge";
|
||||||
|
static const entityButton = "entity-button";
|
||||||
|
static const conditional = "conditional";
|
||||||
|
static const alarmPanel = "alarm-panel";
|
||||||
|
}
|
60
lib/entity_class/cover_entity.class.dart
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
part of '../main.dart';
|
||||||
|
|
||||||
|
class CoverEntity extends Entity {
|
||||||
|
|
||||||
|
static const SUPPORT_OPEN = 1;
|
||||||
|
static const SUPPORT_CLOSE = 2;
|
||||||
|
static const SUPPORT_SET_POSITION = 4;
|
||||||
|
static const SUPPORT_STOP = 8;
|
||||||
|
static const SUPPORT_OPEN_TILT = 16;
|
||||||
|
static const SUPPORT_CLOSE_TILT = 32;
|
||||||
|
static const SUPPORT_STOP_TILT = 64;
|
||||||
|
static const SUPPORT_SET_TILT_POSITION = 128;
|
||||||
|
|
||||||
|
bool get supportOpen => ((attributes["supported_features"] &
|
||||||
|
CoverEntity.SUPPORT_OPEN) ==
|
||||||
|
CoverEntity.SUPPORT_OPEN);
|
||||||
|
bool get supportClose => ((attributes["supported_features"] &
|
||||||
|
CoverEntity.SUPPORT_CLOSE) ==
|
||||||
|
CoverEntity.SUPPORT_CLOSE);
|
||||||
|
bool get supportSetPosition => ((attributes["supported_features"] &
|
||||||
|
CoverEntity.SUPPORT_SET_POSITION) ==
|
||||||
|
CoverEntity.SUPPORT_SET_POSITION);
|
||||||
|
bool get supportStop => ((attributes["supported_features"] &
|
||||||
|
CoverEntity.SUPPORT_STOP) ==
|
||||||
|
CoverEntity.SUPPORT_STOP);
|
||||||
|
|
||||||
|
bool get supportOpenTilt => ((attributes["supported_features"] &
|
||||||
|
CoverEntity.SUPPORT_OPEN_TILT) ==
|
||||||
|
CoverEntity.SUPPORT_OPEN_TILT);
|
||||||
|
bool get supportCloseTilt => ((attributes["supported_features"] &
|
||||||
|
CoverEntity.SUPPORT_CLOSE_TILT) ==
|
||||||
|
CoverEntity.SUPPORT_CLOSE_TILT);
|
||||||
|
bool get supportStopTilt => ((attributes["supported_features"] &
|
||||||
|
CoverEntity.SUPPORT_STOP_TILT) ==
|
||||||
|
CoverEntity.SUPPORT_STOP_TILT);
|
||||||
|
bool get supportSetTiltPosition => ((attributes["supported_features"] &
|
||||||
|
CoverEntity.SUPPORT_SET_TILT_POSITION) ==
|
||||||
|
CoverEntity.SUPPORT_SET_TILT_POSITION);
|
||||||
|
|
||||||
|
|
||||||
|
double get currentPosition => _getDoubleAttributeValue('current_position');
|
||||||
|
double get currentTiltPosition => _getDoubleAttributeValue('current_tilt_position');
|
||||||
|
bool get canBeOpened => ((state != 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;
|
||||||
|
|
||||||
|
CoverEntity(Map rawData) : super(rawData);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget _buildStatePart(BuildContext context) {
|
||||||
|
return CoverStateWidget();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget _buildAdditionalControlsForPage(BuildContext context) {
|
||||||
|
return CoverControlWidget();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
42
lib/entity_class/date_time_entity.class.dart
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
part of '../main.dart';
|
||||||
|
|
||||||
|
class DateTimeEntity extends Entity {
|
||||||
|
bool get hasDate => attributes["has_date"] ?? false;
|
||||||
|
bool get hasTime => attributes["has_time"] ?? false;
|
||||||
|
int get year => attributes["year"] ?? 1970;
|
||||||
|
int get month => attributes["month"] ?? 1;
|
||||||
|
int get day => attributes["day"] ?? 1;
|
||||||
|
int get hour => attributes["hour"] ?? 0;
|
||||||
|
int get minute => attributes["minute"] ?? 0;
|
||||||
|
int get second => attributes["second"] ?? 0;
|
||||||
|
String get formattedState => _getFormattedState();
|
||||||
|
DateTime get dateTimeState => _getDateTimeState();
|
||||||
|
|
||||||
|
DateTimeEntity(Map rawData) : super(rawData);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget _buildStatePart(BuildContext context) {
|
||||||
|
return DateTimeStateWidget();
|
||||||
|
}
|
||||||
|
|
||||||
|
DateTime _getDateTimeState() {
|
||||||
|
return DateTime(
|
||||||
|
this.year, this.month, this.day, this.hour, this.minute, this.second);
|
||||||
|
}
|
||||||
|
|
||||||
|
String _getFormattedState() {
|
||||||
|
String formattedState = "";
|
||||||
|
if (this.hasDate) {
|
||||||
|
formattedState += formatDate(dateTimeState, [M, ' ', d, ', ', yyyy]);
|
||||||
|
}
|
||||||
|
if (this.hasTime) {
|
||||||
|
formattedState += " " + formatDate(dateTimeState, [HH, ':', nn]);
|
||||||
|
}
|
||||||
|
return formattedState;
|
||||||
|
}
|
||||||
|
|
||||||
|
void setNewState(newValue) {
|
||||||
|
eventBus
|
||||||
|
.fire(new ServiceCallEvent(domain, "set_datetime", entityId, newValue));
|
||||||
|
}
|
||||||
|
}
|
174
lib/entity_class/entity.class.dart
Normal file
@ -0,0 +1,174 @@
|
|||||||
|
part of '../main.dart';
|
||||||
|
|
||||||
|
class Entity {
|
||||||
|
|
||||||
|
static List badgeDomains = [
|
||||||
|
"alarm_control_panel",
|
||||||
|
"binary_sensor",
|
||||||
|
"device_tracker",
|
||||||
|
"updater",
|
||||||
|
"sun",
|
||||||
|
"timer",
|
||||||
|
"sensor"
|
||||||
|
];
|
||||||
|
|
||||||
|
Map attributes;
|
||||||
|
String domain;
|
||||||
|
String entityId;
|
||||||
|
String state;
|
||||||
|
DateTime _lastUpdated;
|
||||||
|
|
||||||
|
List<Entity> childEntities = [];
|
||||||
|
List<String> attributesToShow = ["all"];
|
||||||
|
EntityHistoryConfig historyConfig = EntityHistoryConfig(
|
||||||
|
chartType: EntityHistoryWidgetType.simple
|
||||||
|
);
|
||||||
|
|
||||||
|
String get displayName =>
|
||||||
|
attributes["friendly_name"] ?? (attributes["name"] ?? entityId.split(".")[1].replaceAll("_", " "));
|
||||||
|
|
||||||
|
String get deviceClass => attributes["device_class"] ?? null;
|
||||||
|
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 entityPicture => attributes["entity_picture"];
|
||||||
|
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;
|
||||||
|
|
||||||
|
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"];
|
||||||
|
_lastUpdated = DateTime.tryParse(rawData["last_updated"]);
|
||||||
|
}
|
||||||
|
|
||||||
|
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>[
|
||||||
|
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";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
84
lib/entity_class/entity_wrapper.class.dart
Normal file
@ -0,0 +1,84 @@
|
|||||||
|
part of '../main.dart';
|
||||||
|
|
||||||
|
class EntityWrapper {
|
||||||
|
|
||||||
|
String displayName;
|
||||||
|
String icon;
|
||||||
|
String tapAction;
|
||||||
|
String holdAction;
|
||||||
|
String tapActionService;
|
||||||
|
Map<String, dynamic> tapActionServiceData;
|
||||||
|
String holdActionService;
|
||||||
|
Map<String, dynamic> holdActionServiceData;
|
||||||
|
Entity entity;
|
||||||
|
|
||||||
|
|
||||||
|
EntityWrapper({
|
||||||
|
this.entity,
|
||||||
|
String icon,
|
||||||
|
String displayName,
|
||||||
|
this.tapAction: EntityTapAction.moreInfo,
|
||||||
|
this.holdAction: EntityTapAction.none,
|
||||||
|
this.tapActionService,
|
||||||
|
this.tapActionServiceData,
|
||||||
|
this.holdActionService,
|
||||||
|
this.holdActionServiceData
|
||||||
|
}) {
|
||||||
|
this.icon = icon ?? entity.icon;
|
||||||
|
this.displayName = displayName ?? entity.displayName;
|
||||||
|
}
|
||||||
|
|
||||||
|
void handleTap() {
|
||||||
|
TheLogger.debug(tapAction);
|
||||||
|
switch (tapAction) {
|
||||||
|
case EntityTapAction.toggle: {
|
||||||
|
eventBus.fire(
|
||||||
|
ServiceCallEvent("homeassistant", "toggle", entity.entityId, null));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case EntityTapAction.callService: {
|
||||||
|
eventBus.fire(
|
||||||
|
ServiceCallEvent(tapActionService.split(".")[0], tapActionService.split(".")[1], null, tapActionServiceData));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case EntityTapAction.none: {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
default: {
|
||||||
|
eventBus.fire(
|
||||||
|
new ShowEntityPageEvent(entity));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void handleHold() {
|
||||||
|
switch (holdAction) {
|
||||||
|
case EntityTapAction.toggle: {
|
||||||
|
eventBus.fire(
|
||||||
|
ServiceCallEvent("homeassistant", "toggle", entity.entityId, null));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case EntityTapAction.callService: {
|
||||||
|
eventBus.fire(
|
||||||
|
ServiceCallEvent(tapActionService.split(".")[0], tapActionService.split(".")[1], null, tapActionServiceData));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case EntityTapAction.moreInfo: {
|
||||||
|
eventBus.fire(
|
||||||
|
new ShowEntityPageEvent(entity));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
default: {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
32
lib/entity_class/fan_entity.class.dart
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
part of '../main.dart';
|
||||||
|
|
||||||
|
class FanEntity extends Entity {
|
||||||
|
|
||||||
|
static const SUPPORT_SET_SPEED = 1;
|
||||||
|
static const SUPPORT_OSCILLATE = 2;
|
||||||
|
static const SUPPORT_DIRECTION = 4;
|
||||||
|
|
||||||
|
FanEntity(Map rawData) : super(rawData);
|
||||||
|
|
||||||
|
bool get supportSetSpeed => ((attributes["supported_features"] &
|
||||||
|
FanEntity.SUPPORT_SET_SPEED) ==
|
||||||
|
FanEntity.SUPPORT_SET_SPEED);
|
||||||
|
bool get supportOscillate => ((attributes["supported_features"] &
|
||||||
|
FanEntity.SUPPORT_OSCILLATE) ==
|
||||||
|
FanEntity.SUPPORT_OSCILLATE);
|
||||||
|
bool get supportDirection => ((attributes["supported_features"] &
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
}
|
43
lib/entity_class/group_entity.class.dart
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
part of '../main.dart';
|
||||||
|
|
||||||
|
class GroupEntity extends Entity {
|
||||||
|
GroupEntity(Map rawData) : super(rawData);
|
||||||
|
|
||||||
|
final List<String> _domainsForSwitchableGroup = ["switch", "light", "automation", "input_boolean"];
|
||||||
|
String mutualDomain;
|
||||||
|
bool switchable = false;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget _buildStatePart(BuildContext context) {
|
||||||
|
if (switchable) {
|
||||||
|
return SwitchStateWidget(
|
||||||
|
domainForService: "homeassistant",
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return super._buildStatePart(context);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void update(Map rawData) {
|
||||||
|
super.update(rawData);
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
72
lib/entity_class/light_entity.class.dart
Normal file
@ -0,0 +1,72 @@
|
|||||||
|
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 => ((attributes["supported_features"] &
|
||||||
|
LightEntity.SUPPORT_BRIGHTNESS) ==
|
||||||
|
LightEntity.SUPPORT_BRIGHTNESS);
|
||||||
|
bool get supportColorTemp => ((attributes["supported_features"] &
|
||||||
|
LightEntity.SUPPORT_COLOR_TEMP) ==
|
||||||
|
LightEntity.SUPPORT_COLOR_TEMP);
|
||||||
|
bool get supportEffect => ((attributes["supported_features"] &
|
||||||
|
LightEntity.SUPPORT_EFFECT) ==
|
||||||
|
LightEntity.SUPPORT_EFFECT);
|
||||||
|
bool get supportFlash => ((attributes["supported_features"] &
|
||||||
|
LightEntity.SUPPORT_FLASH) ==
|
||||||
|
LightEntity.SUPPORT_FLASH);
|
||||||
|
bool get supportColor => ((attributes["supported_features"] &
|
||||||
|
LightEntity.SUPPORT_COLOR) ==
|
||||||
|
LightEntity.SUPPORT_COLOR);
|
||||||
|
bool get supportTransition => ((attributes["supported_features"] &
|
||||||
|
LightEntity.SUPPORT_TRANSITION) ==
|
||||||
|
LightEntity.SUPPORT_TRANSITION);
|
||||||
|
bool get supportWhiteValue => ((attributes["supported_features"] &
|
||||||
|
LightEntity.SUPPORT_WHITE_VALUE) ==
|
||||||
|
LightEntity.SUPPORT_WHITE_VALUE);
|
||||||
|
|
||||||
|
int get brightness => _getIntAttributeValue("brightness");
|
||||||
|
int get colorTemp => _getIntAttributeValue("color_temp");
|
||||||
|
double get maxMireds => _getDoubleAttributeValue("max_mireds");
|
||||||
|
double get minMireds => _getDoubleAttributeValue("min_mireds");
|
||||||
|
Color get color => _getColor();
|
||||||
|
bool get isAdditionalControls => ((attributes["supported_features"] != null) && (attributes["supported_features"] != 0));
|
||||||
|
List<String> get effectList => getStringListAttributeValue("effect_list");
|
||||||
|
|
||||||
|
LightEntity(Map rawData) : super(rawData);
|
||||||
|
|
||||||
|
Color _getColor() {
|
||||||
|
List rgb = attributes["rgb_color"];
|
||||||
|
try {
|
||||||
|
if ((rgb != null) && (rgb.length > 0)) {
|
||||||
|
return 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) {
|
||||||
|
return Container(height: 0.0, width: 0.0);
|
||||||
|
} else {
|
||||||
|
return LightControlsWidget();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
12
lib/entity_class/lock_entity.class.dart
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
part of '../main.dart';
|
||||||
|
|
||||||
|
class LockEntity extends Entity {
|
||||||
|
LockEntity(Map rawData) : super(rawData);
|
||||||
|
|
||||||
|
bool get isLocked => state == "locked";
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget _buildStatePart(BuildContext context) {
|
||||||
|
return LockStateWidget();
|
||||||
|
}
|
||||||
|
}
|
83
lib/entity_class/media_player_entity.class.dart
Normal file
@ -0,0 +1,83 @@
|
|||||||
|
part of '../main.dart';
|
||||||
|
|
||||||
|
class MediaPlayerEntity extends Entity {
|
||||||
|
|
||||||
|
static const SUPPORT_PAUSE = 1;
|
||||||
|
static const SUPPORT_SEEK = 2;
|
||||||
|
static const SUPPORT_VOLUME_SET = 4;
|
||||||
|
static const SUPPORT_VOLUME_MUTE = 8;
|
||||||
|
static const SUPPORT_PREVIOUS_TRACK = 16;
|
||||||
|
static const SUPPORT_NEXT_TRACK = 32;
|
||||||
|
|
||||||
|
static const SUPPORT_TURN_ON = 128;
|
||||||
|
static const SUPPORT_TURN_OFF = 256;
|
||||||
|
static const SUPPORT_PLAY_MEDIA = 512;
|
||||||
|
static const SUPPORT_VOLUME_STEP = 1024;
|
||||||
|
static const SUPPORT_SELECT_SOURCE = 2048;
|
||||||
|
static const SUPPORT_STOP = 4096;
|
||||||
|
static const SUPPORT_CLEAR_PLAYLIST = 8192;
|
||||||
|
static const SUPPORT_PLAY = 16384;
|
||||||
|
static const SUPPORT_SHUFFLE_SET = 32768;
|
||||||
|
static const SUPPORT_SELECT_SOUND_MODE = 65536;
|
||||||
|
|
||||||
|
MediaPlayerEntity(Map rawData) : super(rawData);
|
||||||
|
|
||||||
|
bool get supportPause => ((attributes["supported_features"] &
|
||||||
|
MediaPlayerEntity.SUPPORT_PAUSE) ==
|
||||||
|
MediaPlayerEntity.SUPPORT_PAUSE);
|
||||||
|
bool get supportSeek => ((attributes["supported_features"] &
|
||||||
|
MediaPlayerEntity.SUPPORT_SEEK) ==
|
||||||
|
MediaPlayerEntity.SUPPORT_SEEK);
|
||||||
|
bool get supportVolumeSet => ((attributes["supported_features"] &
|
||||||
|
MediaPlayerEntity.SUPPORT_VOLUME_SET) ==
|
||||||
|
MediaPlayerEntity.SUPPORT_VOLUME_SET);
|
||||||
|
bool get supportVolumeMute => ((attributes["supported_features"] &
|
||||||
|
MediaPlayerEntity.SUPPORT_VOLUME_MUTE) ==
|
||||||
|
MediaPlayerEntity.SUPPORT_VOLUME_MUTE);
|
||||||
|
bool get supportPreviousTrack => ((attributes["supported_features"] &
|
||||||
|
MediaPlayerEntity.SUPPORT_PREVIOUS_TRACK) ==
|
||||||
|
MediaPlayerEntity.SUPPORT_PREVIOUS_TRACK);
|
||||||
|
bool get supportNextTrack => ((attributes["supported_features"] &
|
||||||
|
MediaPlayerEntity.SUPPORT_NEXT_TRACK) ==
|
||||||
|
MediaPlayerEntity.SUPPORT_NEXT_TRACK);
|
||||||
|
|
||||||
|
bool get supportTurnOn => ((attributes["supported_features"] &
|
||||||
|
MediaPlayerEntity.SUPPORT_TURN_ON) ==
|
||||||
|
MediaPlayerEntity.SUPPORT_TURN_ON);
|
||||||
|
bool get supportTurnOff => ((attributes["supported_features"] &
|
||||||
|
MediaPlayerEntity.SUPPORT_TURN_OFF) ==
|
||||||
|
MediaPlayerEntity.SUPPORT_TURN_OFF);
|
||||||
|
bool get supportPlayMedia => ((attributes["supported_features"] &
|
||||||
|
MediaPlayerEntity.SUPPORT_PLAY_MEDIA) ==
|
||||||
|
MediaPlayerEntity.SUPPORT_PLAY_MEDIA);
|
||||||
|
bool get supportVolumeStep => ((attributes["supported_features"] &
|
||||||
|
MediaPlayerEntity.SUPPORT_VOLUME_STEP) ==
|
||||||
|
MediaPlayerEntity.SUPPORT_VOLUME_STEP);
|
||||||
|
bool get supportSelectSource => ((attributes["supported_features"] &
|
||||||
|
MediaPlayerEntity.SUPPORT_SELECT_SOURCE) ==
|
||||||
|
MediaPlayerEntity.SUPPORT_SELECT_SOURCE);
|
||||||
|
bool get supportStop => ((attributes["supported_features"] &
|
||||||
|
MediaPlayerEntity.SUPPORT_STOP) ==
|
||||||
|
MediaPlayerEntity.SUPPORT_STOP);
|
||||||
|
bool get supportClearPlaylist => ((attributes["supported_features"] &
|
||||||
|
MediaPlayerEntity.SUPPORT_CLEAR_PLAYLIST) ==
|
||||||
|
MediaPlayerEntity.SUPPORT_CLEAR_PLAYLIST);
|
||||||
|
bool get supportPlay => ((attributes["supported_features"] &
|
||||||
|
MediaPlayerEntity.SUPPORT_PLAY) ==
|
||||||
|
MediaPlayerEntity.SUPPORT_PLAY);
|
||||||
|
bool get supportShuffleSet => ((attributes["supported_features"] &
|
||||||
|
MediaPlayerEntity.SUPPORT_SHUFFLE_SET) ==
|
||||||
|
MediaPlayerEntity.SUPPORT_SHUFFLE_SET);
|
||||||
|
bool get supportSelectSoundMode => ((attributes["supported_features"] &
|
||||||
|
MediaPlayerEntity.SUPPORT_SELECT_SOUND_MODE) ==
|
||||||
|
MediaPlayerEntity.SUPPORT_SELECT_SOUND_MODE);
|
||||||
|
|
||||||
|
List<String> get soundModeList => getStringListAttributeValue("sound_mode_list");
|
||||||
|
List<String> get sourceList => getStringListAttributeValue("source_list");
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget _buildAdditionalControlsForPage(BuildContext context) {
|
||||||
|
return MediaPlayerControls();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
17
lib/entity_class/other_entity.class.dart
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
part of '../main.dart';
|
||||||
|
|
||||||
|
class SunEntity extends Entity {
|
||||||
|
SunEntity(Map rawData) : super(rawData);
|
||||||
|
}
|
||||||
|
|
||||||
|
class SensorEntity extends Entity {
|
||||||
|
|
||||||
|
@override
|
||||||
|
EntityHistoryConfig historyConfig = EntityHistoryConfig(
|
||||||
|
chartType: EntityHistoryWidgetType.numericState,
|
||||||
|
numericState: true
|
||||||
|
);
|
||||||
|
|
||||||
|
SensorEntity(Map rawData) : super(rawData);
|
||||||
|
|
||||||
|
}
|
14
lib/entity_class/select_entity.class.dart
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
part of '../main.dart';
|
||||||
|
|
||||||
|
class SelectEntity extends Entity {
|
||||||
|
List<String> get listOptions => attributes["options"] != null
|
||||||
|
? (attributes["options"] as List).cast<String>()
|
||||||
|
: [];
|
||||||
|
|
||||||
|
SelectEntity(Map rawData) : super(rawData);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget _buildStatePart(BuildContext context) {
|
||||||
|
return SelectStateWidget();
|
||||||
|
}
|
||||||
|
}
|
44
lib/entity_class/slider_entity.dart
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
part of '../main.dart';
|
||||||
|
|
||||||
|
class SliderEntity extends Entity {
|
||||||
|
SliderEntity(Map rawData) : super(rawData);
|
||||||
|
|
||||||
|
double get minValue => _getDoubleAttributeValue("min") ?? 0.0;
|
||||||
|
double get maxValue =>_getDoubleAttributeValue("max") ?? 100.0;
|
||||||
|
double get valueStep => _getDoubleAttributeValue("step") ?? 1.0;
|
||||||
|
|
||||||
|
@override
|
||||||
|
EntityHistoryConfig historyConfig = EntityHistoryConfig(
|
||||||
|
chartType: EntityHistoryWidgetType.numericState,
|
||||||
|
numericState: true
|
||||||
|
);
|
||||||
|
|
||||||
|
/*@override
|
||||||
|
Widget _buildStatePart(BuildContext context) {
|
||||||
|
return Expanded(
|
||||||
|
//width: 200.0,
|
||||||
|
child: Row(
|
||||||
|
children: <Widget>[
|
||||||
|
SliderStateWidget(
|
||||||
|
expanded: true,
|
||||||
|
),
|
||||||
|
SimpleEntityState(
|
||||||
|
expanded: false,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget _buildStatePartForPage(BuildContext context) {
|
||||||
|
return SimpleEntityState(
|
||||||
|
expanded: false,
|
||||||
|
);
|
||||||
|
}*/
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget _buildAdditionalControlsForPage(BuildContext context) {
|
||||||
|
return SliderControlsWidget();
|
||||||
|
}
|
||||||
|
}
|
10
lib/entity_class/switch_entity.class.dart
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
part of '../main.dart';
|
||||||
|
|
||||||
|
class SwitchEntity extends Entity {
|
||||||
|
SwitchEntity(Map rawData) : super(rawData);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget _buildStatePart(BuildContext context) {
|
||||||
|
return SwitchStateWidget();
|
||||||
|
}
|
||||||
|
}
|
16
lib/entity_class/text_entity.class.dart
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
part of '../main.dart';
|
||||||
|
|
||||||
|
class TextEntity extends Entity {
|
||||||
|
TextEntity(Map rawData) : super(rawData);
|
||||||
|
|
||||||
|
int get valueMinLength => attributes["min"] ?? -1;
|
||||||
|
int get valueMaxLength => attributes["max"] ?? -1;
|
||||||
|
String get valuePattern => attributes["pattern"] ?? null;
|
||||||
|
bool get isTextField => attributes["mode"] == "text";
|
||||||
|
bool get isPasswordField => attributes["mode"] == "password";
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget _buildStatePart(BuildContext context) {
|
||||||
|
return TextInputStateWidget();
|
||||||
|
}
|
||||||
|
}
|
161
lib/entity_collection.class.dart
Normal file
@ -0,0 +1,161 @@
|
|||||||
|
part of 'main.dart';
|
||||||
|
|
||||||
|
class EntityCollection {
|
||||||
|
|
||||||
|
Map<String, Entity> _allEntities;
|
||||||
|
//Map<String, Entity> views;
|
||||||
|
|
||||||
|
bool get isEmpty => _allEntities.isEmpty;
|
||||||
|
List<Entity> get viewEntities => _allEntities.values.where((entity) => entity.isView).toList();
|
||||||
|
|
||||||
|
EntityCollection() {
|
||||||
|
_allEntities = {};
|
||||||
|
//views = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
bool get hasDefaultView => _allEntities.keys.contains("group.default_view");
|
||||||
|
|
||||||
|
void parse(List rawData) {
|
||||||
|
_allEntities.clear();
|
||||||
|
//views.clear();
|
||||||
|
|
||||||
|
TheLogger.debug("Parsing ${rawData.length} Home Assistant entities");
|
||||||
|
rawData.forEach((rawEntityData) {
|
||||||
|
addFromRaw(rawEntityData);
|
||||||
|
});
|
||||||
|
_allEntities.forEach((entityId, entity){
|
||||||
|
if ((entity.isGroup) && (entity.childEntityIds != null)) {
|
||||||
|
entity.childEntities = getAll(entity.childEntityIds);
|
||||||
|
}
|
||||||
|
/*if (entity.isView) {
|
||||||
|
views[entityId] = entity;
|
||||||
|
}*/
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Entity _createEntityInstance(rawEntityData) {
|
||||||
|
switch (rawEntityData["entity_id"].split(".")[0]) {
|
||||||
|
case 'sun': {
|
||||||
|
return SunEntity(rawEntityData);
|
||||||
|
}
|
||||||
|
case "media_player": {
|
||||||
|
return MediaPlayerEntity(rawEntityData);
|
||||||
|
}
|
||||||
|
case 'sensor': {
|
||||||
|
return SensorEntity(rawEntityData);
|
||||||
|
}
|
||||||
|
case 'lock': {
|
||||||
|
return LockEntity(rawEntityData);
|
||||||
|
}
|
||||||
|
case "automation":
|
||||||
|
case "input_boolean":
|
||||||
|
case "switch": {
|
||||||
|
return SwitchEntity(rawEntityData);
|
||||||
|
}
|
||||||
|
case "light": {
|
||||||
|
return LightEntity(rawEntityData);
|
||||||
|
}
|
||||||
|
case "group": {
|
||||||
|
return GroupEntity(rawEntityData);
|
||||||
|
}
|
||||||
|
case "script":
|
||||||
|
case "scene": {
|
||||||
|
return ButtonEntity(rawEntityData);
|
||||||
|
}
|
||||||
|
case "input_datetime": {
|
||||||
|
return DateTimeEntity(rawEntityData);
|
||||||
|
}
|
||||||
|
case "input_select": {
|
||||||
|
return SelectEntity(rawEntityData);
|
||||||
|
}
|
||||||
|
case "input_number": {
|
||||||
|
return SliderEntity(rawEntityData);
|
||||||
|
}
|
||||||
|
case "input_text": {
|
||||||
|
return TextEntity(rawEntityData);
|
||||||
|
}
|
||||||
|
case "climate": {
|
||||||
|
return ClimateEntity(rawEntityData);
|
||||||
|
}
|
||||||
|
case "cover": {
|
||||||
|
return CoverEntity(rawEntityData);
|
||||||
|
}
|
||||||
|
case "fan": {
|
||||||
|
return FanEntity(rawEntityData);
|
||||||
|
}
|
||||||
|
default: {
|
||||||
|
return Entity(rawEntityData);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void updateState(Map rawStateData) {
|
||||||
|
if (isExist(rawStateData["entity_id"])) {
|
||||||
|
updateFromRaw(rawStateData["new_state"] ?? rawStateData["old_state"]);
|
||||||
|
} else {
|
||||||
|
addFromRaw(rawStateData["new_state"] ?? rawStateData["old_state"]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void add(Entity entity) {
|
||||||
|
_allEntities[entity.entityId] = entity;
|
||||||
|
}
|
||||||
|
|
||||||
|
Entity addFromRaw(Map rawEntityData) {
|
||||||
|
Entity entity = _createEntityInstance(rawEntityData);
|
||||||
|
_allEntities[entity.entityId] = entity;
|
||||||
|
return entity;
|
||||||
|
}
|
||||||
|
|
||||||
|
void updateFromRaw(Map rawEntityData) {
|
||||||
|
get("${rawEntityData["entity_id"]}")?.update(rawEntityData);
|
||||||
|
}
|
||||||
|
|
||||||
|
Entity get(String entityId) {
|
||||||
|
return _allEntities[entityId];
|
||||||
|
}
|
||||||
|
|
||||||
|
List<Entity> getAll(List ids) {
|
||||||
|
List<Entity> result = [];
|
||||||
|
ids.forEach((id){
|
||||||
|
Entity en = get(id);
|
||||||
|
if (en != null) {
|
||||||
|
result.add(en);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool isExist(String entityId) {
|
||||||
|
return _allEntities[entityId] != null;
|
||||||
|
}
|
||||||
|
|
||||||
|
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) {
|
||||||
|
nonGroupEntities.add(entity);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
nonGroupEntities.forEach((entity) {
|
||||||
|
bool foundInGroup = false;
|
||||||
|
groups.forEach((groupEntity) {
|
||||||
|
if (groupEntity.childEntityIds.contains(entity.entityId)) {
|
||||||
|
foundInGroup = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (!foundInGroup) {
|
||||||
|
result.add(entity);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
result.insertAll(0, groups);
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
45
lib/entity_widgets/button_entity_container.dart
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
part of '../main.dart';
|
||||||
|
|
||||||
|
class ButtonEntityContainer extends StatelessWidget {
|
||||||
|
|
||||||
|
ButtonEntityContainer({
|
||||||
|
Key key,
|
||||||
|
}) : super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final EntityWrapper entityWrapper = EntityModel.of(context).entityWrapper;
|
||||||
|
|
||||||
|
return InkWell(
|
||||||
|
onTap: () => entityWrapper.handleTap(),
|
||||||
|
onLongPress: () => entityWrapper.handleHold(),
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: <Widget>[
|
||||||
|
FractionallySizedBox(
|
||||||
|
widthFactor: 0.4,
|
||||||
|
child: FittedBox(
|
||||||
|
fit: BoxFit.fitHeight,
|
||||||
|
child: EntityIcon(
|
||||||
|
padding: EdgeInsets.fromLTRB(2.0, 6.0, 2.0, 2.0),
|
||||||
|
iconSize: Sizes.iconSize,
|
||||||
|
)
|
||||||
|
),
|
||||||
|
),
|
||||||
|
_buildName()
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildName() {
|
||||||
|
return EntityName(
|
||||||
|
padding: EdgeInsets.fromLTRB(Sizes.buttonPadding, 0.0, Sizes.buttonPadding, Sizes.rowPadding),
|
||||||
|
textOverflow: TextOverflow.ellipsis,
|
||||||
|
maxLines: 3,
|
||||||
|
wordsWrap: true,
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
fontSize: Sizes.nameFontSize,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
125
lib/entity_widgets/common/badge.dart
Normal file
@ -0,0 +1,125 @@
|
|||||||
|
part of '../../main.dart';
|
||||||
|
|
||||||
|
class BadgeWidget extends StatelessWidget {
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final entityModel = EntityModel.of(context);
|
||||||
|
double iconSize = 26.0;
|
||||||
|
Widget badgeIcon;
|
||||||
|
String onBadgeTextValue;
|
||||||
|
Color iconColor = EntityColor.badgeColors[entityModel.entityWrapper.entity.domain] ??
|
||||||
|
EntityColor.badgeColors["default"];
|
||||||
|
switch (entityModel.entityWrapper.entity.domain) {
|
||||||
|
case "sun":
|
||||||
|
{
|
||||||
|
badgeIcon = entityModel.entityWrapper.entity.state == "below_horizon"
|
||||||
|
? Icon(
|
||||||
|
MaterialDesignIcons.createIconDataFromIconCode(0xf0dc),
|
||||||
|
size: iconSize,
|
||||||
|
)
|
||||||
|
: Icon(
|
||||||
|
MaterialDesignIcons.createIconDataFromIconCode(0xf5a8),
|
||||||
|
size: iconSize,
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "sensor":
|
||||||
|
{
|
||||||
|
onBadgeTextValue = entityModel.entityWrapper.entity.unitOfMeasurement;
|
||||||
|
badgeIcon = Center(
|
||||||
|
child: Text(
|
||||||
|
"${entityModel.entityWrapper.entity.state}",
|
||||||
|
overflow: TextOverflow.fade,
|
||||||
|
softWrap: false,
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
style: TextStyle(fontSize: 17.0),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "device_tracker":
|
||||||
|
{
|
||||||
|
badgeIcon = MaterialDesignIcons.createIconWidgetFromEntityData(
|
||||||
|
entityModel.entityWrapper, iconSize, Colors.black);
|
||||||
|
onBadgeTextValue = entityModel.entityWrapper.entity.state;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
{
|
||||||
|
badgeIcon = MaterialDesignIcons.createIconWidgetFromEntityData(
|
||||||
|
entityModel.entityWrapper, iconSize, Colors.black);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Widget onBadgeText;
|
||||||
|
if (onBadgeTextValue == null || onBadgeTextValue.length == 0) {
|
||||||
|
onBadgeText = Container(width: 0.0, height: 0.0);
|
||||||
|
} else {
|
||||||
|
onBadgeText = Container(
|
||||||
|
padding: EdgeInsets.fromLTRB(6.0, 2.0, 6.0, 2.0),
|
||||||
|
child: Text("$onBadgeTextValue",
|
||||||
|
style: TextStyle(fontSize: 12.0, color: Colors.white),
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
softWrap: false,
|
||||||
|
overflow: TextOverflow.fade),
|
||||||
|
decoration: new BoxDecoration(
|
||||||
|
// Circle shape
|
||||||
|
//shape: BoxShape.circle,
|
||||||
|
color: iconColor,
|
||||||
|
borderRadius: BorderRadius.circular(9.0),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
return GestureDetector(
|
||||||
|
child: Column(
|
||||||
|
children: <Widget>[
|
||||||
|
Container(
|
||||||
|
margin: EdgeInsets.fromLTRB(0.0, 10.0, 0.0, 10.0),
|
||||||
|
width: 50.0,
|
||||||
|
height: 50.0,
|
||||||
|
decoration: new BoxDecoration(
|
||||||
|
// Circle shape
|
||||||
|
shape: BoxShape.circle,
|
||||||
|
color: Colors.white,
|
||||||
|
// The border you want
|
||||||
|
border: new Border.all(
|
||||||
|
width: 2.0,
|
||||||
|
color: iconColor,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: Stack(
|
||||||
|
overflow: Overflow.visible,
|
||||||
|
children: <Widget>[
|
||||||
|
Positioned(
|
||||||
|
width: 46.0,
|
||||||
|
height: 46.0,
|
||||||
|
top: 0.0,
|
||||||
|
left: 0.0,
|
||||||
|
child: badgeIcon,
|
||||||
|
),
|
||||||
|
Positioned(
|
||||||
|
//width: 50.0,
|
||||||
|
bottom: -9.0,
|
||||||
|
left: -10.0,
|
||||||
|
right: -10.0,
|
||||||
|
child: Center(
|
||||||
|
child: onBadgeText,
|
||||||
|
))
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Container(
|
||||||
|
width: 60.0,
|
||||||
|
child: Text(
|
||||||
|
"${entityModel.entityWrapper.displayName}",
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
style: TextStyle(fontSize: 12.0),
|
||||||
|
softWrap: true,
|
||||||
|
maxLines: 3,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
onTap: () =>
|
||||||
|
eventBus.fire(new ShowEntityPageEvent(entityModel.entityWrapper.entity)));
|
||||||
|
}
|
||||||
|
}
|
57
lib/entity_widgets/common/entity_attributes_list.dart
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
part of '../../main.dart';
|
||||||
|
|
||||||
|
class EntityAttributesList extends StatelessWidget {
|
||||||
|
EntityAttributesList({Key key}) : super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final entityModel = EntityModel.of(context);
|
||||||
|
List<Widget> attrs = [];
|
||||||
|
if ((entityModel.entityWrapper.entity.attributesToShow == null) ||
|
||||||
|
(entityModel.entityWrapper.entity.attributesToShow.contains("all"))) {
|
||||||
|
entityModel.entityWrapper.entity.attributes.forEach((name, value) {
|
||||||
|
attrs.add(_buildSingleAttribute("$name", "$value"));
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
entityModel.entityWrapper.entity.attributesToShow.forEach((String attr) {
|
||||||
|
String attrValue = entityModel.entityWrapper.entity.getAttribute("$attr");
|
||||||
|
if (attrValue != null) {
|
||||||
|
attrs.add(
|
||||||
|
_buildSingleAttribute("$attr", "$attrValue"));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return Column(
|
||||||
|
children: attrs,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildSingleAttribute(String name, String value) {
|
||||||
|
return Row(
|
||||||
|
children: <Widget>[
|
||||||
|
Expanded(
|
||||||
|
child: Padding(
|
||||||
|
padding: EdgeInsets.fromLTRB(
|
||||||
|
Sizes.leftWidgetPadding, Sizes.rowPadding, 0.0, 0.0),
|
||||||
|
child: Text(
|
||||||
|
"$name",
|
||||||
|
textAlign: TextAlign.left,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Expanded(
|
||||||
|
child: Padding(
|
||||||
|
padding: EdgeInsets.fromLTRB(
|
||||||
|
0.0, Sizes.rowPadding, Sizes.rightWidgetPadding, 0.0),
|
||||||
|
child: Text(
|
||||||
|
"$value",
|
||||||
|
textAlign: TextAlign.right,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
18
lib/entity_widgets/common/last_updated.dart
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
part of '../../main.dart';
|
||||||
|
|
||||||
|
class LastUpdatedWidget extends StatelessWidget {
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final entityModel = EntityModel.of(context);
|
||||||
|
return Padding(
|
||||||
|
padding: EdgeInsets.fromLTRB(
|
||||||
|
Sizes.leftWidgetPadding, 0.0, 0.0, 0.0),
|
||||||
|
child: Text(
|
||||||
|
'${entityModel.entityWrapper.entity.lastUpdated}',
|
||||||
|
textAlign: TextAlign.left,
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: Sizes.smallFontSize, color: Colors.black26),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
62
lib/entity_widgets/common/mode_selector.dart
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
part of '../../main.dart';
|
||||||
|
|
||||||
|
class ModeSelectorWidget extends StatelessWidget {
|
||||||
|
|
||||||
|
final String caption;
|
||||||
|
final List<String> options;
|
||||||
|
final String value;
|
||||||
|
final double captionFontSize;
|
||||||
|
final double valueFontSize;
|
||||||
|
final double bottomPadding;
|
||||||
|
final onChange;
|
||||||
|
|
||||||
|
ModeSelectorWidget({
|
||||||
|
Key key,
|
||||||
|
@required this.caption,
|
||||||
|
@required this.options,
|
||||||
|
this.value,
|
||||||
|
@required this.onChange,
|
||||||
|
this.captionFontSize,
|
||||||
|
this.valueFontSize,
|
||||||
|
this.bottomPadding
|
||||||
|
}) : super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: <Widget>[
|
||||||
|
Text("$caption", style: TextStyle(
|
||||||
|
fontSize: captionFontSize ?? Sizes.stateFontSize
|
||||||
|
)),
|
||||||
|
Row(
|
||||||
|
children: <Widget>[
|
||||||
|
Expanded(
|
||||||
|
child: ButtonTheme(
|
||||||
|
alignedDropdown: true,
|
||||||
|
child: DropdownButton<String>(
|
||||||
|
value: value,
|
||||||
|
iconSize: 30.0,
|
||||||
|
isExpanded: true,
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: valueFontSize ?? Sizes.largeFontSize,
|
||||||
|
color: Colors.black,
|
||||||
|
),
|
||||||
|
hint: Text("Select ${caption.toLowerCase()}"),
|
||||||
|
items: options.map((String value) {
|
||||||
|
return new DropdownMenuItem<String>(
|
||||||
|
value: value,
|
||||||
|
child: Text(value),
|
||||||
|
);
|
||||||
|
}).toList(),
|
||||||
|
onChanged: (mode) => onChange(mode),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
Container(height: bottomPadding ?? Sizes.rowPadding,)
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
48
lib/entity_widgets/common/mode_swicth.dart
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
part of '../../main.dart';
|
||||||
|
|
||||||
|
class ModeSwitchWidget extends StatelessWidget {
|
||||||
|
|
||||||
|
final String caption;
|
||||||
|
final onChange;
|
||||||
|
final double captionFontSize;
|
||||||
|
final bool value;
|
||||||
|
final bool expanded;
|
||||||
|
|
||||||
|
ModeSwitchWidget({
|
||||||
|
Key key,
|
||||||
|
@required this.caption,
|
||||||
|
@required this.onChange,
|
||||||
|
this.captionFontSize,
|
||||||
|
this.value,
|
||||||
|
this.expanded: true
|
||||||
|
}) : super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Row(
|
||||||
|
children: <Widget>[
|
||||||
|
_buildCaption(),
|
||||||
|
Switch(
|
||||||
|
onChanged: (value) => onChange(value),
|
||||||
|
value: value ?? false,
|
||||||
|
)
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildCaption() {
|
||||||
|
Widget captionWidget = Text(
|
||||||
|
"$caption",
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: captionFontSize ?? Sizes.stateFontSize
|
||||||
|
),
|
||||||
|
);
|
||||||
|
if (expanded) {
|
||||||
|
return Expanded(
|
||||||
|
child: captionWidget,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return captionWidget;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
54
lib/entity_widgets/common/universal_slider.dart
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
part of '../../main.dart';
|
||||||
|
|
||||||
|
class UniversalSlider extends StatelessWidget {
|
||||||
|
|
||||||
|
final onChanged;
|
||||||
|
final onChangeEnd;
|
||||||
|
final Widget leading;
|
||||||
|
final Widget closing;
|
||||||
|
final String title;
|
||||||
|
final double min;
|
||||||
|
final double max;
|
||||||
|
final double value;
|
||||||
|
|
||||||
|
const UniversalSlider({Key key, this.onChanged, this.onChangeEnd, this.leading, this.closing, this.title, this.min, this.max, this.value}) : super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
List <Widget> row = [];
|
||||||
|
if (leading != null) {
|
||||||
|
row.add(leading);
|
||||||
|
}
|
||||||
|
row.add(
|
||||||
|
Flexible(
|
||||||
|
child: Slider(
|
||||||
|
value: value,
|
||||||
|
min: min,
|
||||||
|
max: max,
|
||||||
|
onChanged: (value) => onChanged(value),
|
||||||
|
onChangeEnd: (value) => onChangeEnd(value),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
);
|
||||||
|
if (closing != null) {
|
||||||
|
row.add(closing);
|
||||||
|
}
|
||||||
|
return Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: <Widget>[
|
||||||
|
Container(height: Sizes.rowPadding,),
|
||||||
|
Text(
|
||||||
|
"$title",
|
||||||
|
style: TextStyle(fontSize: Sizes.stateFontSize),
|
||||||
|
),
|
||||||
|
Container(height: Sizes.rowPadding,),
|
||||||
|
Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: row,
|
||||||
|
),
|
||||||
|
Container(height: Sizes.rowPadding,)
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
467
lib/entity_widgets/controls/climate_controls.dart
Normal file
@ -0,0 +1,467 @@
|
|||||||
|
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;
|
||||||
|
double _tmpTemperature = 0.0;
|
||||||
|
double _tmpTargetLow = 0.0;
|
||||||
|
double _tmpTargetHigh = 0.0;
|
||||||
|
double _tmpTargetHumidity = 0.0;
|
||||||
|
String _tmpOperationMode;
|
||||||
|
String _tmpFanMode;
|
||||||
|
String _tmpSwingMode;
|
||||||
|
bool _tmpAwayMode = false;
|
||||||
|
bool _tmpIsOff = false;
|
||||||
|
bool _tmpAuxHeat = false;
|
||||||
|
|
||||||
|
void _resetVars(ClimateEntity entity) {
|
||||||
|
_tmpTemperature = entity.temperature;
|
||||||
|
_tmpTargetHigh = entity.targetHigh;
|
||||||
|
_tmpTargetLow = entity.targetLow;
|
||||||
|
_tmpOperationMode = entity.operationMode;
|
||||||
|
_tmpFanMode = entity.fanMode;
|
||||||
|
_tmpSwingMode = entity.swingMode;
|
||||||
|
_tmpAwayMode = entity.awayMode;
|
||||||
|
_tmpIsOff = entity.isOff;
|
||||||
|
_tmpAuxHeat = entity.auxHeat;
|
||||||
|
_tmpTargetHumidity = entity.targetHumidity;
|
||||||
|
|
||||||
|
_showPending = false;
|
||||||
|
_changedHere = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
void _temperatureUp(ClimateEntity entity, double step) {
|
||||||
|
_tmpTemperature = ((_tmpTemperature + step) <= entity.maxTemp) ? _tmpTemperature + step : entity.maxTemp;
|
||||||
|
_setTemperature(entity);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _temperatureDown(ClimateEntity entity, double step) {
|
||||||
|
_tmpTemperature = ((_tmpTemperature - step) >= entity.minTemp) ? _tmpTemperature - step : entity.minTemp;
|
||||||
|
_setTemperature(entity);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _targetLowUp(ClimateEntity entity, double step) {
|
||||||
|
_tmpTargetLow = ((_tmpTargetLow + step) <= entity.maxTemp) ? _tmpTargetLow + step : entity.maxTemp;
|
||||||
|
_setTargetTemp(entity);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _targetLowDown(ClimateEntity entity, double step) {
|
||||||
|
_tmpTargetLow = ((_tmpTargetLow - step) >= entity.minTemp) ? _tmpTargetLow - step : entity.minTemp;
|
||||||
|
_setTargetTemp(entity);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _targetHighUp(ClimateEntity entity, double step) {
|
||||||
|
_tmpTargetHigh = ((_tmpTargetHigh + step) <= entity.maxTemp) ? _tmpTargetHigh + step : entity.maxTemp;
|
||||||
|
_setTargetTemp(entity);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _targetHighDown(ClimateEntity entity, double step) {
|
||||||
|
_tmpTargetHigh = ((_tmpTargetHigh - step) >= entity.minTemp) ? _tmpTargetHigh - step : entity.minTemp;
|
||||||
|
_setTargetTemp(entity);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _setTemperature(ClimateEntity entity) {
|
||||||
|
setState(() {
|
||||||
|
_tmpTemperature = double.parse(_tmpTemperature.toStringAsFixed(1));
|
||||||
|
_changedHere = true;
|
||||||
|
eventBus.fire(new ServiceCallEvent(entity.domain, "set_temperature", entity.entityId,{"temperature": "${_tmpTemperature.toStringAsFixed(1)}"}));
|
||||||
|
_resetStateTimer(entity);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void _setTargetTemp(ClimateEntity entity) {
|
||||||
|
setState(() {
|
||||||
|
_tmpTargetLow = double.parse(_tmpTargetLow.toStringAsFixed(1));
|
||||||
|
_tmpTargetHigh = double.parse(_tmpTargetHigh.toStringAsFixed(1));
|
||||||
|
_changedHere = true;
|
||||||
|
eventBus.fire(new ServiceCallEvent(entity.domain, "set_temperature", entity.entityId,{"target_temp_high": "${_tmpTargetHigh.toStringAsFixed(1)}", "target_temp_low": "${_tmpTargetLow.toStringAsFixed(1)}"}));
|
||||||
|
_resetStateTimer(entity);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void _setTargetHumidity(ClimateEntity entity, double value) {
|
||||||
|
setState(() {
|
||||||
|
_tmpTargetHumidity = value.roundToDouble();
|
||||||
|
_changedHere = true;
|
||||||
|
eventBus.fire(new ServiceCallEvent(entity.domain, "set_humidity", entity.entityId,{"humidity": "$_tmpTargetHumidity"}));
|
||||||
|
_resetStateTimer(entity);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void _setOperationMode(ClimateEntity entity, value) {
|
||||||
|
setState(() {
|
||||||
|
_tmpOperationMode = value;
|
||||||
|
_changedHere = true;
|
||||||
|
eventBus.fire(new ServiceCallEvent(entity.domain, "set_operation_mode", entity.entityId,{"operation_mode": "$_tmpOperationMode"}));
|
||||||
|
_resetStateTimer(entity);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void _setSwingMode(ClimateEntity entity, value) {
|
||||||
|
setState(() {
|
||||||
|
_tmpSwingMode = value;
|
||||||
|
_changedHere = true;
|
||||||
|
eventBus.fire(new ServiceCallEvent(entity.domain, "set_swing_mode", entity.entityId,{"swing_mode": "$_tmpSwingMode"}));
|
||||||
|
_resetStateTimer(entity);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void _setFanMode(ClimateEntity entity, value) {
|
||||||
|
setState(() {
|
||||||
|
_tmpFanMode = value;
|
||||||
|
_changedHere = true;
|
||||||
|
eventBus.fire(new ServiceCallEvent(entity.domain, "set_fan_mode", entity.entityId,{"fan_mode": "$_tmpFanMode"}));
|
||||||
|
_resetStateTimer(entity);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void _setAwayMode(ClimateEntity entity, value) {
|
||||||
|
setState(() {
|
||||||
|
_tmpAwayMode = value;
|
||||||
|
_changedHere = true;
|
||||||
|
eventBus.fire(new ServiceCallEvent(entity.domain, "set_away_mode", entity.entityId,{"away_mode": "${_tmpAwayMode ? 'on' : 'off'}"}));
|
||||||
|
_resetStateTimer(entity);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void _setOnOf(ClimateEntity entity, value) {
|
||||||
|
setState(() {
|
||||||
|
_tmpIsOff = !value;
|
||||||
|
_changedHere = true;
|
||||||
|
eventBus.fire(new ServiceCallEvent(entity.domain, "${_tmpIsOff ? 'turn_off' : 'turn_on'}", entity.entityId, null));
|
||||||
|
_resetStateTimer(entity);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void _setAuxHeat(ClimateEntity entity, value) {
|
||||||
|
setState(() {
|
||||||
|
_tmpAuxHeat = value;
|
||||||
|
_changedHere = true;
|
||||||
|
eventBus.fire(new ServiceCallEvent(entity.domain, "set_aux_heat", entity.entityId, {"aux_heat": "$_tmpAuxHeat"}));
|
||||||
|
_resetStateTimer(entity);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void _resetStateTimer(ClimateEntity entity) {
|
||||||
|
if (_resetTimer!=null) {
|
||||||
|
_resetTimer.cancel();
|
||||||
|
}
|
||||||
|
_resetTimer = Timer(Duration(seconds: 3), () {
|
||||||
|
setState(() {});
|
||||||
|
_resetVars(entity);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final entityModel = EntityModel.of(context);
|
||||||
|
final ClimateEntity entity = entityModel.entityWrapper.entity;
|
||||||
|
if (_changedHere) {
|
||||||
|
_showPending = (_tmpTemperature != entity.temperature);
|
||||||
|
_changedHere = false;
|
||||||
|
} else {
|
||||||
|
_resetTimer?.cancel();
|
||||||
|
_resetVars(entity);
|
||||||
|
}
|
||||||
|
return Padding(
|
||||||
|
padding: EdgeInsets.fromLTRB(Sizes.leftWidgetPadding, Sizes.rowPadding, Sizes.rightWidgetPadding, 0.0),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: <Widget>[
|
||||||
|
_buildOnOffControl(entity),
|
||||||
|
_buildTemperatureControls(entity),
|
||||||
|
_buildTargetTemperatureControls(entity),
|
||||||
|
_buildHumidityControls(entity),
|
||||||
|
_buildOperationControl(entity),
|
||||||
|
_buildFanControl(entity),
|
||||||
|
_buildSwingControl(entity),
|
||||||
|
_buildAwayModeControl(entity),
|
||||||
|
_buildAuxHeatControl(entity)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildAwayModeControl(ClimateEntity entity) {
|
||||||
|
if (entity.supportAwayMode) {
|
||||||
|
return ModeSwitchWidget(
|
||||||
|
caption: "Away mode",
|
||||||
|
onChange: (value) => _setAwayMode(entity, value),
|
||||||
|
value: _tmpAwayMode,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return Container(height: 0.0, width: 0.0,);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildOnOffControl(ClimateEntity entity) {
|
||||||
|
if (entity.supportOnOff) {
|
||||||
|
return ModeSwitchWidget(
|
||||||
|
onChange: (value) => _setOnOf(entity, value),
|
||||||
|
caption: "On / Off",
|
||||||
|
value: !_tmpIsOff
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return Container(height: 0.0, width: 0.0,);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildAuxHeatControl(ClimateEntity entity) {
|
||||||
|
if (entity.supportAuxHeat ) {
|
||||||
|
return ModeSwitchWidget(
|
||||||
|
caption: "Aux heat",
|
||||||
|
onChange: (value) => _setAuxHeat(entity, value),
|
||||||
|
value: _tmpAuxHeat
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return Container(height: 0.0, width: 0.0,);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildOperationControl(ClimateEntity entity) {
|
||||||
|
if (entity.supportOperationMode) {
|
||||||
|
return ModeSelectorWidget(
|
||||||
|
onChange: (mode) => _setOperationMode(entity, mode),
|
||||||
|
options: entity.operationList,
|
||||||
|
caption: "Operation",
|
||||||
|
value: _tmpOperationMode,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return Container(height: 0.0, width: 0.0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildFanControl(ClimateEntity entity) {
|
||||||
|
if (entity.supportFanMode) {
|
||||||
|
return ModeSelectorWidget(
|
||||||
|
options: entity.fanList,
|
||||||
|
onChange: (mode) => _setFanMode(entity, mode),
|
||||||
|
caption: "Fan mode",
|
||||||
|
value: _tmpFanMode,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return Container(height: 0.0, width: 0.0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildSwingControl(ClimateEntity entity) {
|
||||||
|
if (entity.supportSwingMode) {
|
||||||
|
return ModeSelectorWidget(
|
||||||
|
onChange: (mode) => _setSwingMode(entity, mode),
|
||||||
|
options: entity.swingList,
|
||||||
|
value: _tmpSwingMode,
|
||||||
|
caption: "Swing mode"
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return Container(height: 0.0, width: 0.0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildTemperatureControls(ClimateEntity entity) {
|
||||||
|
if ((entity.supportTargetTemperature) && (entity.temperature != null)) {
|
||||||
|
return Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: <Widget>[
|
||||||
|
Text("Target temperature", style: TextStyle(
|
||||||
|
fontSize: Sizes.stateFontSize
|
||||||
|
)),
|
||||||
|
TemperatureControlWidget(
|
||||||
|
value: _tmpTemperature,
|
||||||
|
fontColor: _showPending ? Colors.red : Colors.black,
|
||||||
|
onLargeDec: () => _temperatureDown(entity, 0.5),
|
||||||
|
onLargeInc: () => _temperatureUp(entity, 0.5),
|
||||||
|
onSmallDec: () => _temperatureDown(entity, 0.1),
|
||||||
|
onSmallInc: () => _temperatureUp(entity, 0.1),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return Container(width: 0.0, height: 0.0,);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildTargetTemperatureControls(ClimateEntity entity) {
|
||||||
|
List<Widget> controls = [];
|
||||||
|
if ((entity.supportTargetTemperatureLow) && (entity.targetLow != null)) {
|
||||||
|
controls.addAll(<Widget>[
|
||||||
|
TemperatureControlWidget(
|
||||||
|
value: _tmpTargetLow,
|
||||||
|
fontColor: _showPending ? Colors.red : Colors.black,
|
||||||
|
onLargeDec: () => _targetLowDown(entity, 0.5),
|
||||||
|
onLargeInc: () => _targetLowUp(entity, 0.5),
|
||||||
|
onSmallDec: () => _targetLowDown(entity, 0.1),
|
||||||
|
onSmallInc: () => _targetLowUp(entity, 0.1),
|
||||||
|
),
|
||||||
|
Expanded(
|
||||||
|
child: Container(height: 10.0),
|
||||||
|
)
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
if ((entity.supportTargetTemperatureHigh) && (entity.targetHigh != null)) {
|
||||||
|
controls.add(
|
||||||
|
TemperatureControlWidget(
|
||||||
|
value: _tmpTargetHigh,
|
||||||
|
fontColor: _showPending ? Colors.red : Colors.black,
|
||||||
|
onLargeDec: () => _targetHighDown(entity, 0.5),
|
||||||
|
onLargeInc: () => _targetHighUp(entity, 0.5),
|
||||||
|
onSmallDec: () => _targetHighDown(entity, 0.1),
|
||||||
|
onSmallInc: () => _targetHighUp(entity, 0.1),
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (controls.isNotEmpty) {
|
||||||
|
return Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: <Widget>[
|
||||||
|
Text("Target temperature range", style: TextStyle(
|
||||||
|
fontSize: Sizes.stateFontSize
|
||||||
|
)),
|
||||||
|
Row(
|
||||||
|
children: controls,
|
||||||
|
)
|
||||||
|
],
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return Container(width: 0.0, height: 0.0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildHumidityControls(ClimateEntity entity) {
|
||||||
|
List<Widget> result = [];
|
||||||
|
if (entity.supportTargetHumidity) {
|
||||||
|
result.addAll(<Widget>[
|
||||||
|
Text(
|
||||||
|
"$_tmpTargetHumidity%",
|
||||||
|
style: TextStyle(fontSize: Sizes.largeFontSize),
|
||||||
|
),
|
||||||
|
Expanded(
|
||||||
|
child: Slider(
|
||||||
|
value: _tmpTargetHumidity,
|
||||||
|
max: entity.maxHumidity,
|
||||||
|
min: entity.minHumidity,
|
||||||
|
onChanged: ((double val) {
|
||||||
|
setState(() {
|
||||||
|
_changedHere = true;
|
||||||
|
_tmpTargetHumidity = val.roundToDouble();
|
||||||
|
});
|
||||||
|
}),
|
||||||
|
onChangeEnd: (double v) => _setTargetHumidity(entity, v),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
if (result.isNotEmpty) {
|
||||||
|
return Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: <Widget>[
|
||||||
|
Padding(
|
||||||
|
padding: EdgeInsets.fromLTRB(
|
||||||
|
0.0, Sizes.rowPadding, 0.0, Sizes.rowPadding),
|
||||||
|
child: Text("Target humidity", style: TextStyle(
|
||||||
|
fontSize: Sizes.stateFontSize
|
||||||
|
)),
|
||||||
|
),
|
||||||
|
Row(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
|
children: result,
|
||||||
|
),
|
||||||
|
Container(
|
||||||
|
height: Sizes.rowPadding,
|
||||||
|
)
|
||||||
|
],
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return Container(
|
||||||
|
width: 0.0,
|
||||||
|
height: 0.0,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_resetTimer?.cancel();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
class TemperatureControlWidget extends StatelessWidget {
|
||||||
|
final double value;
|
||||||
|
final double fontSize;
|
||||||
|
final Color fontColor;
|
||||||
|
final onSmallInc;
|
||||||
|
final onLargeInc;
|
||||||
|
final onSmallDec;
|
||||||
|
final onLargeDec;
|
||||||
|
|
||||||
|
TemperatureControlWidget(
|
||||||
|
{Key key,
|
||||||
|
@required this.value,
|
||||||
|
@required this.onSmallInc,
|
||||||
|
@required this.onSmallDec,
|
||||||
|
@required this.onLargeInc,
|
||||||
|
@required this.onLargeDec,
|
||||||
|
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.createIconDataFromIconName(
|
||||||
|
'mdi:chevron-up')),
|
||||||
|
iconSize: 30.0,
|
||||||
|
onPressed: () => onSmallInc(),
|
||||||
|
),
|
||||||
|
IconButton(
|
||||||
|
icon: Icon(MaterialDesignIcons.createIconDataFromIconName(
|
||||||
|
'mdi:chevron-down')),
|
||||||
|
iconSize: 30.0,
|
||||||
|
onPressed: () => onSmallDec(),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
Column(
|
||||||
|
children: <Widget>[
|
||||||
|
IconButton(
|
||||||
|
icon: Icon(MaterialDesignIcons.createIconDataFromIconName(
|
||||||
|
'mdi:chevron-double-up')),
|
||||||
|
iconSize: 30.0,
|
||||||
|
onPressed: () => onLargeInc(),
|
||||||
|
),
|
||||||
|
IconButton(
|
||||||
|
icon: Icon(MaterialDesignIcons.createIconDataFromIconName(
|
||||||
|
'mdi:chevron-double-down')),
|
||||||
|
iconSize: 30.0,
|
||||||
|
onPressed: () => onLargeDec(),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
)
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
200
lib/entity_widgets/controls/cover_controls.dart
Normal file
@ -0,0 +1,200 @@
|
|||||||
|
part of '../../main.dart';
|
||||||
|
|
||||||
|
class CoverControlWidget extends StatefulWidget {
|
||||||
|
|
||||||
|
CoverControlWidget({Key key}) : super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
_CoverControlWidgetState createState() => _CoverControlWidgetState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _CoverControlWidgetState extends State<CoverControlWidget> {
|
||||||
|
|
||||||
|
double _tmpPosition = 0.0;
|
||||||
|
double _tmpTiltPosition = 0.0;
|
||||||
|
bool _changedHere = false;
|
||||||
|
|
||||||
|
void _setNewPosition(CoverEntity entity, double position) {
|
||||||
|
setState(() {
|
||||||
|
_tmpPosition = position.roundToDouble();
|
||||||
|
_changedHere = true;
|
||||||
|
eventBus.fire(new ServiceCallEvent(entity.domain, "set_cover_position", entity.entityId,{"position": _tmpPosition.round()}));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void _setNewTiltPosition(CoverEntity entity, double position) {
|
||||||
|
setState(() {
|
||||||
|
_tmpTiltPosition = position.roundToDouble();
|
||||||
|
_changedHere = true;
|
||||||
|
eventBus.fire(new ServiceCallEvent(entity.domain, "set_cover_tilt_position", entity.entityId,{"tilt_position": _tmpTiltPosition.round()}));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void _resetVars(CoverEntity entity) {
|
||||||
|
_tmpPosition = entity.currentPosition;
|
||||||
|
_tmpTiltPosition = entity.currentTiltPosition;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final entityModel = EntityModel.of(context);
|
||||||
|
final CoverEntity entity = entityModel.entityWrapper.entity;
|
||||||
|
if (_changedHere) {
|
||||||
|
_changedHere = false;
|
||||||
|
} else {
|
||||||
|
_resetVars(entity);
|
||||||
|
}
|
||||||
|
return Padding(
|
||||||
|
padding: EdgeInsets.fromLTRB(Sizes.leftWidgetPadding, Sizes.rowPadding, Sizes.rightWidgetPadding, 0.0),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: <Widget>[
|
||||||
|
_buildPositionControls(entity),
|
||||||
|
_buildTiltControls(entity)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildPositionControls(CoverEntity entity) {
|
||||||
|
if (entity.supportSetPosition) {
|
||||||
|
return Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: <Widget>[
|
||||||
|
Padding(
|
||||||
|
padding: EdgeInsets.fromLTRB(
|
||||||
|
0.0, Sizes.rowPadding, 0.0, Sizes.rowPadding),
|
||||||
|
child: Text("Position", style: TextStyle(
|
||||||
|
fontSize: Sizes.stateFontSize
|
||||||
|
)),
|
||||||
|
),
|
||||||
|
Slider(
|
||||||
|
value: _tmpPosition,
|
||||||
|
min: 0.0,
|
||||||
|
max: 100.0,
|
||||||
|
divisions: 10,
|
||||||
|
onChanged: (double value) {
|
||||||
|
setState(() {
|
||||||
|
_tmpPosition = value.roundToDouble();
|
||||||
|
_changedHere = true;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
onChangeEnd: (double value) => _setNewPosition(entity, value),
|
||||||
|
),
|
||||||
|
Container(height: Sizes.rowPadding,)
|
||||||
|
],
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return Container(width: 0.0, height: 0.0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildTiltControls(CoverEntity entity) {
|
||||||
|
List<Widget> controls = [];
|
||||||
|
if (entity.supportCloseTilt || entity.supportOpenTilt || entity.supportStopTilt) {
|
||||||
|
controls.add(
|
||||||
|
CoverTiltControlsWidget()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (entity.supportSetTiltPosition) {
|
||||||
|
controls.addAll(<Widget>[
|
||||||
|
Slider(
|
||||||
|
value: _tmpTiltPosition,
|
||||||
|
min: 0.0,
|
||||||
|
max: 100.0,
|
||||||
|
divisions: 10,
|
||||||
|
onChanged: (double value) {
|
||||||
|
setState(() {
|
||||||
|
_tmpTiltPosition = value.roundToDouble();
|
||||||
|
_changedHere = true;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
onChangeEnd: (double value) => _setNewTiltPosition(entity, value),
|
||||||
|
),
|
||||||
|
Container(height: Sizes.rowPadding,)
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
if (controls.isNotEmpty) {
|
||||||
|
controls.insert(0, Padding(
|
||||||
|
padding: EdgeInsets.fromLTRB(
|
||||||
|
0.0, Sizes.rowPadding, 0.0, Sizes.rowPadding),
|
||||||
|
child: Text("Tilt position", style: TextStyle(
|
||||||
|
fontSize: Sizes.stateFontSize
|
||||||
|
)),
|
||||||
|
));
|
||||||
|
return Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: controls,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return Container(width: 0.0, height: 0.0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
class CoverTiltControlsWidget extends StatelessWidget {
|
||||||
|
void _open(CoverEntity entity) {
|
||||||
|
eventBus.fire(new ServiceCallEvent(
|
||||||
|
entity.domain, "open_cover_tilt", entity.entityId, null));
|
||||||
|
}
|
||||||
|
|
||||||
|
void _close(CoverEntity entity) {
|
||||||
|
eventBus.fire(new ServiceCallEvent(
|
||||||
|
entity.domain, "close_cover_tilt", entity.entityId, null));
|
||||||
|
}
|
||||||
|
|
||||||
|
void _stop(CoverEntity entity) {
|
||||||
|
eventBus.fire(new ServiceCallEvent(
|
||||||
|
entity.domain, "stop_cover_tilt", entity.entityId, null));
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final entityModel = EntityModel.of(context);
|
||||||
|
final CoverEntity entity = entityModel.entityWrapper.entity;
|
||||||
|
List<Widget> buttons = [];
|
||||||
|
if (entity.supportOpenTilt) {
|
||||||
|
buttons.add(IconButton(
|
||||||
|
icon: Icon(
|
||||||
|
MaterialDesignIcons.createIconDataFromIconName(
|
||||||
|
"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.createIconDataFromIconName("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.createIconDataFromIconName(
|
||||||
|
"mdi:arrow-bottom-left"),
|
||||||
|
size: Sizes.iconSize,
|
||||||
|
),
|
||||||
|
onPressed: entity.canTiltBeClosed ? () => _close(entity) : null));
|
||||||
|
} else {
|
||||||
|
buttons.add(Container(
|
||||||
|
width: Sizes.iconSize + 20.0,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
return Row(
|
||||||
|
children: buttons,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
123
lib/entity_widgets/controls/fan_controls.dart
Normal file
@ -0,0 +1,123 @@
|
|||||||
|
part of '../../main.dart';
|
||||||
|
|
||||||
|
class FanControlsWidget extends StatefulWidget {
|
||||||
|
|
||||||
|
@override
|
||||||
|
_FanControlsWidgetState createState() => _FanControlsWidgetState();
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
class _FanControlsWidgetState extends State<FanControlsWidget> {
|
||||||
|
|
||||||
|
bool _tmpOscillate;
|
||||||
|
bool _tmpDirectionForward;
|
||||||
|
bool _changedHere = false;
|
||||||
|
String _tmpSpeed;
|
||||||
|
|
||||||
|
void _resetState(FanEntity entity) {
|
||||||
|
_tmpOscillate = entity.attributes["oscillating"] ?? false;
|
||||||
|
_tmpDirectionForward = entity.attributes["direction"] == "forward";
|
||||||
|
_tmpSpeed = entity.attributes["speed"];
|
||||||
|
}
|
||||||
|
|
||||||
|
void _setOscillate(FanEntity entity, bool oscillate) {
|
||||||
|
setState(() {
|
||||||
|
_tmpOscillate = oscillate;
|
||||||
|
_changedHere = true;
|
||||||
|
eventBus.fire(new ServiceCallEvent(
|
||||||
|
"fan", "oscillate", entity.entityId,
|
||||||
|
{"oscillating": oscillate}));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void _setDirection(FanEntity entity, bool forward) {
|
||||||
|
setState(() {
|
||||||
|
_tmpDirectionForward = forward;
|
||||||
|
_changedHere = true;
|
||||||
|
eventBus.fire(new ServiceCallEvent(
|
||||||
|
"fan", "set_direction", entity.entityId,
|
||||||
|
{"direction": forward ? "forward" : "reverse"}));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void _setSpeed(FanEntity entity, String value) {
|
||||||
|
setState(() {
|
||||||
|
_tmpSpeed = value;
|
||||||
|
_changedHere = true;
|
||||||
|
eventBus.fire(new ServiceCallEvent(
|
||||||
|
"fan", "set_speed", entity.entityId,
|
||||||
|
{"speed": value}));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final entityModel = EntityModel.of(context);
|
||||||
|
final FanEntity entity = entityModel.entityWrapper.entity;
|
||||||
|
if (!_changedHere) {
|
||||||
|
_resetState(entity);
|
||||||
|
} else {
|
||||||
|
_changedHere = false;
|
||||||
|
}
|
||||||
|
return Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
|
children: <Widget>[
|
||||||
|
_buildSpeedControl(entity),
|
||||||
|
_buildOscillateControl(entity),
|
||||||
|
_buildDirectionControl(entity)
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildSpeedControl(FanEntity entity) {
|
||||||
|
if (entity.supportSetSpeed && entity.speedList != null && entity.speedList.isNotEmpty) {
|
||||||
|
return ModeSelectorWidget(
|
||||||
|
onChange: (effect) => _setSpeed(entity, effect),
|
||||||
|
caption: "Speed",
|
||||||
|
options: entity.speedList,
|
||||||
|
value: _tmpSpeed
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return Container(width: 0.0, height: 0.0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildOscillateControl(FanEntity entity) {
|
||||||
|
if (entity.supportOscillate) {
|
||||||
|
return ModeSwitchWidget(
|
||||||
|
onChange: (value) => _setOscillate(entity, value),
|
||||||
|
caption: "Oscillate",
|
||||||
|
value: _tmpOscillate,
|
||||||
|
expanded: false,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return Container(width: 0.0, height: 0.0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildDirectionControl(FanEntity entity) {
|
||||||
|
if (entity.supportDirection) {
|
||||||
|
return Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: <Widget>[
|
||||||
|
IconButton(
|
||||||
|
onPressed: _tmpDirectionForward ?
|
||||||
|
() => _setDirection(entity, false) :
|
||||||
|
null,
|
||||||
|
icon: Icon(Icons.rotate_left),
|
||||||
|
),
|
||||||
|
IconButton(
|
||||||
|
onPressed: !_tmpDirectionForward ?
|
||||||
|
() => _setDirection(entity, true) :
|
||||||
|
null,
|
||||||
|
icon: Icon(Icons.rotate_right),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return Container(width: 0.0, height: 0.0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
206
lib/entity_widgets/controls/light_controls.dart
Normal file
@ -0,0 +1,206 @@
|
|||||||
|
part of '../../main.dart';
|
||||||
|
|
||||||
|
class LightControlsWidget extends StatefulWidget {
|
||||||
|
|
||||||
|
@override
|
||||||
|
_LightControlsWidgetState createState() => _LightControlsWidgetState();
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
class _LightControlsWidgetState extends State<LightControlsWidget> {
|
||||||
|
|
||||||
|
int _tmpBrightness;
|
||||||
|
int _tmpColorTemp;
|
||||||
|
Color _tmpColor;
|
||||||
|
bool _changedHere = false;
|
||||||
|
String _tmpEffect;
|
||||||
|
|
||||||
|
void _resetState(LightEntity entity) {
|
||||||
|
_tmpBrightness = entity.brightness ?? 0;
|
||||||
|
_tmpColorTemp = entity.colorTemp;
|
||||||
|
_tmpColor = entity.color;
|
||||||
|
_tmpEffect = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
void _setBrightness(LightEntity entity, double value) {
|
||||||
|
setState(() {
|
||||||
|
_tmpBrightness = value.round();
|
||||||
|
_changedHere = true;
|
||||||
|
if (_tmpBrightness > 0) {
|
||||||
|
eventBus.fire(new ServiceCallEvent(
|
||||||
|
entity.domain, "turn_on", entity.entityId,
|
||||||
|
{"brightness": _tmpBrightness}));
|
||||||
|
} else {
|
||||||
|
eventBus.fire(new ServiceCallEvent(
|
||||||
|
entity.domain, "turn_off", entity.entityId,
|
||||||
|
null));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void _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, Color color) {
|
||||||
|
setState(() {
|
||||||
|
_tmpColor = color;
|
||||||
|
_changedHere = true;
|
||||||
|
TheLogger.debug( "Color: [${color.red}, ${color.green}, ${color.blue}]");
|
||||||
|
if ((color == Colors.black) || ((color.red == color.green) && (color.green == color.blue))) {
|
||||||
|
eventBus.fire(new ServiceCallEvent(
|
||||||
|
entity.domain, "turn_off", entity.entityId,
|
||||||
|
null));
|
||||||
|
} else {
|
||||||
|
eventBus.fire(new ServiceCallEvent(
|
||||||
|
entity.domain, "turn_on", entity.entityId,
|
||||||
|
{"rgb_color": [color.red, color.green, color.blue]}));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
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),
|
||||||
|
_buildColorTempControl(entity),
|
||||||
|
_buildColorControl(entity),
|
||||||
|
_buildEffectControl(entity)
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildBrightnessControl(LightEntity entity) {
|
||||||
|
if ((entity.supportBrightness) && (_tmpBrightness != null) && (entity.state != EntityState.unavailable)) {
|
||||||
|
return UniversalSlider(
|
||||||
|
onChanged: (value) {
|
||||||
|
setState(() {
|
||||||
|
_changedHere = true;
|
||||||
|
_tmpBrightness = value.round();
|
||||||
|
});
|
||||||
|
},
|
||||||
|
min: 0.0,
|
||||||
|
max: 255.0,
|
||||||
|
onChangeEnd: (value) => _setBrightness(entity, value),
|
||||||
|
value: _tmpBrightness.toDouble(),
|
||||||
|
leading: Icon(Icons.brightness_5),
|
||||||
|
title: "Brightness",
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return Container(width: 0.0, height: 0.0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildColorTempControl(LightEntity entity) {
|
||||||
|
if ((entity.supportColorTemp) && (_tmpColorTemp != null)) {
|
||||||
|
return UniversalSlider(
|
||||||
|
title: "Color temperature",
|
||||||
|
leading: Text("Cold", style: TextStyle(color: Colors.lightBlue),),
|
||||||
|
value: _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) && (entity.color != null)) {
|
||||||
|
return Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
|
children: <Widget>[
|
||||||
|
Container(height: Sizes.rowPadding,),
|
||||||
|
RaisedButton(
|
||||||
|
onPressed: () => _showColorPicker(entity),
|
||||||
|
color: _tmpColor ?? Colors.black45,
|
||||||
|
child: Text(
|
||||||
|
"COLOR",
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 50.0,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: Colors.black12,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Container(height: 2*Sizes.rowPadding,),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return Container(width: 0.0, height: 0.0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _showColorPicker(LightEntity entity) {
|
||||||
|
showDialog(
|
||||||
|
context: context,
|
||||||
|
builder: (BuildContext context) {
|
||||||
|
return AlertDialog(
|
||||||
|
titlePadding: EdgeInsets.all(0.0),
|
||||||
|
contentPadding: EdgeInsets.all(0.0),
|
||||||
|
content: SingleChildScrollView(
|
||||||
|
child: MaterialPicker(
|
||||||
|
pickerColor: _tmpColor,
|
||||||
|
onColorChanged: (color) {
|
||||||
|
_setColor(entity, color);
|
||||||
|
Navigator.of(context).pop();
|
||||||
|
},
|
||||||
|
enableLabel: true,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildEffectControl(LightEntity entity) {
|
||||||
|
if ((entity.supportEffect) && (entity.effectList != null)) {
|
||||||
|
return ModeSelectorWidget(
|
||||||
|
onChange: (effect) => _setEffect(entity, effect),
|
||||||
|
caption: "Effect",
|
||||||
|
options: entity.effectList,
|
||||||
|
value: _tmpEffect
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return Container(width: 0.0, height: 0.0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
466
lib/entity_widgets/controls/media_player_widgets.dart
Normal file
@ -0,0 +1,466 @@
|
|||||||
|
part of '../../main.dart';
|
||||||
|
|
||||||
|
class MediaPlayerWidget extends StatelessWidget {
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final EntityModel entityModel = EntityModel.of(context);
|
||||||
|
final MediaPlayerEntity entity = entityModel.entityWrapper.entity;
|
||||||
|
//TheLogger.debug("stop: ${entity.supportStop}, seek: ${entity.supportSeek}");
|
||||||
|
return Column(
|
||||||
|
children: <Widget>[
|
||||||
|
Stack(
|
||||||
|
alignment: AlignmentDirectional.topEnd,
|
||||||
|
children: <Widget>[
|
||||||
|
_buildImage(entity),
|
||||||
|
Positioned(
|
||||||
|
bottom: 0.0,
|
||||||
|
left: 0.0,
|
||||||
|
right: 0.0,
|
||||||
|
child: Container(
|
||||||
|
color: Colors.black45,
|
||||||
|
child: _buildState(entity),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Positioned(
|
||||||
|
bottom: 0.0,
|
||||||
|
left: 0.0,
|
||||||
|
right: 0.0,
|
||||||
|
child: MediaPlayerProgressWidget()
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
MediaPlayerPlaybackControls()
|
||||||
|
]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildState(MediaPlayerEntity entity) {
|
||||||
|
TextStyle style = TextStyle(
|
||||||
|
fontSize: 14.0,
|
||||||
|
color: Colors.white,
|
||||||
|
fontWeight: FontWeight.normal,
|
||||||
|
height: 1.2
|
||||||
|
);
|
||||||
|
List<Widget> states = [];
|
||||||
|
states.add(Text("${entity.displayName}", style: style));
|
||||||
|
String state = entity.state;
|
||||||
|
if (state == null || state == EntityState.off || state == EntityState.unavailable || state == EntityState.idle) {
|
||||||
|
states.add(Text("${entity.state}", style: style.apply(fontSizeDelta: 4.0),));
|
||||||
|
}
|
||||||
|
if (entity.attributes['media_title'] != null) {
|
||||||
|
states.add(Text(
|
||||||
|
"${entity.attributes['media_title']}",
|
||||||
|
style: style.apply(fontSizeDelta: 6.0, fontWeightDelta: 50),
|
||||||
|
maxLines: 1,
|
||||||
|
softWrap: true,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
if (entity.attributes['media_content_type'] == "music") {
|
||||||
|
states.add(Text("${entity.attributes['media_artist'] ?? entity.attributes['app_name']}", style: style.apply(fontSizeDelta: 4.0),));
|
||||||
|
} else if (entity.attributes['app_name'] != null) {
|
||||||
|
states.add(Text("${entity.attributes['app_name']}", style: style.apply(fontSizeDelta: 4.0),));
|
||||||
|
}
|
||||||
|
return Padding(
|
||||||
|
padding: EdgeInsets.fromLTRB(Sizes.leftWidgetPadding, Sizes.rowPadding, Sizes.rightWidgetPadding, Sizes.rowPadding),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: states,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildImage(MediaPlayerEntity entity) {
|
||||||
|
String state = entity.state;
|
||||||
|
if (homeAssistantWebHost != null && 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("$homeAssistantWebHost${entity.entityPicture}"),
|
||||||
|
height: 240.0,
|
||||||
|
//width: 320.0,
|
||||||
|
fit: BoxFit.contain,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: <Widget>[
|
||||||
|
Icon(
|
||||||
|
MaterialDesignIcons.createIconDataFromIconName("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) {
|
||||||
|
TheLogger.debug("${entity.entityId} turn_on");
|
||||||
|
eventBus.fire(new ServiceCallEvent(
|
||||||
|
entity.domain, "turn_on", entity.entityId,
|
||||||
|
null));
|
||||||
|
} else {
|
||||||
|
TheLogger.debug("${entity.entityId} turn_off");
|
||||||
|
eventBus.fire(new ServiceCallEvent(
|
||||||
|
entity.domain, "turn_off", entity.entityId,
|
||||||
|
null));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _callAction(MediaPlayerEntity entity, String action) {
|
||||||
|
TheLogger.debug("${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.createIconDataFromIconName(
|
||||||
|
"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) {
|
||||||
|
bool isMuted = entity.attributes["is_volume_muted"] ?? false;
|
||||||
|
muteWidget =
|
||||||
|
IconButton(
|
||||||
|
icon: Icon(isMuted ? Icons.volume_off : Icons.volume_up),
|
||||||
|
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.createIconDataFromIconName("mdi:plus")),
|
||||||
|
onPressed: () => _setVolumeUp(entity.entityId)
|
||||||
|
),
|
||||||
|
IconButton(
|
||||||
|
icon: Icon(MaterialDesignIcons.createIconDataFromIconName("mdi:minus")),
|
||||||
|
onPressed: () => _setVolumeDown(entity.entityId)
|
||||||
|
)
|
||||||
|
],
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
volumeStepWidget = Container(width: 0.0, height: 0.0,);
|
||||||
|
}
|
||||||
|
if (entity.supportVolumeSet) {
|
||||||
|
if (!_changedHere) {
|
||||||
|
_newVolumeLevel = entity._getDoubleAttributeValue("volume_level");
|
||||||
|
} else {
|
||||||
|
_changedHere = false;
|
||||||
|
}
|
||||||
|
children.add(
|
||||||
|
UniversalSlider(
|
||||||
|
leading: muteWidget,
|
||||||
|
closing: volumeStepWidget,
|
||||||
|
title: "Volume",
|
||||||
|
onChanged: (value) {
|
||||||
|
setState(() {
|
||||||
|
_changedHere = true;
|
||||||
|
_newVolumeLevel = value;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
value: _newVolumeLevel,
|
||||||
|
onChangeEnd: (value) => _setVolume(value, entity.entityId),
|
||||||
|
max: 1.0,
|
||||||
|
min: 0.0,
|
||||||
|
)
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
children.add(Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: <Widget>[
|
||||||
|
muteWidget,
|
||||||
|
volumeStepWidget
|
||||||
|
],
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (entity.supportSelectSoundMode && entity.soundModeList != null) {
|
||||||
|
if (!_changedHere) {
|
||||||
|
_newSoundMode = entity.attributes["sound_mode"];
|
||||||
|
} else {
|
||||||
|
_changedHere = false;
|
||||||
|
}
|
||||||
|
children.add(
|
||||||
|
ModeSelectorWidget(
|
||||||
|
options: entity.soundModeList,
|
||||||
|
caption: "Sound mode",
|
||||||
|
value: _newSoundMode,
|
||||||
|
onChange: (value) => _setSoundMode(value, entity.entityId)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (entity.supportSelectSource && entity.sourceList != null) {
|
||||||
|
if (!_changedHere) {
|
||||||
|
_newSource = entity.attributes["source"];
|
||||||
|
} else {
|
||||||
|
_changedHere = false;
|
||||||
|
}
|
||||||
|
children.add(
|
||||||
|
ModeSelectorWidget(
|
||||||
|
options: entity.sourceList,
|
||||||
|
caption: "Source",
|
||||||
|
value: _newSource,
|
||||||
|
onChange: (value) => _setSource(value, entity.entityId)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
return Column(
|
||||||
|
children: children,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
class MediaPlayerProgressWidget extends StatefulWidget {
|
||||||
|
@override
|
||||||
|
_MediaPlayerProgressWidgetState createState() => _MediaPlayerProgressWidgetState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _MediaPlayerProgressWidgetState extends State<MediaPlayerProgressWidget> {
|
||||||
|
|
||||||
|
Timer _timer;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final EntityModel entityModel = EntityModel.of(context);
|
||||||
|
final MediaPlayerEntity entity = entityModel.entityWrapper.entity;
|
||||||
|
double progress;
|
||||||
|
try {
|
||||||
|
DateTime lastUpdated = DateTime.parse(
|
||||||
|
entity.attributes["media_position_updated_at"]).toLocal();
|
||||||
|
Duration duration = Duration(seconds: entity._getIntAttributeValue("media_duration") ?? 1);
|
||||||
|
Duration position = Duration(seconds: entity._getIntAttributeValue("media_position") ?? 0);
|
||||||
|
int currentPosition = position.inSeconds;
|
||||||
|
if (entity.state == EntityState.playing) {
|
||||||
|
_timer?.cancel();
|
||||||
|
_timer = Timer(Duration(seconds: 1), () {
|
||||||
|
setState(() {
|
||||||
|
});
|
||||||
|
});
|
||||||
|
int differenceInSeconds = DateTime
|
||||||
|
.now()
|
||||||
|
.difference(lastUpdated)
|
||||||
|
.inSeconds;
|
||||||
|
currentPosition = currentPosition + differenceInSeconds;
|
||||||
|
} else {
|
||||||
|
_timer?.cancel();
|
||||||
|
}
|
||||||
|
progress = currentPosition / duration.inSeconds;
|
||||||
|
return LinearProgressIndicator(
|
||||||
|
value: progress,
|
||||||
|
backgroundColor: Colors.black45,
|
||||||
|
valueColor: AlwaysStoppedAnimation<Color>(EntityColor.stateColor(EntityState.on)),
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
_timer?.cancel();
|
||||||
|
progress = 0.0;
|
||||||
|
}
|
||||||
|
return LinearProgressIndicator(
|
||||||
|
value: progress,
|
||||||
|
backgroundColor: Colors.black45,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_timer?.cancel();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
70
lib/entity_widgets/controls/slider_controls.dart
Normal file
@ -0,0 +1,70 @@
|
|||||||
|
part of '../../main.dart';
|
||||||
|
|
||||||
|
class SliderControlsWidget extends StatefulWidget {
|
||||||
|
|
||||||
|
SliderControlsWidget({Key key}) : super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
_SliderControlsWidgetState createState() => _SliderControlsWidgetState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _SliderControlsWidgetState extends State<SliderControlsWidget> {
|
||||||
|
int _multiplier = 1;
|
||||||
|
double _newValue;
|
||||||
|
bool _changedHere = false;
|
||||||
|
|
||||||
|
void setNewState(newValue, domain, entityId) {
|
||||||
|
setState(() {
|
||||||
|
_newValue = newValue;
|
||||||
|
_changedHere = true;
|
||||||
|
});
|
||||||
|
eventBus.fire(new ServiceCallEvent(domain, "set_value", entityId,
|
||||||
|
{"value": "${newValue.toString()}"}));
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final entityModel = EntityModel.of(context);
|
||||||
|
final SliderEntity entity = entityModel.entityWrapper.entity;
|
||||||
|
if (entity.valueStep < 1) {
|
||||||
|
_multiplier = 10;
|
||||||
|
} else if (entity.valueStep < 0.1) {
|
||||||
|
_multiplier = 100;
|
||||||
|
}
|
||||||
|
if (!_changedHere) {
|
||||||
|
_newValue = entity.doubleState;
|
||||||
|
} else {
|
||||||
|
_changedHere = false;
|
||||||
|
}
|
||||||
|
Widget slider = Slider(
|
||||||
|
min: entity.minValue * _multiplier,
|
||||||
|
max: entity.maxValue * _multiplier,
|
||||||
|
value: (_newValue <= entity.maxValue) &&
|
||||||
|
(_newValue >= entity.minValue)
|
||||||
|
? _newValue * _multiplier
|
||||||
|
: entity.minValue * _multiplier,
|
||||||
|
onChanged: (value) {
|
||||||
|
setState(() {
|
||||||
|
_newValue = (value.roundToDouble() / _multiplier);
|
||||||
|
_changedHere = true;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
onChangeEnd: (value) {
|
||||||
|
setNewState(value.roundToDouble() / _multiplier, entity.domain, entity.entityId);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
return Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
|
children: <Widget>[
|
||||||
|
Text(
|
||||||
|
"$_newValue",
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: Sizes.largeFontSize,
|
||||||
|
color: Colors.blue
|
||||||
|
),
|
||||||
|
),
|
||||||
|
slider
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
40
lib/entity_widgets/default_entity_container.dart
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
part of '../main.dart';
|
||||||
|
|
||||||
|
class DefaultEntityContainer extends StatelessWidget {
|
||||||
|
DefaultEntityContainer({
|
||||||
|
Key key,
|
||||||
|
@required this.state
|
||||||
|
}) : super(key: key);
|
||||||
|
|
||||||
|
final Widget state;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final EntityModel entityModel = EntityModel.of(context);
|
||||||
|
return InkWell(
|
||||||
|
onLongPress: () {
|
||||||
|
if (entityModel.handleTap) {
|
||||||
|
entityModel.entityWrapper.handleHold();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onTap: () {
|
||||||
|
if (entityModel.handleTap) {
|
||||||
|
entityModel.entityWrapper.handleTap();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
child: Row(
|
||||||
|
mainAxisSize: MainAxisSize.max,
|
||||||
|
children: <Widget>[
|
||||||
|
EntityIcon(),
|
||||||
|
|
||||||
|
Flexible(
|
||||||
|
fit: FlexFit.tight,
|
||||||
|
flex: 3,
|
||||||
|
child: EntityName(),
|
||||||
|
),
|
||||||
|
state
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
62
lib/entity_widgets/entity_colors.class.dart
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
part of '../main.dart';
|
||||||
|
|
||||||
|
class EntityColor {
|
||||||
|
|
||||||
|
static const badgeColors = {
|
||||||
|
"default": Color.fromRGBO(223, 76, 30, 1.0),
|
||||||
|
"binary_sensor": Color.fromRGBO(3, 155, 229, 1.0)
|
||||||
|
};
|
||||||
|
|
||||||
|
static const _stateColors = {
|
||||||
|
EntityState.on: Colors.amber,
|
||||||
|
"auto": Colors.amber,
|
||||||
|
EntityState.idle: Colors.amber,
|
||||||
|
EntityState.playing: Colors.amber,
|
||||||
|
"above_horizon": Colors.amber,
|
||||||
|
EntityState.home: Colors.amber,
|
||||||
|
EntityState.open: Colors.amber,
|
||||||
|
EntityState.off: Color.fromRGBO(68, 115, 158, 1.0),
|
||||||
|
EntityState.closed: Color.fromRGBO(68, 115, 158, 1.0),
|
||||||
|
"below_horizon": Color.fromRGBO(68, 115, 158, 1.0),
|
||||||
|
"default": Color.fromRGBO(68, 115, 158, 1.0),
|
||||||
|
"heat": Colors.redAccent,
|
||||||
|
"cool": Colors.lightBlue,
|
||||||
|
EntityState.unavailable: Colors.black26,
|
||||||
|
EntityState.unknown: Colors.black26,
|
||||||
|
};
|
||||||
|
|
||||||
|
static Color stateColor(String state) {
|
||||||
|
return _stateColors[state] ?? _stateColors["default"];
|
||||||
|
}
|
||||||
|
|
||||||
|
static charts.Color chartHistoryStateColor(String state, int id) {
|
||||||
|
Color c = _stateColors[state];
|
||||||
|
if (c != null) {
|
||||||
|
return charts.Color(
|
||||||
|
r: c.red,
|
||||||
|
g: c.green,
|
||||||
|
b: c.blue,
|
||||||
|
a: c.alpha
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
double r = id.toDouble() % 10;
|
||||||
|
return charts.MaterialPalette.getOrderedPalettes(10)[r.round()].shadeDefault;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static Color historyStateColor(String state, int id) {
|
||||||
|
Color c = _stateColors[state];
|
||||||
|
if (c != null) {
|
||||||
|
return c;
|
||||||
|
} else {
|
||||||
|
if (id > -1) {
|
||||||
|
double r = id.toDouble() % 10;
|
||||||
|
charts.Color c1 = charts.MaterialPalette.getOrderedPalettes(10)[r.round()].shadeDefault;
|
||||||
|
return Color.fromARGB(c1.a, c1.r, c1.g, c1.b);
|
||||||
|
} else {
|
||||||
|
return _stateColors[EntityState.on];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
23
lib/entity_widgets/entity_icon.dart
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
part of '../main.dart';
|
||||||
|
|
||||||
|
class EntityIcon extends StatelessWidget {
|
||||||
|
|
||||||
|
final EdgeInsetsGeometry padding;
|
||||||
|
final double iconSize;
|
||||||
|
|
||||||
|
const EntityIcon({Key key, this.iconSize: Sizes.iconSize, this.padding: const EdgeInsets.fromLTRB(
|
||||||
|
Sizes.leftWidgetPadding, 0.0, 12.0, 0.0)}) : super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final EntityWrapper entityWrapper = EntityModel.of(context).entityWrapper;
|
||||||
|
return Padding(
|
||||||
|
padding: padding,
|
||||||
|
child: MaterialDesignIcons.createIconWidgetFromEntityData(
|
||||||
|
entityWrapper,
|
||||||
|
iconSize,
|
||||||
|
EntityColor.stateColor(entityWrapper.entity.state)
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
29
lib/entity_widgets/entity_name.dart
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
part of '../main.dart';
|
||||||
|
|
||||||
|
class EntityName extends StatelessWidget {
|
||||||
|
|
||||||
|
final EdgeInsetsGeometry padding;
|
||||||
|
final TextOverflow textOverflow;
|
||||||
|
final bool wordsWrap;
|
||||||
|
final double fontSize;
|
||||||
|
final TextAlign textAlign;
|
||||||
|
final int maxLines;
|
||||||
|
|
||||||
|
const EntityName({Key key, this.maxLines, this.padding: const EdgeInsets.only(right: 10.0), this.textOverflow: TextOverflow.ellipsis, this.wordsWrap: true, this.fontSize: Sizes.nameFontSize, this.textAlign: TextAlign.left}) : super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final EntityWrapper entityWrapper = EntityModel.of(context).entityWrapper;
|
||||||
|
return Padding(
|
||||||
|
padding: padding,
|
||||||
|
child: Text(
|
||||||
|
"${entityWrapper.displayName}",
|
||||||
|
overflow: textOverflow,
|
||||||
|
softWrap: wordsWrap,
|
||||||
|
maxLines: maxLines,
|
||||||
|
style: TextStyle(fontSize: fontSize),
|
||||||
|
textAlign: textAlign,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
14
lib/entity_widgets/entity_page_container.dart
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
part of '../main.dart';
|
||||||
|
|
||||||
|
class EntityPageContainer extends StatelessWidget {
|
||||||
|
EntityPageContainer({Key key, @required this.children}) : super(key: key);
|
||||||
|
|
||||||
|
final List<Widget> children;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return ListView(
|
||||||
|
children: children,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
84
lib/entity_widgets/glance_entity_container.dart
Normal file
@ -0,0 +1,84 @@
|
|||||||
|
part of '../main.dart';
|
||||||
|
|
||||||
|
class GlanceEntityContainer extends StatelessWidget {
|
||||||
|
|
||||||
|
final bool showName;
|
||||||
|
final bool showState;
|
||||||
|
final bool nameInTheBottom;
|
||||||
|
final double iconSize;
|
||||||
|
final double nameFontSize;
|
||||||
|
final bool wordsWrapInName;
|
||||||
|
|
||||||
|
GlanceEntityContainer({
|
||||||
|
Key key,
|
||||||
|
@required this.showName,
|
||||||
|
@required this.showState,
|
||||||
|
this.nameInTheBottom: false,
|
||||||
|
this.iconSize: Sizes.iconSize,
|
||||||
|
this.nameFontSize: Sizes.smallFontSize,
|
||||||
|
this.wordsWrapInName: false
|
||||||
|
}) : super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final EntityWrapper entityWrapper = EntityModel.of(context).entityWrapper;
|
||||||
|
List<Widget> result = [];
|
||||||
|
if (!nameInTheBottom) {
|
||||||
|
if (showName) {
|
||||||
|
result.add(_buildName());
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (showState) {
|
||||||
|
result.add(_buildState());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
result.add(
|
||||||
|
EntityIcon(
|
||||||
|
padding: EdgeInsets.all(0.0),
|
||||||
|
iconSize: iconSize,
|
||||||
|
)
|
||||||
|
);
|
||||||
|
if (!nameInTheBottom) {
|
||||||
|
if (showState) {
|
||||||
|
result.add(_buildState());
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
result.add(_buildName());
|
||||||
|
}
|
||||||
|
|
||||||
|
return Center(
|
||||||
|
child: InkResponse(
|
||||||
|
child: ConstrainedBox(
|
||||||
|
constraints: BoxConstraints(minWidth: Sizes.iconSize * 2),
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
//mainAxisAlignment: MainAxisAlignment.start,
|
||||||
|
//crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
|
children: result,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
onTap: () => entityWrapper.handleTap(),
|
||||||
|
onLongPress: () => entityWrapper.handleHold(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildName() {
|
||||||
|
return EntityName(
|
||||||
|
padding: EdgeInsets.only(bottom: Sizes.rowPadding),
|
||||||
|
textOverflow: TextOverflow.ellipsis,
|
||||||
|
wordsWrap: wordsWrapInName,
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
fontSize: nameFontSize,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildState() {
|
||||||
|
return SimpleEntityState(
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
expanded: false,
|
||||||
|
maxLines: 1,
|
||||||
|
padding: EdgeInsets.only(top: Sizes.rowPadding),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
230
lib/entity_widgets/history_chart/combined_history_chart.dart
Normal file
@ -0,0 +1,230 @@
|
|||||||
|
part of '../../main.dart';
|
||||||
|
|
||||||
|
class CombinedHistoryChartWidget extends StatefulWidget {
|
||||||
|
final rawHistory;
|
||||||
|
final EntityHistoryConfig config;
|
||||||
|
|
||||||
|
const CombinedHistoryChartWidget({Key key, @required this.rawHistory, @required this.config}) : super(key: key);
|
||||||
|
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<StatefulWidget> createState() {
|
||||||
|
return new _CombinedHistoryChartWidgetState();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
class _CombinedHistoryChartWidgetState extends State<CombinedHistoryChartWidget> {
|
||||||
|
|
||||||
|
int _selectedId = -1;
|
||||||
|
List<charts.Series<EntityHistoryMoment, DateTime>> _parsedHistory;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
// TODO: implement initState
|
||||||
|
super.initState();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
_parsedHistory = _parseHistory();
|
||||||
|
DateTime selectedTime;
|
||||||
|
List<String> selectedStates = [];
|
||||||
|
List<int> colorIndexes = [];
|
||||||
|
if ((_selectedId > -1) && (_parsedHistory != null) && (_parsedHistory.first.data.length >= (_selectedId + 1))) {
|
||||||
|
selectedTime = _parsedHistory.first.data[_selectedId].startTime;
|
||||||
|
_parsedHistory.where((item) { return item.id == "state"; }).forEach((item) {
|
||||||
|
selectedStates.add(item.data[_selectedId].state);
|
||||||
|
colorIndexes.add(item.data[_selectedId].colorId);
|
||||||
|
});
|
||||||
|
_parsedHistory.where((item) { return item.id == "value"; }).forEach((item) {
|
||||||
|
selectedStates.add("${item.data[_selectedId].value ?? '-'}");
|
||||||
|
colorIndexes.add(item.data[_selectedId].colorId);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.start,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
|
children: <Widget>[
|
||||||
|
HistoryControlWidget(
|
||||||
|
selectedTimeStart: selectedTime,
|
||||||
|
selectedStates: selectedStates,
|
||||||
|
onPrevTap: () => _selectPrev(),
|
||||||
|
onNextTap: () => _selectNext(),
|
||||||
|
colorIndexes: colorIndexes,
|
||||||
|
),
|
||||||
|
SizedBox(
|
||||||
|
height: 150.0,
|
||||||
|
child: charts.TimeSeriesChart(
|
||||||
|
_parsedHistory,
|
||||||
|
animate: false,
|
||||||
|
primaryMeasureAxis: new charts.NumericAxisSpec(
|
||||||
|
tickProviderSpec:
|
||||||
|
new charts.BasicNumericTickProviderSpec(zeroBound: false)),
|
||||||
|
dateTimeFactory: const charts.LocalDateTimeFactory(),
|
||||||
|
defaultRenderer: charts.LineRendererConfig(
|
||||||
|
includeArea: false,
|
||||||
|
includePoints: true
|
||||||
|
),
|
||||||
|
selectionModels: [
|
||||||
|
new charts.SelectionModelConfig(
|
||||||
|
type: charts.SelectionModelType.info,
|
||||||
|
changedListener: (model) => _onSelectionChanged(model),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
customSeriesRenderers: [
|
||||||
|
new charts.SymbolAnnotationRendererConfig(
|
||||||
|
customRendererId: "stateBars"
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
double _parseToDouble(temp1) {
|
||||||
|
if (temp1 is int) {
|
||||||
|
return temp1.toDouble();
|
||||||
|
} else if (temp1 is double) {
|
||||||
|
return temp1;
|
||||||
|
} else {
|
||||||
|
return double.tryParse("$temp1");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
List<charts.Series<EntityHistoryMoment, DateTime>> _parseHistory() {
|
||||||
|
TheLogger.debug(" parsing history...");
|
||||||
|
Map<String, List<EntityHistoryMoment>> numericDataLists = {};
|
||||||
|
int colorIdCounter = 0;
|
||||||
|
widget.config.numericAttributesToShow.forEach((String attrName) {
|
||||||
|
TheLogger.debug(" parsing attribute $attrName");
|
||||||
|
List<EntityHistoryMoment> data = [];
|
||||||
|
DateTime now = DateTime.now();
|
||||||
|
for (var i = 0; i < widget.rawHistory.length; i++) {
|
||||||
|
var stateData = widget.rawHistory[i];
|
||||||
|
DateTime startTime = DateTime.tryParse(stateData["last_updated"])?.toLocal();
|
||||||
|
DateTime endTime;
|
||||||
|
bool hiddenLine;
|
||||||
|
double value;
|
||||||
|
double previousValue = 0.0;
|
||||||
|
value = _parseToDouble(stateData["attributes"]["$attrName"]);
|
||||||
|
bool hiddenDot = (value == null);
|
||||||
|
if (hiddenDot && i > 0) {
|
||||||
|
previousValue = data[i-1].value ?? data[i-1].previousValue;
|
||||||
|
}
|
||||||
|
if (i < (widget.rawHistory.length - 1)) {
|
||||||
|
endTime = DateTime.tryParse(widget.rawHistory[i+1]["last_updated"])?.toLocal();
|
||||||
|
double nextValue = _parseToDouble(widget.rawHistory[i+1]["attributes"]["$attrName"]);
|
||||||
|
hiddenLine = (nextValue == null || hiddenDot);
|
||||||
|
} else {
|
||||||
|
hiddenLine = hiddenDot;
|
||||||
|
endTime = now;
|
||||||
|
}
|
||||||
|
data.add(EntityHistoryMoment(
|
||||||
|
value: value,
|
||||||
|
previousValue: previousValue,
|
||||||
|
hiddenDot: hiddenDot,
|
||||||
|
hiddenLine: hiddenLine,
|
||||||
|
state: stateData["state"],
|
||||||
|
startTime: startTime,
|
||||||
|
endTime: endTime,
|
||||||
|
id: i,
|
||||||
|
colorId: colorIdCounter
|
||||||
|
));
|
||||||
|
}
|
||||||
|
data.add(EntityHistoryMoment(
|
||||||
|
value: data.last.value,
|
||||||
|
previousValue: data.last.previousValue,
|
||||||
|
hiddenDot: data.last.hiddenDot,
|
||||||
|
hiddenLine: data.last.hiddenLine,
|
||||||
|
state: data.last.state,
|
||||||
|
startTime: now,
|
||||||
|
id: widget.rawHistory.length,
|
||||||
|
colorId: colorIdCounter
|
||||||
|
));
|
||||||
|
numericDataLists.addAll({attrName: data});
|
||||||
|
colorIdCounter += 1;
|
||||||
|
});
|
||||||
|
|
||||||
|
if ((_selectedId == -1) && (numericDataLists.isNotEmpty)) {
|
||||||
|
_selectedId = 0;
|
||||||
|
}
|
||||||
|
List<charts.Series<EntityHistoryMoment, DateTime>> result = [];
|
||||||
|
numericDataLists.forEach((attrName, dataList) {
|
||||||
|
TheLogger.debug(" adding ${dataList.length} data values");
|
||||||
|
result.add(
|
||||||
|
new charts.Series<EntityHistoryMoment, DateTime>(
|
||||||
|
id: "value",
|
||||||
|
colorFn: (EntityHistoryMoment historyMoment, __) => EntityColor.chartHistoryStateColor("_", historyMoment.colorId),
|
||||||
|
radiusPxFn: (EntityHistoryMoment historyMoment, __) {
|
||||||
|
if (historyMoment.hiddenDot) {
|
||||||
|
return 0.0;
|
||||||
|
} else if (historyMoment.id == _selectedId) {
|
||||||
|
return 5.0;
|
||||||
|
} else {
|
||||||
|
return 1.0;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
strokeWidthPxFn: (EntityHistoryMoment historyMoment, __) => historyMoment.hiddenLine ? 0.0 : 2.0,
|
||||||
|
domainFn: (EntityHistoryMoment historyMoment, _) => historyMoment.startTime,
|
||||||
|
measureFn: (EntityHistoryMoment historyMoment, _) => historyMoment.value ?? historyMoment.previousValue,
|
||||||
|
data: dataList,
|
||||||
|
/*domainLowerBoundFn: (CombinedEntityStateHistoryMoment historyMoment, _) => historyMoment.time.subtract(Duration(hours: 1)),
|
||||||
|
domainUpperBoundFn: (CombinedEntityStateHistoryMoment historyMoment, _) => historyMoment.time.add(Duration(hours: 1)),*/
|
||||||
|
)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
result.add(
|
||||||
|
new charts.Series<EntityHistoryMoment, DateTime>(
|
||||||
|
id: 'state',
|
||||||
|
radiusPxFn: (EntityHistoryMoment historyMoment, __) => (historyMoment.id == _selectedId) ? 5.0 : 4.0,
|
||||||
|
colorFn: (EntityHistoryMoment historyMoment, __) => EntityColor.chartHistoryStateColor(historyMoment.state, historyMoment.colorId),
|
||||||
|
domainFn: (EntityHistoryMoment historyMoment, _) => historyMoment.startTime,
|
||||||
|
domainLowerBoundFn: (EntityHistoryMoment historyMoment, _) => historyMoment.startTime,
|
||||||
|
domainUpperBoundFn: (EntityHistoryMoment historyMoment, _) => historyMoment.endTime ?? DateTime.now(),
|
||||||
|
// No measure values are needed for symbol annotations.
|
||||||
|
measureFn: (_, __) => null,
|
||||||
|
data: numericDataLists[numericDataLists.keys.first],
|
||||||
|
)
|
||||||
|
// Configure our custom symbol annotation renderer for this series.
|
||||||
|
..setAttribute(charts.rendererIdKey, 'stateBars')
|
||||||
|
// Optional radius for the annotation shape. If not specified, this will
|
||||||
|
// default to the same radius as the points.
|
||||||
|
//..setAttribute(charts.boundsLineRadiusPxKey, 3.5)
|
||||||
|
);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
void _selectPrev() {
|
||||||
|
if (_selectedId > 0) {
|
||||||
|
setState(() {
|
||||||
|
_selectedId -= 1;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _selectNext() {
|
||||||
|
if (_selectedId < (_parsedHistory.first.data.length - 1)) {
|
||||||
|
setState(() {
|
||||||
|
_selectedId += 1;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _onSelectionChanged(charts.SelectionModel model) {
|
||||||
|
final selectedDatum = model.selectedDatum;
|
||||||
|
|
||||||
|
int selectedId;
|
||||||
|
|
||||||
|
if (selectedDatum.isNotEmpty) {
|
||||||
|
selectedId = selectedDatum.first.datum.id;
|
||||||
|
setState(() {
|
||||||
|
_selectedId = selectedId;
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
setState(() {
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
134
lib/entity_widgets/history_chart/entity_history.dart
Normal file
@ -0,0 +1,134 @@
|
|||||||
|
part of '../../main.dart';
|
||||||
|
|
||||||
|
class EntityHistoryWidgetType {
|
||||||
|
static const int simple = 0;
|
||||||
|
static const int numericState = 1;
|
||||||
|
static const int numericAttributes = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
class EntityHistoryConfig {
|
||||||
|
final int chartType;
|
||||||
|
final List<String> numericAttributesToShow;
|
||||||
|
final bool numericState;
|
||||||
|
|
||||||
|
EntityHistoryConfig({this.chartType, this.numericAttributesToShow, this.numericState: true});
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
class EntityHistoryWidget extends StatefulWidget {
|
||||||
|
|
||||||
|
final EntityHistoryConfig config;
|
||||||
|
|
||||||
|
const EntityHistoryWidget({Key key, @required this.config}) : super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
_EntityHistoryWidgetState createState() {
|
||||||
|
return new _EntityHistoryWidgetState();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _EntityHistoryWidgetState extends State<EntityHistoryWidget> {
|
||||||
|
|
||||||
|
List _history;
|
||||||
|
bool _needToUpdateHistory;
|
||||||
|
DateTime _historyLastUpdated;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_needToUpdateHistory = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void _loadHistory(HomeAssistant ha, String entityId) {
|
||||||
|
DateTime now = DateTime.now();
|
||||||
|
if (_historyLastUpdated != null) {
|
||||||
|
TheLogger.debug("History was updated ${now.difference(_historyLastUpdated).inSeconds} seconds ago");
|
||||||
|
}
|
||||||
|
if (_historyLastUpdated == null || now.difference(_historyLastUpdated).inSeconds > 30) {
|
||||||
|
_historyLastUpdated = now;
|
||||||
|
ha.getHistory(entityId).then((history){
|
||||||
|
setState(() {
|
||||||
|
_history = history.isNotEmpty ? history[0] : [];
|
||||||
|
_needToUpdateHistory = false;
|
||||||
|
});
|
||||||
|
}).catchError((e) {
|
||||||
|
TheLogger.error("Error loading $entityId history: $e");
|
||||||
|
setState(() {
|
||||||
|
_history = [];
|
||||||
|
_needToUpdateHistory = false;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final HomeAssistantModel homeAssistantModel = HomeAssistantModel.of(context);
|
||||||
|
final EntityModel entityModel = EntityModel.of(context);
|
||||||
|
final Entity entity = entityModel.entityWrapper.entity;
|
||||||
|
if (!_needToUpdateHistory) {
|
||||||
|
_needToUpdateHistory = true;
|
||||||
|
} else {
|
||||||
|
_loadHistory(homeAssistantModel.homeAssistant, entity.entityId);
|
||||||
|
}
|
||||||
|
return _buildChart();
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildChart() {
|
||||||
|
List<Widget> children = [];
|
||||||
|
if (_history == null) {
|
||||||
|
children.add(
|
||||||
|
Text("Loading history...")
|
||||||
|
);
|
||||||
|
} else if (_history.isEmpty) {
|
||||||
|
children.add(
|
||||||
|
Text("No history")
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
children.add(
|
||||||
|
_selectChartWidget()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
children.add(Divider());
|
||||||
|
return Padding(
|
||||||
|
padding: EdgeInsets.fromLTRB(0.0, 0.0, 0.0, Sizes.rowPadding),
|
||||||
|
child: Column(
|
||||||
|
children: children,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _selectChartWidget() {
|
||||||
|
switch (widget.config.chartType) {
|
||||||
|
|
||||||
|
case EntityHistoryWidgetType.simple: {
|
||||||
|
return SimpleStateHistoryChartWidget(
|
||||||
|
rawHistory: _history,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
case EntityHistoryWidgetType.numericState: {
|
||||||
|
return NumericStateHistoryChartWidget(
|
||||||
|
rawHistory: _history,
|
||||||
|
config: widget.config,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
case EntityHistoryWidgetType.numericAttributes: {
|
||||||
|
return CombinedHistoryChartWidget(
|
||||||
|
rawHistory: _history,
|
||||||
|
config: widget.config,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
default: {
|
||||||
|
TheLogger.debug(" Simple selected as default");
|
||||||
|
return SimpleStateHistoryChartWidget(
|
||||||
|
rawHistory: _history,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
25
lib/entity_widgets/history_chart/entity_history_moment.dart
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
part of '../../main.dart';
|
||||||
|
|
||||||
|
class EntityHistoryMoment {
|
||||||
|
final DateTime startTime;
|
||||||
|
final DateTime endTime;
|
||||||
|
final double value;
|
||||||
|
final double previousValue;
|
||||||
|
final int id;
|
||||||
|
final int colorId;
|
||||||
|
final String state;
|
||||||
|
final bool hiddenDot;
|
||||||
|
final bool hiddenLine;
|
||||||
|
|
||||||
|
EntityHistoryMoment({
|
||||||
|
this.value,
|
||||||
|
this.previousValue,
|
||||||
|
this.hiddenDot,
|
||||||
|
this.hiddenLine,
|
||||||
|
this.state,
|
||||||
|
@required this.startTime,
|
||||||
|
this.endTime,
|
||||||
|
@required this.id,
|
||||||
|
this.colorId
|
||||||
|
});
|
||||||
|
}
|
86
lib/entity_widgets/history_chart/history_control_widget.dart
Normal file
@ -0,0 +1,86 @@
|
|||||||
|
part of '../../main.dart';
|
||||||
|
|
||||||
|
class HistoryControlWidget extends StatelessWidget {
|
||||||
|
|
||||||
|
final Function onPrevTap;
|
||||||
|
final Function onNextTap;
|
||||||
|
final DateTime selectedTimeStart;
|
||||||
|
final DateTime selectedTimeEnd;
|
||||||
|
final List<String> selectedStates;
|
||||||
|
final List<int> colorIndexes;
|
||||||
|
|
||||||
|
const HistoryControlWidget({Key key, this.onPrevTap, this.onNextTap, this.selectedTimeStart, this.selectedTimeEnd, this.selectedStates, @ required this.colorIndexes}) : super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
if (selectedTimeStart != null) {
|
||||||
|
return
|
||||||
|
Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
|
children: <Widget>[
|
||||||
|
IconButton(
|
||||||
|
icon: Icon(Icons.chevron_left),
|
||||||
|
padding: EdgeInsets.all(0.0),
|
||||||
|
iconSize: 40.0,
|
||||||
|
onPressed: onPrevTap,
|
||||||
|
),
|
||||||
|
Expanded(
|
||||||
|
child: Padding(
|
||||||
|
padding: EdgeInsets.only(right: 10.0),
|
||||||
|
child: _buildStates(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
_buildTime(),
|
||||||
|
IconButton(
|
||||||
|
icon: Icon(Icons.chevron_right),
|
||||||
|
padding: EdgeInsets.all(0.0),
|
||||||
|
iconSize: 40.0,
|
||||||
|
onPressed: onNextTap,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
} else {
|
||||||
|
return Container(height: 48.0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildStates() {
|
||||||
|
List<Widget> children = [];
|
||||||
|
for (int i = 0; i < selectedStates.length; i++) {
|
||||||
|
children.add(
|
||||||
|
Text(
|
||||||
|
"${selectedStates[i] ?? '-'}",
|
||||||
|
textAlign: TextAlign.right,
|
||||||
|
style: TextStyle(
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: EntityColor.historyStateColor(selectedStates[i], colorIndexes[i]),
|
||||||
|
fontSize: 22.0
|
||||||
|
),
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.end,
|
||||||
|
children: children,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildTime() {
|
||||||
|
List<Widget> children = [];
|
||||||
|
children.add(
|
||||||
|
Text("${formatDate(selectedTimeStart, [M, ' ', d, ', ', HH, ':', nn, ':', ss])}", textAlign: TextAlign.left,)
|
||||||
|
);
|
||||||
|
if (selectedTimeEnd != null) {
|
||||||
|
children.add(
|
||||||
|
Text("${formatDate(selectedTimeEnd, [M, ' ', d, ', ', HH, ':', nn, ':', ss])}", textAlign: TextAlign.left,)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: children,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,160 @@
|
|||||||
|
part of '../../main.dart';
|
||||||
|
|
||||||
|
class NumericStateHistoryChartWidget extends StatefulWidget {
|
||||||
|
final rawHistory;
|
||||||
|
final EntityHistoryConfig config;
|
||||||
|
|
||||||
|
const NumericStateHistoryChartWidget({Key key, @required this.rawHistory, @required this.config}) : super(key: key);
|
||||||
|
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<StatefulWidget> createState() {
|
||||||
|
return new _NumericStateHistoryChartWidgetState();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
class _NumericStateHistoryChartWidgetState extends State<NumericStateHistoryChartWidget> {
|
||||||
|
|
||||||
|
int _selectedId = -1;
|
||||||
|
List<charts.Series<EntityHistoryMoment, DateTime>> _parsedHistory;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
_parsedHistory = _parseHistory();
|
||||||
|
DateTime selectedTime;
|
||||||
|
double selectedState;
|
||||||
|
if ((_selectedId > -1) && (_parsedHistory != null) && (_parsedHistory.first.data.length >= (_selectedId + 1))) {
|
||||||
|
selectedTime = _parsedHistory.first.data[_selectedId].startTime;
|
||||||
|
selectedState = _parsedHistory.first.data[_selectedId].value;
|
||||||
|
}
|
||||||
|
return Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.start,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
|
children: <Widget>[
|
||||||
|
HistoryControlWidget(
|
||||||
|
selectedTimeStart: selectedTime,
|
||||||
|
selectedStates: ["${selectedState ?? '-'}"],
|
||||||
|
onPrevTap: () => _selectPrev(),
|
||||||
|
onNextTap: () => _selectNext(),
|
||||||
|
colorIndexes: [-1],
|
||||||
|
),
|
||||||
|
SizedBox(
|
||||||
|
height: 150.0,
|
||||||
|
child: charts.TimeSeriesChart(
|
||||||
|
_parsedHistory,
|
||||||
|
animate: false,
|
||||||
|
primaryMeasureAxis: new charts.NumericAxisSpec(
|
||||||
|
tickProviderSpec:
|
||||||
|
new charts.BasicNumericTickProviderSpec(zeroBound: false)),
|
||||||
|
dateTimeFactory: const charts.LocalDateTimeFactory(),
|
||||||
|
defaultRenderer: charts.LineRendererConfig(
|
||||||
|
includePoints: true
|
||||||
|
),
|
||||||
|
/*primaryMeasureAxis: charts.NumericAxisSpec(
|
||||||
|
renderSpec: charts.NoneRenderSpec()
|
||||||
|
),*/
|
||||||
|
selectionModels: [
|
||||||
|
new charts.SelectionModelConfig(
|
||||||
|
type: charts.SelectionModelType.info,
|
||||||
|
changedListener: (model) => _onSelectionChanged(model),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
List<charts.Series<EntityHistoryMoment, DateTime>> _parseHistory() {
|
||||||
|
List<EntityHistoryMoment> data = [];
|
||||||
|
DateTime now = DateTime.now();
|
||||||
|
for (var i = 0; i < widget.rawHistory.length; i++) {
|
||||||
|
var stateData = widget.rawHistory[i];
|
||||||
|
DateTime time = DateTime.tryParse(stateData["last_updated"])?.toLocal();
|
||||||
|
double value = double.tryParse(stateData["state"]);
|
||||||
|
double previousValue = 0.0;
|
||||||
|
bool hiddenDot = (value == null);
|
||||||
|
bool hiddenLine;
|
||||||
|
if (hiddenDot && i > 0) {
|
||||||
|
previousValue = data[i-1].value ?? data[i-1].previousValue;
|
||||||
|
}
|
||||||
|
if (i < (widget.rawHistory.length - 1)) {
|
||||||
|
double nextValue = double.tryParse(widget.rawHistory[i+1]["state"]);
|
||||||
|
hiddenLine = (nextValue == null || hiddenDot);
|
||||||
|
} else {
|
||||||
|
hiddenLine = hiddenDot;
|
||||||
|
}
|
||||||
|
data.add(EntityHistoryMoment(
|
||||||
|
value: value,
|
||||||
|
previousValue: previousValue,
|
||||||
|
hiddenDot: hiddenDot,
|
||||||
|
hiddenLine: hiddenLine,
|
||||||
|
startTime: time,
|
||||||
|
id: i
|
||||||
|
));
|
||||||
|
}
|
||||||
|
data.add(EntityHistoryMoment(
|
||||||
|
value: data.last.value,
|
||||||
|
previousValue: data.last.previousValue,
|
||||||
|
hiddenDot: data.last.hiddenDot,
|
||||||
|
hiddenLine: data.last.hiddenLine,
|
||||||
|
startTime: now,
|
||||||
|
id: widget.rawHistory.length
|
||||||
|
));
|
||||||
|
if (_selectedId == -1) {
|
||||||
|
_selectedId = 0;
|
||||||
|
}
|
||||||
|
return [
|
||||||
|
new charts.Series<EntityHistoryMoment, DateTime>(
|
||||||
|
id: 'State',
|
||||||
|
colorFn: (EntityHistoryMoment historyMoment, __) => EntityColor.chartHistoryStateColor(EntityState.on, -1),
|
||||||
|
domainFn: (EntityHistoryMoment historyMoment, _) => historyMoment.startTime,
|
||||||
|
measureFn: (EntityHistoryMoment historyMoment, _) => historyMoment.value ?? historyMoment.previousValue,
|
||||||
|
data: data,
|
||||||
|
strokeWidthPxFn: (EntityHistoryMoment historyMoment, __) => historyMoment.hiddenLine ? 0.0 : 2.0,
|
||||||
|
radiusPxFn: (EntityHistoryMoment historyMoment, __) {
|
||||||
|
if (historyMoment.hiddenDot) {
|
||||||
|
return 0.0;
|
||||||
|
} else if (historyMoment.id == _selectedId) {
|
||||||
|
return 5.0;
|
||||||
|
} else {
|
||||||
|
return 1.0;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
void _selectPrev() {
|
||||||
|
if (_selectedId > 0) {
|
||||||
|
setState(() {
|
||||||
|
_selectedId -= 1;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _selectNext() {
|
||||||
|
if (_selectedId < (_parsedHistory.first.data.length - 1)) {
|
||||||
|
setState(() {
|
||||||
|
_selectedId += 1;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _onSelectionChanged(charts.SelectionModel model) {
|
||||||
|
final selectedDatum = model.selectedDatum;
|
||||||
|
|
||||||
|
int selectedId;
|
||||||
|
|
||||||
|
if (selectedDatum.isNotEmpty) {
|
||||||
|
selectedId = selectedDatum.first.datum.id;
|
||||||
|
setState(() {
|
||||||
|
_selectedId = selectedId;
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
setState(() {
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
176
lib/entity_widgets/history_chart/simple_state_history_chart.dart
Normal file
@ -0,0 +1,176 @@
|
|||||||
|
part of '../../main.dart';
|
||||||
|
|
||||||
|
class SimpleStateHistoryChartWidget extends StatefulWidget {
|
||||||
|
final rawHistory;
|
||||||
|
|
||||||
|
const SimpleStateHistoryChartWidget({Key key, this.rawHistory}) : super(key: key);
|
||||||
|
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<StatefulWidget> createState() {
|
||||||
|
return new _SimpleStateHistoryChartWidgetState();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
class _SimpleStateHistoryChartWidgetState extends State<SimpleStateHistoryChartWidget> {
|
||||||
|
|
||||||
|
int _selectedId = -1;
|
||||||
|
List<charts.Series<EntityHistoryMoment, DateTime>> _parsedHistory;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
_parsedHistory = _parseHistory();
|
||||||
|
DateTime selectedTimeStart;
|
||||||
|
DateTime selectedTimeEnd;
|
||||||
|
String selectedState;
|
||||||
|
if ((_selectedId > -1) && (_parsedHistory != null) && (_parsedHistory.first.data.length >= (_selectedId + 1))) {
|
||||||
|
selectedTimeStart = _parsedHistory.first.data[_selectedId].startTime;
|
||||||
|
selectedTimeEnd = _parsedHistory.first.data[_selectedId].endTime;
|
||||||
|
selectedState = _parsedHistory.first.data[_selectedId].state;
|
||||||
|
}
|
||||||
|
return Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.start,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
|
children: <Widget>[
|
||||||
|
HistoryControlWidget(
|
||||||
|
selectedTimeStart: selectedTimeStart,
|
||||||
|
selectedTimeEnd: selectedTimeEnd,
|
||||||
|
selectedStates: [selectedState],
|
||||||
|
onPrevTap: () => _selectPrev(),
|
||||||
|
onNextTap: () => _selectNext(),
|
||||||
|
colorIndexes: [_parsedHistory.first.data[_selectedId].colorId],
|
||||||
|
),
|
||||||
|
SizedBox(
|
||||||
|
height: 70.0,
|
||||||
|
child: charts.TimeSeriesChart(
|
||||||
|
_parsedHistory,
|
||||||
|
animate: false,
|
||||||
|
dateTimeFactory: const charts.LocalDateTimeFactory(),
|
||||||
|
primaryMeasureAxis: charts.NumericAxisSpec(
|
||||||
|
renderSpec: charts.NoneRenderSpec()
|
||||||
|
),
|
||||||
|
selectionModels: [
|
||||||
|
new charts.SelectionModelConfig(
|
||||||
|
type: charts.SelectionModelType.info,
|
||||||
|
changedListener: (model) => _onSelectionChanged(model),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
customSeriesRenderers: [
|
||||||
|
new charts.PointRendererConfig(
|
||||||
|
// ID used to link series to this renderer.
|
||||||
|
customRendererId: 'startValuePoints'),
|
||||||
|
new charts.PointRendererConfig(
|
||||||
|
// ID used to link series to this renderer.
|
||||||
|
customRendererId: 'endValuePoints')
|
||||||
|
],
|
||||||
|
),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
List<charts.Series<EntityHistoryMoment, DateTime>> _parseHistory() {
|
||||||
|
List<EntityHistoryMoment> data = [];
|
||||||
|
DateTime now = DateTime.now();
|
||||||
|
Map<String, int> cachedStates = {};
|
||||||
|
for (var i = 0; i < widget.rawHistory.length; i++) {
|
||||||
|
var stateData = widget.rawHistory[i];
|
||||||
|
DateTime startTime = DateTime.tryParse(stateData["last_updated"])?.toLocal();
|
||||||
|
DateTime endTime;
|
||||||
|
if (i < (widget.rawHistory.length - 1)) {
|
||||||
|
endTime = DateTime.tryParse(widget.rawHistory[i+1]["last_updated"])?.toLocal();
|
||||||
|
} else {
|
||||||
|
endTime = now;
|
||||||
|
}
|
||||||
|
if (cachedStates[stateData["state"]] == null) {
|
||||||
|
cachedStates.addAll({"${stateData["state"]}": cachedStates.length});
|
||||||
|
}
|
||||||
|
data.add(EntityHistoryMoment(
|
||||||
|
state: stateData["state"],
|
||||||
|
startTime: startTime,
|
||||||
|
endTime: endTime,
|
||||||
|
id: i,
|
||||||
|
colorId: cachedStates[stateData["state"]]
|
||||||
|
));
|
||||||
|
}
|
||||||
|
data.add(EntityHistoryMoment(
|
||||||
|
state: data.last.state,
|
||||||
|
startTime: now,
|
||||||
|
id: widget.rawHistory.length,
|
||||||
|
colorId: data.last.colorId
|
||||||
|
));
|
||||||
|
if (_selectedId == -1) {
|
||||||
|
_selectedId = 0;
|
||||||
|
}
|
||||||
|
return [
|
||||||
|
new charts.Series<EntityHistoryMoment, DateTime>(
|
||||||
|
id: 'State',
|
||||||
|
strokeWidthPxFn: (EntityHistoryMoment historyMoment, __) => (historyMoment.id == _selectedId) ? 6.0 : 3.0,
|
||||||
|
colorFn: (EntityHistoryMoment historyMoment, __) => EntityColor.chartHistoryStateColor(historyMoment.state, historyMoment.colorId),
|
||||||
|
domainFn: (EntityHistoryMoment historyMoment, _) => historyMoment.startTime,
|
||||||
|
measureFn: (EntityHistoryMoment historyMoment, _) => 10,
|
||||||
|
data: data,
|
||||||
|
),
|
||||||
|
new charts.Series<EntityHistoryMoment, DateTime>(
|
||||||
|
id: 'State',
|
||||||
|
radiusPxFn: (EntityHistoryMoment historyMoment, __) => (historyMoment.id == _selectedId) ? 5.0 : 3.0,
|
||||||
|
colorFn: (EntityHistoryMoment historyMoment, __) => EntityColor.chartHistoryStateColor(historyMoment.state, historyMoment.colorId),
|
||||||
|
domainFn: (EntityHistoryMoment historyMoment, _) => historyMoment.startTime,
|
||||||
|
measureFn: (EntityHistoryMoment historyMoment, _) => 10,
|
||||||
|
data: data,
|
||||||
|
)..setAttribute(charts.rendererIdKey, 'startValuePoints'),
|
||||||
|
new charts.Series<EntityHistoryMoment, DateTime>(
|
||||||
|
id: 'State',
|
||||||
|
radiusPxFn: (EntityHistoryMoment historyMoment, __) => (historyMoment.id == _selectedId) ? 5.0 : 3.0,
|
||||||
|
colorFn: (EntityHistoryMoment historyMoment, __) => EntityColor.chartHistoryStateColor(historyMoment.state, historyMoment.colorId),
|
||||||
|
domainFn: (EntityHistoryMoment historyMoment, _) => historyMoment.endTime ?? DateTime.now(),
|
||||||
|
measureFn: (EntityHistoryMoment historyMoment, _) => 10,
|
||||||
|
data: data,
|
||||||
|
)..setAttribute(charts.rendererIdKey, 'endValuePoints')
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
void _selectPrev() {
|
||||||
|
if (_selectedId > 0) {
|
||||||
|
setState(() {
|
||||||
|
_selectedId -= 1;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _selectNext() {
|
||||||
|
if (_selectedId < (_parsedHistory.first.data.length - 2)) {
|
||||||
|
setState(() {
|
||||||
|
_selectedId += 1;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _onSelectionChanged(charts.SelectionModel model) {
|
||||||
|
final selectedDatum = model.selectedDatum;
|
||||||
|
|
||||||
|
int selectedId;
|
||||||
|
|
||||||
|
if ((selectedDatum.isNotEmpty) &&(selectedDatum.first.datum.endTime != null)) {
|
||||||
|
selectedId = selectedDatum.first.datum.id;
|
||||||
|
setState(() {
|
||||||
|
_selectedId = selectedId;
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
setState(() {
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
class SimpleEntityStateHistoryMoment {
|
||||||
|
final DateTime startTime;
|
||||||
|
final DateTime endTime;
|
||||||
|
final String state;
|
||||||
|
final int id;
|
||||||
|
final int colorId;
|
||||||
|
|
||||||
|
SimpleEntityStateHistoryMoment(this.state, this.startTime, this.endTime, this.id, this.colorId);
|
||||||
|
}*/
|
42
lib/entity_widgets/model_widgets.dart
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
part of '../main.dart';
|
||||||
|
|
||||||
|
class EntityModel extends InheritedWidget {
|
||||||
|
const EntityModel({
|
||||||
|
Key key,
|
||||||
|
@required this.entityWrapper,
|
||||||
|
@required this.handleTap,
|
||||||
|
@required Widget child,
|
||||||
|
}) : super(key: key, child: child);
|
||||||
|
|
||||||
|
final EntityWrapper entityWrapper;
|
||||||
|
final bool handleTap;
|
||||||
|
|
||||||
|
static EntityModel of(BuildContext context) {
|
||||||
|
return context.inheritFromWidgetOfExactType(EntityModel);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool updateShouldNotify(InheritedWidget oldWidget) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class HomeAssistantModel extends InheritedWidget {
|
||||||
|
|
||||||
|
const HomeAssistantModel({
|
||||||
|
Key key,
|
||||||
|
@required this.homeAssistant,
|
||||||
|
@required Widget child,
|
||||||
|
}) : super(key: key, child: child);
|
||||||
|
|
||||||
|
final HomeAssistant homeAssistant;
|
||||||
|
|
||||||
|
static HomeAssistantModel of(BuildContext context) {
|
||||||
|
return context.inheritFromWidgetOfExactType(HomeAssistantModel);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool updateShouldNotify(InheritedWidget oldWidget) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
27
lib/entity_widgets/state/button_state.dart
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
part of '../../main.dart';
|
||||||
|
|
||||||
|
class ButtonStateWidget extends StatelessWidget {
|
||||||
|
|
||||||
|
void _setNewState(Entity entity) {
|
||||||
|
eventBus.fire(new ServiceCallEvent(entity.domain, "turn_on", entity.entityId, null));
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final entityModel = EntityModel.of(context);
|
||||||
|
return SizedBox(
|
||||||
|
height: 34.0,
|
||||||
|
child: FlatButton(
|
||||||
|
onPressed: (() {
|
||||||
|
_setNewState(entityModel.entityWrapper.entity);
|
||||||
|
}),
|
||||||
|
child: Text(
|
||||||
|
"EXECUTE",
|
||||||
|
textAlign: TextAlign.right,
|
||||||
|
style:
|
||||||
|
new TextStyle(fontSize: Sizes.stateFontSize, color: Colors.blue),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
52
lib/entity_widgets/state/climate_state.dart
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
part of '../../main.dart';
|
||||||
|
|
||||||
|
class ClimateStateWidget extends StatelessWidget {
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final entityModel = EntityModel.of(context);
|
||||||
|
final ClimateEntity entity = entityModel.entityWrapper.entity;
|
||||||
|
String targetTemp = "-";
|
||||||
|
if ((entity.supportTargetTemperature) && (entity.temperature != null)) {
|
||||||
|
targetTemp = "${entity.temperature}";
|
||||||
|
} else if ((entity.supportTargetTemperatureLow) &&
|
||||||
|
(entity.targetLow != null)) {
|
||||||
|
targetTemp = "${entity.targetLow}";
|
||||||
|
if ((entity.supportTargetTemperatureHigh) &&
|
||||||
|
(entity.targetHigh != null)) {
|
||||||
|
targetTemp += " - ${entity.targetHigh}";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Padding(
|
||||||
|
padding: EdgeInsets.fromLTRB(
|
||||||
|
0.0, 0.0, Sizes.rightWidgetPadding, 0.0),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.end,
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: <Widget>[
|
||||||
|
Row(
|
||||||
|
children: <Widget>[
|
||||||
|
Text("${entity.state}",
|
||||||
|
textAlign: TextAlign.right,
|
||||||
|
style: new TextStyle(
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
fontSize: Sizes.stateFontSize,
|
||||||
|
)),
|
||||||
|
Text(" $targetTemp",
|
||||||
|
textAlign: TextAlign.right,
|
||||||
|
style: new TextStyle(
|
||||||
|
fontSize: Sizes.stateFontSize,
|
||||||
|
))
|
||||||
|
],
|
||||||
|
),
|
||||||
|
entity.attributes["current_temperature"] != null ?
|
||||||
|
Text("Currently: ${entity.attributes["current_temperature"]}",
|
||||||
|
textAlign: TextAlign.right,
|
||||||
|
style: new TextStyle(
|
||||||
|
fontSize: Sizes.stateFontSize,
|
||||||
|
color: Colors.black45)
|
||||||
|
) :
|
||||||
|
Container(height: 0.0,)
|
||||||
|
],
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
65
lib/entity_widgets/state/cover_state.dart
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
part of '../../main.dart';
|
||||||
|
|
||||||
|
class CoverStateWidget extends StatelessWidget {
|
||||||
|
void _open(CoverEntity entity) {
|
||||||
|
eventBus.fire(new ServiceCallEvent(
|
||||||
|
entity.domain, "open_cover", entity.entityId, null));
|
||||||
|
}
|
||||||
|
|
||||||
|
void _close(CoverEntity entity) {
|
||||||
|
eventBus.fire(new ServiceCallEvent(
|
||||||
|
entity.domain, "close_cover", entity.entityId, null));
|
||||||
|
}
|
||||||
|
|
||||||
|
void _stop(CoverEntity entity) {
|
||||||
|
eventBus.fire(new ServiceCallEvent(
|
||||||
|
entity.domain, "stop_cover", entity.entityId, null));
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final entityModel = EntityModel.of(context);
|
||||||
|
final CoverEntity entity = entityModel.entityWrapper.entity;
|
||||||
|
List<Widget> buttons = [];
|
||||||
|
if (entity.supportOpen) {
|
||||||
|
buttons.add(IconButton(
|
||||||
|
icon: Icon(
|
||||||
|
MaterialDesignIcons.createIconDataFromIconName("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.createIconDataFromIconName("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.createIconDataFromIconName("mdi:arrow-down"),
|
||||||
|
size: Sizes.iconSize,
|
||||||
|
),
|
||||||
|
onPressed: entity.canBeClosed ? () => _close(entity) : null));
|
||||||
|
} else {
|
||||||
|
buttons.add(Container(
|
||||||
|
width: Sizes.iconSize + 20.0,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
return Row(
|
||||||
|
children: buttons,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
75
lib/entity_widgets/state/date_time_state.dart
Normal file
@ -0,0 +1,75 @@
|
|||||||
|
part of '../../main.dart';
|
||||||
|
|
||||||
|
class DateTimeStateWidget extends StatelessWidget {
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final entityModel = EntityModel.of(context);
|
||||||
|
final DateTimeEntity entity = entityModel.entityWrapper.entity;
|
||||||
|
return Padding(
|
||||||
|
padding: EdgeInsets.fromLTRB(0.0, 0.0, Sizes.rightWidgetPadding, 0.0),
|
||||||
|
child: GestureDetector(
|
||||||
|
child: Text("${entity.formattedState}",
|
||||||
|
textAlign: TextAlign.right,
|
||||||
|
style: new TextStyle(
|
||||||
|
fontSize: Sizes.stateFontSize,
|
||||||
|
)),
|
||||||
|
onTap: () => _handleStateTap(context, entity),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
void _handleStateTap(BuildContext context, DateTimeEntity entity) {
|
||||||
|
if (entity.hasDate) {
|
||||||
|
_showDatePicker(context, entity).then((date) {
|
||||||
|
if (date != null) {
|
||||||
|
if (entity.hasTime) {
|
||||||
|
_showTimePicker(context, entity).then((time) {
|
||||||
|
entity.setNewState({
|
||||||
|
"date": "${formatDate(date, [yyyy, '-', mm, '-', dd])}",
|
||||||
|
"time":
|
||||||
|
"${formatDate(DateTime(1970, 1, 1, time.hour, time.minute), [
|
||||||
|
HH,
|
||||||
|
':',
|
||||||
|
nn
|
||||||
|
])}"
|
||||||
|
});
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
entity.setNewState({
|
||||||
|
"date": "${formatDate(date, [yyyy, '-', mm, '-', dd])}"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else if (entity.hasTime) {
|
||||||
|
_showTimePicker(context, entity).then((time) {
|
||||||
|
if (time != null) {
|
||||||
|
entity.setNewState({
|
||||||
|
"time":
|
||||||
|
"${formatDate(DateTime(1970, 1, 1, time.hour, time.minute), [
|
||||||
|
HH,
|
||||||
|
':',
|
||||||
|
nn
|
||||||
|
])}"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
TheLogger.warning( "${entity.entityId} has no date and no time");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future _showDatePicker(BuildContext context, DateTimeEntity entity) {
|
||||||
|
return showDatePicker(
|
||||||
|
context: context,
|
||||||
|
initialDate: entity.dateTimeState,
|
||||||
|
firstDate: DateTime(1970),
|
||||||
|
lastDate: DateTime(2037) //Unix timestamp will finish at Jan 19, 2038
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future _showTimePicker(BuildContext context, DateTimeEntity entity) {
|
||||||
|
return showTimePicker(
|
||||||
|
context: context,
|
||||||
|
initialTime: TimeOfDay.fromDateTime(entity.dateTimeState));
|
||||||
|
}
|
||||||
|
}
|
32
lib/entity_widgets/state/lock_state.dart
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
part of '../../main.dart';
|
||||||
|
|
||||||
|
class LockStateWidget extends StatelessWidget {
|
||||||
|
|
||||||
|
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;
|
||||||
|
return SizedBox(
|
||||||
|
height: 34.0,
|
||||||
|
child: FlatButton(
|
||||||
|
onPressed: (() {
|
||||||
|
entity.isLocked ? _unlock(entity) : _lock(entity);
|
||||||
|
}),
|
||||||
|
child: Text(
|
||||||
|
entity.isLocked ? "UNLOCK" : "LOCK",
|
||||||
|
textAlign: TextAlign.right,
|
||||||
|
style:
|
||||||
|
new TextStyle(fontSize: Sizes.stateFontSize, color: Colors.blue),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
49
lib/entity_widgets/state/select_state.dart
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
part of '../../main.dart';
|
||||||
|
|
||||||
|
class SelectStateWidget extends StatefulWidget {
|
||||||
|
|
||||||
|
SelectStateWidget({Key key}) : super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
_SelectStateWidgetState createState() => _SelectStateWidgetState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _SelectStateWidgetState extends State<SelectStateWidget> {
|
||||||
|
|
||||||
|
void setNewState(domain, entityId, newValue) {
|
||||||
|
eventBus.fire(new ServiceCallEvent(domain, "select_option", entityId,
|
||||||
|
{"option": "$newValue"}));
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final entityModel = EntityModel.of(context);
|
||||||
|
final SelectEntity entity = entityModel.entityWrapper.entity;
|
||||||
|
Widget ctrl;
|
||||||
|
if (entity.listOptions.isNotEmpty) {
|
||||||
|
ctrl = DropdownButton<String>(
|
||||||
|
value: entity.state,
|
||||||
|
isExpanded: true,
|
||||||
|
items: entity.listOptions.map((String value) {
|
||||||
|
return new DropdownMenuItem<String>(
|
||||||
|
value: value,
|
||||||
|
child: new Text(value),
|
||||||
|
);
|
||||||
|
}).toList(),
|
||||||
|
onChanged: (_) {
|
||||||
|
setNewState(entity.domain, entity.entityId,_);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
ctrl = Text('---');
|
||||||
|
}
|
||||||
|
return Flexible(
|
||||||
|
flex: 2,
|
||||||
|
fit: FlexFit.tight,
|
||||||
|
//width: Entity.INPUT_WIDTH,
|
||||||
|
child: ctrl,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
38
lib/entity_widgets/state/simple_state.dart
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
part of '../../main.dart';
|
||||||
|
|
||||||
|
class SimpleEntityState extends StatelessWidget {
|
||||||
|
|
||||||
|
final bool expanded;
|
||||||
|
final TextAlign textAlign;
|
||||||
|
final EdgeInsetsGeometry padding;
|
||||||
|
final int maxLines;
|
||||||
|
|
||||||
|
const SimpleEntityState({Key key, this.maxLines: 10, this.expanded: true, this.textAlign: TextAlign.right, this.padding: const EdgeInsets.fromLTRB(0.0, 0.0, Sizes.rightWidgetPadding, 0.0)}) : super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final entityModel = EntityModel.of(context);
|
||||||
|
Widget result = Padding(
|
||||||
|
padding: padding,
|
||||||
|
child: Text(
|
||||||
|
"${entityModel.entityWrapper.entity.state} ${entityModel.entityWrapper.entity.unitOfMeasurement}",
|
||||||
|
textAlign: textAlign,
|
||||||
|
maxLines: maxLines,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
softWrap: true,
|
||||||
|
style: new TextStyle(
|
||||||
|
fontSize: Sizes.stateFontSize,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
if (expanded) {
|
||||||
|
return Flexible(
|
||||||
|
fit: FlexFit.tight,
|
||||||
|
flex: 2,
|
||||||
|
child: result,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
89
lib/entity_widgets/state/switch_state.dart
Normal file
@ -0,0 +1,89 @@
|
|||||||
|
part of '../../main.dart';
|
||||||
|
|
||||||
|
class SwitchStateWidget extends StatefulWidget {
|
||||||
|
|
||||||
|
final String domainForService;
|
||||||
|
|
||||||
|
const SwitchStateWidget({Key key, this.domainForService}) : super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
_SwitchStateWidgetState createState() => _SwitchStateWidgetState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _SwitchStateWidgetState extends State<SwitchStateWidget> {
|
||||||
|
|
||||||
|
String newState;
|
||||||
|
bool updatedHere = false;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
}
|
||||||
|
|
||||||
|
void _setNewState(newValue, Entity entity) {
|
||||||
|
setState(() {
|
||||||
|
newState = newValue ? EntityState.on : EntityState.off;
|
||||||
|
updatedHere = true;
|
||||||
|
});
|
||||||
|
Timer(Duration(seconds: 2), (){
|
||||||
|
setState(() {
|
||||||
|
newState = entity.state;
|
||||||
|
updatedHere = true;
|
||||||
|
//TheLogger.debug("Timer@!!");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
String domain;
|
||||||
|
if (widget.domainForService != null) {
|
||||||
|
domain = widget.domainForService;
|
||||||
|
} else {
|
||||||
|
domain = entity.domain;
|
||||||
|
}
|
||||||
|
eventBus.fire(new ServiceCallEvent(
|
||||||
|
domain, (newValue as bool) ? "turn_on" : "turn_off", entity.entityId, null));
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final entityModel = EntityModel.of(context);
|
||||||
|
final entity = entityModel.entityWrapper.entity;
|
||||||
|
if (!updatedHere) {
|
||||||
|
newState = entity.state;
|
||||||
|
} else {
|
||||||
|
updatedHere = false;
|
||||||
|
}
|
||||||
|
if (entity.state == EntityState.unavailable || entity.state == EntityState.unknown) {
|
||||||
|
return SimpleEntityState();
|
||||||
|
} else if ((entity.attributes["assumed_state"] == null) || (entity.attributes["assumed_state"] == false)) {
|
||||||
|
return SizedBox(
|
||||||
|
height: 32.0,
|
||||||
|
child: Switch(
|
||||||
|
value: newState == EntityState.on,
|
||||||
|
onChanged: ((switchState) {
|
||||||
|
_setNewState(switchState, entity);
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return SizedBox(
|
||||||
|
height: 32.0,
|
||||||
|
child: Row(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: <Widget>[
|
||||||
|
IconButton(
|
||||||
|
onPressed: () => _setNewState(false, entity),
|
||||||
|
icon: Icon(MaterialDesignIcons.createIconDataFromIconName("mdi:flash-off")),
|
||||||
|
color: newState == EntityState.on ? Colors.black : Colors.blue,
|
||||||
|
iconSize: Sizes.iconSize,
|
||||||
|
),
|
||||||
|
IconButton(
|
||||||
|
onPressed: () => _setNewState(true, entity),
|
||||||
|
icon: Icon(MaterialDesignIcons.createIconDataFromIconName("mdi:flash")),
|
||||||
|
color: newState == EntityState.on ? Colors.blue : Colors.black,
|
||||||
|
iconSize: Sizes.iconSize
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
100
lib/entity_widgets/state/text_input_state.dart
Normal file
@ -0,0 +1,100 @@
|
|||||||
|
part of '../../main.dart';
|
||||||
|
|
||||||
|
class TextInputStateWidget extends StatefulWidget {
|
||||||
|
|
||||||
|
TextInputStateWidget({Key key}) : super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
_TextInputStateWidgetState createState() => _TextInputStateWidgetState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _TextInputStateWidgetState extends State<TextInputStateWidget> {
|
||||||
|
String _tmpValue;
|
||||||
|
String _entityState;
|
||||||
|
String _entityDomain;
|
||||||
|
String _entityId;
|
||||||
|
int _minLength;
|
||||||
|
int _maxLength;
|
||||||
|
FocusNode _focusNode = FocusNode();
|
||||||
|
bool validValue = false;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_focusNode.addListener(_focusListener);
|
||||||
|
}
|
||||||
|
|
||||||
|
void setNewState(newValue, domain, entityId) {
|
||||||
|
if (validate(newValue, _minLength, _maxLength)) {
|
||||||
|
eventBus.fire(new ServiceCallEvent(domain, "set_value", entityId,
|
||||||
|
{"value": "$newValue"}));
|
||||||
|
} else {
|
||||||
|
setState(() {
|
||||||
|
_tmpValue = _entityState;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool validate(newValue, minLength, maxLength) {
|
||||||
|
if (newValue is String) {
|
||||||
|
validValue = (newValue.length >= minLength) &&
|
||||||
|
(maxLength == -1 ||
|
||||||
|
(newValue.length <= maxLength));
|
||||||
|
} else {
|
||||||
|
validValue = true;
|
||||||
|
}
|
||||||
|
return validValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
void _focusListener() {
|
||||||
|
if (!_focusNode.hasFocus && (_tmpValue != _entityState)) {
|
||||||
|
setNewState(_tmpValue, _entityDomain, _entityId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final entityModel = EntityModel.of(context);
|
||||||
|
final TextEntity entity = entityModel.entityWrapper.entity;
|
||||||
|
_entityState = entity.state;
|
||||||
|
_entityDomain = entity.domain;
|
||||||
|
_entityId = entity.entityId;
|
||||||
|
_minLength = entity.valueMinLength;
|
||||||
|
_maxLength = entity.valueMaxLength;
|
||||||
|
|
||||||
|
if (!_focusNode.hasFocus && (_tmpValue != entity.state)) {
|
||||||
|
_tmpValue = entity.state;
|
||||||
|
}
|
||||||
|
if (entity.isTextField || entity.isPasswordField) {
|
||||||
|
return Flexible(
|
||||||
|
fit: FlexFit.tight,
|
||||||
|
flex: 2,
|
||||||
|
//width: Entity.INPUT_WIDTH,
|
||||||
|
child: TextField(
|
||||||
|
focusNode: _focusNode,
|
||||||
|
obscureText: entity.isPasswordField,
|
||||||
|
controller: new TextEditingController.fromValue(
|
||||||
|
new TextEditingValue(
|
||||||
|
text: _tmpValue,
|
||||||
|
selection:
|
||||||
|
new TextSelection.collapsed(offset: _tmpValue.length)
|
||||||
|
)
|
||||||
|
),
|
||||||
|
onChanged: (value) {
|
||||||
|
_tmpValue = value;
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
TheLogger.warning( "Unsupported input mode for ${entity.entityId}");
|
||||||
|
return SimpleEntityState();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_focusNode.removeListener(_focusListener);
|
||||||
|
_focusNode.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
640
lib/home_assistant.class.dart
Normal file
@ -0,0 +1,640 @@
|
|||||||
|
part of 'main.dart';
|
||||||
|
|
||||||
|
class HomeAssistant {
|
||||||
|
String _webSocketAPIEndpoint;
|
||||||
|
String _password;
|
||||||
|
String _authType;
|
||||||
|
bool _useLovelace = false;
|
||||||
|
|
||||||
|
IOWebSocketChannel _hassioChannel;
|
||||||
|
SendMessageQueue _messageQueue;
|
||||||
|
|
||||||
|
int _currentMessageId = 0;
|
||||||
|
int _statesMessageId = 0;
|
||||||
|
int _servicesMessageId = 0;
|
||||||
|
int _subscriptionMessageId = 0;
|
||||||
|
int _configMessageId = 0;
|
||||||
|
int _userInfoMessageId = 0;
|
||||||
|
int _lovelaceMessageId = 0;
|
||||||
|
EntityCollection entities;
|
||||||
|
HomeAssistantUI ui;
|
||||||
|
Map _instanceConfig = {};
|
||||||
|
String _userName;
|
||||||
|
|
||||||
|
Map _rawLovelaceData;
|
||||||
|
|
||||||
|
Completer _fetchCompleter;
|
||||||
|
Completer _statesCompleter;
|
||||||
|
Completer _servicesCompleter;
|
||||||
|
Completer _lovelaceCompleter;
|
||||||
|
Completer _configCompleter;
|
||||||
|
Completer _connectionCompleter;
|
||||||
|
Completer _userInfoCompleter;
|
||||||
|
Timer _connectionTimer;
|
||||||
|
Timer _fetchTimer;
|
||||||
|
bool autoReconnect = false;
|
||||||
|
|
||||||
|
StreamSubscription _socketSubscription;
|
||||||
|
|
||||||
|
int messageExpirationTime = 30; //seconds
|
||||||
|
Duration fetchTimeout = Duration(seconds: 30);
|
||||||
|
Duration connectTimeout = Duration(seconds: 15);
|
||||||
|
|
||||||
|
String get locationName {
|
||||||
|
if (_useLovelace) {
|
||||||
|
return ui?.title ?? "";
|
||||||
|
} else {
|
||||||
|
return _instanceConfig["location_name"] ?? "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
String get userName => _userName ?? locationName;
|
||||||
|
String get userAvatarText => userName.length > 0 ? userName[0] : "";
|
||||||
|
//int get viewsCount => entities.views.length ?? 0;
|
||||||
|
|
||||||
|
HomeAssistant() {
|
||||||
|
entities = EntityCollection();
|
||||||
|
_messageQueue = SendMessageQueue(messageExpirationTime);
|
||||||
|
}
|
||||||
|
|
||||||
|
void updateSettings(String url, String password, String authType, bool useLovelace) {
|
||||||
|
_webSocketAPIEndpoint = url;
|
||||||
|
_password = password;
|
||||||
|
_authType = authType;
|
||||||
|
_useLovelace = useLovelace;
|
||||||
|
TheLogger.debug( "Use lovelace is $_useLovelace");
|
||||||
|
}
|
||||||
|
|
||||||
|
Future fetch() {
|
||||||
|
if ((_fetchCompleter != null) && (!_fetchCompleter.isCompleted)) {
|
||||||
|
TheLogger.warning("Previous fetch is not complited");
|
||||||
|
} else {
|
||||||
|
_fetchCompleter = new Completer();
|
||||||
|
_fetchTimer = Timer(fetchTimeout, () {
|
||||||
|
TheLogger.error( "Data fetching timeout");
|
||||||
|
disconnect().then((_) {
|
||||||
|
_completeFetching({
|
||||||
|
"errorCode": 9,
|
||||||
|
"errorMessage": "Couldn't get data from server"
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
_connection().then((r) {
|
||||||
|
_getData();
|
||||||
|
}).catchError((e) {
|
||||||
|
_completeFetching(e);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return _fetchCompleter.future;
|
||||||
|
}
|
||||||
|
|
||||||
|
disconnect() async {
|
||||||
|
if ((_hassioChannel != null) && (_hassioChannel.closeCode == null) && (_hassioChannel.sink != null)) {
|
||||||
|
await _hassioChannel.sink.close().timeout(Duration(seconds: 3),
|
||||||
|
onTimeout: () => TheLogger.debug( "Socket sink closed")
|
||||||
|
);
|
||||||
|
await _socketSubscription.cancel();
|
||||||
|
_hassioChannel = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
Future _connection() {
|
||||||
|
if ((_connectionCompleter != null) && (!_connectionCompleter.isCompleted)) {
|
||||||
|
TheLogger.debug("Previous connection is not complited");
|
||||||
|
} else {
|
||||||
|
if ((_hassioChannel == null) || (_hassioChannel.closeCode != null)) {
|
||||||
|
_connectionCompleter = new Completer();
|
||||||
|
autoReconnect = false;
|
||||||
|
disconnect().then((_){
|
||||||
|
TheLogger.debug( "Socket connecting...");
|
||||||
|
_connectionTimer = Timer(connectTimeout, () {
|
||||||
|
TheLogger.error( "Socket connection timeout");
|
||||||
|
_handleSocketError(null);
|
||||||
|
});
|
||||||
|
if (_socketSubscription != null) {
|
||||||
|
_socketSubscription.cancel();
|
||||||
|
}
|
||||||
|
_hassioChannel = IOWebSocketChannel.connect(
|
||||||
|
_webSocketAPIEndpoint, pingInterval: Duration(seconds: 30));
|
||||||
|
_socketSubscription = _hassioChannel.stream.listen(
|
||||||
|
(message) => _handleMessage(message),
|
||||||
|
cancelOnError: true,
|
||||||
|
onDone: () => _handleSocketClose(),
|
||||||
|
onError: (e) => _handleSocketError(e)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
_completeConnecting(null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return _connectionCompleter.future;
|
||||||
|
}
|
||||||
|
|
||||||
|
void _handleSocketClose() {
|
||||||
|
TheLogger.debug("Socket disconnected. Automatic reconnect is $autoReconnect");
|
||||||
|
if (autoReconnect) {
|
||||||
|
_reconnect();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _handleSocketError(e) {
|
||||||
|
TheLogger.error("Socket stream Error: $e");
|
||||||
|
TheLogger.debug("Automatic reconnect is $autoReconnect");
|
||||||
|
if (autoReconnect) {
|
||||||
|
_reconnect();
|
||||||
|
} else {
|
||||||
|
disconnect().then((_) {
|
||||||
|
_completeConnecting({
|
||||||
|
"errorCode": 1,
|
||||||
|
"errorMessage": "Couldn't connect to Home Assistant. Check network connection or connection settings."
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _reconnect() {
|
||||||
|
disconnect().then((_) {
|
||||||
|
_connection().catchError((e){
|
||||||
|
_completeConnecting(e);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
_getData() async {
|
||||||
|
List<Future> futures = [];
|
||||||
|
futures.add(_getStates());
|
||||||
|
if (_useLovelace) {
|
||||||
|
futures.add(_getLovelace());
|
||||||
|
}
|
||||||
|
futures.add(_getConfig());
|
||||||
|
futures.add(_getServices());
|
||||||
|
futures.add(_getUserInfo());
|
||||||
|
try {
|
||||||
|
await Future.wait(futures);
|
||||||
|
_createUI();
|
||||||
|
_completeFetching(null);
|
||||||
|
} catch (error) {
|
||||||
|
_completeFetching(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _completeFetching(error) {
|
||||||
|
_fetchTimer.cancel();
|
||||||
|
_completeConnecting(error);
|
||||||
|
if (!_fetchCompleter.isCompleted) {
|
||||||
|
if (error != null) {
|
||||||
|
_fetchCompleter.completeError(error);
|
||||||
|
} else {
|
||||||
|
autoReconnect = true;
|
||||||
|
TheLogger.debug( "Fetch complete successful");
|
||||||
|
_fetchCompleter.complete();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _completeConnecting(error) {
|
||||||
|
_connectionTimer.cancel();
|
||||||
|
if (!_connectionCompleter.isCompleted) {
|
||||||
|
if (error != null) {
|
||||||
|
_connectionCompleter.completeError(error);
|
||||||
|
} else {
|
||||||
|
_connectionCompleter.complete();
|
||||||
|
}
|
||||||
|
} else if (error != null) {
|
||||||
|
if (error is Error) {
|
||||||
|
eventBus.fire(ShowErrorEvent(error.toString(), 12));
|
||||||
|
} else {
|
||||||
|
eventBus.fire(ShowErrorEvent(error["errorMessage"], error["errorCode"]));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_handleMessage(String message) {
|
||||||
|
var data = json.decode(message);
|
||||||
|
if (data["type"] == "auth_required") {
|
||||||
|
_sendAuthMessageRaw('{"type": "auth","$_authType": "$_password"}');
|
||||||
|
} else if (data["type"] == "auth_ok") {
|
||||||
|
_completeConnecting(null);
|
||||||
|
_sendSubscribe();
|
||||||
|
} else if (data["type"] == "auth_invalid") {
|
||||||
|
_completeConnecting({"errorCode": 6, "errorMessage": "${data["message"]}"});
|
||||||
|
} else if (data["type"] == "result") {
|
||||||
|
TheLogger.debug("[Received] <== id:${data["id"]}, ${data['success'] ? 'success' : 'error'}");
|
||||||
|
if (data["id"] == _configMessageId) {
|
||||||
|
_parseConfig(data);
|
||||||
|
} else if (data["id"] == _statesMessageId) {
|
||||||
|
_parseEntities(data);
|
||||||
|
} else if (data["id"] == _lovelaceMessageId) {
|
||||||
|
_handleLovelace(data);
|
||||||
|
} else if (data["id"] == _servicesMessageId) {
|
||||||
|
_parseServices(data);
|
||||||
|
} else if (data["id"] == _userInfoMessageId) {
|
||||||
|
_parseUserInfo(data);
|
||||||
|
}
|
||||||
|
} else if (data["type"] == "event") {
|
||||||
|
if ((data["event"] != null) && (data["event"]["event_type"] == "state_changed")) {
|
||||||
|
TheLogger.debug("[Received] <== ${data['type']}.${data["event"]["event_type"]}: ${data["event"]["data"]["entity_id"]}");
|
||||||
|
_handleEntityStateChange(data["event"]["data"]);
|
||||||
|
} else if (data["event"] != null) {
|
||||||
|
TheLogger.warning("Unhandled event type: ${data["event"]["event_type"]}");
|
||||||
|
} else {
|
||||||
|
TheLogger.error("Event is null: $message");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
TheLogger.warning("Unknown message type: $message");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _sendSubscribe() {
|
||||||
|
_incrementMessageId();
|
||||||
|
_subscriptionMessageId = _currentMessageId;
|
||||||
|
_sendMessageRaw('{"id": $_subscriptionMessageId, "type": "subscribe_events", "event_type": "state_changed"}', false);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future _getConfig() {
|
||||||
|
_configCompleter = new Completer();
|
||||||
|
_incrementMessageId();
|
||||||
|
_configMessageId = _currentMessageId;
|
||||||
|
_sendMessageRaw('{"id": $_configMessageId, "type": "get_config"}', false);
|
||||||
|
|
||||||
|
return _configCompleter.future;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future _getStates() {
|
||||||
|
_statesCompleter = new Completer();
|
||||||
|
_incrementMessageId();
|
||||||
|
_statesMessageId = _currentMessageId;
|
||||||
|
_sendMessageRaw('{"id": $_statesMessageId, "type": "get_states"}', false);
|
||||||
|
|
||||||
|
return _statesCompleter.future;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future _getLovelace() {
|
||||||
|
_lovelaceCompleter = new Completer();
|
||||||
|
_incrementMessageId();
|
||||||
|
_lovelaceMessageId = _currentMessageId;
|
||||||
|
_sendMessageRaw('{"id": $_lovelaceMessageId, "type": "lovelace/config"}', false);
|
||||||
|
|
||||||
|
return _lovelaceCompleter.future;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future _getUserInfo() {
|
||||||
|
_userInfoCompleter = new Completer();
|
||||||
|
_incrementMessageId();
|
||||||
|
_userInfoMessageId = _currentMessageId;
|
||||||
|
_sendMessageRaw('{"id": $_userInfoMessageId, "type": "auth/current_user"}', false);
|
||||||
|
|
||||||
|
return _userInfoCompleter.future;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future _getServices() {
|
||||||
|
_servicesCompleter = new Completer();
|
||||||
|
_incrementMessageId();
|
||||||
|
_servicesMessageId = _currentMessageId;
|
||||||
|
_sendMessageRaw('{"id": $_servicesMessageId, "type": "get_services"}', false);
|
||||||
|
|
||||||
|
return _servicesCompleter.future;
|
||||||
|
}
|
||||||
|
|
||||||
|
_incrementMessageId() {
|
||||||
|
_currentMessageId += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
void _sendAuthMessageRaw(String message) {
|
||||||
|
TheLogger.debug( "[Sending] ==> auth request");
|
||||||
|
_hassioChannel.sink.add(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
_sendMessageRaw(String message, bool queued) {
|
||||||
|
var sendCompleter = Completer();
|
||||||
|
if (queued) _messageQueue.add(message);
|
||||||
|
_connection().then((r) {
|
||||||
|
_messageQueue.getActualMessages().forEach((message){
|
||||||
|
TheLogger.debug( "[Sending queued] ==> $message");
|
||||||
|
_hassioChannel.sink.add(message);
|
||||||
|
});
|
||||||
|
if (!queued) {
|
||||||
|
TheLogger.debug( "[Sending] ==> $message");
|
||||||
|
_hassioChannel.sink.add(message);
|
||||||
|
}
|
||||||
|
sendCompleter.complete();
|
||||||
|
}).catchError((e){
|
||||||
|
sendCompleter.completeError(e);
|
||||||
|
});
|
||||||
|
return sendCompleter.future;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future callService(String domain, String service, String entityId, Map<String, dynamic> additionalParams) {
|
||||||
|
_incrementMessageId();
|
||||||
|
String message = "";
|
||||||
|
if (entityId != null) {
|
||||||
|
message = '{"id": $_currentMessageId, "type": "call_service", "domain": "$domain", "service": "$service", "service_data": {"entity_id": "$entityId"';
|
||||||
|
if (additionalParams != null) {
|
||||||
|
additionalParams.forEach((name, value) {
|
||||||
|
if ((value is double) || (value is int) || (value is List)) {
|
||||||
|
message += ', "$name" : $value';
|
||||||
|
} else {
|
||||||
|
message += ', "$name" : "$value"';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
message += '}}';
|
||||||
|
} else {
|
||||||
|
message = '{"id": $_currentMessageId, "type": "call_service", "domain": "$domain", "service": "$service"';
|
||||||
|
if (additionalParams != null && additionalParams.isNotEmpty) {
|
||||||
|
message += ', "service_data": {';
|
||||||
|
bool first = true;
|
||||||
|
additionalParams.forEach((name, value) {
|
||||||
|
if (!first) {
|
||||||
|
message += ', ';
|
||||||
|
}
|
||||||
|
if ((value is double) || (value is int) || (value is List)) {
|
||||||
|
message += '"$name" : $value';
|
||||||
|
} else {
|
||||||
|
message += '"$name" : "$value"';
|
||||||
|
}
|
||||||
|
first = false;
|
||||||
|
});
|
||||||
|
|
||||||
|
message += '}';
|
||||||
|
}
|
||||||
|
message += '}';
|
||||||
|
}
|
||||||
|
return _sendMessageRaw(message, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _handleEntityStateChange(Map eventData) {
|
||||||
|
//TheLogger.debug( "New state for ${eventData['entity_id']}");
|
||||||
|
Map data = Map.from(eventData);
|
||||||
|
entities.updateState(data);
|
||||||
|
eventBus.fire(new StateChangedEvent(data["entity_id"], null));
|
||||||
|
}
|
||||||
|
|
||||||
|
void _parseConfig(Map data) {
|
||||||
|
if (data["success"] == true) {
|
||||||
|
_instanceConfig = Map.from(data["result"]);
|
||||||
|
_configCompleter.complete();
|
||||||
|
} else {
|
||||||
|
_configCompleter.completeError({"errorCode": 2, "errorMessage": data["error"]["message"]});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _parseUserInfo(Map data) {
|
||||||
|
if (data["success"] == true) {
|
||||||
|
_userName = data["result"]["name"];
|
||||||
|
} else {
|
||||||
|
_userName = null;
|
||||||
|
TheLogger.warning("There was an error getting current user: $data");
|
||||||
|
}
|
||||||
|
_userInfoCompleter.complete();
|
||||||
|
}
|
||||||
|
|
||||||
|
void _parseServices(response) {
|
||||||
|
_servicesCompleter.complete();
|
||||||
|
}
|
||||||
|
|
||||||
|
void _handleLovelace(response) {
|
||||||
|
if (response["success"] == true) {
|
||||||
|
_rawLovelaceData = response["result"];
|
||||||
|
} else {
|
||||||
|
TheLogger.error("There was an error getting Lovelace config: $response");
|
||||||
|
_rawLovelaceData = null;
|
||||||
|
}
|
||||||
|
_lovelaceCompleter.complete();
|
||||||
|
}
|
||||||
|
|
||||||
|
void _parseLovelace() {
|
||||||
|
TheLogger.debug("--Title: ${_rawLovelaceData["title"]}");
|
||||||
|
ui.title = _rawLovelaceData["title"];
|
||||||
|
int viewCounter = 0;
|
||||||
|
TheLogger.debug("--Views count: ${_rawLovelaceData['views'].length}");
|
||||||
|
_rawLovelaceData["views"].forEach((rawView){
|
||||||
|
TheLogger.debug("----view id: ${rawView['id']}");
|
||||||
|
HAView view = HAView(
|
||||||
|
count: viewCounter,
|
||||||
|
id: "${rawView['id']}",
|
||||||
|
name: rawView['title'],
|
||||||
|
iconName: rawView['icon']
|
||||||
|
);
|
||||||
|
view.cards.addAll(_createLovelaceCards(rawView["cards"] ?? []));
|
||||||
|
ui.views.add(
|
||||||
|
view
|
||||||
|
);
|
||||||
|
viewCounter += 1;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
List<HACard> _createLovelaceCards(List rawCards) {
|
||||||
|
List<HACard> result = [];
|
||||||
|
rawCards.forEach((rawCard){
|
||||||
|
bool isThereCardOptionsInside = rawCard["card"] != null;
|
||||||
|
HACard card = HACard(
|
||||||
|
id: "card",
|
||||||
|
name: isThereCardOptionsInside ? rawCard["card"]["title"] ?? rawCard["card"]["name"] : rawCard["title"] ?? rawCard["name"],
|
||||||
|
type: isThereCardOptionsInside ? rawCard["card"]['type'] : rawCard['type'],
|
||||||
|
columnsCount: isThereCardOptionsInside ? rawCard["card"]['columns'] ?? 4 : rawCard['columns'] ?? 4,
|
||||||
|
showName: isThereCardOptionsInside ? rawCard["card"]['show_name'] ?? true : rawCard['show_name'] ?? true,
|
||||||
|
showState: isThereCardOptionsInside ? rawCard["card"]['show_state'] ?? true : rawCard['show_state'] ?? true,
|
||||||
|
showEmpty: rawCard['show_empty'] ?? true,
|
||||||
|
stateFilter: rawCard['state_filter'] ?? []
|
||||||
|
);
|
||||||
|
if (rawCard["cards"] != null) {
|
||||||
|
card.childCards = _createLovelaceCards(rawCard["cards"]);
|
||||||
|
}
|
||||||
|
rawCard["entities"]?.forEach((rawEntity) {
|
||||||
|
if (rawEntity is String) {
|
||||||
|
if (entities.isExist(rawEntity)) {
|
||||||
|
card.entities.add(EntityWrapper(entity: entities.get(rawEntity)));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (entities.isExist(rawEntity["entity"])) {
|
||||||
|
Entity e = entities.get(rawEntity["entity"]);
|
||||||
|
String tapAction = EntityTapAction.moreInfo;
|
||||||
|
String holdAction = EntityTapAction.none;
|
||||||
|
if (card.type == CardType.glance || card.type == CardType.entityButton) {
|
||||||
|
if (rawEntity["tap_action"] != null) {
|
||||||
|
if (rawEntity["tap_action"] is String) {
|
||||||
|
tapAction = rawEntity["tap_action"];
|
||||||
|
holdAction = rawEntity["hold_action"];
|
||||||
|
} else {
|
||||||
|
tapAction = rawEntity["tap_action"]["action"];
|
||||||
|
holdAction = rawEntity["hold_action"]["action"];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
card.entities.add(
|
||||||
|
EntityWrapper(
|
||||||
|
entity: e,
|
||||||
|
displayName: rawEntity["name"],
|
||||||
|
icon: rawEntity["icon"],
|
||||||
|
tapAction: tapAction,
|
||||||
|
holdAction: holdAction,
|
||||||
|
//tapActionService: rawEntity["service"],
|
||||||
|
//tapActionServiceData: rawEntity["service_data"] ?? {"entity_id": e.entityId}
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (rawCard["entity"] != null) {
|
||||||
|
var en = rawCard["entity"];
|
||||||
|
String tapAction = EntityTapAction.moreInfo;
|
||||||
|
String holdAction = EntityTapAction.none;
|
||||||
|
if (rawCard["tap_action"] != null) {
|
||||||
|
if (rawCard["tap_action"] is String) {
|
||||||
|
tapAction = rawCard["tap_action"];
|
||||||
|
holdAction = rawCard["hold_action"];
|
||||||
|
} else {
|
||||||
|
tapAction = rawCard["tap_action"]["action"];
|
||||||
|
holdAction = rawCard["hold_action"]["action"];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (en is String) {
|
||||||
|
if (entities.isExist(en)) {
|
||||||
|
Entity e = entities.get(en);
|
||||||
|
card.linkedEntityWrapper = EntityWrapper(
|
||||||
|
entity: e,
|
||||||
|
tapAction: tapAction,
|
||||||
|
holdAction: holdAction,
|
||||||
|
//tapActionService: rawCard["service"],
|
||||||
|
//tapActionServiceData: rawCard["service_data"] ?? {"entity_id": e.entityId}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (entities.isExist(en["entity"])) {
|
||||||
|
Entity e = entities.get(en["entity"]);
|
||||||
|
card.linkedEntityWrapper = EntityWrapper(
|
||||||
|
entity: e,
|
||||||
|
icon: en["icon"],
|
||||||
|
displayName: en["name"],
|
||||||
|
tapAction: tapAction,
|
||||||
|
holdAction: holdAction,
|
||||||
|
tapActionService: rawCard["service"],
|
||||||
|
tapActionServiceData: rawCard["service_data"] ?? {"entity_id": e.entityId}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
result.add(card);
|
||||||
|
});
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
void _parseEntities(response) async {
|
||||||
|
if (response["success"] == false) {
|
||||||
|
_statesCompleter.completeError({"errorCode": 3, "errorMessage": response["error"]["message"]});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
entities.parse(response["result"]);
|
||||||
|
_statesCompleter.complete();
|
||||||
|
}
|
||||||
|
|
||||||
|
void _createUI() {
|
||||||
|
ui = HomeAssistantUI();
|
||||||
|
if ((_useLovelace) && (_rawLovelaceData != null)) {
|
||||||
|
TheLogger.debug("Creating Lovelace UI");
|
||||||
|
_parseLovelace();
|
||||||
|
} else {
|
||||||
|
TheLogger.debug("Creating group-based UI");
|
||||||
|
int viewCounter = 0;
|
||||||
|
if (!entities.hasDefaultView) {
|
||||||
|
HAView view = HAView(
|
||||||
|
count: viewCounter,
|
||||||
|
id: "group.default_view",
|
||||||
|
name: "Home",
|
||||||
|
childEntities: entities.filterEntitiesForDefaultView()
|
||||||
|
);
|
||||||
|
ui.views.add(
|
||||||
|
view
|
||||||
|
);
|
||||||
|
viewCounter += 1;
|
||||||
|
}
|
||||||
|
entities.viewEntities.forEach((viewEntity) {
|
||||||
|
HAView view = HAView(
|
||||||
|
count: viewCounter,
|
||||||
|
id: viewEntity.entityId,
|
||||||
|
name: viewEntity.displayName,
|
||||||
|
childEntities: viewEntity.childEntities
|
||||||
|
);
|
||||||
|
view.linkedEntity = viewEntity;
|
||||||
|
ui.views.add(
|
||||||
|
view
|
||||||
|
);
|
||||||
|
viewCounter += 1;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget buildViews(BuildContext context, bool lovelace) {
|
||||||
|
return ui.build(context);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<List> getHistory(String entityId) async {
|
||||||
|
DateTime now = DateTime.now();
|
||||||
|
//String endTime = formatDate(now, [yyyy, '-', mm, '-', dd, 'T', HH, ':', nn, ':', ss, z]);
|
||||||
|
String startTime = formatDate(now.subtract(Duration(hours: 24)), [yyyy, '-', mm, '-', dd, 'T', HH, ':', nn, ':', ss, z]);
|
||||||
|
String url = "$homeAssistantWebHost/api/history/period/$startTime?&filter_entity_id=$entityId";
|
||||||
|
TheLogger.debug("[Sending] ==> $url");
|
||||||
|
http.Response historyResponse;
|
||||||
|
if (_authType == "access_token") {
|
||||||
|
historyResponse = await http.get(url, headers: {
|
||||||
|
"authorization": "Bearer $_password",
|
||||||
|
"Content-Type": "application/json"
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
historyResponse = await http.get(url, headers: {
|
||||||
|
"X-HA-Access": "$_password",
|
||||||
|
"Content-Type": "application/json"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
var history = json.decode(historyResponse.body);
|
||||||
|
if (history is List) {
|
||||||
|
TheLogger.debug( "[Received] <== ${history.first.length} history recors");
|
||||||
|
return history;
|
||||||
|
} else {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class SendMessageQueue {
|
||||||
|
int _messageTimeout;
|
||||||
|
List<HAMessage> _queue = [];
|
||||||
|
|
||||||
|
SendMessageQueue(this._messageTimeout);
|
||||||
|
|
||||||
|
void add(String message) {
|
||||||
|
_queue.add(HAMessage(_messageTimeout, message));
|
||||||
|
}
|
||||||
|
|
||||||
|
List<String> getActualMessages() {
|
||||||
|
_queue.removeWhere((item) => item.isExpired());
|
||||||
|
List<String> result = [];
|
||||||
|
_queue.forEach((haMessage){
|
||||||
|
result.add(haMessage.message);
|
||||||
|
});
|
||||||
|
this.clear();
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
void clear() {
|
||||||
|
_queue.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
class HAMessage {
|
||||||
|
DateTime _timeStamp;
|
||||||
|
int _messageTimeout;
|
||||||
|
String message;
|
||||||
|
|
||||||
|
HAMessage(this._messageTimeout, this.message) {
|
||||||
|
_timeStamp = DateTime.now();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool isExpired() {
|
||||||
|
return DateTime.now().difference(_timeStamp).inSeconds > _messageTimeout;
|
||||||
|
}
|
||||||
|
}
|
@ -10,11 +10,7 @@ class LogViewPage extends StatefulWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class _LogViewPageState extends State<LogViewPage> {
|
class _LogViewPageState extends State<LogViewPage> {
|
||||||
String _hassioDomain = "";
|
String _logData;
|
||||||
String _hassioPort = "8123";
|
|
||||||
String _hassioPassword = "";
|
|
||||||
String _socketProtocol = "wss";
|
|
||||||
String _authType = "access_token";
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
@ -23,7 +19,7 @@ class _LogViewPageState extends State<LogViewPage> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
_loadLog() async {
|
_loadLog() async {
|
||||||
//
|
_logData = TheLogger.getLog();
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -36,12 +32,19 @@ class _LogViewPageState extends State<LogViewPage> {
|
|||||||
// Here we take the value from the MyHomePage object that was created by
|
// Here we take the value from the MyHomePage object that was created by
|
||||||
// the App.build method, and use it to set our appbar title.
|
// the App.build method, and use it to set our appbar title.
|
||||||
title: new Text(widget.title),
|
title: new Text(widget.title),
|
||||||
|
actions: <Widget>[
|
||||||
|
IconButton(
|
||||||
|
icon: Icon(Icons.content_copy),
|
||||||
|
onPressed: () {
|
||||||
|
Clipboard.setData(new ClipboardData(text: _logData));
|
||||||
|
},
|
||||||
|
)
|
||||||
|
],
|
||||||
),
|
),
|
||||||
body: TextField(
|
body: TextField(
|
||||||
maxLines: null,
|
maxLines: null,
|
||||||
|
|
||||||
controller: TextEditingController(
|
controller: TextEditingController(
|
||||||
text: TheLogger.getLog()
|
text: _logData
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
);
|
);
|
979
lib/main.dart
@ -1,362 +1,5 @@
|
|||||||
part of 'main.dart';
|
part of 'main.dart';
|
||||||
|
|
||||||
class StateChangedEvent {
|
|
||||||
String entityId;
|
|
||||||
|
|
||||||
StateChangedEvent(this.entityId);
|
|
||||||
}
|
|
||||||
|
|
||||||
class SettingsChangedEvent {
|
|
||||||
bool reconnect;
|
|
||||||
|
|
||||||
SettingsChangedEvent(this.reconnect);
|
|
||||||
}
|
|
||||||
|
|
||||||
class HassioDataModel {
|
|
||||||
String _hassioAPIEndpoint;
|
|
||||||
String _hassioPassword;
|
|
||||||
String _hassioAuthType;
|
|
||||||
IOWebSocketChannel _hassioChannel;
|
|
||||||
int _currentMessageId = 0;
|
|
||||||
int _statesMessageId = 0;
|
|
||||||
int _servicesMessageId = 0;
|
|
||||||
int _subscriptionMessageId = 0;
|
|
||||||
int _configMessageId = 0;
|
|
||||||
Map _entitiesData = {};
|
|
||||||
Map _servicesData = {};
|
|
||||||
Map _uiStructure = {};
|
|
||||||
Map _instanceConfig = {};
|
|
||||||
Completer _fetchCompleter;
|
|
||||||
Completer _statesCompleter;
|
|
||||||
Completer _servicesCompleter;
|
|
||||||
Completer _configCompleter;
|
|
||||||
Timer _fetchingTimer;
|
|
||||||
List _topBadgeDomains = ["alarm_control_panel", "binary_sensor", "device_tracker", "updater", "sun", "timer", "sensor"];
|
|
||||||
|
|
||||||
Map get entities => _entitiesData;
|
|
||||||
Map get services => _servicesData;
|
|
||||||
Map get uiStructure => _uiStructure;
|
|
||||||
Map get instanceConfig => _instanceConfig;
|
|
||||||
|
|
||||||
HassioDataModel(String url, String password, String authType) {
|
|
||||||
_hassioAPIEndpoint = url;
|
|
||||||
_hassioPassword = password;
|
|
||||||
_hassioAuthType = authType;
|
|
||||||
}
|
|
||||||
|
|
||||||
Future fetch() {
|
|
||||||
if ((_fetchCompleter != null) && (!_fetchCompleter.isCompleted)) {
|
|
||||||
TheLogger.log("Warning","Previous fetch is not complited");
|
|
||||||
} else {
|
|
||||||
//TODO: Fetch timeout timer. Should be removed after #21 fix
|
|
||||||
_fetchingTimer = Timer(Duration(seconds: 15), () {
|
|
||||||
closeConnection();
|
|
||||||
_fetchCompleter.completeError({"errorCode" : 1,"errorMessage": "Connection timeout"});
|
|
||||||
});
|
|
||||||
_fetchCompleter = new Completer();
|
|
||||||
_reConnectSocket().then((r) {
|
|
||||||
_getData();
|
|
||||||
}).catchError((e) {
|
|
||||||
_finishFetching(e);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return _fetchCompleter.future;
|
|
||||||
}
|
|
||||||
|
|
||||||
closeConnection() {
|
|
||||||
if (_hassioChannel?.closeCode == null) {
|
|
||||||
_hassioChannel?.sink?.close();
|
|
||||||
}
|
|
||||||
_hassioChannel = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
Future _reConnectSocket() {
|
|
||||||
var _connectionCompleter = new Completer();
|
|
||||||
if ((_hassioChannel == null) || (_hassioChannel.closeCode != null)) {
|
|
||||||
TheLogger.log("Debug","Socket connecting...");
|
|
||||||
_hassioChannel = IOWebSocketChannel.connect(_hassioAPIEndpoint);
|
|
||||||
_hassioChannel.stream.handleError((e) {
|
|
||||||
TheLogger.log("Error","Unhandled socket error: ${e.toString()}");
|
|
||||||
});
|
|
||||||
_hassioChannel.stream.listen((message) =>
|
|
||||||
_handleMessage(_connectionCompleter, message));
|
|
||||||
} else {
|
|
||||||
_connectionCompleter.complete();
|
|
||||||
}
|
|
||||||
return _connectionCompleter.future;
|
|
||||||
}
|
|
||||||
|
|
||||||
_getData() {
|
|
||||||
_getConfig().then((result) {
|
|
||||||
_getStates().then((result) {
|
|
||||||
_getServices().then((result) {
|
|
||||||
_finishFetching(null);
|
|
||||||
}).catchError((e) {
|
|
||||||
_finishFetching(e);
|
|
||||||
});
|
|
||||||
}).catchError((e) {
|
|
||||||
_finishFetching(e);
|
|
||||||
});
|
|
||||||
}).catchError((e) {
|
|
||||||
_finishFetching(e);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
_finishFetching(error) {
|
|
||||||
_fetchingTimer.cancel();
|
|
||||||
if (error != null) {
|
|
||||||
_fetchCompleter.completeError(error);
|
|
||||||
} else {
|
|
||||||
_fetchCompleter.complete();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
_handleMessage(Completer connectionCompleter, String message) {
|
|
||||||
var data = json.decode(message);
|
|
||||||
TheLogger.log("Debug","[Received] => Message type: ${data['type']}");
|
|
||||||
if (data["type"] == "auth_required") {
|
|
||||||
_sendMessageRaw('{"type": "auth","$_hassioAuthType": "$_hassioPassword"}');
|
|
||||||
} else if (data["type"] == "auth_ok") {
|
|
||||||
_sendSubscribe();
|
|
||||||
connectionCompleter.complete();
|
|
||||||
} else if (data["type"] == "auth_invalid") {
|
|
||||||
connectionCompleter.completeError({"errorCode": 6, "errorMessage": "${data["message"]}"});
|
|
||||||
} else if (data["type"] == "result") {
|
|
||||||
if (data["id"] == _configMessageId) {
|
|
||||||
_parseConfig(data);
|
|
||||||
} else if (data["id"] == _statesMessageId) {
|
|
||||||
_parseEntities(data);
|
|
||||||
} else if (data["id"] == _servicesMessageId) {
|
|
||||||
_parseServices(data);
|
|
||||||
} else if (data["id"] == _currentMessageId) {
|
|
||||||
TheLogger.log("Debug","Request id:$_currentMessageId was successful");
|
|
||||||
}
|
|
||||||
} else if (data["type"] == "event") {
|
|
||||||
if ((data["event"] != null) && (data["event"]["event_type"] == "state_changed")) {
|
|
||||||
_handleEntityStateChange(data["event"]["data"]);
|
|
||||||
} else if (data["event"] != null) {
|
|
||||||
TheLogger.log("Warning","Unhandled event type: ${data["event"]["event_type"]}");
|
|
||||||
} else {
|
|
||||||
TheLogger.log("Error","Event is null: $message");
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
TheLogger.log("Warning","Unknown message type: $message");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void _sendSubscribe() {
|
|
||||||
_incrementMessageId();
|
|
||||||
_subscriptionMessageId = _currentMessageId;
|
|
||||||
_sendMessageRaw('{"id": $_subscriptionMessageId, "type": "subscribe_events", "event_type": "state_changed"}');
|
|
||||||
}
|
|
||||||
|
|
||||||
Future _getConfig() {
|
|
||||||
_configCompleter = new Completer();
|
|
||||||
_incrementMessageId();
|
|
||||||
_configMessageId = _currentMessageId;
|
|
||||||
_sendMessageRaw('{"id": $_configMessageId, "type": "get_config"}');
|
|
||||||
|
|
||||||
return _configCompleter.future;
|
|
||||||
}
|
|
||||||
|
|
||||||
Future _getStates() {
|
|
||||||
_statesCompleter = new Completer();
|
|
||||||
_incrementMessageId();
|
|
||||||
_statesMessageId = _currentMessageId;
|
|
||||||
_sendMessageRaw('{"id": $_statesMessageId, "type": "get_states"}');
|
|
||||||
|
|
||||||
return _statesCompleter.future;
|
|
||||||
}
|
|
||||||
|
|
||||||
Future _getServices() {
|
|
||||||
_servicesCompleter = new Completer();
|
|
||||||
_incrementMessageId();
|
|
||||||
_servicesMessageId = _currentMessageId;
|
|
||||||
_sendMessageRaw('{"id": $_servicesMessageId, "type": "get_services"}');
|
|
||||||
|
|
||||||
return _servicesCompleter.future;
|
|
||||||
}
|
|
||||||
|
|
||||||
_incrementMessageId() {
|
|
||||||
_currentMessageId += 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
_sendMessageRaw(String message) {
|
|
||||||
if (message.indexOf('"type": "auth"') > 0) {
|
|
||||||
TheLogger.log("Debug", "[Sending] ==> auth request");
|
|
||||||
} else {
|
|
||||||
TheLogger.log("Debug", "[Sending] ==> $message");
|
|
||||||
}
|
|
||||||
_hassioChannel.sink.add(message);
|
|
||||||
}
|
|
||||||
|
|
||||||
void _handleEntityStateChange(Map eventData) {
|
|
||||||
TheLogger.log("Debug", "Parsing new state for ${eventData['entity_id']}");
|
|
||||||
if (eventData["new_state"] == null) {
|
|
||||||
TheLogger.log("Error", "No new_state found");
|
|
||||||
} else {
|
|
||||||
var parsedEntityData = _parseEntity(eventData["new_state"]);
|
|
||||||
String entityId = parsedEntityData["entity_id"];
|
|
||||||
if (_entitiesData[entityId] == null) {
|
|
||||||
_entitiesData[entityId] = parsedEntityData;
|
|
||||||
} else {
|
|
||||||
_entitiesData[entityId].addAll(parsedEntityData);
|
|
||||||
}
|
|
||||||
eventBus.fire(new StateChangedEvent(eventData["entity_id"]));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void _parseConfig(Map data) {
|
|
||||||
if (data["success"] == true) {
|
|
||||||
_instanceConfig = Map.from(data["result"]);
|
|
||||||
_configCompleter.complete();
|
|
||||||
} else {
|
|
||||||
_configCompleter.completeError({"errorCode": 2, "errorMessage": data["error"]["message"]});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void _parseServices(response) {
|
|
||||||
if (response["success"] == false) {
|
|
||||||
_servicesCompleter.completeError({"errorCode": 4, "errorMessage": response["error"]["message"]});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
Map data = response["result"];
|
|
||||||
Map result = {};
|
|
||||||
TheLogger.log("Debug","Parsing ${data.length} Home Assistant service domains");
|
|
||||||
data.forEach((domain, services) {
|
|
||||||
result[domain] = Map.from(services);
|
|
||||||
services.forEach((serviceName, serviceData) {
|
|
||||||
if (_entitiesData["$domain.$serviceName"] != null) {
|
|
||||||
result[domain].remove(serviceName);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
_servicesData = result;
|
|
||||||
_servicesCompleter.complete();
|
|
||||||
} catch (e) {
|
|
||||||
//TODO hadle it properly
|
|
||||||
TheLogger.log("Error","Error parsing services. But they are not used :-)");
|
|
||||||
_servicesCompleter.complete();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void _parseEntities(response) async {
|
|
||||||
_entitiesData.clear();
|
|
||||||
_uiStructure.clear();
|
|
||||||
if (response["success"] == false) {
|
|
||||||
_statesCompleter.completeError({"errorCode": 3, "errorMessage": response["error"]["message"]});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
List data = response["result"];
|
|
||||||
TheLogger.log("Debug","Parsing ${data.length} Home Assistant entities");
|
|
||||||
List<String> uiGroups = [];
|
|
||||||
data.forEach((entity) {
|
|
||||||
try {
|
|
||||||
var composedEntity = _parseEntity(entity);
|
|
||||||
|
|
||||||
if (composedEntity["attributes"] != null) {
|
|
||||||
if ((composedEntity["domain"] == "group") &&
|
|
||||||
(composedEntity["attributes"]["view"] == true)) {
|
|
||||||
uiGroups.add(composedEntity["entity_id"]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
_entitiesData[entity["entity_id"]] = composedEntity;
|
|
||||||
} catch (error) {
|
|
||||||
TheLogger.log("Error","Error parsing entity: ${entity['entity_id']}");
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
//Gethering information for UI
|
|
||||||
TheLogger.log("Debug","Gethering views");
|
|
||||||
int viewCounter = 0;
|
|
||||||
uiGroups.forEach((viewId) { //Each view
|
|
||||||
try {
|
|
||||||
Map viewGroupStructure = {};
|
|
||||||
viewCounter += 1;
|
|
||||||
var viewGroup = _entitiesData[viewId];
|
|
||||||
if (viewGroup != null) {
|
|
||||||
viewGroupStructure["groups"] = {};
|
|
||||||
viewGroupStructure["state"] = "on";
|
|
||||||
viewGroupStructure["entity_id"] = viewGroup["entity_id"];
|
|
||||||
viewGroupStructure["badges"] = {"children": []};
|
|
||||||
viewGroupStructure["attributes"] = viewGroup["attributes"] != null ? {
|
|
||||||
"icon": viewGroup["attributes"]["icon"]
|
|
||||||
} : {"icon": "none"};
|
|
||||||
|
|
||||||
|
|
||||||
viewGroup["attributes"]["entity_id"].forEach((
|
|
||||||
entityId) { //Each entity or group in view
|
|
||||||
Map newGroup = {};
|
|
||||||
String domain = _entitiesData[entityId]["domain"];
|
|
||||||
if (domain != "group") {
|
|
||||||
if (_topBadgeDomains.contains(domain)) {
|
|
||||||
viewGroupStructure["badges"]["children"].add(entityId);
|
|
||||||
} else {
|
|
||||||
String autoGroupID = "$domain.$domain$viewCounter";
|
|
||||||
if (viewGroupStructure["groups"]["$autoGroupID"] == null) {
|
|
||||||
newGroup["entity_id"] = "$domain.$domain$viewCounter";
|
|
||||||
newGroup["friendly_name"] = "$domain";
|
|
||||||
newGroup["children"] = [];
|
|
||||||
newGroup["children"].add(entityId);
|
|
||||||
viewGroupStructure["groups"]["$autoGroupID"] =
|
|
||||||
Map.from(newGroup);
|
|
||||||
} else {
|
|
||||||
viewGroupStructure["groups"]["$autoGroupID"]["children"].add(
|
|
||||||
entityId);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
newGroup["entity_id"] = entityId;
|
|
||||||
newGroup["friendly_name"] =
|
|
||||||
(_entitiesData[entityId]['attributes'] != null)
|
|
||||||
? (_entitiesData[entityId]['attributes']['friendly_name'] ??
|
|
||||||
"")
|
|
||||||
: "";
|
|
||||||
newGroup["children"] = List<String>();
|
|
||||||
_entitiesData[entityId]["attributes"]["entity_id"].forEach((
|
|
||||||
groupedEntityId) {
|
|
||||||
newGroup["children"].add(groupedEntityId);
|
|
||||||
});
|
|
||||||
viewGroupStructure["groups"]["$entityId"] = Map.from(newGroup);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
_uiStructure[viewId.split(".")[1]] = viewGroupStructure;
|
|
||||||
} catch (error) {
|
|
||||||
TheLogger.log("Error","Error parsing view: $viewId");
|
|
||||||
}
|
|
||||||
});
|
|
||||||
_statesCompleter.complete();
|
|
||||||
}
|
|
||||||
|
|
||||||
Map _parseEntity(rawData) {
|
|
||||||
var composedEntity = Map.from(rawData);
|
|
||||||
String entityDomain = rawData["entity_id"].split(".")[0];
|
|
||||||
composedEntity["display_name"] = "${rawData["attributes"]!=null ? rawData["attributes"]["friendly_name"] ?? rawData["attributes"]["name"] : "_"}";
|
|
||||||
composedEntity["domain"] = entityDomain;
|
|
||||||
return composedEntity;
|
|
||||||
}
|
|
||||||
|
|
||||||
Future callService(String domain, String service, String entity_id) {
|
|
||||||
var sendCompleter = Completer();
|
|
||||||
//TODO: Send service call timeout timer. Should be removed after #21 fix
|
|
||||||
Timer _sendTimer = Timer(Duration(seconds: 7), () {
|
|
||||||
sendCompleter.completeError({"errorCode" : 8,"errorMessage": "Connection timeout"});
|
|
||||||
});
|
|
||||||
_reConnectSocket().then((r) {
|
|
||||||
_incrementMessageId();
|
|
||||||
_sendMessageRaw('{"id": $_currentMessageId, "type": "call_service", "domain": "$domain", "service": "$service", "service_data": {"entity_id": "$entity_id"}}');
|
|
||||||
_sendTimer.cancel();
|
|
||||||
sendCompleter.complete();
|
|
||||||
}).catchError((e){
|
|
||||||
_sendTimer.cancel();
|
|
||||||
sendCompleter.completeError(e);
|
|
||||||
});
|
|
||||||
return sendCompleter.future;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class MaterialDesignIcons {
|
class MaterialDesignIcons {
|
||||||
static Map _defaultIconsByDomains = {
|
static Map _defaultIconsByDomains = {
|
||||||
"light": "mdi:lightbulb",
|
"light": "mdi:lightbulb",
|
||||||
@ -371,7 +14,17 @@ class MaterialDesignIcons {
|
|||||||
"input_number": "mdi:ray-vertex",
|
"input_number": "mdi:ray-vertex",
|
||||||
"input_select": "mdi:format-list-bulleted",
|
"input_select": "mdi:format-list-bulleted",
|
||||||
"input_text": "mdi:textbox",
|
"input_text": "mdi:textbox",
|
||||||
"sun": "mdi:white-balance-sunny"
|
"sun": "mdi:white-balance-sunny",
|
||||||
|
"scene": "mdi:google-pages",
|
||||||
|
"media_player": "mdi:cast",
|
||||||
|
"climate": "mdi:thermostat",
|
||||||
|
"cover.open": "mdi:window-open",
|
||||||
|
"cover.closed": "mdi:window-closed",
|
||||||
|
"cover.closing": "mdi:window-open",
|
||||||
|
"cover.opening": "mdi:window-open",
|
||||||
|
"lock.locked": "mdi:lock",
|
||||||
|
"lock.unlocked": "mdi:lock-open",
|
||||||
|
"fan": "mdi:fan"
|
||||||
};
|
};
|
||||||
|
|
||||||
static Map _defaultIconsByDeviceClass = {
|
static Map _defaultIconsByDeviceClass = {
|
||||||
@ -423,7 +76,14 @@ class MaterialDesignIcons {
|
|||||||
//"sensor.illuminance": "mdi:",
|
//"sensor.illuminance": "mdi:",
|
||||||
"sensor.temperature": "mdi:thermometer",
|
"sensor.temperature": "mdi:thermometer",
|
||||||
//"cover.window": "mdi:",
|
//"cover.window": "mdi:",
|
||||||
//"cover.garage": "mdi:",
|
"cover.garage.closed": "mdi:garage",
|
||||||
|
"cover.garage.open": "mdi:garage-open",
|
||||||
|
"cover.garage.opening": "mdi:garage-open",
|
||||||
|
"cover.garage.closing": "mdi:garage-open",
|
||||||
|
"cover.window.open": "mdi:window-open",
|
||||||
|
"cover.window.closed": "mdi:window-closed",
|
||||||
|
"cover.window.closing": "mdi:window-open",
|
||||||
|
"cover.window.opening": "mdi:window-open",
|
||||||
};
|
};
|
||||||
static Map _iconsDataMap = {
|
static Map _iconsDataMap = {
|
||||||
"mdi:access-point": 0xf002,
|
"mdi:access-point": 0xf002,
|
||||||
@ -3223,35 +2883,35 @@ class MaterialDesignIcons {
|
|||||||
"mdi:blank": 0xf68c
|
"mdi:blank": 0xf68c
|
||||||
};
|
};
|
||||||
|
|
||||||
static Widget createIconFromEntityData(Map data, double size, Color color) {
|
static Widget createIconWidgetFromEntityData(EntityWrapper data, double size, Color color) {
|
||||||
if ((data["attributes"] != null) && (data["attributes"]["entity_picture"] != null)) {
|
if (data == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (data.entity.entityPicture != null) {
|
||||||
if (homeAssistantWebHost != null) {
|
if (homeAssistantWebHost != null) {
|
||||||
return CircleAvatar(
|
return CircleAvatar(
|
||||||
|
radius: size/2,
|
||||||
backgroundColor: Colors.white,
|
backgroundColor: Colors.white,
|
||||||
backgroundImage: CachedNetworkImageProvider(
|
backgroundImage: CachedNetworkImageProvider(
|
||||||
"$homeAssistantWebHost${data["attributes"]["entity_picture"]}",
|
"$homeAssistantWebHost${data.entity.entityPicture}",
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
return Container(width: 0.0, height: 0.0);
|
return Container(width: 0.0, height: 0.0);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
String iconName = data["attributes"] != null
|
String iconName = data.icon;
|
||||||
? data["attributes"]["icon"]
|
|
||||||
: null;
|
|
||||||
int iconCode = 0;
|
int iconCode = 0;
|
||||||
if (iconName != null) {
|
if (iconName.length > 0) {
|
||||||
iconCode = getIconCodeByIconName(iconName);
|
iconCode = getIconCodeByIconName(iconName);
|
||||||
} else {
|
} else {
|
||||||
iconCode = getDefaultIconByEntityId(data["entity_id"],
|
iconCode = getDefaultIconByEntityId(data.entity.entityId,
|
||||||
data["attributes"] != null
|
data.entity.deviceClass, data.entity.state); //
|
||||||
? data["attributes"]["device_class"]
|
|
||||||
: null, data["state"]); //
|
|
||||||
}
|
}
|
||||||
return Icon(
|
return Icon(
|
||||||
IconData(iconCode, fontFamily: 'Material Design Icons'),
|
IconData(iconCode, fontFamily: 'Material Design Icons'),
|
||||||
size: size,
|
size: size,
|
||||||
color: color,
|
color: color,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -3270,7 +2930,7 @@ class MaterialDesignIcons {
|
|||||||
|
|
||||||
static int getDefaultIconByEntityId(String entityId, String deviceClass, String state) {
|
static int getDefaultIconByEntityId(String entityId, String deviceClass, String state) {
|
||||||
String domain = entityId.split(".")[0];
|
String domain = entityId.split(".")[0];
|
||||||
String iconNameByDomain = _defaultIconsByDomains[domain];
|
String iconNameByDomain = _defaultIconsByDomains["$domain.$state"] ?? _defaultIconsByDomains["$domain"];
|
||||||
String iconNameByDeviceClass;
|
String iconNameByDeviceClass;
|
||||||
if (deviceClass != null) {
|
if (deviceClass != null) {
|
||||||
iconNameByDeviceClass = _defaultIconsByDeviceClass["$domain.$deviceClass.$state"] ?? _defaultIconsByDeviceClass["$domain.$deviceClass"];
|
iconNameByDeviceClass = _defaultIconsByDeviceClass["$domain.$deviceClass.$state"] ?? _defaultIconsByDeviceClass["$domain.$deviceClass"];
|
220
lib/settings.page.dart
Normal file
@ -0,0 +1,220 @@
|
|||||||
|
part of 'main.dart';
|
||||||
|
|
||||||
|
class ConnectionSettingsPage extends StatefulWidget {
|
||||||
|
ConnectionSettingsPage({Key key, this.title}) : super(key: key);
|
||||||
|
|
||||||
|
final String title;
|
||||||
|
|
||||||
|
@override
|
||||||
|
_ConnectionSettingsPageState createState() => new _ConnectionSettingsPageState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _ConnectionSettingsPageState extends State<ConnectionSettingsPage> {
|
||||||
|
String _hassioDomain = "";
|
||||||
|
String _newHassioDomain = "";
|
||||||
|
String _hassioPort = "";
|
||||||
|
String _newHassioPort = "";
|
||||||
|
String _hassioPassword = "";
|
||||||
|
String _newHassioPassword = "";
|
||||||
|
String _socketProtocol = "wss";
|
||||||
|
String _newSocketProtocol = "wss";
|
||||||
|
String _authType = "access_token";
|
||||||
|
String _newAuthType = "access_token";
|
||||||
|
bool _useLovelace = false;
|
||||||
|
bool _newUseLovelace = false;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_loadSettings();
|
||||||
|
}
|
||||||
|
|
||||||
|
_loadSettings() async {
|
||||||
|
SharedPreferences prefs = await SharedPreferences.getInstance();
|
||||||
|
|
||||||
|
setState(() {
|
||||||
|
_hassioDomain = _newHassioDomain = prefs.getString("hassio-domain")?? "";
|
||||||
|
_hassioPort = _newHassioPort = prefs.getString("hassio-port") ?? "";
|
||||||
|
_hassioPassword = _newHassioPassword = prefs.getString("hassio-password") ?? "";
|
||||||
|
_socketProtocol = _newSocketProtocol = prefs.getString("hassio-protocol") ?? 'wss';
|
||||||
|
_authType = _newAuthType = prefs.getString("hassio-auth-type") ?? 'access_token';
|
||||||
|
try {
|
||||||
|
_useLovelace = _newUseLovelace = prefs.getBool("use-lovelace") ?? false;
|
||||||
|
} catch (e) {
|
||||||
|
_useLovelace = _newUseLovelace = false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
bool _checkConfigChanged() {
|
||||||
|
return ((_newHassioPassword != _hassioPassword) ||
|
||||||
|
(_newHassioPort != _hassioPort) ||
|
||||||
|
(_newHassioDomain != _hassioDomain) ||
|
||||||
|
(_newSocketProtocol != _socketProtocol) ||
|
||||||
|
(_newAuthType != _authType) ||
|
||||||
|
(_newUseLovelace != _useLovelace));
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
_saveSettings() async {
|
||||||
|
if (_newHassioDomain.indexOf("http") == 0 && _newHassioDomain.indexOf("//") > 0) {
|
||||||
|
_newHassioDomain = _newHassioDomain.split("//")[1];
|
||||||
|
}
|
||||||
|
SharedPreferences prefs = await SharedPreferences.getInstance();
|
||||||
|
prefs.setString("hassio-domain", _newHassioDomain);
|
||||||
|
prefs.setString("hassio-port", _newHassioPort);
|
||||||
|
prefs.setString("hassio-password", _newHassioPassword);
|
||||||
|
prefs.setString("hassio-protocol", _newSocketProtocol);
|
||||||
|
prefs.setString("hassio-res-protocol", _newSocketProtocol == "wss" ? "https" : "http");
|
||||||
|
prefs.setString("hassio-auth-type", _newAuthType);
|
||||||
|
prefs.setBool("use-lovelace", _newUseLovelace);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return new Scaffold(
|
||||||
|
appBar: new AppBar(
|
||||||
|
leading: IconButton(icon: Icon(Icons.arrow_back), onPressed: (){
|
||||||
|
Navigator.pop(context);
|
||||||
|
}),
|
||||||
|
title: new Text(widget.title),
|
||||||
|
actions: <Widget>[
|
||||||
|
IconButton(
|
||||||
|
icon: Icon(Icons.check),
|
||||||
|
onPressed: (){
|
||||||
|
if (_checkConfigChanged()) {
|
||||||
|
TheLogger.debug("Settings changed. Saving...");
|
||||||
|
_saveSettings().then((r) {
|
||||||
|
Navigator.pop(context);
|
||||||
|
eventBus.fire(SettingsChangedEvent(true));
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
TheLogger.debug("Settings was not changed");
|
||||||
|
Navigator.pop(context);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
body: ListView(
|
||||||
|
scrollDirection: Axis.vertical,
|
||||||
|
padding: const EdgeInsets.all(20.0),
|
||||||
|
children: <Widget>[
|
||||||
|
Text(
|
||||||
|
"Connection settings",
|
||||||
|
style: TextStyle(
|
||||||
|
color: Colors.black45,
|
||||||
|
fontSize: 20.0
|
||||||
|
),
|
||||||
|
),
|
||||||
|
new Row(
|
||||||
|
children: [
|
||||||
|
Text("Use ssl (HTTPS)"),
|
||||||
|
Switch(
|
||||||
|
value: (_newSocketProtocol == "wss"),
|
||||||
|
onChanged: (value) {
|
||||||
|
setState(() {
|
||||||
|
_newSocketProtocol = value ? "wss" : "ws";
|
||||||
|
});
|
||||||
|
},
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
new TextField(
|
||||||
|
decoration: InputDecoration(
|
||||||
|
labelText: "Home Assistant domain or ip address"
|
||||||
|
),
|
||||||
|
controller: new TextEditingController.fromValue(
|
||||||
|
new TextEditingValue(
|
||||||
|
text: _newHassioDomain,
|
||||||
|
selection:
|
||||||
|
new TextSelection.collapsed(offset: _newHassioDomain.length)
|
||||||
|
)
|
||||||
|
),
|
||||||
|
onChanged: (value) {
|
||||||
|
_newHassioDomain = value;
|
||||||
|
}
|
||||||
|
),
|
||||||
|
new TextField(
|
||||||
|
decoration: InputDecoration(
|
||||||
|
labelText: "Home Assistant port (default is 8123)"
|
||||||
|
),
|
||||||
|
controller: new TextEditingController.fromValue(
|
||||||
|
new TextEditingValue(
|
||||||
|
text: _newHassioPort,
|
||||||
|
selection:
|
||||||
|
new TextSelection.collapsed(offset: _newHassioPort.length)
|
||||||
|
)
|
||||||
|
),
|
||||||
|
onChanged: (value) {
|
||||||
|
_newHassioPort = value;
|
||||||
|
}
|
||||||
|
),
|
||||||
|
new Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
Flexible(
|
||||||
|
child: Text(
|
||||||
|
"Login with access token (HA >= 0.78.0)",
|
||||||
|
softWrap: true,
|
||||||
|
maxLines: 2,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Switch(
|
||||||
|
value: (_newAuthType == "access_token"),
|
||||||
|
onChanged: (value) {
|
||||||
|
setState(() {
|
||||||
|
_newAuthType = value ? "access_token" : "api_password";
|
||||||
|
});
|
||||||
|
},
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
new TextField(
|
||||||
|
decoration: InputDecoration(
|
||||||
|
labelText: _newAuthType == "access_token" ? "Access token" : "API password"
|
||||||
|
),
|
||||||
|
controller: new TextEditingController.fromValue(
|
||||||
|
new TextEditingValue(
|
||||||
|
text: _newHassioPassword,
|
||||||
|
selection:
|
||||||
|
new TextSelection.collapsed(offset: _newHassioPassword.length)
|
||||||
|
)
|
||||||
|
),
|
||||||
|
onChanged: (value) {
|
||||||
|
_newHassioPassword = value;
|
||||||
|
}
|
||||||
|
),
|
||||||
|
Padding(
|
||||||
|
padding: EdgeInsets.only(top: 20.0),
|
||||||
|
child: Text(
|
||||||
|
"UI",
|
||||||
|
style: TextStyle(
|
||||||
|
color: Colors.black45,
|
||||||
|
fontSize: 20.0
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
new Row(
|
||||||
|
children: [
|
||||||
|
Text("Use Lovelace UI"),
|
||||||
|
Switch(
|
||||||
|
value: _newUseLovelace,
|
||||||
|
onChanged: (value) {
|
||||||
|
setState(() {
|
||||||
|
_newUseLovelace = value;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
}
|
@ -1,135 +0,0 @@
|
|||||||
part of 'main.dart';
|
|
||||||
|
|
||||||
class ConnectionSettingsPage extends StatefulWidget {
|
|
||||||
ConnectionSettingsPage({Key key, this.title}) : super(key: key);
|
|
||||||
|
|
||||||
final String title;
|
|
||||||
|
|
||||||
@override
|
|
||||||
_ConnectionSettingsPageState createState() => new _ConnectionSettingsPageState();
|
|
||||||
}
|
|
||||||
|
|
||||||
class _ConnectionSettingsPageState extends State<ConnectionSettingsPage> {
|
|
||||||
String _hassioDomain = "";
|
|
||||||
String _hassioPort = "8123";
|
|
||||||
String _hassioPassword = "";
|
|
||||||
String _socketProtocol = "wss";
|
|
||||||
String _authType = "access_token";
|
|
||||||
|
|
||||||
@override
|
|
||||||
void initState() {
|
|
||||||
super.initState();
|
|
||||||
_loadSettings();
|
|
||||||
}
|
|
||||||
|
|
||||||
_loadSettings() async {
|
|
||||||
SharedPreferences prefs = await SharedPreferences.getInstance();
|
|
||||||
|
|
||||||
setState(() {
|
|
||||||
_hassioDomain = prefs.getString("hassio-domain");
|
|
||||||
_hassioPort = prefs.getString("hassio-port") ?? '8123';
|
|
||||||
_hassioPassword = prefs.getString("hassio-password");
|
|
||||||
_socketProtocol = prefs.getString("hassio-protocol") ?? 'wss';
|
|
||||||
_authType = prefs.getString("hassio-auth-type") ?? 'access_token';
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
_saveSettings() async {
|
|
||||||
if (_hassioDomain.indexOf("http") == 0 && _hassioDomain.indexOf("//") > 0) {
|
|
||||||
_hassioDomain = _hassioDomain.split("//")[1];
|
|
||||||
}
|
|
||||||
SharedPreferences prefs = await SharedPreferences.getInstance();
|
|
||||||
prefs.setString("hassio-domain", _hassioDomain);
|
|
||||||
prefs.setString("hassio-port", _hassioPort);
|
|
||||||
prefs.setString("hassio-password", _hassioPassword);
|
|
||||||
prefs.setString("hassio-protocol", _socketProtocol);
|
|
||||||
prefs.setString("hassio-res-protocol", _socketProtocol == "wss" ? "https" : "http");
|
|
||||||
prefs.setString("hassio-auth-type", _authType);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return new Scaffold(
|
|
||||||
appBar: new AppBar(
|
|
||||||
leading: IconButton(icon: Icon(Icons.arrow_back), onPressed: (){
|
|
||||||
_saveSettings().then((r){
|
|
||||||
Navigator.pop(context);
|
|
||||||
});
|
|
||||||
eventBus.fire(SettingsChangedEvent(true));
|
|
||||||
}),
|
|
||||||
// Here we take the value from the MyHomePage object that was created by
|
|
||||||
// the App.build method, and use it to set our appbar title.
|
|
||||||
title: new Text(widget.title),
|
|
||||||
),
|
|
||||||
body: ListView(
|
|
||||||
padding: const EdgeInsets.all(20.0),
|
|
||||||
children: <Widget>[
|
|
||||||
new Row(
|
|
||||||
children: [
|
|
||||||
Text("HTTPS"),
|
|
||||||
Switch(
|
|
||||||
value: (_socketProtocol == "wss"),
|
|
||||||
onChanged: (value) {
|
|
||||||
setState(() {
|
|
||||||
_socketProtocol = value ? "wss" : "ws";
|
|
||||||
});
|
|
||||||
_saveSettings();
|
|
||||||
},
|
|
||||||
)
|
|
||||||
],
|
|
||||||
),
|
|
||||||
new TextField(
|
|
||||||
decoration: InputDecoration(
|
|
||||||
labelText: "Home Assistant domain or ip address"
|
|
||||||
),
|
|
||||||
controller: TextEditingController(
|
|
||||||
text: _hassioDomain
|
|
||||||
),
|
|
||||||
onChanged: (value) {
|
|
||||||
_hassioDomain = value;
|
|
||||||
_saveSettings();
|
|
||||||
},
|
|
||||||
),
|
|
||||||
new TextField(
|
|
||||||
decoration: InputDecoration(
|
|
||||||
labelText: "Home Assistant port"
|
|
||||||
),
|
|
||||||
controller: TextEditingController(
|
|
||||||
text: _hassioPort
|
|
||||||
),
|
|
||||||
onChanged: (value) {
|
|
||||||
_hassioPort = value;
|
|
||||||
_saveSettings();
|
|
||||||
},
|
|
||||||
),
|
|
||||||
new Row(
|
|
||||||
children: [
|
|
||||||
Text("Login with access token (HA >= 0.78.0)"),
|
|
||||||
Switch(
|
|
||||||
value: (_authType == "access_token"),
|
|
||||||
onChanged: (value) {
|
|
||||||
setState(() {
|
|
||||||
_authType = value ? "access_token" : "api_password";
|
|
||||||
});
|
|
||||||
_saveSettings();
|
|
||||||
},
|
|
||||||
)
|
|
||||||
],
|
|
||||||
),
|
|
||||||
new TextField(
|
|
||||||
decoration: InputDecoration(
|
|
||||||
labelText: _authType == "access_token" ? "Access token" : "API password"
|
|
||||||
),
|
|
||||||
controller: TextEditingController(
|
|
||||||
text: _hassioPassword
|
|
||||||
),
|
|
||||||
onChanged: (value) {
|
|
||||||
_hassioPassword = value;
|
|
||||||
_saveSettings();
|
|
||||||
},
|
|
||||||
)
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
46
lib/ui_class/card.class.dart
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
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;
|
||||||
|
|
||||||
|
HACard({
|
||||||
|
this.name,
|
||||||
|
this.id,
|
||||||
|
this.linkedEntityWrapper,
|
||||||
|
this.columnsCount: 4,
|
||||||
|
this.showName: true,
|
||||||
|
this.showState: true,
|
||||||
|
this.stateFilter: const [],
|
||||||
|
this.showEmpty: true,
|
||||||
|
@required this.type
|
||||||
|
});
|
||||||
|
|
||||||
|
List<EntityWrapper> getEntitiesToShow() {
|
||||||
|
return entities.where((entityWrapper) {
|
||||||
|
if (entityWrapper.entity.isHidden) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (stateFilter.isNotEmpty) {
|
||||||
|
return stateFilter.contains(entityWrapper.entity.state);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}).toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return CardWidget(
|
||||||
|
card: this,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
16
lib/ui_class/sizes_class.dart
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
part of '../main.dart';
|
||||||
|
|
||||||
|
class Sizes {
|
||||||
|
static const rightWidgetPadding = 14.0;
|
||||||
|
static const leftWidgetPadding = 8.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;
|
||||||
|
}
|
27
lib/ui_class/ui.dart
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
part of '../main.dart';
|
||||||
|
|
||||||
|
class HomeAssistantUI {
|
||||||
|
List<HAView> views;
|
||||||
|
String title;
|
||||||
|
|
||||||
|
HomeAssistantUI() {
|
||||||
|
views = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return TabBarView(
|
||||||
|
children: _buildViews(context)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
List<Widget> _buildViews(BuildContext context) {
|
||||||
|
List<Widget> result = [];
|
||||||
|
views.forEach((view) {
|
||||||
|
result.add(
|
||||||
|
view.build(context)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
114
lib/ui_class/view.class.dart
Normal file
@ -0,0 +1,114 @@
|
|||||||
|
part of '../main.dart';
|
||||||
|
|
||||||
|
class HAView {
|
||||||
|
List<HACard> cards = [];
|
||||||
|
List<Entity> badges = [];
|
||||||
|
Entity linkedEntity;
|
||||||
|
String name;
|
||||||
|
String id;
|
||||||
|
String iconName;
|
||||||
|
int count;
|
||||||
|
|
||||||
|
HAView({
|
||||||
|
this.name,
|
||||||
|
this.id,
|
||||||
|
this.count,
|
||||||
|
this.iconName,
|
||||||
|
List<Entity> childEntities
|
||||||
|
}) {
|
||||||
|
if (childEntities != null) {
|
||||||
|
_fillView(childEntities);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _fillView(List<Entity> childEntities) {
|
||||||
|
List<HACard> autoGeneratedCards = [];
|
||||||
|
badges.addAll(childEntities.where((entity){ return entity.isBadge;}));
|
||||||
|
childEntities.where((entity){ return entity.domain == "media_player";}).forEach((e){
|
||||||
|
HACard card = HACard(
|
||||||
|
name: e.displayName,
|
||||||
|
id: e.entityId,
|
||||||
|
linkedEntityWrapper: EntityWrapper(entity: e),
|
||||||
|
type: CardType.mediaControl
|
||||||
|
);
|
||||||
|
cards.add(card);
|
||||||
|
});
|
||||||
|
childEntities.where((e){return (!e.isBadge && e.domain != "media_player");}).forEach((entity) {
|
||||||
|
if (!entity.isGroup) {
|
||||||
|
String groupIdToAdd = "${entity.domain}.${entity.domain}$count";
|
||||||
|
if (autoGeneratedCards.every((HACard card) => card.id != groupIdToAdd )) {
|
||||||
|
HACard card = HACard(
|
||||||
|
id: groupIdToAdd,
|
||||||
|
name: entity.domain,
|
||||||
|
type: CardType.entities
|
||||||
|
);
|
||||||
|
card.entities.add(EntityWrapper(entity: entity));
|
||||||
|
autoGeneratedCards.add(card);
|
||||||
|
} else {
|
||||||
|
autoGeneratedCards.firstWhere((card) => card.id == groupIdToAdd).entities.add(EntityWrapper(entity: entity));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
HACard card = HACard(
|
||||||
|
name: entity.displayName,
|
||||||
|
id: entity.entityId,
|
||||||
|
linkedEntityWrapper: EntityWrapper(entity: entity),
|
||||||
|
type: CardType.entities
|
||||||
|
);
|
||||||
|
card.entities.addAll(entity.childEntities.where((entity) {return entity.domain != "media_player";}).map((e) {return EntityWrapper(entity: e);}));
|
||||||
|
entity.childEntities.where((entity) {return entity.domain == "media_player";}).forEach((entity){
|
||||||
|
HACard mediaCard = HACard(
|
||||||
|
name: entity.displayName,
|
||||||
|
id: entity.entityId,
|
||||||
|
linkedEntityWrapper: EntityWrapper(entity: entity),
|
||||||
|
type: CardType.mediaControl
|
||||||
|
);
|
||||||
|
cards.add(mediaCard);
|
||||||
|
});
|
||||||
|
cards.add(card);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
cards.addAll(autoGeneratedCards);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget buildTab() {
|
||||||
|
if (linkedEntity == null) {
|
||||||
|
if (iconName != null) {
|
||||||
|
return
|
||||||
|
Tab(
|
||||||
|
icon:
|
||||||
|
Icon(
|
||||||
|
MaterialDesignIcons.createIconDataFromIconName(
|
||||||
|
iconName ?? "mdi:home-assistant"),
|
||||||
|
size: 24.0,
|
||||||
|
)
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return
|
||||||
|
Tab(
|
||||||
|
text: name.toUpperCase(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (linkedEntity.icon != null && linkedEntity.icon.length > 0) {
|
||||||
|
return Tab(
|
||||||
|
icon: Icon(
|
||||||
|
MaterialDesignIcons.createIconDataFromIconName(
|
||||||
|
linkedEntity.icon),
|
||||||
|
size: 24.0,
|
||||||
|
)
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return Tab(
|
||||||
|
text: linkedEntity.displayName.toUpperCase(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return ViewWidget(
|
||||||
|
view: this,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
25
lib/ui_widgets/card_header_widget.dart
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
part of '../main.dart';
|
||||||
|
|
||||||
|
class CardHeaderWidget extends StatelessWidget {
|
||||||
|
|
||||||
|
final String name;
|
||||||
|
|
||||||
|
const CardHeaderWidget({Key key, this.name}) : super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
var result;
|
||||||
|
if ((name != null) && (name.trim().length > 0)) {
|
||||||
|
result = new ListTile(
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
206
lib/ui_widgets/card_widget.dart
Normal file
@ -0,0 +1,206 @@
|
|||||||
|
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) && (card.linkedEntityWrapper.entity.isHidden)) {
|
||||||
|
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.mediaControl: {
|
||||||
|
return _buildMediaControlsCard(context);
|
||||||
|
}
|
||||||
|
|
||||||
|
case CardType.entityButton: {
|
||||||
|
return _buildEntityButtonCard(context);
|
||||||
|
}
|
||||||
|
|
||||||
|
case CardType.horizontalStack: {
|
||||||
|
if (card.childCards.isNotEmpty) {
|
||||||
|
List<Widget> children = [];
|
||||||
|
card.childCards.forEach((card) {
|
||||||
|
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.verticalStack: {
|
||||||
|
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(CardHeaderWidget(name: card.name));
|
||||||
|
entitiesToShow.forEach((EntityWrapper entity) {
|
||||||
|
if (!entity.entity.isHidden) {
|
||||||
|
body.add(
|
||||||
|
Padding(
|
||||||
|
padding: EdgeInsets.fromLTRB(0.0, Sizes.rowPadding, 0.0, Sizes.rowPadding),
|
||||||
|
child: EntityModel(
|
||||||
|
entityWrapper: entity,
|
||||||
|
handleTap: true,
|
||||||
|
child: entity.entity.buildDefaultWidget(context)
|
||||||
|
),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return Card(
|
||||||
|
child: new Column(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(CardHeaderWidget(name: card.name));
|
||||||
|
|
||||||
|
List<Widget> result = [];
|
||||||
|
int columnsCount = entitiesToShow.length >= card.columnsCount ? card.columnsCount : entitiesToShow.length;
|
||||||
|
|
||||||
|
entitiesToShow.forEach((EntityWrapper entity) {
|
||||||
|
result.add(
|
||||||
|
FractionallySizedBox(
|
||||||
|
widthFactor: 1/columnsCount,
|
||||||
|
child: EntityModel(
|
||||||
|
entityWrapper: entity,
|
||||||
|
child: GlanceEntityContainer(
|
||||||
|
showName: card.showName,
|
||||||
|
showState: card.showState,
|
||||||
|
),
|
||||||
|
handleTap: true
|
||||||
|
),
|
||||||
|
)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
rows.add(
|
||||||
|
Padding(
|
||||||
|
padding: EdgeInsets.fromLTRB(0.0, Sizes.rowPadding, 0.0, 2*Sizes.rowPadding),
|
||||||
|
child: Wrap(
|
||||||
|
//alignment: WrapAlignment.spaceAround,
|
||||||
|
runSpacing: Sizes.rowPadding*2,
|
||||||
|
children: result,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
return Card(
|
||||||
|
child: new 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: ButtonEntityContainer(),
|
||||||
|
handleTap: true
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildUnsupportedCard(BuildContext context) {
|
||||||
|
List<Widget> body = [];
|
||||||
|
body.add(CardHeaderWidget(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
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
96
lib/ui_widgets/view.dart
Normal file
@ -0,0 +1,96 @@
|
|||||||
|
part of '../main.dart';
|
||||||
|
|
||||||
|
class ViewWidget extends StatefulWidget {
|
||||||
|
final HAView view;
|
||||||
|
|
||||||
|
const ViewWidget({
|
||||||
|
Key key,
|
||||||
|
this.view
|
||||||
|
}) : super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<StatefulWidget> createState() {
|
||||||
|
return ViewWidgetState();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
class ViewWidgetState extends State<ViewWidget> {
|
||||||
|
|
||||||
|
StreamSubscription _refreshDataSubscription;
|
||||||
|
Completer _refreshCompleter;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_refreshDataSubscription = eventBus.on<RefreshDataFinishedEvent>().listen((event) {
|
||||||
|
if ((_refreshCompleter != null) && (!_refreshCompleter.isCompleted)) {
|
||||||
|
_refreshCompleter.complete();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return RefreshIndicator(
|
||||||
|
color: Colors.amber,
|
||||||
|
child: ListView(
|
||||||
|
padding: EdgeInsets.all(0.0),
|
||||||
|
physics: const AlwaysScrollableScrollPhysics(),
|
||||||
|
children: _buildChildren(context),
|
||||||
|
),
|
||||||
|
onRefresh: () => _refreshData(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
List<Widget> _buildChildren(BuildContext context) {
|
||||||
|
List<Widget> result = [];
|
||||||
|
|
||||||
|
if (widget.view.badges.isNotEmpty) {
|
||||||
|
result.insert(0,
|
||||||
|
Wrap(
|
||||||
|
alignment: WrapAlignment.center,
|
||||||
|
spacing: 10.0,
|
||||||
|
runSpacing: 1.0,
|
||||||
|
children: _buildBadges(context),
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
widget.view.cards.forEach((HACard card){
|
||||||
|
result.add(
|
||||||
|
card.build(context)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
List<Widget> _buildBadges(BuildContext context) {
|
||||||
|
List<Widget> result = [];
|
||||||
|
widget.view.badges.forEach((Entity entity) {
|
||||||
|
if (!entity.isHidden) {
|
||||||
|
result.add(entity.buildBadgeWidget(context));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future _refreshData() {
|
||||||
|
if ((_refreshCompleter != null) && (!_refreshCompleter.isCompleted)) {
|
||||||
|
TheLogger.debug("Previous data refresh is still in progress");
|
||||||
|
} else {
|
||||||
|
_refreshCompleter = Completer();
|
||||||
|
eventBus.fire(RefreshDataEvent());
|
||||||
|
}
|
||||||
|
return _refreshCompleter.future;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_refreshDataSubscription.cancel();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
99
lib/utils.class.dart
Normal file
@ -0,0 +1,99 @@
|
|||||||
|
part of 'main.dart';
|
||||||
|
|
||||||
|
class TheLogger {
|
||||||
|
|
||||||
|
static List<String> _log = [];
|
||||||
|
|
||||||
|
static String getLog() {
|
||||||
|
String res = '';
|
||||||
|
_log.forEach((line) {
|
||||||
|
res += "$line\n";
|
||||||
|
});
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool get isInDebugMode {
|
||||||
|
bool inDebugMode = false;
|
||||||
|
|
||||||
|
assert(inDebugMode = true);
|
||||||
|
|
||||||
|
return inDebugMode;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void error(String message) {
|
||||||
|
_writeToLog("Error", message);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void warning(String message) {
|
||||||
|
_writeToLog("Warning", message);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void debug(String message) {
|
||||||
|
_writeToLog("Debug", message);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void _writeToLog(String level, String message) {
|
||||||
|
if (isInDebugMode) {
|
||||||
|
debugPrint('$message');
|
||||||
|
}
|
||||||
|
DateTime t = DateTime.now();
|
||||||
|
_log.add("${formatDate(t, ["mm","dd"," ","HH",":","nn",":","ss"])} [$level] : $message");
|
||||||
|
if (_log.length > 100) {
|
||||||
|
_log.removeAt(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
class HAUtils {
|
||||||
|
static void launchURL(String url) async {
|
||||||
|
if (await canLaunch(url)) {
|
||||||
|
await launch(url);
|
||||||
|
} else {
|
||||||
|
TheLogger.error( "Could not launch $url");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class StateChangedEvent {
|
||||||
|
String entityId;
|
||||||
|
String newState;
|
||||||
|
|
||||||
|
StateChangedEvent(this.entityId, this.newState);
|
||||||
|
}
|
||||||
|
|
||||||
|
class SettingsChangedEvent {
|
||||||
|
bool reconnect;
|
||||||
|
|
||||||
|
SettingsChangedEvent(this.reconnect);
|
||||||
|
}
|
||||||
|
|
||||||
|
class RefreshDataEvent {
|
||||||
|
RefreshDataEvent();
|
||||||
|
}
|
||||||
|
|
||||||
|
class RefreshDataFinishedEvent {
|
||||||
|
RefreshDataFinishedEvent();
|
||||||
|
}
|
||||||
|
|
||||||
|
class ServiceCallEvent {
|
||||||
|
String domain;
|
||||||
|
String service;
|
||||||
|
String entityId;
|
||||||
|
Map<String, dynamic> additionalParams;
|
||||||
|
|
||||||
|
ServiceCallEvent(this.domain, this.service, this.entityId, this.additionalParams);
|
||||||
|
}
|
||||||
|
|
||||||
|
class ShowEntityPageEvent {
|
||||||
|
Entity entity;
|
||||||
|
|
||||||
|
ShowEntityPageEvent(this.entity);
|
||||||
|
}
|
||||||
|
|
||||||
|
class ShowErrorEvent {
|
||||||
|
String text;
|
||||||
|
int errorCode;
|
||||||
|
|
||||||
|
ShowErrorEvent(this.text, this.errorCode);
|
||||||
|
}
|
260
pubspec.lock
@ -1,13 +1,6 @@
|
|||||||
# Generated by pub
|
# Generated by pub
|
||||||
# See https://www.dartlang.org/tools/pub/glossary#lockfile
|
# See https://www.dartlang.org/tools/pub/glossary#lockfile
|
||||||
packages:
|
packages:
|
||||||
analyzer:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: analyzer
|
|
||||||
url: "https://pub.dartlang.org"
|
|
||||||
source: hosted
|
|
||||||
version: "0.32.4"
|
|
||||||
archive:
|
archive:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -21,7 +14,7 @@ packages:
|
|||||||
name: args
|
name: args
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.5.0"
|
version: "1.5.1"
|
||||||
async:
|
async:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -42,7 +35,7 @@ packages:
|
|||||||
name: cached_network_image
|
name: cached_network_image
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.4.2"
|
version: "0.5.1"
|
||||||
charcode:
|
charcode:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -50,6 +43,20 @@ packages:
|
|||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.1.2"
|
version: "1.1.2"
|
||||||
|
charts_common:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: charts_common
|
||||||
|
url: "https://pub.dartlang.org"
|
||||||
|
source: hosted
|
||||||
|
version: "0.5.0"
|
||||||
|
charts_flutter:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: charts_flutter
|
||||||
|
url: "https://pub.dartlang.org"
|
||||||
|
source: hosted
|
||||||
|
version: "0.5.0"
|
||||||
collection:
|
collection:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -71,22 +78,22 @@ packages:
|
|||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.0.6"
|
version: "2.0.6"
|
||||||
csslib:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: csslib
|
|
||||||
url: "https://pub.dartlang.org"
|
|
||||||
source: hosted
|
|
||||||
version: "0.14.5"
|
|
||||||
dart_config:
|
dart_config:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
path: "."
|
path: "."
|
||||||
ref: HEAD
|
ref: HEAD
|
||||||
resolved-ref: c5727795659e886a7db8b39a14e2c8987280fe1f
|
resolved-ref: e26916e095244a7e5ea61315b030d298d127ed26
|
||||||
url: "https://github.com/MarkOSullivan94/dart_config.git"
|
url: "https://github.com/MarkOSullivan94/dart_config.git"
|
||||||
source: git
|
source: git
|
||||||
version: "0.5.0"
|
version: "0.5.0"
|
||||||
|
date_format:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: date_format
|
||||||
|
url: "https://pub.dartlang.org"
|
||||||
|
source: hosted
|
||||||
|
version: "1.0.5"
|
||||||
event_bus:
|
event_bus:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@ -105,54 +112,33 @@ packages:
|
|||||||
name: flutter_cache_manager
|
name: flutter_cache_manager
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.1.2"
|
version: "0.2.0+1"
|
||||||
flutter_launcher_icons:
|
flutter_colorpicker:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: flutter_colorpicker
|
||||||
|
url: "https://pub.dartlang.org"
|
||||||
|
source: hosted
|
||||||
|
version: "0.1.0"
|
||||||
|
flutter_launcher_icons:
|
||||||
|
dependency: "direct dev"
|
||||||
description:
|
description:
|
||||||
name: flutter_launcher_icons
|
name: flutter_launcher_icons
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.6.1"
|
version: "0.7.0"
|
||||||
flutter_test:
|
flutter_test:
|
||||||
dependency: "direct dev"
|
dependency: "direct dev"
|
||||||
description: flutter
|
description: flutter
|
||||||
source: sdk
|
source: sdk
|
||||||
version: "0.0.0"
|
version: "0.0.0"
|
||||||
front_end:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: front_end
|
|
||||||
url: "https://pub.dartlang.org"
|
|
||||||
source: hosted
|
|
||||||
version: "0.1.4"
|
|
||||||
glob:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: glob
|
|
||||||
url: "https://pub.dartlang.org"
|
|
||||||
source: hosted
|
|
||||||
version: "1.1.7"
|
|
||||||
html:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: html
|
|
||||||
url: "https://pub.dartlang.org"
|
|
||||||
source: hosted
|
|
||||||
version: "0.13.3+3"
|
|
||||||
http:
|
http:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: http
|
name: http
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.11.3+17"
|
version: "0.12.0"
|
||||||
http_multi_server:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: http_multi_server
|
|
||||||
url: "https://pub.dartlang.org"
|
|
||||||
source: hosted
|
|
||||||
version: "2.0.5"
|
|
||||||
http_parser:
|
http_parser:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -166,35 +152,14 @@ packages:
|
|||||||
name: image
|
name: image
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.0.4"
|
version: "2.0.5"
|
||||||
io:
|
intl:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: io
|
name: intl
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.3.3"
|
version: "0.15.7"
|
||||||
js:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: js
|
|
||||||
url: "https://pub.dartlang.org"
|
|
||||||
source: hosted
|
|
||||||
version: "0.6.1+1"
|
|
||||||
json_rpc_2:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: json_rpc_2
|
|
||||||
url: "https://pub.dartlang.org"
|
|
||||||
source: hosted
|
|
||||||
version: "2.0.9"
|
|
||||||
kernel:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: kernel
|
|
||||||
url: "https://pub.dartlang.org"
|
|
||||||
source: hosted
|
|
||||||
version: "0.3.4"
|
|
||||||
logging:
|
logging:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -216,48 +181,6 @@ packages:
|
|||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.1.6"
|
version: "1.1.6"
|
||||||
mime:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: mime
|
|
||||||
url: "https://pub.dartlang.org"
|
|
||||||
source: hosted
|
|
||||||
version: "0.9.6+2"
|
|
||||||
multi_server_socket:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: multi_server_socket
|
|
||||||
url: "https://pub.dartlang.org"
|
|
||||||
source: hosted
|
|
||||||
version: "1.0.2"
|
|
||||||
node_preamble:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: node_preamble
|
|
||||||
url: "https://pub.dartlang.org"
|
|
||||||
source: hosted
|
|
||||||
version: "1.4.4"
|
|
||||||
package_config:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: package_config
|
|
||||||
url: "https://pub.dartlang.org"
|
|
||||||
source: hosted
|
|
||||||
version: "1.0.5"
|
|
||||||
package_info:
|
|
||||||
dependency: "direct main"
|
|
||||||
description:
|
|
||||||
name: package_info
|
|
||||||
url: "https://pub.dartlang.org"
|
|
||||||
source: hosted
|
|
||||||
version: "0.3.2"
|
|
||||||
package_resolver:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: package_resolver
|
|
||||||
url: "https://pub.dartlang.org"
|
|
||||||
source: hosted
|
|
||||||
version: "1.0.4"
|
|
||||||
path:
|
path:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -278,21 +201,7 @@ packages:
|
|||||||
name: petitparser
|
name: petitparser
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.0.1"
|
version: "2.0.2"
|
||||||
plugin:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: plugin
|
|
||||||
url: "https://pub.dartlang.org"
|
|
||||||
source: hosted
|
|
||||||
version: "0.2.0+3"
|
|
||||||
pool:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: pool
|
|
||||||
url: "https://pub.dartlang.org"
|
|
||||||
source: hosted
|
|
||||||
version: "1.3.6"
|
|
||||||
progress_indicators:
|
progress_indicators:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@ -300,74 +209,25 @@ packages:
|
|||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.1.2"
|
version: "0.1.2"
|
||||||
pub_semver:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: pub_semver
|
|
||||||
url: "https://pub.dartlang.org"
|
|
||||||
source: hosted
|
|
||||||
version: "1.4.2"
|
|
||||||
quiver:
|
quiver:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: quiver
|
name: quiver
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.0.0+1"
|
version: "2.0.1"
|
||||||
shared_preferences:
|
shared_preferences:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: shared_preferences
|
name: shared_preferences
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.4.2"
|
version: "0.4.3"
|
||||||
shelf:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: shelf
|
|
||||||
url: "https://pub.dartlang.org"
|
|
||||||
source: hosted
|
|
||||||
version: "0.7.3+3"
|
|
||||||
shelf_packages_handler:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: shelf_packages_handler
|
|
||||||
url: "https://pub.dartlang.org"
|
|
||||||
source: hosted
|
|
||||||
version: "1.0.4"
|
|
||||||
shelf_static:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: shelf_static
|
|
||||||
url: "https://pub.dartlang.org"
|
|
||||||
source: hosted
|
|
||||||
version: "0.2.8"
|
|
||||||
shelf_web_socket:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: shelf_web_socket
|
|
||||||
url: "https://pub.dartlang.org"
|
|
||||||
source: hosted
|
|
||||||
version: "0.2.2+4"
|
|
||||||
sky_engine:
|
sky_engine:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description: flutter
|
description: flutter
|
||||||
source: sdk
|
source: sdk
|
||||||
version: "0.0.99"
|
version: "0.0.99"
|
||||||
source_map_stack_trace:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: source_map_stack_trace
|
|
||||||
url: "https://pub.dartlang.org"
|
|
||||||
source: hosted
|
|
||||||
version: "1.1.5"
|
|
||||||
source_maps:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: source_maps
|
|
||||||
url: "https://pub.dartlang.org"
|
|
||||||
source: hosted
|
|
||||||
version: "0.10.7"
|
|
||||||
source_span:
|
source_span:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -410,13 +270,13 @@ packages:
|
|||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.0.1"
|
version: "1.0.1"
|
||||||
test:
|
test_api:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: test
|
name: test_api
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.3.0"
|
version: "0.2.1"
|
||||||
typed_data:
|
typed_data:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -424,13 +284,13 @@ packages:
|
|||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.1.6"
|
version: "1.1.6"
|
||||||
utf:
|
url_launcher:
|
||||||
dependency: transitive
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: utf
|
name: url_launcher
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.9.0+5"
|
version: "4.0.2"
|
||||||
uuid:
|
uuid:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -445,22 +305,8 @@ packages:
|
|||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.0.8"
|
version: "2.0.8"
|
||||||
vm_service_client:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: vm_service_client
|
|
||||||
url: "https://pub.dartlang.org"
|
|
||||||
source: hosted
|
|
||||||
version: "0.2.6"
|
|
||||||
watcher:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: watcher
|
|
||||||
url: "https://pub.dartlang.org"
|
|
||||||
source: hosted
|
|
||||||
version: "0.9.7+10"
|
|
||||||
web_socket_channel:
|
web_socket_channel:
|
||||||
dependency: transitive
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: web_socket_channel
|
name: web_socket_channel
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
@ -472,7 +318,7 @@ packages:
|
|||||||
name: xml
|
name: xml
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.2.1"
|
version: "3.2.3"
|
||||||
yaml:
|
yaml:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -481,5 +327,5 @@ packages:
|
|||||||
source: hosted
|
source: hosted
|
||||||
version: "2.1.15"
|
version: "2.1.15"
|
||||||
sdks:
|
sdks:
|
||||||
dart: ">=2.0.0 <=2.1.0-dev.3.1.flutter-760a9690c2"
|
dart: ">=2.0.0 <3.0.0"
|
||||||
flutter: ">=0.1.4 <2.0.0"
|
flutter: ">=0.5.6 <2.0.0"
|
||||||
|
16
pubspec.yaml
@ -1,7 +1,7 @@
|
|||||||
name: hass_client
|
name: hass_client
|
||||||
description: Home Assistant Android Client
|
description: Home Assistant Android Client
|
||||||
|
|
||||||
version: 0.1.1-alpha
|
version: 0.3.11+78
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
sdk: ">=2.0.0-dev.68.0 <3.0.0"
|
sdk: ">=2.0.0-dev.68.0 <3.0.0"
|
||||||
@ -9,12 +9,15 @@ environment:
|
|||||||
dependencies:
|
dependencies:
|
||||||
flutter:
|
flutter:
|
||||||
sdk: flutter
|
sdk: flutter
|
||||||
|
web_socket_channel: any
|
||||||
shared_preferences: any
|
shared_preferences: any
|
||||||
progress_indicators: ^0.1.2
|
progress_indicators: any
|
||||||
event_bus: ^1.0.1
|
event_bus: any
|
||||||
package_info: ^0.3.2
|
cached_network_image: any
|
||||||
flutter_launcher_icons: ^0.6.1
|
url_launcher: any
|
||||||
cached_network_image: ^0.4.1
|
date_format: any
|
||||||
|
flutter_colorpicker: any
|
||||||
|
charts_flutter: any
|
||||||
|
|
||||||
# The following adds the Cupertino Icons font to your application.
|
# The following adds the Cupertino Icons font to your application.
|
||||||
# Use with the CupertinoIcons class for iOS style icons.
|
# Use with the CupertinoIcons class for iOS style icons.
|
||||||
@ -23,6 +26,7 @@ dependencies:
|
|||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
flutter_test:
|
flutter_test:
|
||||||
sdk: flutter
|
sdk: flutter
|
||||||
|
flutter_launcher_icons: any
|
||||||
|
|
||||||
flutter_icons:
|
flutter_icons:
|
||||||
android: true
|
android: true
|
||||||
|
@ -12,7 +12,7 @@ import 'package:hass_client/main.dart';
|
|||||||
void main() {
|
void main() {
|
||||||
testWidgets('Counter increments smoke test', (WidgetTester tester) async {
|
testWidgets('Counter increments smoke test', (WidgetTester tester) async {
|
||||||
// Build our app and trigger a frame.
|
// Build our app and trigger a frame.
|
||||||
await tester.pumpWidget(new HassClientApp());
|
await tester.pumpWidget(new HAClientApp());
|
||||||
|
|
||||||
// Verify that our counter starts at 0.
|
// Verify that our counter starts at 0.
|
||||||
expect(find.text('0'), findsOneWidget);
|
expect(find.text('0'), findsOneWidget);
|
||||||
|