Compare commits
394 Commits
0.6.3
...
beta/0.8.6
Author | SHA1 | Date | |
---|---|---|---|
29ee360ec4 | |||
c0faaafd04 | |||
bc045344a5 | |||
7d746fd546 | |||
3ff55f181e | |||
187e12dd79 | |||
10daf2d952 | |||
31c6509d13 | |||
cb74108814 | |||
9efded2139 | |||
96b3e7c739 | |||
b029146bf3 | |||
d715aaf5e5 | |||
0dc12963f0 | |||
4da3b40d55 | |||
f7d05a57ad | |||
df01599fe0 | |||
2c3335ebf3 | |||
05c1427aa8 | |||
02bfaf7db6 | |||
f488c0810b | |||
8dbfb91234 | |||
aee99e3925 | |||
50d3280803 | |||
a90eb5c4db | |||
16c06a2d48 | |||
513bf85cae | |||
82d7aeba02 | |||
12f7cb86de | |||
b65c885467 | |||
2a828a1289 | |||
291f12ba97 | |||
6afbd37d71 | |||
0e8869878f | |||
7c2cfe3215 | |||
c376c0e952 | |||
da5f663396 | |||
0e92418a33 | |||
2eef7cfe5e | |||
de4e0bfb3a | |||
8bf2d31e72 | |||
2125c46143 | |||
5402eb84df | |||
ad5aa0898f | |||
040d40b614 | |||
8e58f22c56 | |||
c91695fbe5 | |||
c43741da49 | |||
f2563a0397 | |||
fba4017819 | |||
5f23e108a1 | |||
68d14bd13d | |||
022622522f | |||
89513ca4e5 | |||
a934ee2335 | |||
49aeea634f | |||
e18b9ebe14 | |||
08ee3f3d80 | |||
62d07bf8b9 | |||
ab398cbdc3 | |||
007d12719c | |||
524d195800 | |||
405de64249 | |||
f53554702e | |||
379e1a4a7e | |||
d6f7096055 | |||
37c721e4f6 | |||
d94235ef6d | |||
eb4184713f | |||
a0a0cb4612 | |||
f448a20784 | |||
36eff26862 | |||
5b2a1163b9 | |||
e627a8b963 | |||
4432124e8c | |||
b8ba3c59e9 | |||
c40a496b6b | |||
a7c3b46061 | |||
dfbaaeb06b | |||
f6ab20c6e8 | |||
7625099d74 | |||
32c8e76855 | |||
0aa2c974d5 | |||
9524c8587b | |||
c075db8b1a | |||
d0b7cc1929 | |||
d8df32f140 | |||
293b5e0242 | |||
2f517a3ad5 | |||
56d8e389db | |||
1377843350 | |||
8e31eaf8bb | |||
5ced01463f | |||
a3548455eb | |||
c40fceea4f | |||
6ad3938a91 | |||
bc642f81ad | |||
14ce608bbb | |||
c4c67747c5 | |||
5b3ceecb0e | |||
bf53e4b9df | |||
7e09d92fdf | |||
1ba9106d0b | |||
d727a29991 | |||
c5d617477f | |||
244a1984cc | |||
b00b745f27 | |||
959ff21b9b | |||
e6a7fd2dfe | |||
216276e5f3 | |||
3e6229cf3e | |||
fc4cb80b74 | |||
b907ff1e82 | |||
7536a52771 | |||
73a8c111d1 | |||
86a19eeec2 | |||
fba4459977 | |||
06f994a827 | |||
35d8607484 | |||
2f4c06e9b5 | |||
92e008a380 | |||
14c272af92 | |||
710de9f2b8 | |||
d9ad3b3083 | |||
b2686cb105 | |||
959e89de2b | |||
6e448d3458 | |||
6695756727 | |||
ed732e9b77 | |||
f495a6affc | |||
c8d7e1a95f | |||
e1ca2638e3 | |||
01226cb9eb | |||
8a80d0c5d1 | |||
f26f3e87c7 | |||
b750417415 | |||
2c35dd7c21 | |||
cff4a4feed | |||
62174b0651 | |||
d3ea4210c1 | |||
1c782bf64d | |||
bc96dab339 | |||
0f7179b944 | |||
1e3bfa8ff7 | |||
2bce86f905 | |||
0be00acc3a | |||
4e61adaeb1 | |||
49a8f08153 | |||
ce15658462 | |||
16d73ba7dd | |||
9f3e3c1917 | |||
f29e382a19 | |||
073562373a | |||
4298ebcd66 | |||
a121295bef | |||
9303e4c0a5 | |||
831fc98ab1 | |||
2003005e56 | |||
fda8fb7182 | |||
cf6039b279 | |||
41e552dce5 | |||
90043b5806 | |||
9eb74b5a8d | |||
9cc60a136b | |||
78eb1e779c | |||
8db2d8508e | |||
3f1ece26ec | |||
d1912a44c6 | |||
36a05eb390 | |||
4f39ea1ad8 | |||
a241cc1d61 | |||
8b4df98cb9 | |||
7d30c2f9d5 | |||
44acabadfe | |||
6f3a2bb78d | |||
dd5f8b155d | |||
cd81fc72fd | |||
890da650dc | |||
9897b6a44b | |||
7969f54d3b | |||
7c18454de3 | |||
dcf5efddd1 | |||
a6541134e0 | |||
90504047b4 | |||
ca1eec6602 | |||
edc01d14b7 | |||
6cb5463b13 | |||
63a789ebfb | |||
a0994e9a60 | |||
8d1b728194 | |||
1a9fec8b98 | |||
e634253282 | |||
64b23ec7cc | |||
afe207a878 | |||
4bac0c092f | |||
74c8ae35a1 | |||
7856637456 | |||
965f80a6ca | |||
198c2ba49a | |||
4b9ec5ca6e | |||
5792652619 | |||
2c900333a5 | |||
1f782d7cd3 | |||
89cc1833de | |||
1262d8c9aa | |||
85b0c4f814 | |||
551a8dfa31 | |||
139533d2ca | |||
889682f771 | |||
f16c98057f | |||
26ec807c25 | |||
45af6cbe3c | |||
5dd9cde12d | |||
472fb1d367 | |||
8b372fbc0b | |||
40d72eb6e1 | |||
ced008a7c1 | |||
d1f652282a | |||
f656528d5b | |||
bcdb2a648c | |||
8a78745aa7 | |||
2a3eaabbe4 | |||
bcd175fbfb | |||
f9f013636d | |||
b34cc97080 | |||
327f623ef7 | |||
4d0877e5ae | |||
0eac217399 | |||
9c42ad687d | |||
5cda98da46 | |||
958f545f65 | |||
44165993b4 | |||
283ae6cfd4 | |||
4068b295bd | |||
e36b33dcec | |||
4b12912697 | |||
49a21967cc | |||
cf36406f2a | |||
872ad044f1 | |||
345301c03a | |||
117923413d | |||
24ccbc58c4 | |||
89c91b4b01 | |||
4494da1f4f | |||
c263542c54 | |||
c70f52a73d | |||
423813d6fb | |||
ec6a86f4b0 | |||
64cf18cb23 | |||
e0e064bc67 | |||
5cee6cbd9c | |||
43659b26f7 | |||
98e15ad429 | |||
90728cdf8b | |||
d1ec4f36cc | |||
079070071e | |||
520fd6bc38 | |||
085aead36b | |||
fcbaf298cc | |||
eedc0c9b22 | |||
f70c1e12ff | |||
ec094a4362 | |||
11646c840e | |||
86987c57c9 | |||
e4d6e842f5 | |||
cfe4dd1c59 | |||
3387ab2850 | |||
abd23e27ea | |||
2f110b20bb | |||
f88e6f9b61 | |||
2836973dca | |||
a4477e9f83 | |||
96fa7ece25 | |||
b84caa4cc3 | |||
49c212632e | |||
92165aa7ed | |||
cbbdb754aa | |||
7e3fe0608d | |||
781f39f281 | |||
bfb80f6f8c | |||
801b8f9288 | |||
b988fcfcdd | |||
dff6457cb2 | |||
f50f68f318 | |||
c869ad41d9 | |||
cd41f9a236 | |||
1dbe162bf0 | |||
1a52203bd7 | |||
753df3c724 | |||
dc62a08da3 | |||
0c26aff498 | |||
6323f8f2e6 | |||
885c0b1316 | |||
14958d9165 | |||
bf6a52e0b9 | |||
72aad5cc16 | |||
340e8569cc | |||
8fc7d0b61e | |||
5dcb27ada7 | |||
db1a076132 | |||
6707201e23 | |||
b8b92171a8 | |||
3dd7069292 | |||
7177419472 | |||
c37313cf07 | |||
a65f42d0fd | |||
78dd7df686 | |||
2ea7d9440c | |||
abdcd49368 | |||
6da7a5ab90 | |||
20ffe03139 | |||
a71213c589 | |||
d61103ac42 | |||
298a64b7ae | |||
9e2c673966 | |||
092469d668 | |||
bcf3dab0e2 | |||
7ecfc8a9ff | |||
ecf0a696f7 | |||
dc5db28e01 | |||
555f305c22 | |||
76bf07cfcd | |||
c4663576d1 | |||
a64aa73aae | |||
a3a60dd707 | |||
9c28b0085b | |||
d5baabdd53 | |||
56a333a852 | |||
c5922368de | |||
8c2316a51a | |||
e2e6c015de | |||
0a6ff4586d | |||
fc228d85ae | |||
61823cb43b | |||
127e0b8182 | |||
38c37fa212 | |||
dfaf2a2924 | |||
c90c40c046 | |||
d2049b726a | |||
6508f109f7 | |||
37e63637a7 | |||
6650c5c145 | |||
9160dbf7f2 | |||
243fcd7c49 | |||
c114bcfb35 | |||
83defb08f1 | |||
57ebdbbe85 | |||
c6aceed623 | |||
ba4c88ec5d | |||
ee1685e981 | |||
996fbf7bba | |||
56cd8963d7 | |||
5759aad0cb | |||
02717332f7 | |||
8d1b159f56 | |||
fb335e1100 | |||
5f0bc83d67 | |||
6a8cee2cc2 | |||
0d2f1cf9aa | |||
8efeb3da8a | |||
620aa3b8d8 | |||
ab5bf3b807 | |||
6663bcad72 | |||
113cd29f74 | |||
f2fdfb0a32 | |||
691e48a36b | |||
2036cc117f | |||
389d28a1e1 | |||
27e6198d83 | |||
de762a4878 | |||
e8efefe25d | |||
21f3e8985a | |||
622543d405 | |||
abdc0fc1c8 | |||
1ecb839042 | |||
cece4d1e16 | |||
623634cb6e | |||
f9c37f5084 | |||
3e12f4f8a4 | |||
b07ff6fe71 | |||
5a3b57c28e | |||
e858eee83b | |||
73f00d3bd7 | |||
eea59cf11b | |||
61b459ed8a | |||
dca8c309aa | |||
be53500104 | |||
bc1a791608 | |||
b112ff980a | |||
7beab9ae93 | |||
8c0d1f90a3 | |||
05c05ba768 | |||
13102a6b04 | |||
57c3083f9f |
22
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
22
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
---
|
||||||
|
name: Bug report
|
||||||
|
about: Create a report to help improve HA Client
|
||||||
|
title: ''
|
||||||
|
labels: ''
|
||||||
|
assignees: ''
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**HA Client version:** [Main menu -> About HA Client]
|
||||||
|
|
||||||
|
**Home Assistant version:**
|
||||||
|
|
||||||
|
**Device name:**
|
||||||
|
|
||||||
|
**Android version:**
|
||||||
|
|
||||||
|
**Description**
|
||||||
|
[Replace with description]
|
||||||
|
|
||||||
|
**Screenshots**
|
||||||
|
[Replace with screenshots]
|
12
.github/ISSUE_TEMPLATE/entity-support-request.md
vendored
Normal file
12
.github/ISSUE_TEMPLATE/entity-support-request.md
vendored
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
---
|
||||||
|
name: Entity support request
|
||||||
|
about: Suggest to add support of any entity type
|
||||||
|
title: ''
|
||||||
|
labels: ENTITY, feature/improvement
|
||||||
|
assignees: ''
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Entity type:**
|
||||||
|
|
||||||
|
**Link to documentation:**
|
17
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
17
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
---
|
||||||
|
name: Feature request
|
||||||
|
about: Suggest an idea for HA Client if it is not a card or entity support
|
||||||
|
title: ''
|
||||||
|
labels: feature/improvement
|
||||||
|
assignees: ''
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Is your feature request related to a problem? Please describe.**
|
||||||
|
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
|
||||||
|
|
||||||
|
**Describe the solution you'd like**
|
||||||
|
A clear and concise description of what you want to happen.
|
||||||
|
|
||||||
|
**Additional context**
|
||||||
|
Add any other context or screenshots about the feature request here.
|
12
.github/ISSUE_TEMPLATE/lovelace-card-support-request.md
vendored
Normal file
12
.github/ISSUE_TEMPLATE/lovelace-card-support-request.md
vendored
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
---
|
||||||
|
name: Lovelace Card support request
|
||||||
|
about: Suggest to add any Lovelace card support
|
||||||
|
title: ''
|
||||||
|
labels: CARD, feature/improvement
|
||||||
|
assignees: ''
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Card name:**
|
||||||
|
|
||||||
|
**Link to card repository or web page:**
|
11
.github/no-response.yml
vendored
Normal file
11
.github/no-response.yml
vendored
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
# Configuration for probot-no-response - https://github.com/probot/no-response
|
||||||
|
|
||||||
|
# Number of days of inactivity before an Issue is closed for lack of response
|
||||||
|
daysUntilClose: 14
|
||||||
|
# Label requiring a response
|
||||||
|
responseRequiredLabel: more info needed
|
||||||
|
# Comment to post when closing an Issue for lack of response. Set to `false` to disable
|
||||||
|
closeComment: >
|
||||||
|
This issue has been automatically closed because there has been no response
|
||||||
|
to our request for more information from the original author. If the issue still relevant
|
||||||
|
feel free to reopen this issue and add more information, or report a new one.
|
8
.gitignore
vendored
8
.gitignore
vendored
@ -9,6 +9,14 @@ build/
|
|||||||
.flutter-plugins
|
.flutter-plugins
|
||||||
|
|
||||||
.idea/
|
.idea/
|
||||||
|
.vscode/
|
||||||
|
.theia/
|
||||||
|
.project/
|
||||||
|
.settings/
|
||||||
|
|
||||||
|
flutter_export_environment.sh
|
||||||
|
.flutter-plugins-dependencies
|
||||||
|
|
||||||
key.properties
|
key.properties
|
||||||
|
.secrets.dart
|
||||||
pubspec.lock
|
pubspec.lock
|
8
.gitpod.dockerfile
Normal file
8
.gitpod.dockerfile
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
FROM gitpod/workspace-full:latest
|
||||||
|
|
||||||
|
ENV ANDROID_HOME=/workspace/android-sdk \
|
||||||
|
FLUTTER_ROOT=/workspace/flutter \
|
||||||
|
FLUTTER_HOME=/workspace/flutter
|
||||||
|
|
||||||
|
RUN bash -c ". /home/gitpod/.sdkman/bin/sdkman-init.sh \
|
||||||
|
&& sdk install java 8.0.242.j9-adpt"
|
26
.gitpod.yml
Normal file
26
.gitpod.yml
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
image:
|
||||||
|
file: .gitpod.dockerfile
|
||||||
|
|
||||||
|
tasks:
|
||||||
|
- before: |
|
||||||
|
export PATH=$FLUTTER_HOME/bin:$FLUTTER_HOME/bin/cache/dart-sdk/bin:$ANDROID_HOME/bin:$ANDROID_HOME/platform-tools:$PATH
|
||||||
|
mkdir -p /home/gitpod/.android
|
||||||
|
touch /home/gitpod/.android/repositories.cfg
|
||||||
|
init: |
|
||||||
|
echo "Installing Flutter SDK..."
|
||||||
|
cd /workspace && wget -qO flutter_sdk.tar.xz https://storage.googleapis.com/flutter_infra/releases/stable/linux/flutter_linux_v1.12.13+hotfix.7-stable.tar.xz && tar -xf flutter_sdk.tar.xz && rm -f flutter_sdk.tar.xz
|
||||||
|
echo "Installing Android SDK..."
|
||||||
|
mkdir -p /workspace/android-sdk && cd /workspace/android-sdk && wget https://dl.google.com/android/repository/sdk-tools-linux-4333796.zip && unzip sdk-tools-linux-4333796.zip && rm -f sdk-tools-linux-4333796.zip
|
||||||
|
/workspace/android-sdk/tools/bin/sdkmanager "platform-tools" "platforms;android-28" "build-tools;28.0.3"
|
||||||
|
echo "Init Flutter..."
|
||||||
|
cd /workspace/ha_client
|
||||||
|
flutter upgrade
|
||||||
|
flutter doctor --android-licenses
|
||||||
|
flutter pub get
|
||||||
|
command: |
|
||||||
|
echo "Ready to go!"
|
||||||
|
flutter doctor
|
||||||
|
vscode:
|
||||||
|
extensions:
|
||||||
|
- Dart-Code.dart-code@3.5.0-beta.1:Wg2nTABftVR/Dry4tqeY1w==
|
||||||
|
- Dart-Code.flutter@3.5.0:/kOacEWdiDRLyN/idUiM4A==
|
BIN
.gradle/6.0.1/fileChanges/last-build.bin
Normal file
BIN
.gradle/6.0.1/fileChanges/last-build.bin
Normal file
Binary file not shown.
BIN
.gradle/6.0.1/fileHashes/fileHashes.lock
Normal file
BIN
.gradle/6.0.1/fileHashes/fileHashes.lock
Normal file
Binary file not shown.
0
.gradle/6.0.1/gc.properties
Normal file
0
.gradle/6.0.1/gc.properties
Normal file
16
README.md
16
README.md
@ -1,12 +1,16 @@
|
|||||||
[](https://somegeeky.website/badges/flutter) [](https://somegeeky.website/badges/dart)
|
|
||||||
# HA Client
|
# HA Client
|
||||||
## Native Android client for Home Assistant
|
## Native Android client for Home Assistant
|
||||||
### With Lovelace UI support
|
### With notifications and Lovelace UI support
|
||||||
|
|
||||||
Visit [homemade.systems](http://ha-client.homemade.systems/) for more info.
|
Visit [ha-client.app](http://ha-client.app/) 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)
|
||||||
|
|
||||||
Download the app from [Google Play](https://play.google.com/apps/testing/com.keyboardcrumbs.haclient) after joining the group
|
Discuss it on [Discord](https://discord.gg/nd6FZQ) or at [Home Assistant community](https://community.home-assistant.io/c/mobile-apps/ha-client-android)
|
||||||
|
|
||||||
Discuss it in [Home Assistant community](https://community.home-assistant.io/t/alpha-testing-ha-client-native-android-client-for-home-assistant/69912)
|
[](https://gitpod.io/#https://github.com/estevez-dev/ha_client)
|
||||||
|
|
||||||
|
#### Pre-release CI build
|
||||||
|
[](https://codemagic.io/apps/5da8bdab9f20ef798f7c2c65/5da8bdab9f20ef798f7c2c64/latest_build)
|
||||||
|
#### Beta CI build
|
||||||
|
[](https://codemagic.io/apps/5da8bdab9f20ef798f7c2c65/5db1862025dc3f0b0288a57a/latest_build)
|
||||||
|
1
android/.gitignore
vendored
1
android/.gitignore
vendored
@ -8,3 +8,4 @@
|
|||||||
/build
|
/build
|
||||||
/captures
|
/captures
|
||||||
GeneratedPluginRegistrant.java
|
GeneratedPluginRegistrant.java
|
||||||
|
.project/
|
17
android/.project
Normal file
17
android/.project
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<projectDescription>
|
||||||
|
<name>android</name>
|
||||||
|
<comment>Project android created by Buildship.</comment>
|
||||||
|
<projects>
|
||||||
|
</projects>
|
||||||
|
<buildSpec>
|
||||||
|
<buildCommand>
|
||||||
|
<name>org.eclipse.buildship.core.gradleprojectbuilder</name>
|
||||||
|
<arguments>
|
||||||
|
</arguments>
|
||||||
|
</buildCommand>
|
||||||
|
</buildSpec>
|
||||||
|
<natures>
|
||||||
|
<nature>org.eclipse.buildship.core.gradleprojectnature</nature>
|
||||||
|
</natures>
|
||||||
|
</projectDescription>
|
6
android/app/.classpath
Normal file
6
android/app/.classpath
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<classpath>
|
||||||
|
<classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/JavaSE-1.8/"/>
|
||||||
|
<classpathentry kind="con" path="org.eclipse.buildship.core.gradleclasspathcontainer"/>
|
||||||
|
<classpathentry kind="output" path="bin/default"/>
|
||||||
|
</classpath>
|
23
android/app/.project
Normal file
23
android/app/.project
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<projectDescription>
|
||||||
|
<name>app</name>
|
||||||
|
<comment>Project app created by Buildship.</comment>
|
||||||
|
<projects>
|
||||||
|
</projects>
|
||||||
|
<buildSpec>
|
||||||
|
<buildCommand>
|
||||||
|
<name>org.eclipse.jdt.core.javabuilder</name>
|
||||||
|
<arguments>
|
||||||
|
</arguments>
|
||||||
|
</buildCommand>
|
||||||
|
<buildCommand>
|
||||||
|
<name>org.eclipse.buildship.core.gradleprojectbuilder</name>
|
||||||
|
<arguments>
|
||||||
|
</arguments>
|
||||||
|
</buildCommand>
|
||||||
|
</buildSpec>
|
||||||
|
<natures>
|
||||||
|
<nature>org.eclipse.jdt.core.javanature</nature>
|
||||||
|
<nature>org.eclipse.buildship.core.gradleprojectnature</nature>
|
||||||
|
</natures>
|
||||||
|
</projectDescription>
|
@ -50,6 +50,14 @@ android {
|
|||||||
}
|
}
|
||||||
|
|
||||||
signingConfigs {
|
signingConfigs {
|
||||||
|
if (!System.getenv()["CI"]) {
|
||||||
|
debug {
|
||||||
|
keyAlias keystoreProperties['debugKeyAlias']
|
||||||
|
keyPassword keystoreProperties['debugKeyPassword']
|
||||||
|
storeFile file(keystoreProperties['debugStoreFile'])
|
||||||
|
storePassword keystoreProperties['debugStorePassword']
|
||||||
|
}
|
||||||
|
}
|
||||||
release {
|
release {
|
||||||
keyAlias keystoreProperties['keyAlias']
|
keyAlias keystoreProperties['keyAlias']
|
||||||
keyPassword keystoreProperties['keyPassword']
|
keyPassword keystoreProperties['keyPassword']
|
||||||
@ -70,10 +78,11 @@ flutter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation 'com.google.firebase:firebase-core:16.0.8'
|
implementation 'com.google.firebase:firebase-analytics:17.2.2'
|
||||||
testImplementation 'junit:junit:4.12'
|
testImplementation 'junit:junit:4.12'
|
||||||
androidTestImplementation 'com.android.support.test:runner:1.0.2'
|
androidTestImplementation 'com.android.support.test:runner:1.0.2'
|
||||||
androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2'
|
androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
apply plugin: 'io.fabric'
|
||||||
apply plugin: 'com.google.gms.google-services'
|
apply plugin: 'com.google.gms.google-services'
|
||||||
|
@ -14,6 +14,30 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"oauth_client": [
|
"oauth_client": [
|
||||||
|
{
|
||||||
|
"client_id": "441874387819-uqmkibhf361828od1982o2jhl0n3m0ov.apps.googleusercontent.com",
|
||||||
|
"client_type": 1,
|
||||||
|
"android_info": {
|
||||||
|
"package_name": "com.keyboardcrumbs.haclient",
|
||||||
|
"certificate_hash": "bebe4d970fbebf0bff2c93244fdc7fcbcefb3470"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"client_id": "441874387819-5q7vmimci4s2jl3v0ncugv1ocp4m48nb.apps.googleusercontent.com",
|
||||||
|
"client_type": 1,
|
||||||
|
"android_info": {
|
||||||
|
"package_name": "com.keyboardcrumbs.haclient",
|
||||||
|
"certificate_hash": "0ea12348468be44bc2aa5792ee7e8924c633da81"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"client_id": "441874387819-joi8plo5345ebt8i1dug27u2aenv5tg7.apps.googleusercontent.com",
|
||||||
|
"client_type": 1,
|
||||||
|
"android_info": {
|
||||||
|
"package_name": "com.keyboardcrumbs.haclient",
|
||||||
|
"certificate_hash": "fcbc805d965ccf6a4d5417398d191edc9c9890b0"
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"client_id": "441874387819-id0hqsfprj3b5kc312faqv3lmdfpm7l8.apps.googleusercontent.com",
|
"client_id": "441874387819-id0hqsfprj3b5kc312faqv3lmdfpm7l8.apps.googleusercontent.com",
|
||||||
"client_type": 3
|
"client_type": 3
|
||||||
@ -25,15 +49,13 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"services": {
|
"services": {
|
||||||
"analytics_service": {
|
|
||||||
"status": 1
|
|
||||||
},
|
|
||||||
"appinvite_service": {
|
"appinvite_service": {
|
||||||
"status": 1,
|
"other_platform_oauth_client": [
|
||||||
"other_platform_oauth_client": []
|
{
|
||||||
},
|
"client_id": "441874387819-id0hqsfprj3b5kc312faqv3lmdfpm7l8.apps.googleusercontent.com",
|
||||||
"ads_service": {
|
"client_type": 3
|
||||||
"status": 2
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,12 +1,15 @@
|
|||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
package="com.keyboardcrumbs.hassclient">
|
package="com.keyboardcrumbs.hassclient">
|
||||||
|
|
||||||
<!-- The INTERNET permission is required for development. Specifically,
|
<uses-feature android:name="android.hardware.touchscreen"
|
||||||
flutter needs it to communicate with the running application
|
android:required="false" />
|
||||||
to allow setting breakpoints, to provide hot reload, etc.
|
|
||||||
-->
|
|
||||||
<uses-permission android:name="android.permission.INTERNET"/>
|
<uses-permission android:name="android.permission.INTERNET"/>
|
||||||
<uses-permission android:name="android.permission.VIBRATE" />
|
<uses-permission android:name="android.permission.VIBRATE" />
|
||||||
|
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
|
||||||
|
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
|
||||||
|
<uses-permission android:name="android.permission.WAKE_LOCK"/>
|
||||||
|
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
|
||||||
|
|
||||||
|
|
||||||
<!-- io.flutter.app.FlutterApplication is an android.app.Application that
|
<!-- io.flutter.app.FlutterApplication is an android.app.Application that
|
||||||
calls FlutterMain.startInitialization(this); in its onCreate method.
|
calls FlutterMain.startInitialization(this); in its onCreate method.
|
||||||
@ -19,6 +22,10 @@
|
|||||||
android:icon="@mipmap/ic_launcher"
|
android:icon="@mipmap/ic_launcher"
|
||||||
android:usesCleartextTraffic="true">
|
android:usesCleartextTraffic="true">
|
||||||
|
|
||||||
|
<meta-data
|
||||||
|
android:name="flutterEmbedding"
|
||||||
|
android:value="2" />
|
||||||
|
|
||||||
<meta-data
|
<meta-data
|
||||||
android:name="com.google.firebase.messaging.default_notification_channel_id"
|
android:name="com.google.firebase.messaging.default_notification_channel_id"
|
||||||
android:value="ha_notify" />
|
android:value="ha_notify" />
|
||||||
@ -30,13 +37,12 @@
|
|||||||
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|locale|layoutDirection|fontScale|screenLayout|density"
|
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|locale|layoutDirection|fontScale|screenLayout|density"
|
||||||
android:hardwareAccelerated="true"
|
android:hardwareAccelerated="true"
|
||||||
android:windowSoftInputMode="adjustResize">
|
android:windowSoftInputMode="adjustResize">
|
||||||
<!-- This keeps the window background of the activity showing
|
|
||||||
until Flutter renders its first frame. It can be removed if
|
|
||||||
there is no splash screen (such as the default splash screen
|
|
||||||
defined in @style/LaunchTheme).
|
|
||||||
<meta-data
|
<meta-data
|
||||||
android:name="io.flutter.app.android.SplashScreenUntilFirstFrame"
|
android:name="io.flutter.embedding.android.SplashScreenDrawable"
|
||||||
android:value="true" />-->
|
android:resource="@drawable/launch_background" />
|
||||||
|
<meta-data
|
||||||
|
android:name="io.flutter.embedding.android.NormalTheme"
|
||||||
|
android:resource="@style/NormalTheme" />
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="FLUTTER_NOTIFICATION_CLICK" />
|
<action android:name="FLUTTER_NOTIFICATION_CLICK" />
|
||||||
<category android:name="android.intent.category.DEFAULT" />
|
<category android:name="android.intent.category.DEFAULT" />
|
||||||
@ -46,5 +52,20 @@
|
|||||||
<category android:name="android.intent.category.LAUNCHER"/>
|
<category android:name="android.intent.category.LAUNCHER"/>
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</activity>
|
</activity>
|
||||||
|
|
||||||
|
<service
|
||||||
|
android:name="io.flutter.plugins.androidalarmmanager.AlarmService"
|
||||||
|
android:permission="android.permission.BIND_JOB_SERVICE"
|
||||||
|
android:exported="false"/>
|
||||||
|
<receiver
|
||||||
|
android:name="io.flutter.plugins.androidalarmmanager.AlarmBroadcastReceiver"
|
||||||
|
android:exported="false"/>
|
||||||
|
<receiver
|
||||||
|
android:name="io.flutter.plugins.androidalarmmanager.RebootBroadcastReceiver"
|
||||||
|
android:enabled="false">
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.intent.action.BOOT_COMPLETED"></action>
|
||||||
|
</intent-filter>
|
||||||
|
</receiver>
|
||||||
</application>
|
</application>
|
||||||
</manifest>
|
</manifest>
|
||||||
|
@ -1,13 +1,15 @@
|
|||||||
package com.keyboardcrumbs.hassclient;
|
package com.keyboardcrumbs.hassclient;
|
||||||
|
|
||||||
import android.os.Bundle;
|
import androidx.annotation.NonNull;
|
||||||
import io.flutter.app.FlutterActivity;
|
import io.flutter.embedding.android.FlutterActivity;
|
||||||
|
import io.flutter.embedding.engine.FlutterEngine;
|
||||||
import io.flutter.plugins.GeneratedPluginRegistrant;
|
import io.flutter.plugins.GeneratedPluginRegistrant;
|
||||||
|
|
||||||
public class MainActivity extends FlutterActivity {
|
public class MainActivity extends FlutterActivity {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void onCreate(Bundle savedInstanceState) {
|
public void configureFlutterEngine(@NonNull FlutterEngine flutterEngine) {
|
||||||
super.onCreate(savedInstanceState);
|
GeneratedPluginRegistrant.registerWith(flutterEngine);
|
||||||
GeneratedPluginRegistrant.registerWith(this);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -5,4 +5,7 @@
|
|||||||
Flutter draws its first frame -->
|
Flutter draws its first frame -->
|
||||||
<item name="android:windowBackground">@drawable/launch_background</item>
|
<item name="android:windowBackground">@drawable/launch_background</item>
|
||||||
</style>
|
</style>
|
||||||
|
<style name="NormalTheme" parent="@android:style/Theme.Black.NoTitleBar">
|
||||||
|
<item name="android:windowBackground">@drawable/launch_background</item>
|
||||||
|
</style>
|
||||||
</resources>
|
</resources>
|
||||||
|
@ -2,11 +2,15 @@ buildscript {
|
|||||||
repositories {
|
repositories {
|
||||||
google()
|
google()
|
||||||
jcenter()
|
jcenter()
|
||||||
|
maven {
|
||||||
|
url 'https://maven.fabric.io/public'
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
classpath 'com.android.tools.build:gradle:3.3.2'
|
classpath 'com.android.tools.build:gradle:3.3.2'
|
||||||
classpath 'com.google.gms:google-services:4.2.0'
|
classpath 'com.google.gms:google-services:4.3.3'
|
||||||
|
classpath 'io.fabric.tools:gradle:1.26.1'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -14,6 +18,9 @@ allprojects {
|
|||||||
repositories {
|
repositories {
|
||||||
google()
|
google()
|
||||||
jcenter()
|
jcenter()
|
||||||
|
maven {
|
||||||
|
url 'https://maven.fabric.io/public'
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -3,3 +3,4 @@ org.gradle.daemon=true
|
|||||||
org.gradle.caching=true
|
org.gradle.caching=true
|
||||||
android.useAndroidX=true
|
android.useAndroidX=true
|
||||||
android.enableJetifier=true
|
android.enableJetifier=true
|
||||||
|
android.enableR8=true
|
||||||
|
1
android/settings_aar.gradle
Normal file
1
android/settings_aar.gradle
Normal file
@ -0,0 +1 @@
|
|||||||
|
include ':app'
|
61
assets/html/cameraLiveView.html
Normal file
61
assets/html/cameraLiveView.html
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/hls.js@latest"></script>
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
widows: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
video {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<script>
|
||||||
|
var messageChannel = '{{message_channel}}';
|
||||||
|
</script>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<video id="screen" width="100%" controls></video>
|
||||||
|
<script>
|
||||||
|
if (Hls.isSupported()) {
|
||||||
|
var video = document.getElementById('screen');
|
||||||
|
var hls = new Hls();
|
||||||
|
hls.on(Hls.Events.ERROR, function (event, data) {
|
||||||
|
if (data.fatal) {
|
||||||
|
switch(data.type) {
|
||||||
|
case Hls.ErrorTypes.NETWORK_ERROR:
|
||||||
|
// try to recover network error
|
||||||
|
console.log("fatal network error encountered, try to recover");
|
||||||
|
hls.startLoad();
|
||||||
|
break;
|
||||||
|
case Hls.ErrorTypes.MEDIA_ERROR:
|
||||||
|
console.log("fatal media error encountered, try to recover");
|
||||||
|
hls.recoverMediaError();
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
// cannot recover
|
||||||
|
hls.destroy();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
// bind them together
|
||||||
|
hls.attachMedia(video);
|
||||||
|
hls.on(Hls.Events.MEDIA_ATTACHED, function () {
|
||||||
|
console.log("video and hls.js are now bound together !");
|
||||||
|
hls.loadSource("{{stream_url}}");
|
||||||
|
hls.on(Hls.Events.MANIFEST_PARSED, function (event, data) {
|
||||||
|
console.log("manifest loaded, found " + data.levels.length + " quality level");
|
||||||
|
video.play();
|
||||||
|
video.onloadedmetadata = function() {
|
||||||
|
window[messageChannel].postMessage(document.body.clientWidth / video.offsetHeight);
|
||||||
|
};
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
28
assets/html/cameraView.html
Normal file
28
assets/html/cameraView.html
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
widows: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
img {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<script>
|
||||||
|
var messageChannel = '{{message_channel}}';
|
||||||
|
window.onload = function() {
|
||||||
|
var img = document.getElementById('screen');
|
||||||
|
if (img) {
|
||||||
|
window[messageChannel].postMessage(document.body.clientWidth / img.offsetHeight);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<img id="screen" src="{{stream_url}}">
|
||||||
|
</body>
|
||||||
|
</html>
|
35
assets/js/externalAuth.js
Normal file
35
assets/js/externalAuth.js
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
window.externalApp = {};
|
||||||
|
window.externalApp.getExternalAuth = function(options) {
|
||||||
|
console.log("Starting external auth");
|
||||||
|
var options = JSON.parse(options);
|
||||||
|
if (options && options.callback) {
|
||||||
|
var responseData = {
|
||||||
|
access_token: "[token]",
|
||||||
|
expires_in: 1800
|
||||||
|
};
|
||||||
|
console.log("Waiting for callback to be added");
|
||||||
|
setTimeout(function(){
|
||||||
|
console.log("Calling a callback");
|
||||||
|
window[options.callback](true, responseData);
|
||||||
|
}, 500);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
window.externalApp.externalBus = function(message) {
|
||||||
|
console.log("External bus message: " + message);
|
||||||
|
var messageObj = JSON.parse(message);
|
||||||
|
if (messageObj.type == "config/get") {
|
||||||
|
var responseData = {
|
||||||
|
id: messageObj.id,
|
||||||
|
type: "result",
|
||||||
|
success: true,
|
||||||
|
result: {
|
||||||
|
hasSettingsScreen: true
|
||||||
|
}
|
||||||
|
};
|
||||||
|
setTimeout(function(){
|
||||||
|
window.externalBus(responseData);
|
||||||
|
}, 500);
|
||||||
|
} else if (messageObj.type == "config_screen/show") {
|
||||||
|
HAClient.postMessage('show-settings');
|
||||||
|
}
|
||||||
|
};
|
41
flutter_01.log
Normal file
41
flutter_01.log
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
Flutter crash report; please file at https://github.com/flutter/flutter/issues.
|
||||||
|
|
||||||
|
## command
|
||||||
|
|
||||||
|
flutter --no-color run --machine --track-widget-creation --device-id=89AY052S4 lib/main.dart
|
||||||
|
|
||||||
|
## exception
|
||||||
|
|
||||||
|
_InternalLinkedHashMap<String, dynamic>: {code: 105, message: Isolate must be runnable, data: {request: {method: _reloadSources, params: {pause: true, rootLibUri: file:///data/user/0/com.keyboardcrumbs.haclient/code_cache/ha_clientSYJJZI/ha_client/lib/main.dart.incremental.dill, packagesUri: file:///data/user/0/com.keyboardcrumbs.haclient/code_cache/ha_clientSYJJZI/ha_client/.packages, isolateId: isolates/68989666}}, details: Isolate must be runnable before this request is made.}}
|
||||||
|
|
||||||
|
```
|
||||||
|
null```
|
||||||
|
|
||||||
|
## flutter doctor
|
||||||
|
|
||||||
|
```
|
||||||
|
[✓] Flutter (Channel stable, v1.7.8+hotfix.4, on Linux, locale en_US.UTF-8)
|
||||||
|
• Flutter version 1.7.8+hotfix.4 at /home/estevez/sdk/flutter
|
||||||
|
• Framework revision 20e59316b8 (6 weeks ago), 2019-07-18 20:04:33 -0700
|
||||||
|
• Engine revision fee001c93f
|
||||||
|
• Dart version 2.4.0
|
||||||
|
|
||||||
|
[✓] Android toolchain - develop for Android devices (Android SDK version 29.0.2)
|
||||||
|
• Android SDK at /home/estevez/Android/Sdk
|
||||||
|
• Android NDK location not configured (optional; useful for native profiling support)
|
||||||
|
• Platform android-29, build-tools 29.0.2
|
||||||
|
• Java binary at: /home/estevez/bin/android-studio/jre/bin/java
|
||||||
|
• Java version OpenJDK Runtime Environment (build 1.8.0_202-release-1483-b49-5587405)
|
||||||
|
• All Android licenses accepted.
|
||||||
|
|
||||||
|
[✓] Android Studio (version 3.5)
|
||||||
|
• Android Studio at /home/estevez/bin/android-studio
|
||||||
|
• Flutter plugin version 38.2.3
|
||||||
|
• Dart plugin version 191.8423
|
||||||
|
• Java version OpenJDK Runtime Environment (build 1.8.0_202-release-1483-b49-5587405)
|
||||||
|
|
||||||
|
[✓] Connected device (1 available)
|
||||||
|
• Pixel 3 XL • 89AY052S4 • android-arm64 • Android 9 (API 28)
|
||||||
|
|
||||||
|
• No issues found!
|
||||||
|
```
|
41
flutter_02.log
Normal file
41
flutter_02.log
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
Flutter crash report; please file at https://github.com/flutter/flutter/issues.
|
||||||
|
|
||||||
|
## command
|
||||||
|
|
||||||
|
flutter --no-color run --machine --track-widget-creation --device-id=89AY052S4 lib/main.dart
|
||||||
|
|
||||||
|
## exception
|
||||||
|
|
||||||
|
_InternalLinkedHashMap<String, dynamic>: {code: 105, message: Isolate must be runnable, data: {request: {method: _reloadSources, params: {pause: true, rootLibUri: file:///data/user/0/com.keyboardcrumbs.haclient/code_cache/ha_clientWYMXDL/ha_client/lib/main.dart.incremental.dill, packagesUri: file:///data/user/0/com.keyboardcrumbs.haclient/code_cache/ha_clientWYMXDL/ha_client/.packages, isolateId: isolates/289688365}}, details: Isolate must be runnable before this request is made.}}
|
||||||
|
|
||||||
|
```
|
||||||
|
null```
|
||||||
|
|
||||||
|
## flutter doctor
|
||||||
|
|
||||||
|
```
|
||||||
|
[✓] Flutter (Channel stable, v1.7.8+hotfix.4, on Linux, locale en_US.UTF-8)
|
||||||
|
• Flutter version 1.7.8+hotfix.4 at /home/estevez/sdk/flutter
|
||||||
|
• Framework revision 20e59316b8 (6 weeks ago), 2019-07-18 20:04:33 -0700
|
||||||
|
• Engine revision fee001c93f
|
||||||
|
• Dart version 2.4.0
|
||||||
|
|
||||||
|
[✓] Android toolchain - develop for Android devices (Android SDK version 29.0.2)
|
||||||
|
• Android SDK at /home/estevez/Android/Sdk
|
||||||
|
• Android NDK location not configured (optional; useful for native profiling support)
|
||||||
|
• Platform android-29, build-tools 29.0.2
|
||||||
|
• Java binary at: /home/estevez/bin/android-studio/jre/bin/java
|
||||||
|
• Java version OpenJDK Runtime Environment (build 1.8.0_202-release-1483-b49-5587405)
|
||||||
|
• All Android licenses accepted.
|
||||||
|
|
||||||
|
[✓] Android Studio (version 3.5)
|
||||||
|
• Android Studio at /home/estevez/bin/android-studio
|
||||||
|
• Flutter plugin version 38.2.3
|
||||||
|
• Dart plugin version 191.8423
|
||||||
|
• Java version OpenJDK Runtime Environment (build 1.8.0_202-release-1483-b49-5587405)
|
||||||
|
|
||||||
|
[✓] Connected device (1 available)
|
||||||
|
• Pixel 3 XL • 89AY052S4 • android-arm64 • Android 9 (API 28)
|
||||||
|
|
||||||
|
• No issues found!
|
||||||
|
```
|
41
flutter_03.log
Normal file
41
flutter_03.log
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
Flutter crash report; please file at https://github.com/flutter/flutter/issues.
|
||||||
|
|
||||||
|
## command
|
||||||
|
|
||||||
|
flutter --no-color run --machine --track-widget-creation --device-id=89AY052S4 lib/main.dart
|
||||||
|
|
||||||
|
## exception
|
||||||
|
|
||||||
|
_InternalLinkedHashMap<String, dynamic>: {code: 105, message: Isolate must be runnable, data: {request: {method: _reloadSources, params: {pause: true, rootLibUri: file:///data/user/0/com.keyboardcrumbs.haclient/code_cache/ha_clientLNSJAH/ha_client/lib/main.dart.incremental.dill, packagesUri: file:///data/user/0/com.keyboardcrumbs.haclient/code_cache/ha_clientLNSJAH/ha_client/.packages, isolateId: isolates/866521062}}, details: Isolate must be runnable before this request is made.}}
|
||||||
|
|
||||||
|
```
|
||||||
|
null```
|
||||||
|
|
||||||
|
## flutter doctor
|
||||||
|
|
||||||
|
```
|
||||||
|
[✓] Flutter (Channel stable, v1.7.8+hotfix.4, on Linux, locale en_US.UTF-8)
|
||||||
|
• Flutter version 1.7.8+hotfix.4 at /home/estevez/sdk/flutter
|
||||||
|
• Framework revision 20e59316b8 (6 weeks ago), 2019-07-18 20:04:33 -0700
|
||||||
|
• Engine revision fee001c93f
|
||||||
|
• Dart version 2.4.0
|
||||||
|
|
||||||
|
[✓] Android toolchain - develop for Android devices (Android SDK version 29.0.2)
|
||||||
|
• Android SDK at /home/estevez/Android/Sdk
|
||||||
|
• Android NDK location not configured (optional; useful for native profiling support)
|
||||||
|
• Platform android-29, build-tools 29.0.2
|
||||||
|
• Java binary at: /home/estevez/bin/android-studio/jre/bin/java
|
||||||
|
• Java version OpenJDK Runtime Environment (build 1.8.0_202-release-1483-b49-5587405)
|
||||||
|
• All Android licenses accepted.
|
||||||
|
|
||||||
|
[✓] Android Studio (version 3.5)
|
||||||
|
• Android Studio at /home/estevez/bin/android-studio
|
||||||
|
• Flutter plugin version 38.2.3
|
||||||
|
• Dart plugin version 191.8423
|
||||||
|
• Java version OpenJDK Runtime Environment (build 1.8.0_202-release-1483-b49-5587405)
|
||||||
|
|
||||||
|
[✓] Connected device (1 available)
|
||||||
|
• Pixel 3 XL • 89AY052S4 • android-arm64 • Android 9 (API 28)
|
||||||
|
|
||||||
|
• No issues found!
|
||||||
|
```
|
Binary file not shown.
BIN
fonts/materialdesignicons-webfont-4.5.95.ttf
Normal file
BIN
fonts/materialdesignicons-webfont-4.5.95.ttf
Normal file
Binary file not shown.
62
lib/cards/alarm_panel_card.dart
Normal file
62
lib/cards/alarm_panel_card.dart
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
part of '../main.dart';
|
||||||
|
|
||||||
|
class AlarmPanelCard extends StatelessWidget {
|
||||||
|
final AlarmPanelCardData card;
|
||||||
|
|
||||||
|
const AlarmPanelCard({Key key, this.card}) : super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
if (card.entity.entity.statelessType == StatelessEntityType.missed) {
|
||||||
|
return EntityModel(
|
||||||
|
entityWrapper: card.entity,
|
||||||
|
child: MissedEntityWidget(),
|
||||||
|
handleTap: false,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
List<Widget> body = [];
|
||||||
|
body.add(CardHeader(
|
||||||
|
name: card.name ?? "",
|
||||||
|
subtitle: Text("${card.entity.entity.displayState}",
|
||||||
|
),
|
||||||
|
trailing: Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
mainAxisAlignment: MainAxisAlignment.end,
|
||||||
|
children: [
|
||||||
|
EntityIcon(
|
||||||
|
size: 50.0,
|
||||||
|
),
|
||||||
|
Container(
|
||||||
|
width: 26.0,
|
||||||
|
child: IconButton(
|
||||||
|
padding: EdgeInsets.all(0.0),
|
||||||
|
alignment: Alignment.centerRight,
|
||||||
|
icon: Icon(MaterialDesignIcons.getIconDataFromIconName(
|
||||||
|
"mdi:dots-vertical")),
|
||||||
|
onPressed: () => eventBus.fire(new ShowEntityPageEvent(entity: card.entity.entity))
|
||||||
|
)
|
||||||
|
)
|
||||||
|
]
|
||||||
|
),
|
||||||
|
));
|
||||||
|
body.add(
|
||||||
|
AlarmControlPanelControlsWidget(
|
||||||
|
extended: true,
|
||||||
|
states: card.states,
|
||||||
|
)
|
||||||
|
);
|
||||||
|
return CardWrapper(
|
||||||
|
child: EntityModel(
|
||||||
|
entityWrapper: card.entity,
|
||||||
|
handleTap: null,
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: body
|
||||||
|
)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
537
lib/cards/card.class.dart
Normal file
537
lib/cards/card.class.dart
Normal file
@ -0,0 +1,537 @@
|
|||||||
|
part of '../main.dart';
|
||||||
|
|
||||||
|
class CardData {
|
||||||
|
|
||||||
|
String type;
|
||||||
|
List<EntityWrapper> entities = [];
|
||||||
|
List conditions;
|
||||||
|
bool showEmpty;
|
||||||
|
List stateFilter;
|
||||||
|
bool stateColor = true;
|
||||||
|
|
||||||
|
EntityWrapper get entity => entities.isNotEmpty ? entities[0] : null;
|
||||||
|
|
||||||
|
factory CardData.parse(Map<String, dynamic> rawData) {
|
||||||
|
try {
|
||||||
|
switch (rawData['type']) {
|
||||||
|
case CardType.ENTITIES:
|
||||||
|
return EntitiesCardData(rawData);
|
||||||
|
break;
|
||||||
|
case CardType.ALARM_PANEL:
|
||||||
|
return AlarmPanelCardData(rawData);
|
||||||
|
break;
|
||||||
|
case CardType.BUTTON:
|
||||||
|
return ButtonCardData(rawData);
|
||||||
|
break;
|
||||||
|
case CardType.ENTITY_BUTTON:
|
||||||
|
return ButtonCardData(rawData);
|
||||||
|
break;
|
||||||
|
case CardType.CONDITIONAL:
|
||||||
|
return CardData.parse(rawData['card']);
|
||||||
|
break;
|
||||||
|
case CardType.ENTITY_FILTER:
|
||||||
|
Map<String, dynamic> cardData = Map.from(rawData);
|
||||||
|
cardData.remove('type');
|
||||||
|
if (rawData.containsKey('card')) {
|
||||||
|
cardData.addAll(rawData['card']);
|
||||||
|
}
|
||||||
|
cardData['type'] ??= CardType.ENTITIES;
|
||||||
|
return CardData.parse(cardData);
|
||||||
|
break;
|
||||||
|
case CardType.GAUGE:
|
||||||
|
return GaugeCardData(rawData);
|
||||||
|
break;
|
||||||
|
case CardType.GLANCE:
|
||||||
|
return GlanceCardData(rawData);
|
||||||
|
break;
|
||||||
|
case CardType.HORIZONTAL_STACK:
|
||||||
|
return HorizontalStackCardData(rawData);
|
||||||
|
break;
|
||||||
|
case CardType.VERTICAL_STACK:
|
||||||
|
return VerticalStackCardData(rawData);
|
||||||
|
break;
|
||||||
|
case CardType.MARKDOWN:
|
||||||
|
return MarkdownCardData(rawData);
|
||||||
|
break;
|
||||||
|
case CardType.MEDIA_CONTROL:
|
||||||
|
return MediaControlCardData(rawData);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
if (rawData.containsKey('entities')) {
|
||||||
|
return EntitiesCardData(rawData);
|
||||||
|
} else if (rawData.containsKey('entity')) {
|
||||||
|
rawData['entities'] = [rawData['entity']];
|
||||||
|
return EntitiesCardData(rawData);
|
||||||
|
}
|
||||||
|
return CardData(rawData);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
Logger.e('Error parsing card: $error');
|
||||||
|
return ErrorCardData(rawData);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
CardData(Map<String, dynamic> rawData) {
|
||||||
|
if (rawData != null) {
|
||||||
|
type = rawData['type'] ?? CardType.ENTITIES;
|
||||||
|
conditions = rawData['conditions'] ?? [];
|
||||||
|
showEmpty = rawData['show_empty'] ?? true;
|
||||||
|
stateFilter = rawData['state_filter'] ?? [];
|
||||||
|
} else {
|
||||||
|
type = CardType.UNKNOWN;
|
||||||
|
conditions = [];
|
||||||
|
showEmpty = true;
|
||||||
|
stateFilter = [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget buildCardWidget() {
|
||||||
|
return UnsupportedCard(card: this);
|
||||||
|
}
|
||||||
|
|
||||||
|
List<EntityWrapper> getEntitiesToShow() {
|
||||||
|
return entities.where((entityWrapper) {
|
||||||
|
if (HomeAssistant().autoUi && entityWrapper.entity.isHidden) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
List currentStateFilter;
|
||||||
|
if (entityWrapper.stateFilter != null && entityWrapper.stateFilter.isNotEmpty) {
|
||||||
|
currentStateFilter = entityWrapper.stateFilter;
|
||||||
|
} else {
|
||||||
|
currentStateFilter = stateFilter;
|
||||||
|
}
|
||||||
|
bool showByFilter = currentStateFilter.isEmpty;
|
||||||
|
for (var allowedState in currentStateFilter) {
|
||||||
|
if (allowedState is String && allowedState == entityWrapper.entity.state) {
|
||||||
|
showByFilter = true;
|
||||||
|
break;
|
||||||
|
} else if (allowedState is Map) {
|
||||||
|
try {
|
||||||
|
var tmpVal = allowedState['attribute'] != null ? entityWrapper.entity.getAttribute(allowedState['attribute']) : entityWrapper.entity.state;
|
||||||
|
var valToCompareWith = allowedState['value'];
|
||||||
|
var valToCompare;
|
||||||
|
if (valToCompareWith is! String && tmpVal is String) {
|
||||||
|
valToCompare = double.tryParse(tmpVal);
|
||||||
|
} else {
|
||||||
|
valToCompare = tmpVal;
|
||||||
|
}
|
||||||
|
if (valToCompare != null) {
|
||||||
|
bool result;
|
||||||
|
switch (allowedState['operator']) {
|
||||||
|
case '<=': { result = valToCompare <= valToCompareWith;}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case '<': { result = valToCompare < valToCompareWith;}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case '>=': { result = valToCompare >= valToCompareWith;}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case '>': { result = valToCompare > valToCompareWith;}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case '!=': { result = valToCompare != valToCompareWith;}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'regex': {
|
||||||
|
RegExp regExp = RegExp(valToCompareWith.toString());
|
||||||
|
result = regExp.hasMatch(valToCompare.toString());
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
default: {
|
||||||
|
result = valToCompare == valToCompareWith;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (result) {
|
||||||
|
showByFilter = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
Logger.e('Error filtering ${entityWrapper.entity.entityId} by $allowedState');
|
||||||
|
Logger.e('$e');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return showByFilter;
|
||||||
|
}).toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
class EntitiesCardData extends CardData {
|
||||||
|
|
||||||
|
String title;
|
||||||
|
String icon;
|
||||||
|
bool showHeaderToggle;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget buildCardWidget() {
|
||||||
|
return EntitiesCard(card: this);
|
||||||
|
}
|
||||||
|
|
||||||
|
EntitiesCardData(Map<String, dynamic> rawData) : super(rawData) {
|
||||||
|
//Parsing card data
|
||||||
|
title = rawData["title"];
|
||||||
|
icon = rawData['icon'];
|
||||||
|
stateColor = rawData['state_color'] ?? false;
|
||||||
|
showHeaderToggle = rawData['show_header_toggle'] ?? false;
|
||||||
|
//Parsing entities
|
||||||
|
var rawEntities = rawData["entities"] ?? [];
|
||||||
|
rawEntities.forEach((rawEntity) {
|
||||||
|
if (rawEntity is String) {
|
||||||
|
if (HomeAssistant().entities.isExist(rawEntity)) {
|
||||||
|
entities.add(EntityWrapper(entity: HomeAssistant().entities.get(rawEntity)));
|
||||||
|
} else {
|
||||||
|
entities.add(EntityWrapper(entity: Entity.missed(rawEntity)));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (rawEntity["type"] == "divider") {
|
||||||
|
entities.add(EntityWrapper(entity: Entity.divider()));
|
||||||
|
} else if (rawEntity["type"] == "section") {
|
||||||
|
entities.add(EntityWrapper(entity: Entity.section(rawEntity["label"] ?? "")));
|
||||||
|
} else if (rawEntity["type"] == "call-service") {
|
||||||
|
Map uiActionData = {
|
||||||
|
"tap_action": {
|
||||||
|
"action": EntityUIAction.callService,
|
||||||
|
"service": rawEntity["service"],
|
||||||
|
"service_data": rawEntity["service_data"]
|
||||||
|
},
|
||||||
|
"hold_action": EntityUIAction.none
|
||||||
|
};
|
||||||
|
entities.add(
|
||||||
|
EntityWrapper(
|
||||||
|
entity: Entity.callService(
|
||||||
|
icon: rawEntity["icon"],
|
||||||
|
name: rawEntity["name"],
|
||||||
|
service: rawEntity["service"],
|
||||||
|
actionName: rawEntity["action_name"]
|
||||||
|
),
|
||||||
|
stateColor: rawEntity["state_color"] ?? stateColor,
|
||||||
|
uiAction: EntityUIAction(rawEntityData: uiActionData)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
} else if (rawEntity["type"] == "weblink") {
|
||||||
|
Map uiActionData = {
|
||||||
|
"tap_action": {
|
||||||
|
"action": EntityUIAction.navigate,
|
||||||
|
"service": rawEntity["url"]
|
||||||
|
},
|
||||||
|
"hold_action": EntityUIAction.none
|
||||||
|
};
|
||||||
|
entities.add(EntityWrapper(
|
||||||
|
entity: Entity.weblink(
|
||||||
|
icon: rawEntity["icon"],
|
||||||
|
name: rawEntity["name"],
|
||||||
|
url: rawEntity["url"]
|
||||||
|
),
|
||||||
|
stateColor: rawEntity["state_color"] ?? stateColor,
|
||||||
|
uiAction: EntityUIAction(rawEntityData: uiActionData)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
} else if (HomeAssistant().entities.isExist(rawEntity["entity"])) {
|
||||||
|
Entity e = HomeAssistant().entities.get(rawEntity["entity"]);
|
||||||
|
entities.add(
|
||||||
|
EntityWrapper(
|
||||||
|
entity: e,
|
||||||
|
stateColor: rawEntity["state_color"] ?? stateColor,
|
||||||
|
overrideName: rawEntity["name"],
|
||||||
|
overrideIcon: rawEntity["icon"],
|
||||||
|
stateFilter: rawEntity['state_filter'] ?? [],
|
||||||
|
uiAction: EntityUIAction(rawEntityData: rawEntity)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
entities.add(EntityWrapper(entity: Entity.missed(rawEntity["entity"])));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
class AlarmPanelCardData extends CardData {
|
||||||
|
|
||||||
|
String name;
|
||||||
|
List<dynamic> states;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget buildCardWidget() {
|
||||||
|
return AlarmPanelCard(card: this);
|
||||||
|
}
|
||||||
|
|
||||||
|
AlarmPanelCardData(Map<String, dynamic> rawData) : super(rawData) {
|
||||||
|
//Parsing card data
|
||||||
|
name = rawData['name'];
|
||||||
|
states = rawData['states'];
|
||||||
|
//Parsing entity
|
||||||
|
var entitiId = rawData["entity"];
|
||||||
|
if (entitiId != null && entitiId is String) {
|
||||||
|
if (HomeAssistant().entities.isExist(entitiId)) {
|
||||||
|
entities.add(EntityWrapper(
|
||||||
|
entity: HomeAssistant().entities.get(entitiId),
|
||||||
|
stateColor: true,
|
||||||
|
overrideName: name
|
||||||
|
));
|
||||||
|
} else {
|
||||||
|
entities.add(EntityWrapper(entity: Entity.missed(entitiId)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
class ButtonCardData extends CardData {
|
||||||
|
|
||||||
|
String name;
|
||||||
|
String icon;
|
||||||
|
bool showName;
|
||||||
|
bool showIcon;
|
||||||
|
double iconHeightPx = 0;
|
||||||
|
double iconHeightRem = 0;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget buildCardWidget() {
|
||||||
|
return EntityButtonCard(card: this);
|
||||||
|
}
|
||||||
|
|
||||||
|
ButtonCardData(Map<String, dynamic> rawData) : super(rawData) {
|
||||||
|
//Parsing card data
|
||||||
|
name = rawData['name'];
|
||||||
|
icon = rawData['icon'];
|
||||||
|
showName = rawData['show_name'] ?? true;
|
||||||
|
showIcon = rawData['show_icon'] ?? true;
|
||||||
|
stateColor = rawData['state_color'] ?? true;
|
||||||
|
var rawHeight = rawData['icon_height'];
|
||||||
|
if (rawHeight != null && rawHeight is String) {
|
||||||
|
if (rawHeight.contains('px')) {
|
||||||
|
iconHeightPx = double.tryParse(rawHeight.replaceFirst('px', '')) ?? 0;
|
||||||
|
} else if (rawHeight.contains('rem')) {
|
||||||
|
iconHeightRem = double.tryParse(rawHeight.replaceFirst('rem', '')) ?? 0;
|
||||||
|
} else if (rawHeight.contains('em')) {
|
||||||
|
iconHeightRem = double.tryParse(rawHeight.replaceFirst('em', '')) ?? 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
//Parsing entity
|
||||||
|
var entitiId = rawData["entity"];
|
||||||
|
if (entitiId != null && entitiId is String) {
|
||||||
|
if (HomeAssistant().entities.isExist(entitiId)) {
|
||||||
|
entities.add(EntityWrapper(
|
||||||
|
entity: HomeAssistant().entities.get(entitiId),
|
||||||
|
overrideName: name,
|
||||||
|
overrideIcon: icon,
|
||||||
|
stateColor: stateColor,
|
||||||
|
uiAction: EntityUIAction(
|
||||||
|
rawEntityData: rawData
|
||||||
|
)
|
||||||
|
));
|
||||||
|
} else {
|
||||||
|
entities.add(EntityWrapper(entity: Entity.missed(entitiId)));
|
||||||
|
}
|
||||||
|
} else if (entitiId == null) {
|
||||||
|
entities.add(
|
||||||
|
EntityWrapper(
|
||||||
|
entity: Entity.ghost(
|
||||||
|
name,
|
||||||
|
icon,
|
||||||
|
),
|
||||||
|
stateColor: stateColor,
|
||||||
|
uiAction: EntityUIAction(
|
||||||
|
rawEntityData: rawData
|
||||||
|
)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class GaugeCardData extends CardData {
|
||||||
|
|
||||||
|
String name;
|
||||||
|
String unit;
|
||||||
|
int min;
|
||||||
|
int max;
|
||||||
|
Map severity;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget buildCardWidget() {
|
||||||
|
return GaugeCard(card: this);
|
||||||
|
}
|
||||||
|
|
||||||
|
GaugeCardData(Map<String, dynamic> rawData) : super(rawData) {
|
||||||
|
//Parsing card data
|
||||||
|
name = rawData['name'];
|
||||||
|
unit = rawData['unit'];
|
||||||
|
min = rawData['min'] ?? 0;
|
||||||
|
max = rawData['max'] ?? 100;
|
||||||
|
severity = rawData['severity'];
|
||||||
|
//Parsing entity
|
||||||
|
var entitiId = rawData["entity"];
|
||||||
|
if (entitiId != null && entitiId is String) {
|
||||||
|
if (HomeAssistant().entities.isExist(entitiId)) {
|
||||||
|
entities.add(EntityWrapper(
|
||||||
|
entity: HomeAssistant().entities.get(entitiId),
|
||||||
|
overrideName: name
|
||||||
|
));
|
||||||
|
} else {
|
||||||
|
entities.add(EntityWrapper(entity: Entity.missed(entitiId)));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
entities.add(EntityWrapper(entity: Entity.missed(entitiId)));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
class GlanceCardData extends CardData {
|
||||||
|
|
||||||
|
String title;
|
||||||
|
bool showName;
|
||||||
|
bool showIcon;
|
||||||
|
bool showState;
|
||||||
|
bool stateColor;
|
||||||
|
int columnsCount;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget buildCardWidget() {
|
||||||
|
return GlanceCard(card: this);
|
||||||
|
}
|
||||||
|
|
||||||
|
GlanceCardData(Map<String, dynamic> rawData) : super(rawData) {
|
||||||
|
//Parsing card data
|
||||||
|
title = rawData["title"];
|
||||||
|
showName = rawData['show_name'] ?? true;
|
||||||
|
showIcon = rawData['show_icon'] ?? true;
|
||||||
|
showState = rawData['show_state'] ?? true;
|
||||||
|
stateColor = rawData['state_color'] ?? true;
|
||||||
|
columnsCount = rawData['columns'] ?? 4;
|
||||||
|
//Parsing entities
|
||||||
|
var rawEntities = rawData["entities"] ?? [];
|
||||||
|
rawEntities.forEach((rawEntity) {
|
||||||
|
if (rawEntity is String) {
|
||||||
|
if (HomeAssistant().entities.isExist(rawEntity)) {
|
||||||
|
entities.add(EntityWrapper(entity: HomeAssistant().entities.get(rawEntity)));
|
||||||
|
} else {
|
||||||
|
entities.add(EntityWrapper(entity: Entity.missed(rawEntity)));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (HomeAssistant().entities.isExist(rawEntity["entity"])) {
|
||||||
|
Entity e = HomeAssistant().entities.get(rawEntity["entity"]);
|
||||||
|
entities.add(
|
||||||
|
EntityWrapper(
|
||||||
|
entity: e,
|
||||||
|
stateColor: stateColor,
|
||||||
|
overrideName: rawEntity["name"],
|
||||||
|
overrideIcon: rawEntity["icon"],
|
||||||
|
stateFilter: rawEntity['state_filter'] ?? [],
|
||||||
|
uiAction: EntityUIAction(rawEntityData: rawEntity)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
entities.add(EntityWrapper(entity: Entity.missed(rawEntity["entity"])));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
class HorizontalStackCardData extends CardData {
|
||||||
|
|
||||||
|
List<CardData> childCards;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget buildCardWidget() {
|
||||||
|
return HorizontalStackCard(card: this);
|
||||||
|
}
|
||||||
|
|
||||||
|
HorizontalStackCardData(Map<String, dynamic> rawData) : super(rawData) {
|
||||||
|
if (rawData.containsKey('cards')) {
|
||||||
|
childCards = rawData['cards'].map<CardData>((childCard) {
|
||||||
|
return CardData.parse(childCard);
|
||||||
|
}).toList();
|
||||||
|
} else {
|
||||||
|
childCards = [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
class VerticalStackCardData extends CardData {
|
||||||
|
|
||||||
|
List<CardData> childCards;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget buildCardWidget() {
|
||||||
|
return VerticalStackCard(card: this);
|
||||||
|
}
|
||||||
|
|
||||||
|
VerticalStackCardData(Map<String, dynamic> rawData) : super(rawData) {
|
||||||
|
if (rawData.containsKey('cards')) {
|
||||||
|
childCards = rawData['cards'].map<CardData>((childCard) {
|
||||||
|
return CardData.parse(childCard);
|
||||||
|
}).toList();
|
||||||
|
} else {
|
||||||
|
childCards = [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
class MarkdownCardData extends CardData {
|
||||||
|
|
||||||
|
String title;
|
||||||
|
String content;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget buildCardWidget() {
|
||||||
|
return MarkdownCard(card: this);
|
||||||
|
}
|
||||||
|
|
||||||
|
MarkdownCardData(Map<String, dynamic> rawData) : super(rawData) {
|
||||||
|
//Parsing card data
|
||||||
|
title = rawData['title'];
|
||||||
|
content = rawData['content'];
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
class MediaControlCardData extends CardData {
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget buildCardWidget() {
|
||||||
|
return MediaControlsCard(card: this);
|
||||||
|
}
|
||||||
|
|
||||||
|
MediaControlCardData(Map<String, dynamic> rawData) : super(rawData) {
|
||||||
|
var entitiId = rawData["entity"];
|
||||||
|
if (entitiId != null && entitiId is String) {
|
||||||
|
if (HomeAssistant().entities.isExist(entitiId)) {
|
||||||
|
entities.add(EntityWrapper(
|
||||||
|
entity: HomeAssistant().entities.get(entitiId),
|
||||||
|
));
|
||||||
|
} else {
|
||||||
|
entities.add(EntityWrapper(entity: Entity.missed(entitiId)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
class ErrorCardData extends CardData {
|
||||||
|
|
||||||
|
String cardConfig;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget buildCardWidget() {
|
||||||
|
return ErrorCard(card: this);
|
||||||
|
}
|
||||||
|
|
||||||
|
ErrorCardData(Map<String, dynamic> rawData) : super(rawData) {
|
||||||
|
cardConfig = '$rawData';
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
79
lib/cards/entities_card.dart
Normal file
79
lib/cards/entities_card.dart
Normal file
@ -0,0 +1,79 @@
|
|||||||
|
part of '../main.dart';
|
||||||
|
|
||||||
|
class EntitiesCard extends StatelessWidget {
|
||||||
|
final EntitiesCardData card;
|
||||||
|
|
||||||
|
const EntitiesCard({Key key, this.card}) : super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
List<EntityWrapper> entitiesToShow = card.getEntitiesToShow();
|
||||||
|
if (entitiesToShow.isEmpty && !card.showEmpty) {
|
||||||
|
return Container(height: 0.0, width: 0.0,);
|
||||||
|
}
|
||||||
|
List<Widget> body = [];
|
||||||
|
Widget headerSwitch;
|
||||||
|
if (card.showHeaderToggle) {
|
||||||
|
bool headerToggleVal = entitiesToShow.any((EntityWrapper en){ return en.entity.state == EntityState.on; });
|
||||||
|
List<String> entitiesToToggle = entitiesToShow.where((EntityWrapper enw) {
|
||||||
|
return <String>["switch", "light", "automation", "input_boolean"].contains(enw.entity.domain);
|
||||||
|
}).map((EntityWrapper en) {
|
||||||
|
return en.entity.entityId;
|
||||||
|
}).toList();
|
||||||
|
headerSwitch = Switch(
|
||||||
|
value: headerToggleVal,
|
||||||
|
onChanged: (val) {
|
||||||
|
if (entitiesToToggle.isNotEmpty) {
|
||||||
|
ConnectionManager().callService(
|
||||||
|
domain: "homeassistant",
|
||||||
|
service: val ? "turn_on" : "turn_off",
|
||||||
|
entityId: entitiesToToggle
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
body.add(
|
||||||
|
CardHeader(
|
||||||
|
name: card.title,
|
||||||
|
trailing: headerSwitch,
|
||||||
|
emptyPadding: Sizes.rowPadding,
|
||||||
|
leading: card.icon != null ? Icon(
|
||||||
|
MaterialDesignIcons.getIconDataFromIconName(card.icon),
|
||||||
|
size: Sizes.iconSize,
|
||||||
|
color: Theme.of(context).textTheme.headline.color
|
||||||
|
) : null,
|
||||||
|
)
|
||||||
|
);
|
||||||
|
body.addAll(
|
||||||
|
entitiesToShow.map((EntityWrapper entity) {
|
||||||
|
return Padding(
|
||||||
|
padding: EdgeInsets.fromLTRB(0.0, 4.0, 0.0, 4.0),
|
||||||
|
child: EntityModel(
|
||||||
|
entityWrapper: entity,
|
||||||
|
handleTap: true,
|
||||||
|
child: entity.entity.buildDefaultWidget(context)
|
||||||
|
),
|
||||||
|
);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
return CardWrapper(
|
||||||
|
child: Padding(
|
||||||
|
padding: EdgeInsets.only(
|
||||||
|
right: Sizes.rightWidgetPadding,
|
||||||
|
left: Sizes.leftWidgetPadding,
|
||||||
|
bottom: Sizes.rowPadding,
|
||||||
|
),
|
||||||
|
child: Center(
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
mainAxisAlignment: MainAxisAlignment.start,
|
||||||
|
children: body
|
||||||
|
)
|
||||||
|
),
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
93
lib/cards/entity_button_card.dart
Normal file
93
lib/cards/entity_button_card.dart
Normal file
@ -0,0 +1,93 @@
|
|||||||
|
part of '../main.dart';
|
||||||
|
|
||||||
|
class EntityButtonCard extends StatelessWidget {
|
||||||
|
|
||||||
|
final ButtonCardData card;
|
||||||
|
|
||||||
|
EntityButtonCard({
|
||||||
|
Key key, this.card
|
||||||
|
}) : super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
EntityWrapper entityWrapper = card.entity;
|
||||||
|
if (entityWrapper.entity.statelessType == StatelessEntityType.missed) {
|
||||||
|
return EntityModel(
|
||||||
|
entityWrapper: card.entity,
|
||||||
|
child: MissedEntityWidget(),
|
||||||
|
handleTap: false,
|
||||||
|
);
|
||||||
|
} else if (entityWrapper.entity.statelessType != StatelessEntityType.ghost && entityWrapper.entity.statelessType != StatelessEntityType.none) {
|
||||||
|
return Container(width: 0.0, height: 0.0,);
|
||||||
|
}
|
||||||
|
|
||||||
|
double iconSize = math.max(card.iconHeightPx, card.iconHeightRem * Theme.of(context).textTheme.body1.fontSize);
|
||||||
|
|
||||||
|
Widget buttonIcon;
|
||||||
|
if (!card.showIcon) {
|
||||||
|
buttonIcon = Container(height: Sizes.rowPadding, width: 10);
|
||||||
|
} else if (iconSize > 0) {
|
||||||
|
buttonIcon = SizedBox(
|
||||||
|
height: iconSize,
|
||||||
|
child: FractionallySizedBox(
|
||||||
|
widthFactor: 0.5,
|
||||||
|
child: FittedBox(
|
||||||
|
fit: BoxFit.contain,
|
||||||
|
child: EntityIcon(
|
||||||
|
//padding: EdgeInsets.only(top: 6),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
buttonIcon = AspectRatio(
|
||||||
|
aspectRatio: 2,
|
||||||
|
child: FractionallySizedBox(
|
||||||
|
widthFactor: 0.5,
|
||||||
|
child: FittedBox(
|
||||||
|
fit: BoxFit.fitWidth,
|
||||||
|
child: EntityIcon(
|
||||||
|
//padding: EdgeInsets.only(top: 6),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return CardWrapper(
|
||||||
|
child: EntityModel(
|
||||||
|
entityWrapper: card.entity,
|
||||||
|
child: InkWell(
|
||||||
|
onTap: () => entityWrapper.handleTap(),
|
||||||
|
onLongPress: () => entityWrapper.handleHold(),
|
||||||
|
onDoubleTap: () => entityWrapper.handleDoubleTap(),
|
||||||
|
child: Center(
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
|
children: <Widget>[
|
||||||
|
buttonIcon,
|
||||||
|
_buildName(context)
|
||||||
|
],
|
||||||
|
)
|
||||||
|
),
|
||||||
|
),
|
||||||
|
handleTap: true
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildName(BuildContext context) {
|
||||||
|
if (card.showName) {
|
||||||
|
return EntityName(
|
||||||
|
padding: EdgeInsets.fromLTRB(Sizes.buttonPadding, 0.0, Sizes.buttonPadding, Sizes.rowPadding),
|
||||||
|
textOverflow: TextOverflow.ellipsis,
|
||||||
|
maxLines: 3,
|
||||||
|
textStyle: Theme.of(context).textTheme.subhead,
|
||||||
|
wordsWrap: true,
|
||||||
|
textAlign: TextAlign.center
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return Container(width: 0, height: 0);
|
||||||
|
}
|
||||||
|
}
|
38
lib/cards/error_card.dart
Normal file
38
lib/cards/error_card.dart
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
part of '../main.dart';
|
||||||
|
|
||||||
|
class ErrorCard extends StatelessWidget {
|
||||||
|
final ErrorCardData card;
|
||||||
|
|
||||||
|
const ErrorCard({Key key, this.card}) : super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return CardWrapper(
|
||||||
|
child: Padding(
|
||||||
|
padding: EdgeInsets.fromLTRB(Sizes.leftWidgetPadding, Sizes.rowPadding, Sizes.rightWidgetPadding, Sizes.rowPadding),
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
|
children: <Widget>[
|
||||||
|
Text(
|
||||||
|
'There was an error rendering card: ${card.type}. Please copy card config to clipboard and report this issue. Thanks!',
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
),
|
||||||
|
RaisedButton(
|
||||||
|
onPressed: () {
|
||||||
|
Clipboard.setData(new ClipboardData(text: card.cardConfig));
|
||||||
|
},
|
||||||
|
child: Text('Copy card config'),
|
||||||
|
),
|
||||||
|
RaisedButton(
|
||||||
|
onPressed: () {
|
||||||
|
Launcher.launchURL("https://github.com/estevez-dev/ha_client/issues/new?assignees=&labels=&template=bug_report.md&title=");
|
||||||
|
},
|
||||||
|
child: Text('Report issue'),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
198
lib/cards/gauge_card.dart
Normal file
198
lib/cards/gauge_card.dart
Normal file
@ -0,0 +1,198 @@
|
|||||||
|
part of '../main.dart';
|
||||||
|
|
||||||
|
class GaugeCard extends StatelessWidget {
|
||||||
|
|
||||||
|
final GaugeCardData card;
|
||||||
|
|
||||||
|
GaugeCard({Key key, this.card}) : super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
EntityWrapper entityWrapper = card.entity;
|
||||||
|
if (entityWrapper.entity.statelessType == StatelessEntityType.missed) {
|
||||||
|
return EntityModel(
|
||||||
|
entityWrapper: card.entity,
|
||||||
|
child: MissedEntityWidget(),
|
||||||
|
handleTap: false,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
entityWrapper.overrideName = card.name ??
|
||||||
|
entityWrapper.displayName;
|
||||||
|
entityWrapper.unitOfMeasurementOverride = card.unit ??
|
||||||
|
entityWrapper.unitOfMeasurement;
|
||||||
|
double fixedValue;
|
||||||
|
double value = entityWrapper.entity.doubleState;
|
||||||
|
if (value > card.max) {
|
||||||
|
fixedValue = card.max.toDouble();
|
||||||
|
} else if (value < card.min) {
|
||||||
|
fixedValue = card.min.toDouble();
|
||||||
|
} else {
|
||||||
|
fixedValue = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
List<GaugeRange> ranges;
|
||||||
|
Color currentColor;
|
||||||
|
if (card.severity != null && card.severity["green"] is int && card.severity["red"] is int && card.severity["yellow"] is int) {
|
||||||
|
List<RangeContainer> rangesList = <RangeContainer>[
|
||||||
|
RangeContainer(card.severity["green"], HAClientTheme().getGreenGaugeColor()),
|
||||||
|
RangeContainer(card.severity["red"], HAClientTheme().getRedGaugeColor()),
|
||||||
|
RangeContainer(card.severity["yellow"], HAClientTheme().getYellowGaugeColor())
|
||||||
|
];
|
||||||
|
rangesList.sort((current, next) {
|
||||||
|
if (current.startFrom > next.startFrom) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
if (current.startFrom < next.startFrom) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (fixedValue < rangesList[1].startFrom) {
|
||||||
|
currentColor = rangesList[0].color;
|
||||||
|
} else if (fixedValue < rangesList[2].startFrom && fixedValue >= rangesList[1].startFrom) {
|
||||||
|
currentColor = rangesList[1].color;
|
||||||
|
} else {
|
||||||
|
currentColor = rangesList[2].color;
|
||||||
|
}
|
||||||
|
|
||||||
|
ranges = [
|
||||||
|
GaugeRange(
|
||||||
|
startValue: rangesList[0].startFrom.toDouble(),
|
||||||
|
endValue: rangesList[1].startFrom.toDouble(),
|
||||||
|
color: rangesList[0].color.withOpacity(0.1),
|
||||||
|
sizeUnit: GaugeSizeUnit.factor,
|
||||||
|
endWidth: 0.3,
|
||||||
|
startWidth: 0.3
|
||||||
|
),
|
||||||
|
GaugeRange(
|
||||||
|
startValue: rangesList[1].startFrom.toDouble(),
|
||||||
|
endValue: rangesList[2].startFrom.toDouble(),
|
||||||
|
color: rangesList[1].color.withOpacity(0.1),
|
||||||
|
sizeUnit: GaugeSizeUnit.factor,
|
||||||
|
endWidth: 0.3,
|
||||||
|
startWidth: 0.3
|
||||||
|
),
|
||||||
|
GaugeRange(
|
||||||
|
startValue: rangesList[2].startFrom.toDouble(),
|
||||||
|
endValue: card.max.toDouble(),
|
||||||
|
color: rangesList[2].color.withOpacity(0.1),
|
||||||
|
sizeUnit: GaugeSizeUnit.factor,
|
||||||
|
endWidth: 0.3,
|
||||||
|
startWidth: 0.3
|
||||||
|
)
|
||||||
|
];
|
||||||
|
}
|
||||||
|
if (ranges == null) {
|
||||||
|
currentColor = Theme.of(context).primaryColorDark;
|
||||||
|
ranges = <GaugeRange>[
|
||||||
|
GaugeRange(
|
||||||
|
startValue: card.min.toDouble(),
|
||||||
|
endValue: card.max.toDouble(),
|
||||||
|
color: Theme.of(context).primaryColorDark.withOpacity(0.1),
|
||||||
|
sizeUnit: GaugeSizeUnit.factor,
|
||||||
|
endWidth: 0.3,
|
||||||
|
startWidth: 0.3,
|
||||||
|
)
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
return CardWrapper(
|
||||||
|
padding: EdgeInsets.all(4),
|
||||||
|
child: EntityModel(
|
||||||
|
entityWrapper: entityWrapper,
|
||||||
|
child: InkWell(
|
||||||
|
onTap: () => entityWrapper.handleTap(),
|
||||||
|
onLongPress: () => entityWrapper.handleHold(),
|
||||||
|
onDoubleTap: () => entityWrapper.handleDoubleTap(),
|
||||||
|
child: AspectRatio(
|
||||||
|
aspectRatio: 1.8,
|
||||||
|
child: Stack(
|
||||||
|
alignment: Alignment.bottomCenter,
|
||||||
|
children: <Widget>[
|
||||||
|
SfRadialGauge(
|
||||||
|
axes: <RadialAxis>[
|
||||||
|
RadialAxis(
|
||||||
|
maximum: card.max.toDouble(),
|
||||||
|
minimum: card.min.toDouble(),
|
||||||
|
showLabels: false,
|
||||||
|
useRangeColorForAxis: true,
|
||||||
|
showTicks: false,
|
||||||
|
canScaleToFit: true,
|
||||||
|
ranges: ranges,
|
||||||
|
axisLineStyle: AxisLineStyle(
|
||||||
|
thickness: 0.3,
|
||||||
|
thicknessUnit: GaugeSizeUnit.factor,
|
||||||
|
color: Colors.transparent
|
||||||
|
),
|
||||||
|
startAngle: 180,
|
||||||
|
endAngle: 0,
|
||||||
|
pointers: <GaugePointer>[
|
||||||
|
RangePointer(
|
||||||
|
value: fixedValue,
|
||||||
|
sizeUnit: GaugeSizeUnit.factor,
|
||||||
|
width: 0.3,
|
||||||
|
color: currentColor,
|
||||||
|
enableAnimation: true,
|
||||||
|
animationType: AnimationType.bounceOut,
|
||||||
|
)
|
||||||
|
]
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
Column(
|
||||||
|
mainAxisSize: MainAxisSize.max,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
|
children: <Widget>[
|
||||||
|
Flexible(
|
||||||
|
flex: 8,
|
||||||
|
fit: FlexFit.tight,
|
||||||
|
child: Container()
|
||||||
|
),
|
||||||
|
Flexible(
|
||||||
|
flex: 6,
|
||||||
|
fit: FlexFit.tight,
|
||||||
|
child: FractionallySizedBox(
|
||||||
|
widthFactor: 0.4,
|
||||||
|
child: FittedBox(
|
||||||
|
fit: BoxFit.contain,
|
||||||
|
alignment: Alignment.bottomCenter,
|
||||||
|
child: SimpleEntityState(
|
||||||
|
padding: EdgeInsets.all(0),
|
||||||
|
expanded: false,
|
||||||
|
maxLines: 1,
|
||||||
|
textAlign: TextAlign.center
|
||||||
|
),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
),
|
||||||
|
Flexible(
|
||||||
|
flex: 3,
|
||||||
|
fit: FlexFit.tight,
|
||||||
|
child: FittedBox(
|
||||||
|
fit: BoxFit.contain,
|
||||||
|
child: EntityName(
|
||||||
|
padding: EdgeInsets.all(0),
|
||||||
|
textStyle: Theme.of(context).textTheme.subhead
|
||||||
|
),
|
||||||
|
)
|
||||||
|
),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
],
|
||||||
|
)
|
||||||
|
),
|
||||||
|
),
|
||||||
|
handleTap: true
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
class RangeContainer {
|
||||||
|
final int startFrom;
|
||||||
|
Color color;
|
||||||
|
|
||||||
|
RangeContainer(this.startFrom, this.color);
|
||||||
|
}
|
120
lib/cards/glance_card.dart
Normal file
120
lib/cards/glance_card.dart
Normal file
@ -0,0 +1,120 @@
|
|||||||
|
part of '../main.dart';
|
||||||
|
|
||||||
|
class GlanceCard extends StatelessWidget {
|
||||||
|
final GlanceCardData card;
|
||||||
|
|
||||||
|
const GlanceCard({Key key, this.card}) : super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
List<EntityWrapper> entitiesToShow = card.getEntitiesToShow();
|
||||||
|
if (entitiesToShow.isEmpty && !card.showEmpty) {
|
||||||
|
return Container(height: 0.0, width: 0.0,);
|
||||||
|
}
|
||||||
|
int length = entitiesToShow.length;
|
||||||
|
int rowsCount;
|
||||||
|
int columnsCount;
|
||||||
|
if (length == 0) {
|
||||||
|
columnsCount = 0;
|
||||||
|
rowsCount = 0;
|
||||||
|
} else {
|
||||||
|
columnsCount = length >= card.columnsCount ? card.columnsCount : entitiesToShow.length;
|
||||||
|
rowsCount = (length / columnsCount).round();
|
||||||
|
}
|
||||||
|
List<TableRow> rows = [];
|
||||||
|
for (int i = 0; i < rowsCount; i++) {
|
||||||
|
int start = i*columnsCount;
|
||||||
|
int end = start + math.min(columnsCount, length - start);
|
||||||
|
List<Widget> rowChildren = [];
|
||||||
|
rowChildren.addAll(entitiesToShow.sublist(
|
||||||
|
start, end
|
||||||
|
).map(
|
||||||
|
(EntityWrapper entity){
|
||||||
|
return EntityModel(
|
||||||
|
entityWrapper: entity,
|
||||||
|
child: _buildEntityContainer(context, entity),
|
||||||
|
handleTap: true
|
||||||
|
);
|
||||||
|
}
|
||||||
|
).toList()
|
||||||
|
);
|
||||||
|
while (rowChildren.length < columnsCount) {
|
||||||
|
rowChildren.add(
|
||||||
|
Container()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
rows.add(
|
||||||
|
TableRow(
|
||||||
|
children: rowChildren
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return CardWrapper(
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: <Widget>[
|
||||||
|
CardHeader(name: card.title),
|
||||||
|
Padding(
|
||||||
|
padding: EdgeInsets.symmetric(vertical: Sizes.rowPadding),
|
||||||
|
child: Table(
|
||||||
|
children: rows
|
||||||
|
)
|
||||||
|
)
|
||||||
|
],
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildEntityContainer(BuildContext context, EntityWrapper entityWrapper) {
|
||||||
|
if (entityWrapper.entity.statelessType == StatelessEntityType.missed) {
|
||||||
|
return MissedEntityWidget();
|
||||||
|
} else if (entityWrapper.entity.statelessType != StatelessEntityType.none) {
|
||||||
|
return Container(width: 0.0, height: 0.0,);
|
||||||
|
}
|
||||||
|
List<Widget> result = [];
|
||||||
|
if (card.showName) {
|
||||||
|
result.add(_buildName(context));
|
||||||
|
}
|
||||||
|
result.add(
|
||||||
|
EntityIcon(
|
||||||
|
padding: EdgeInsets.all(0.0),
|
||||||
|
size: Sizes.iconSize,
|
||||||
|
)
|
||||||
|
);
|
||||||
|
if (card.showState) {
|
||||||
|
result.add(_buildState());
|
||||||
|
}
|
||||||
|
|
||||||
|
return Center(
|
||||||
|
child: InkResponse(
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: result,
|
||||||
|
),
|
||||||
|
onTap: () => entityWrapper.handleTap(),
|
||||||
|
onLongPress: () => entityWrapper.handleHold(),
|
||||||
|
onDoubleTap: () => entityWrapper.handleDoubleTap(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildName(BuildContext context) {
|
||||||
|
return EntityName(
|
||||||
|
padding: EdgeInsets.only(bottom: Sizes.rowPadding),
|
||||||
|
textOverflow: TextOverflow.ellipsis,
|
||||||
|
wordsWrap: false,
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
textStyle: Theme.of(context).textTheme.body1,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildState() {
|
||||||
|
return SimpleEntityState(
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
expanded: false,
|
||||||
|
maxLines: 1,
|
||||||
|
padding: EdgeInsets.only(top: Sizes.rowPadding),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
30
lib/cards/horizontal_srack_card.dart
Normal file
30
lib/cards/horizontal_srack_card.dart
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
part of '../main.dart';
|
||||||
|
|
||||||
|
class HorizontalStackCard extends StatelessWidget {
|
||||||
|
final HorizontalStackCardData card;
|
||||||
|
|
||||||
|
const HorizontalStackCard({Key key, this.card}) : super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
if (card.childCards.isNotEmpty) {
|
||||||
|
List<Widget> children = [];
|
||||||
|
children = card.childCards.map((childCard) => Flexible(
|
||||||
|
fit: FlexFit.tight,
|
||||||
|
child: childCard.buildCardWidget()
|
||||||
|
)
|
||||||
|
).toList();
|
||||||
|
return IntrinsicHeight(
|
||||||
|
child: Row(
|
||||||
|
mainAxisSize: MainAxisSize.max,
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
|
children: children,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return Container(height: 0.0, width: 0.0,);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
33
lib/cards/markdown_card.dart
Normal file
33
lib/cards/markdown_card.dart
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
part of '../main.dart';
|
||||||
|
|
||||||
|
class MarkdownCard extends StatelessWidget {
|
||||||
|
final MarkdownCardData card;
|
||||||
|
|
||||||
|
const MarkdownCard({Key key, this.card}) : super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
if (card.content == null) {
|
||||||
|
return Container(height: 0.0, width: 0.0,);
|
||||||
|
} else if (card.content == '***') {
|
||||||
|
return Container(height: Sizes.rowPadding, width: 0.0,);
|
||||||
|
}
|
||||||
|
return CardWrapper(
|
||||||
|
child: Padding(
|
||||||
|
padding: EdgeInsets.fromLTRB(Sizes.leftWidgetPadding, Sizes.rowPadding, Sizes.rightWidgetPadding, Sizes.rowPadding),
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
|
children: <Widget>[
|
||||||
|
CardHeader(name: card.title),
|
||||||
|
MarkdownBody(
|
||||||
|
data: card.content,
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
27
lib/cards/media_control_card.dart
Normal file
27
lib/cards/media_control_card.dart
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
part of '../main.dart';
|
||||||
|
|
||||||
|
class MediaControlsCard extends StatelessWidget {
|
||||||
|
final MediaControlCardData card;
|
||||||
|
|
||||||
|
const MediaControlsCard({Key key, this.card}) : super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
if (card.entity.entity.statelessType == StatelessEntityType.missed) {
|
||||||
|
return EntityModel(
|
||||||
|
entityWrapper: card.entity,
|
||||||
|
child: MissedEntityWidget(),
|
||||||
|
handleTap: false,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return CardWrapper(
|
||||||
|
child: EntityModel(
|
||||||
|
entityWrapper: card.entity,
|
||||||
|
handleTap: null,
|
||||||
|
child: MediaPlayerWidget()
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
17
lib/cards/unsupported_card.dart
Normal file
17
lib/cards/unsupported_card.dart
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
part of '../main.dart';
|
||||||
|
|
||||||
|
class UnsupportedCard extends StatelessWidget {
|
||||||
|
final CardData card;
|
||||||
|
|
||||||
|
const UnsupportedCard({Key key, this.card}) : super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return CardWrapper(
|
||||||
|
child: Padding(
|
||||||
|
padding: EdgeInsets.fromLTRB(Sizes.leftWidgetPadding, Sizes.rowPadding, Sizes.rightWidgetPadding, Sizes.rowPadding),
|
||||||
|
child: Text("'${card.type}' card is not supported yet"),
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
23
lib/cards/vertical_stack_card.dart
Normal file
23
lib/cards/vertical_stack_card.dart
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
part of '../main.dart';
|
||||||
|
|
||||||
|
class VerticalStackCard extends StatelessWidget {
|
||||||
|
final VerticalStackCardData card;
|
||||||
|
|
||||||
|
const VerticalStackCard({Key key, this.card}) : super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
if (card.childCards.isNotEmpty) {
|
||||||
|
return Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
mainAxisAlignment: MainAxisAlignment.start,
|
||||||
|
children: card.childCards.map<Widget>(
|
||||||
|
(childCard) => childCard.buildCardWidget()
|
||||||
|
).toList(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return Container(height: 0.0, width: 0.0,);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
@ -1,12 +1,14 @@
|
|||||||
part of '../main.dart';
|
part of '../../main.dart';
|
||||||
|
|
||||||
class CardHeaderWidget extends StatelessWidget {
|
class CardHeader extends StatelessWidget {
|
||||||
|
|
||||||
final String name;
|
final String name;
|
||||||
final Widget trailing;
|
final Widget trailing;
|
||||||
|
final Widget leading;
|
||||||
final Widget subtitle;
|
final Widget subtitle;
|
||||||
|
final double emptyPadding;
|
||||||
|
|
||||||
const CardHeaderWidget({Key key, this.name, this.trailing, this.subtitle}) : super(key: key);
|
const CardHeader({Key key, this.name, this.leading, this.emptyPadding: 0, this.trailing, this.subtitle}) : super(key: key);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
@ -14,14 +16,15 @@ class CardHeaderWidget extends StatelessWidget {
|
|||||||
if ((name != null) && (name.trim().length > 0)) {
|
if ((name != null) && (name.trim().length > 0)) {
|
||||||
result = new ListTile(
|
result = new ListTile(
|
||||||
trailing: trailing,
|
trailing: trailing,
|
||||||
|
leading: leading,
|
||||||
subtitle: subtitle,
|
subtitle: subtitle,
|
||||||
title: Text("$name",
|
title: Text("$name",
|
||||||
textAlign: TextAlign.left,
|
textAlign: TextAlign.left,
|
||||||
overflow: TextOverflow.ellipsis,
|
overflow: TextOverflow.ellipsis,
|
||||||
style: new TextStyle(fontWeight: FontWeight.bold, fontSize: Sizes.largeFontSize)),
|
style: Theme.of(context).textTheme.headline),
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
result = new Container(width: 0.0, height: 0.0);
|
result = new Container(width: 0.0, height: emptyPadding);
|
||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
21
lib/cards/widgets/card_wrapper.widget.dart
Normal file
21
lib/cards/widgets/card_wrapper.widget.dart
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
part of '../../main.dart';
|
||||||
|
|
||||||
|
class CardWrapper extends StatelessWidget {
|
||||||
|
|
||||||
|
final Widget child;
|
||||||
|
final EdgeInsets padding;
|
||||||
|
|
||||||
|
const CardWrapper({Key key, this.child, this.padding: const EdgeInsets.all(0)}) : super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Card(
|
||||||
|
child: Padding(
|
||||||
|
padding: padding,
|
||||||
|
child: child
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
@ -29,6 +29,11 @@ class EntityState {
|
|||||||
static const ok = 'ok';
|
static const ok = 'ok';
|
||||||
static const problem = 'problem';
|
static const problem = 'problem';
|
||||||
static const active = 'active';
|
static const active = 'active';
|
||||||
|
static const cleaning = 'cleaning';
|
||||||
|
static const docked = 'docked';
|
||||||
|
static const returning = 'returning';
|
||||||
|
static const error = 'error';
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class EntityUIAction {
|
class EntityUIAction {
|
||||||
@ -46,6 +51,10 @@ class EntityUIAction {
|
|||||||
String holdNavigationPath;
|
String holdNavigationPath;
|
||||||
String holdService;
|
String holdService;
|
||||||
Map<String, dynamic> holdServiceData;
|
Map<String, dynamic> holdServiceData;
|
||||||
|
String doubleTapAction = EntityUIAction.none;
|
||||||
|
String doubleTapNavigationPath;
|
||||||
|
String doubleTapService;
|
||||||
|
Map<String, dynamic> doubleTapServiceData;
|
||||||
|
|
||||||
EntityUIAction({rawEntityData}) {
|
EntityUIAction({rawEntityData}) {
|
||||||
if (rawEntityData != null) {
|
if (rawEntityData != null) {
|
||||||
@ -71,29 +80,64 @@ class EntityUIAction {
|
|||||||
holdServiceData = rawEntityData["hold_action"]["service_data"];
|
holdServiceData = rawEntityData["hold_action"]["service_data"];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (rawEntityData["double_tap_action"] != null) {
|
||||||
|
if (rawEntityData["double_tap_action"] is String) {
|
||||||
|
doubleTapAction = rawEntityData["double_tap_action"];
|
||||||
|
} else {
|
||||||
|
doubleTapAction =
|
||||||
|
rawEntityData["double_tap_action"]["action"] ?? EntityUIAction.none;
|
||||||
|
doubleTapNavigationPath = rawEntityData["double_tap_action"]["navigation_path"];
|
||||||
|
doubleTapService = rawEntityData["double_tap_action"]["service"];
|
||||||
|
doubleTapServiceData = rawEntityData["double_tap_action"]["service_data"];
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class CardType {
|
class CardType {
|
||||||
static const horizontalStack = "horizontal-stack";
|
static const HORIZONTAL_STACK = "horizontal-stack";
|
||||||
static const verticalStack = "vertical-stack";
|
static const VERTICAL_STACK = "vertical-stack";
|
||||||
static const entities = "entities";
|
static const ENTITIES = "entities";
|
||||||
static const glance = "glance";
|
static const GLANCE = "glance";
|
||||||
static const mediaControl = "media-control";
|
static const MEDIA_CONTROL = "media-control";
|
||||||
static const weatherForecast = "weather-forecast";
|
static const WEATHER_FORECAST = "weather-forecast";
|
||||||
static const thermostat = "thermostat";
|
static const THERMOSTAT = "thermostat";
|
||||||
static const sensor = "sensor";
|
static const SENSOR = "sensor";
|
||||||
static const plantStatus = "plant-status";
|
static const PLANT_STATUS = "plant-status";
|
||||||
static const pictureEntity = "picture-entity";
|
static const PICTURE_ENTITY = "picture-entity";
|
||||||
static const pictureElements = "picture-elements";
|
static const PICTURE_ELEMENTS = "picture-elements";
|
||||||
static const picture = "picture";
|
static const PICTURE = "picture";
|
||||||
static const map = "map";
|
static const MAP = "map";
|
||||||
static const iframe = "iframe";
|
static const IFRAME = "iframe";
|
||||||
static const gauge = "gauge";
|
static const GAUGE = "gauge";
|
||||||
static const entityButton = "entity-button";
|
static const ENTITY_BUTTON = "entity-button";
|
||||||
static const conditional = "conditional";
|
static const BUTTON = "button";
|
||||||
static const alarmPanel = "alarm-panel";
|
static const CONDITIONAL = "conditional";
|
||||||
static const markdown = "markdown";
|
static const ALARM_PANEL = "alarm-panel";
|
||||||
|
static const MARKDOWN = "markdown";
|
||||||
|
static const LIGHT = "light";
|
||||||
|
static const ENTITY_FILTER = "entity-filter";
|
||||||
|
static const UNKNOWN = "unknown";
|
||||||
|
}
|
||||||
|
|
||||||
|
class Sizes {
|
||||||
|
static const rightWidgetPadding = 10.0;
|
||||||
|
static const leftWidgetPadding = 10.0;
|
||||||
|
static const buttonPadding = 4.0;
|
||||||
|
static const extendedWidgetHeight = 50.0;
|
||||||
|
static const iconSize = 28.0;
|
||||||
|
static const largeIconSize = 46.0;
|
||||||
|
//static const stateFontSize = 15.0;
|
||||||
|
//static const nameFontSize = 15.0;
|
||||||
|
//static const smallFontSize = 14.0;
|
||||||
|
//static const largeFontSize = 24.0;
|
||||||
|
static const inputWidth = 160.0;
|
||||||
|
static const rowPadding = 10.0;
|
||||||
|
static const doubleRowPadding = rowPadding*2;
|
||||||
|
static const minViewColumnWidth = 350;
|
||||||
|
static const entityPageMaxWidth = 400.0;
|
||||||
|
static const mainPageScreenSeparatorWidth = 5.0;
|
||||||
|
static const tabletMinWidth = minViewColumnWidth + entityPageMaxWidth + 5;
|
||||||
}
|
}
|
@ -25,9 +25,12 @@ class _AlarmControlPanelControlsWidgetWidgetState extends State<AlarmControlPane
|
|||||||
|
|
||||||
|
|
||||||
void _callService(AlarmControlPanelEntity entity, String service) {
|
void _callService(AlarmControlPanelEntity entity, String service) {
|
||||||
eventBus.fire(new ServiceCallEvent(
|
ConnectionManager().callService(
|
||||||
entity.domain, service, entity.entityId,
|
domain: entity.domain,
|
||||||
{"code": "$code"}));
|
service: service,
|
||||||
|
entityId: entity.entityId,
|
||||||
|
data: {"code": "$code"}
|
||||||
|
);
|
||||||
setState(() {
|
setState(() {
|
||||||
code = "";
|
code = "";
|
||||||
});
|
});
|
||||||
@ -58,7 +61,11 @@ class _AlarmControlPanelControlsWidgetWidgetState extends State<AlarmControlPane
|
|||||||
FlatButton(
|
FlatButton(
|
||||||
child: new Text("Yes"),
|
child: new Text("Yes"),
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
eventBus.fire(new ServiceCallEvent(entity.domain, "alarm_trigger", entity.entityId, null));
|
ConnectionManager().callService(
|
||||||
|
domain: entity.domain,
|
||||||
|
service: "alarm_trigger",
|
||||||
|
entityId: entity.entityId
|
||||||
|
);
|
||||||
Navigator.of(context).pop();
|
Navigator.of(context).pop();
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
@ -241,7 +248,9 @@ class _AlarmControlPanelControlsWidgetWidgetState extends State<AlarmControlPane
|
|||||||
FlatButton(
|
FlatButton(
|
||||||
child: Text(
|
child: Text(
|
||||||
"TRIGGER",
|
"TRIGGER",
|
||||||
style: TextStyle(color: Colors.redAccent)
|
style: Theme.of(context).textTheme.subhead.copyWith(
|
||||||
|
color: Theme.of(context).errorColor
|
||||||
|
)
|
||||||
),
|
),
|
||||||
onPressed: () => _askToTrigger(entity),
|
onPressed: () => _askToTrigger(entity),
|
||||||
)
|
)
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
part of '../../main.dart';
|
part of '../main.dart';
|
||||||
|
|
||||||
class BadgeWidget extends StatelessWidget {
|
class BadgeWidget extends StatelessWidget {
|
||||||
@override
|
@override
|
||||||
@ -7,8 +7,7 @@ class BadgeWidget extends StatelessWidget {
|
|||||||
double iconSize = 26.0;
|
double iconSize = 26.0;
|
||||||
Widget badgeIcon;
|
Widget badgeIcon;
|
||||||
String onBadgeTextValue;
|
String onBadgeTextValue;
|
||||||
Color iconColor = EntityColor.badgeColors[entityModel.entityWrapper.entity.domain] ??
|
Color iconColor = HAClientTheme().getBadgeColor(entityModel.entityWrapper.entity.domain);
|
||||||
EntityColor.badgeColors["default"];
|
|
||||||
switch (entityModel.entityWrapper.entity.domain) {
|
switch (entityModel.entityWrapper.entity.domain) {
|
||||||
case "sun":
|
case "sun":
|
||||||
{
|
{
|
||||||
@ -30,7 +29,7 @@ class BadgeWidget extends StatelessWidget {
|
|||||||
badgeIcon = EntityIcon(
|
badgeIcon = EntityIcon(
|
||||||
padding: EdgeInsets.all(0.0),
|
padding: EdgeInsets.all(0.0),
|
||||||
size: iconSize,
|
size: iconSize,
|
||||||
color: Colors.black
|
color: Theme.of(context).textTheme.body1.color
|
||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@ -40,7 +39,7 @@ class BadgeWidget extends StatelessWidget {
|
|||||||
badgeIcon = EntityIcon(
|
badgeIcon = EntityIcon(
|
||||||
padding: EdgeInsets.all(0.0),
|
padding: EdgeInsets.all(0.0),
|
||||||
size: iconSize,
|
size: iconSize,
|
||||||
color: Colors.black
|
color: Theme.of(context).textTheme.body1.color
|
||||||
);
|
);
|
||||||
onBadgeTextValue = entityModel.entityWrapper.entity.displayState;
|
onBadgeTextValue = entityModel.entityWrapper.entity.displayState;
|
||||||
break;
|
break;
|
||||||
@ -57,14 +56,16 @@ class BadgeWidget extends StatelessWidget {
|
|||||||
} else if (entityModel.entityWrapper.entity.displayState.length <= 10) {
|
} else if (entityModel.entityWrapper.entity.displayState.length <= 10) {
|
||||||
stateFontSize = 8.0;
|
stateFontSize = 8.0;
|
||||||
}
|
}
|
||||||
onBadgeTextValue = entityModel.entityWrapper.entity.unitOfMeasurement;
|
onBadgeTextValue = entityModel.entityWrapper.unitOfMeasurement;
|
||||||
badgeIcon = Center(
|
badgeIcon = Center(
|
||||||
child: Text(
|
child: Text(
|
||||||
"${entityModel.entityWrapper.entity.displayState}",
|
"${entityModel.entityWrapper.entity.displayState}",
|
||||||
overflow: TextOverflow.fade,
|
overflow: TextOverflow.fade,
|
||||||
softWrap: false,
|
softWrap: false,
|
||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.center,
|
||||||
style: TextStyle(fontSize: stateFontSize),
|
style: Theme.of(context).textTheme.body1.copyWith(
|
||||||
|
fontSize: stateFontSize
|
||||||
|
)
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
@ -77,7 +78,9 @@ class BadgeWidget extends StatelessWidget {
|
|||||||
onBadgeText = Container(
|
onBadgeText = Container(
|
||||||
padding: EdgeInsets.fromLTRB(6.0, 2.0, 6.0, 2.0),
|
padding: EdgeInsets.fromLTRB(6.0, 2.0, 6.0, 2.0),
|
||||||
child: Text("$onBadgeTextValue",
|
child: Text("$onBadgeTextValue",
|
||||||
style: TextStyle(fontSize: 12.0, color: Colors.white),
|
style: Theme.of(context).textTheme.overline.copyWith(
|
||||||
|
color: HAClientTheme().getOnBadgeTextColor()
|
||||||
|
),
|
||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.center,
|
||||||
softWrap: false,
|
softWrap: false,
|
||||||
overflow: TextOverflow.fade),
|
overflow: TextOverflow.fade),
|
||||||
@ -98,7 +101,7 @@ class BadgeWidget extends StatelessWidget {
|
|||||||
decoration: new BoxDecoration(
|
decoration: new BoxDecoration(
|
||||||
// Circle shape
|
// Circle shape
|
||||||
shape: BoxShape.circle,
|
shape: BoxShape.circle,
|
||||||
color: Colors.white,
|
color: Theme.of(context).cardColor,
|
||||||
// The border you want
|
// The border you want
|
||||||
border: new Border.all(
|
border: new Border.all(
|
||||||
width: 2.0,
|
width: 2.0,
|
||||||
@ -131,7 +134,7 @@ class BadgeWidget extends StatelessWidget {
|
|||||||
child: Text(
|
child: Text(
|
||||||
"${entityModel.entityWrapper.displayName}",
|
"${entityModel.entityWrapper.displayName}",
|
||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.center,
|
||||||
style: TextStyle(fontSize: 12.0),
|
style: Theme.of(context).textTheme.caption,
|
||||||
softWrap: true,
|
softWrap: true,
|
||||||
maxLines: 3,
|
maxLines: 3,
|
||||||
overflow: TextOverflow.ellipsis,
|
overflow: TextOverflow.ellipsis,
|
||||||
@ -140,6 +143,6 @@ class BadgeWidget extends StatelessWidget {
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
onTap: () =>
|
onTap: () =>
|
||||||
eventBus.fire(new ShowEntityPageEvent(entityModel.entityWrapper.entity)));
|
eventBus.fire(new ShowEntityPageEvent(entity: entityModel.entityWrapper.entity)));
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -3,12 +3,16 @@ part of '../../main.dart';
|
|||||||
class CameraEntity extends Entity {
|
class CameraEntity extends Entity {
|
||||||
|
|
||||||
static const SUPPORT_ON_OFF = 1;
|
static const SUPPORT_ON_OFF = 1;
|
||||||
|
static const SUPPORT_STREAM = 2;
|
||||||
|
|
||||||
CameraEntity(Map rawData, String webHost) : super(rawData, webHost);
|
CameraEntity(Map rawData, String webHost) : super(rawData, webHost);
|
||||||
|
|
||||||
bool get supportOnOff => ((supportedFeatures &
|
bool get supportOnOff => ((supportedFeatures &
|
||||||
CameraEntity.SUPPORT_ON_OFF) ==
|
CameraEntity.SUPPORT_ON_OFF) ==
|
||||||
CameraEntity.SUPPORT_ON_OFF);
|
CameraEntity.SUPPORT_ON_OFF);
|
||||||
|
bool get supportStream => ((supportedFeatures &
|
||||||
|
CameraEntity.SUPPORT_STREAM) ==
|
||||||
|
CameraEntity.SUPPORT_STREAM);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget _buildAdditionalControlsForPage(BuildContext context) {
|
Widget _buildAdditionalControlsForPage(BuildContext context) {
|
||||||
|
191
lib/entities/camera/widgets/camera_stream_view.dart
Normal file
191
lib/entities/camera/widgets/camera_stream_view.dart
Normal file
@ -0,0 +1,191 @@
|
|||||||
|
part of '../../../main.dart';
|
||||||
|
|
||||||
|
class CameraStreamView extends StatefulWidget {
|
||||||
|
|
||||||
|
final bool withControls;
|
||||||
|
|
||||||
|
CameraStreamView({Key key, this.withControls: true}) : super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
_CameraStreamViewState createState() => _CameraStreamViewState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _CameraStreamViewState extends State<CameraStreamView> {
|
||||||
|
|
||||||
|
CameraEntity _entity;
|
||||||
|
String _streamUrl = "";
|
||||||
|
bool _isLoaded = false;
|
||||||
|
double _aspectRatio = 1.33;
|
||||||
|
String _webViewHtml;
|
||||||
|
String _jsMessageChannelName = 'unknown';
|
||||||
|
Completer _loading;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future _loadResources() {
|
||||||
|
if (_loading != null && !_loading.isCompleted) {
|
||||||
|
Logger.d("[Camera Player] Resources loading is not finished yet");
|
||||||
|
return _loading.future;
|
||||||
|
}
|
||||||
|
Logger.d("[Camera Player] Loading resources");
|
||||||
|
_loading = Completer();
|
||||||
|
_entity = EntityModel
|
||||||
|
.of(context)
|
||||||
|
.entityWrapper
|
||||||
|
.entity;
|
||||||
|
if (_entity.supportStream && HomeAssistant().isComponentEnabled('stream')) {
|
||||||
|
HomeAssistant().getCameraStream(_entity.entityId)
|
||||||
|
.then((data) {
|
||||||
|
_jsMessageChannelName = 'HA_${_entity.entityId.replaceAll('.', '_')}';
|
||||||
|
rootBundle.loadString('assets/html/cameraLiveView.html').then((file) {
|
||||||
|
_webViewHtml = Uri.dataFromString(
|
||||||
|
file.replaceFirst('{{stream_url}}', '${ConnectionManager().httpWebHost}${data["url"]}').replaceFirst('{{message_channel}}', _jsMessageChannelName),
|
||||||
|
mimeType: 'text/html',
|
||||||
|
encoding: Encoding.getByName('utf-8')
|
||||||
|
).toString();
|
||||||
|
_loading.complete();
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.catchError((e) {
|
||||||
|
if (e == 'start_stream_failed') {
|
||||||
|
Logger.e("[Camera Player] Home Assistant failed starting stream. Forcing MJPEG: $e");
|
||||||
|
_loadMJPEG().then((_) {
|
||||||
|
_loading.complete();
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
_loading.completeError(e);
|
||||||
|
Logger.e("[Camera Player] Error loading stream: $e");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
_loadMJPEG().then((_) {
|
||||||
|
_loading.complete();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return _loading.future;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future _loadMJPEG() async {
|
||||||
|
_streamUrl = '${ConnectionManager().httpWebHost}/api/camera_proxy_stream/${_entity
|
||||||
|
.entityId}?token=${_entity.attributes['access_token']}';
|
||||||
|
_jsMessageChannelName = 'HA_${_entity.entityId.replaceAll('.', '_')}';
|
||||||
|
var file = await rootBundle.loadString('assets/html/cameraView.html');
|
||||||
|
_webViewHtml = Uri.dataFromString(
|
||||||
|
file.replaceFirst('{{stream_url}}', _streamUrl).replaceFirst('{{message_channel}}', _jsMessageChannelName),
|
||||||
|
mimeType: 'text/html',
|
||||||
|
encoding: Encoding.getByName('utf-8')
|
||||||
|
).toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildScreen() {
|
||||||
|
Widget screenWidget;
|
||||||
|
if (!_isLoaded) {
|
||||||
|
screenWidget = Center(
|
||||||
|
child: EntityPicture(
|
||||||
|
fit: BoxFit.contain,
|
||||||
|
)
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
screenWidget = WebView(
|
||||||
|
initialUrl: _webViewHtml,
|
||||||
|
initialMediaPlaybackPolicy: AutoMediaPlaybackPolicy.always_allow,
|
||||||
|
debuggingEnabled: Logger.isInDebugMode,
|
||||||
|
gestureNavigationEnabled: false,
|
||||||
|
javascriptMode: JavascriptMode.unrestricted,
|
||||||
|
javascriptChannels: {
|
||||||
|
JavascriptChannel(
|
||||||
|
name: _jsMessageChannelName,
|
||||||
|
onMessageReceived: ((message) {
|
||||||
|
Logger.d('[Camera Player] Message from page: $message');
|
||||||
|
setState((){
|
||||||
|
_aspectRatio = double.tryParse(message.message) ?? 1.33;
|
||||||
|
});
|
||||||
|
})
|
||||||
|
)
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return AspectRatio(
|
||||||
|
aspectRatio: _aspectRatio,
|
||||||
|
child: screenWidget
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildControls() {
|
||||||
|
return Row(
|
||||||
|
mainAxisSize: MainAxisSize.max,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: <Widget>[
|
||||||
|
IconButton(
|
||||||
|
icon: Icon(Icons.refresh),
|
||||||
|
iconSize: 40,
|
||||||
|
color: Theme.of(context).accentColor,
|
||||||
|
onPressed: _isLoaded ? () {
|
||||||
|
setState(() {
|
||||||
|
_isLoaded = false;
|
||||||
|
});
|
||||||
|
} : null,
|
||||||
|
),
|
||||||
|
Expanded(
|
||||||
|
child: Container(),
|
||||||
|
),
|
||||||
|
IconButton(
|
||||||
|
icon: Icon(Icons.fullscreen),
|
||||||
|
iconSize: 40,
|
||||||
|
color: Theme.of(context).accentColor,
|
||||||
|
onPressed: _isLoaded ? () {
|
||||||
|
eventBus.fire(ShowEntityPageEvent());
|
||||||
|
Navigator.of(context).push(
|
||||||
|
MaterialPageRoute(
|
||||||
|
builder: (conext) => FullScreenPage(
|
||||||
|
child: EntityModel(
|
||||||
|
child: CameraStreamView(
|
||||||
|
withControls: false
|
||||||
|
),
|
||||||
|
handleTap: false,
|
||||||
|
entityWrapper: EntityWrapper(
|
||||||
|
entity: _entity
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
fullscreenDialog: true
|
||||||
|
)
|
||||||
|
).then((_) {
|
||||||
|
eventBus.fire(ShowEntityPageEvent(entity: _entity));
|
||||||
|
});
|
||||||
|
} : null,
|
||||||
|
)
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
if (!_isLoaded && (_loading == null || _loading.isCompleted)) {
|
||||||
|
_loadResources().then((_) => setState((){ _isLoaded = true; }));
|
||||||
|
}
|
||||||
|
if (widget.withControls) {
|
||||||
|
return Card(
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: <Widget>[
|
||||||
|
_buildScreen(),
|
||||||
|
_buildControls()
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return _buildScreen();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
}
|
@ -10,9 +10,8 @@ class ClimateControlWidget extends StatefulWidget {
|
|||||||
|
|
||||||
class _ClimateControlWidgetState extends State<ClimateControlWidget> {
|
class _ClimateControlWidgetState extends State<ClimateControlWidget> {
|
||||||
|
|
||||||
bool _showPending = false;
|
bool _temperaturePending = false;
|
||||||
bool _changedHere = false;
|
bool _changedHere = false;
|
||||||
Timer _resetTimer;
|
|
||||||
Timer _tempThrottleTimer;
|
Timer _tempThrottleTimer;
|
||||||
Timer _targetTempThrottleTimer;
|
Timer _targetTempThrottleTimer;
|
||||||
double _tmpTemperature = 0.0;
|
double _tmpTemperature = 0.0;
|
||||||
@ -27,9 +26,11 @@ class _ClimateControlWidgetState extends State<ClimateControlWidget> {
|
|||||||
bool _tmpAuxHeat = false;
|
bool _tmpAuxHeat = false;
|
||||||
|
|
||||||
void _resetVars(ClimateEntity entity) {
|
void _resetVars(ClimateEntity entity) {
|
||||||
|
if (!_temperaturePending) {
|
||||||
_tmpTemperature = entity.temperature;
|
_tmpTemperature = entity.temperature;
|
||||||
_tmpTargetHigh = entity.targetHigh;
|
_tmpTargetHigh = entity.targetHigh;
|
||||||
_tmpTargetLow = entity.targetLow;
|
_tmpTargetLow = entity.targetLow;
|
||||||
|
}
|
||||||
_tmpHVACMode = entity.state;
|
_tmpHVACMode = entity.state;
|
||||||
_tmpFanMode = entity.fanMode;
|
_tmpFanMode = entity.fanMode;
|
||||||
_tmpSwingMode = entity.swingMode;
|
_tmpSwingMode = entity.swingMode;
|
||||||
@ -38,7 +39,6 @@ class _ClimateControlWidgetState extends State<ClimateControlWidget> {
|
|||||||
_tmpAuxHeat = entity.auxHeat;
|
_tmpAuxHeat = entity.auxHeat;
|
||||||
_tmpTargetHumidity = entity.targetHumidity;
|
_tmpTargetHumidity = entity.targetHumidity;
|
||||||
|
|
||||||
_showPending = false;
|
|
||||||
_changedHere = false;
|
_changedHere = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -73,36 +73,44 @@ class _ClimateControlWidgetState extends State<ClimateControlWidget> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void _setTemperature(ClimateEntity entity) {
|
void _setTemperature(ClimateEntity entity) {
|
||||||
if (_tempThrottleTimer!=null) {
|
_tempThrottleTimer?.cancel();
|
||||||
_tempThrottleTimer.cancel();
|
|
||||||
}
|
|
||||||
setState(() {
|
setState(() {
|
||||||
_changedHere = true;
|
_changedHere = true;
|
||||||
|
_temperaturePending = true;
|
||||||
_tmpTemperature = double.parse(_tmpTemperature.toStringAsFixed(1));
|
_tmpTemperature = double.parse(_tmpTemperature.toStringAsFixed(1));
|
||||||
});
|
});
|
||||||
_tempThrottleTimer = Timer(Duration(seconds: 2), () {
|
_tempThrottleTimer = Timer(Duration(seconds: 2), () {
|
||||||
setState(() {
|
setState(() {
|
||||||
_changedHere = true;
|
_changedHere = true;
|
||||||
eventBus.fire(new ServiceCallEvent(entity.domain, "set_temperature", entity.entityId,{"temperature": "${_tmpTemperature.toStringAsFixed(1)}"}));
|
_temperaturePending = false;
|
||||||
_resetStateTimer(entity);
|
ConnectionManager().callService(
|
||||||
|
domain: entity.domain,
|
||||||
|
service: "set_temperature",
|
||||||
|
entityId: entity.entityId,
|
||||||
|
data: {"temperature": "${_tmpTemperature.toStringAsFixed(1)}"}
|
||||||
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
void _setTargetTemp(ClimateEntity entity) {
|
void _setTargetTemp(ClimateEntity entity) {
|
||||||
if (_targetTempThrottleTimer!=null) {
|
_targetTempThrottleTimer?.cancel();
|
||||||
_targetTempThrottleTimer.cancel();
|
|
||||||
}
|
|
||||||
setState(() {
|
setState(() {
|
||||||
_changedHere = true;
|
_changedHere = true;
|
||||||
|
_temperaturePending = true;
|
||||||
_tmpTargetLow = double.parse(_tmpTargetLow.toStringAsFixed(1));
|
_tmpTargetLow = double.parse(_tmpTargetLow.toStringAsFixed(1));
|
||||||
_tmpTargetHigh = double.parse(_tmpTargetHigh.toStringAsFixed(1));
|
_tmpTargetHigh = double.parse(_tmpTargetHigh.toStringAsFixed(1));
|
||||||
});
|
});
|
||||||
_targetTempThrottleTimer = Timer(Duration(seconds: 2), () {
|
_targetTempThrottleTimer = Timer(Duration(seconds: 2), () {
|
||||||
setState(() {
|
setState(() {
|
||||||
_changedHere = true;
|
_changedHere = true;
|
||||||
eventBus.fire(new ServiceCallEvent(entity.domain, "set_temperature", entity.entityId,{"target_temp_high": "${_tmpTargetHigh.toStringAsFixed(1)}", "target_temp_low": "${_tmpTargetLow.toStringAsFixed(1)}"}));
|
_temperaturePending = false;
|
||||||
_resetStateTimer(entity);
|
ConnectionManager().callService(
|
||||||
|
domain: entity.domain,
|
||||||
|
service: "set_temperature",
|
||||||
|
entityId: entity.entityId,
|
||||||
|
data: {"target_temp_high": "${_tmpTargetHigh.toStringAsFixed(1)}", "target_temp_low": "${_tmpTargetLow.toStringAsFixed(1)}"}
|
||||||
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -111,8 +119,12 @@ class _ClimateControlWidgetState extends State<ClimateControlWidget> {
|
|||||||
setState(() {
|
setState(() {
|
||||||
_tmpTargetHumidity = value.roundToDouble();
|
_tmpTargetHumidity = value.roundToDouble();
|
||||||
_changedHere = true;
|
_changedHere = true;
|
||||||
eventBus.fire(new ServiceCallEvent(entity.domain, "set_humidity", entity.entityId,{"humidity": "$_tmpTargetHumidity"}));
|
ConnectionManager().callService(
|
||||||
_resetStateTimer(entity);
|
domain: entity.domain,
|
||||||
|
service: "set_humidity",
|
||||||
|
entityId: entity.entityId,
|
||||||
|
data: {"humidity": "$_tmpTargetHumidity"}
|
||||||
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -120,8 +132,12 @@ class _ClimateControlWidgetState extends State<ClimateControlWidget> {
|
|||||||
setState(() {
|
setState(() {
|
||||||
_tmpHVACMode = value;
|
_tmpHVACMode = value;
|
||||||
_changedHere = true;
|
_changedHere = true;
|
||||||
eventBus.fire(new ServiceCallEvent(entity.domain, "set_hvac_mode", entity.entityId,{"hvac_mode": "$_tmpHVACMode"}));
|
ConnectionManager().callService(
|
||||||
_resetStateTimer(entity);
|
domain: entity.domain,
|
||||||
|
service: "set_hvac_mode",
|
||||||
|
entityId: entity.entityId,
|
||||||
|
data: {"hvac_mode": "$_tmpHVACMode"}
|
||||||
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -129,8 +145,12 @@ class _ClimateControlWidgetState extends State<ClimateControlWidget> {
|
|||||||
setState(() {
|
setState(() {
|
||||||
_tmpSwingMode = value;
|
_tmpSwingMode = value;
|
||||||
_changedHere = true;
|
_changedHere = true;
|
||||||
eventBus.fire(new ServiceCallEvent(entity.domain, "set_swing_mode", entity.entityId,{"swing_mode": "$_tmpSwingMode"}));
|
ConnectionManager().callService(
|
||||||
_resetStateTimer(entity);
|
domain: entity.domain,
|
||||||
|
service: "set_swing_mode",
|
||||||
|
entityId: entity.entityId,
|
||||||
|
data: {"swing_mode": "$_tmpSwingMode"}
|
||||||
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -138,8 +158,7 @@ class _ClimateControlWidgetState extends State<ClimateControlWidget> {
|
|||||||
setState(() {
|
setState(() {
|
||||||
_tmpFanMode = value;
|
_tmpFanMode = value;
|
||||||
_changedHere = true;
|
_changedHere = true;
|
||||||
eventBus.fire(new ServiceCallEvent(entity.domain, "set_fan_mode", entity.entityId,{"fan_mode": "$_tmpFanMode"}));
|
ConnectionManager().callService(domain: entity.domain, service: "set_fan_mode", entityId: entity.entityId, data: {"fan_mode": "$_tmpFanMode"});
|
||||||
_resetStateTimer(entity);
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -147,8 +166,7 @@ class _ClimateControlWidgetState extends State<ClimateControlWidget> {
|
|||||||
setState(() {
|
setState(() {
|
||||||
_tmpPresetMode = value;
|
_tmpPresetMode = value;
|
||||||
_changedHere = true;
|
_changedHere = true;
|
||||||
eventBus.fire(new ServiceCallEvent(entity.domain, "set_preset_mode", entity.entityId,{"preset_mode": "$_tmpPresetMode"}));
|
ConnectionManager().callService(domain: entity.domain, service: "set_preset_mode", entityId: entity.entityId, data: {"preset_mode": "$_tmpPresetMode"});
|
||||||
_resetStateTimer(entity);
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -165,18 +183,7 @@ class _ClimateControlWidgetState extends State<ClimateControlWidget> {
|
|||||||
setState(() {
|
setState(() {
|
||||||
_tmpAuxHeat = value;
|
_tmpAuxHeat = value;
|
||||||
_changedHere = true;
|
_changedHere = true;
|
||||||
eventBus.fire(new ServiceCallEvent(entity.domain, "set_aux_heat", entity.entityId, {"aux_heat": "$_tmpAuxHeat"}));
|
ConnectionManager().callService(domain: entity.domain, service: "set_aux_heat", entityId: entity.entityId, data: {"aux_heat": "$_tmpAuxHeat"});
|
||||||
_resetStateTimer(entity);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
void _resetStateTimer(ClimateEntity entity) {
|
|
||||||
if (_resetTimer!=null) {
|
|
||||||
_resetTimer.cancel();
|
|
||||||
}
|
|
||||||
_resetTimer = Timer(Duration(seconds: 3), () {
|
|
||||||
setState(() {});
|
|
||||||
_resetVars(entity);
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -184,11 +191,11 @@ class _ClimateControlWidgetState extends State<ClimateControlWidget> {
|
|||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final entityModel = EntityModel.of(context);
|
final entityModel = EntityModel.of(context);
|
||||||
final ClimateEntity entity = entityModel.entityWrapper.entity;
|
final ClimateEntity entity = entityModel.entityWrapper.entity;
|
||||||
|
Logger.d("[Climate widget build] changed here = $_changedHere");
|
||||||
if (_changedHere) {
|
if (_changedHere) {
|
||||||
_showPending = (_tmpTemperature != entity.temperature || _tmpTargetHigh != entity.targetHigh || _tmpTargetLow != entity.targetLow);
|
//_showPending = (_tmpTemperature != entity.temperature || _tmpTargetHigh != entity.targetHigh || _tmpTargetLow != entity.targetLow);
|
||||||
_changedHere = false;
|
_changedHere = false;
|
||||||
} else {
|
} else {
|
||||||
_resetTimer?.cancel();
|
|
||||||
_resetVars(entity);
|
_resetVars(entity);
|
||||||
}
|
}
|
||||||
return Padding(
|
return Padding(
|
||||||
@ -197,20 +204,20 @@ class _ClimateControlWidgetState extends State<ClimateControlWidget> {
|
|||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
//_buildOnOffControl(entity),
|
//_buildOnOffControl(entity),
|
||||||
_buildTemperatureControls(entity),
|
_buildTemperatureControls(entity, context),
|
||||||
_buildTargetTemperatureControls(entity),
|
_buildTargetTemperatureControls(entity, context),
|
||||||
_buildHumidityControls(entity),
|
_buildHumidityControls(entity, context),
|
||||||
_buildOperationControl(entity),
|
_buildOperationControl(entity, context),
|
||||||
_buildFanControl(entity),
|
_buildFanControl(entity, context),
|
||||||
_buildSwingControl(entity),
|
_buildSwingControl(entity, context),
|
||||||
_buildPresetModeControl(entity),
|
_buildPresetModeControl(entity, context),
|
||||||
_buildAuxHeatControl(entity)
|
_buildAuxHeatControl(entity, context)
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildPresetModeControl(ClimateEntity entity) {
|
Widget _buildPresetModeControl(ClimateEntity entity, BuildContext context) {
|
||||||
if (entity.supportPresetMode) {
|
if (entity.supportPresetMode) {
|
||||||
return ModeSelectorWidget(
|
return ModeSelectorWidget(
|
||||||
options: entity.presetModes,
|
options: entity.presetModes,
|
||||||
@ -235,7 +242,7 @@ class _ClimateControlWidgetState extends State<ClimateControlWidget> {
|
|||||||
}
|
}
|
||||||
}*/
|
}*/
|
||||||
|
|
||||||
Widget _buildAuxHeatControl(ClimateEntity entity) {
|
Widget _buildAuxHeatControl(ClimateEntity entity, BuildContext context) {
|
||||||
if (entity.supportAuxHeat ) {
|
if (entity.supportAuxHeat ) {
|
||||||
return ModeSwitchWidget(
|
return ModeSwitchWidget(
|
||||||
caption: "Aux heat",
|
caption: "Aux heat",
|
||||||
@ -247,7 +254,7 @@ class _ClimateControlWidgetState extends State<ClimateControlWidget> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildOperationControl(ClimateEntity entity) {
|
Widget _buildOperationControl(ClimateEntity entity, BuildContext context) {
|
||||||
if (entity.hvacModes != null) {
|
if (entity.hvacModes != null) {
|
||||||
return ModeSelectorWidget(
|
return ModeSelectorWidget(
|
||||||
onChange: (mode) => _setHVACMode(entity, mode),
|
onChange: (mode) => _setHVACMode(entity, mode),
|
||||||
@ -260,7 +267,7 @@ class _ClimateControlWidgetState extends State<ClimateControlWidget> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildFanControl(ClimateEntity entity) {
|
Widget _buildFanControl(ClimateEntity entity, BuildContext context) {
|
||||||
if (entity.supportFanMode) {
|
if (entity.supportFanMode) {
|
||||||
return ModeSelectorWidget(
|
return ModeSelectorWidget(
|
||||||
options: entity.fanModes,
|
options: entity.fanModes,
|
||||||
@ -273,7 +280,7 @@ class _ClimateControlWidgetState extends State<ClimateControlWidget> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildSwingControl(ClimateEntity entity) {
|
Widget _buildSwingControl(ClimateEntity entity, BuildContext context) {
|
||||||
if (entity.supportSwingMode) {
|
if (entity.supportSwingMode) {
|
||||||
return ModeSelectorWidget(
|
return ModeSelectorWidget(
|
||||||
onChange: (mode) => _setSwingMode(entity, mode),
|
onChange: (mode) => _setSwingMode(entity, mode),
|
||||||
@ -286,17 +293,15 @@ class _ClimateControlWidgetState extends State<ClimateControlWidget> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildTemperatureControls(ClimateEntity entity) {
|
Widget _buildTemperatureControls(ClimateEntity entity, BuildContext context) {
|
||||||
if ((entity.supportTargetTemperature) && (entity.temperature != null)) {
|
if ((entity.supportTargetTemperature) && (entity.temperature != null)) {
|
||||||
return Column(
|
return Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
Text("Target temperature", style: TextStyle(
|
Text("Target temperature", style: Theme.of(context).textTheme.body1),
|
||||||
fontSize: Sizes.stateFontSize
|
|
||||||
)),
|
|
||||||
TemperatureControlWidget(
|
TemperatureControlWidget(
|
||||||
value: _tmpTemperature,
|
value: _tmpTemperature,
|
||||||
fontColor: _showPending ? Colors.red : Colors.black,
|
active: _temperaturePending,
|
||||||
onDec: () => _temperatureDown(entity),
|
onDec: () => _temperatureDown(entity),
|
||||||
onInc: () => _temperatureUp(entity),
|
onInc: () => _temperatureUp(entity),
|
||||||
)
|
)
|
||||||
@ -307,13 +312,13 @@ class _ClimateControlWidgetState extends State<ClimateControlWidget> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildTargetTemperatureControls(ClimateEntity entity) {
|
Widget _buildTargetTemperatureControls(ClimateEntity entity, BuildContext context) {
|
||||||
List<Widget> controls = [];
|
List<Widget> controls = [];
|
||||||
if ((entity.supportTargetTemperatureRange) && (entity.targetLow != null)) {
|
if ((entity.supportTargetTemperatureRange) && (entity.targetLow != null)) {
|
||||||
controls.addAll(<Widget>[
|
controls.addAll(<Widget>[
|
||||||
TemperatureControlWidget(
|
TemperatureControlWidget(
|
||||||
value: _tmpTargetLow,
|
value: _tmpTargetLow,
|
||||||
fontColor: _showPending ? Colors.red : Colors.black,
|
active: _temperaturePending,
|
||||||
onDec: () => _targetLowDown(entity),
|
onDec: () => _targetLowDown(entity),
|
||||||
onInc: () => _targetLowUp(entity),
|
onInc: () => _targetLowUp(entity),
|
||||||
),
|
),
|
||||||
@ -326,7 +331,7 @@ class _ClimateControlWidgetState extends State<ClimateControlWidget> {
|
|||||||
controls.add(
|
controls.add(
|
||||||
TemperatureControlWidget(
|
TemperatureControlWidget(
|
||||||
value: _tmpTargetHigh,
|
value: _tmpTargetHigh,
|
||||||
fontColor: _showPending ? Colors.red : Colors.black,
|
active: _temperaturePending,
|
||||||
onDec: () => _targetHighDown(entity),
|
onDec: () => _targetHighDown(entity),
|
||||||
onInc: () => _targetHighUp(entity),
|
onInc: () => _targetHighUp(entity),
|
||||||
)
|
)
|
||||||
@ -336,9 +341,7 @@ class _ClimateControlWidgetState extends State<ClimateControlWidget> {
|
|||||||
return Column(
|
return Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
Text("Target temperature range", style: TextStyle(
|
Text("Target temperature range", style: Theme.of(context).textTheme.body1),
|
||||||
fontSize: Sizes.stateFontSize
|
|
||||||
)),
|
|
||||||
Row(
|
Row(
|
||||||
children: controls,
|
children: controls,
|
||||||
)
|
)
|
||||||
@ -349,13 +352,13 @@ class _ClimateControlWidgetState extends State<ClimateControlWidget> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildHumidityControls(ClimateEntity entity) {
|
Widget _buildHumidityControls(ClimateEntity entity, BuildContext context) {
|
||||||
List<Widget> result = [];
|
List<Widget> result = [];
|
||||||
if (entity.supportTargetHumidity) {
|
if (entity.supportTargetHumidity) {
|
||||||
result.addAll(<Widget>[
|
result.addAll(<Widget>[
|
||||||
Text(
|
Text(
|
||||||
"$_tmpTargetHumidity%",
|
"$_tmpTargetHumidity%",
|
||||||
style: TextStyle(fontSize: Sizes.largeFontSize),
|
style: Theme.of(context).textTheme.display1,
|
||||||
),
|
),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Slider(
|
child: Slider(
|
||||||
@ -380,9 +383,7 @@ class _ClimateControlWidgetState extends State<ClimateControlWidget> {
|
|||||||
Padding(
|
Padding(
|
||||||
padding: EdgeInsets.fromLTRB(
|
padding: EdgeInsets.fromLTRB(
|
||||||
0.0, Sizes.rowPadding, 0.0, Sizes.rowPadding),
|
0.0, Sizes.rowPadding, 0.0, Sizes.rowPadding),
|
||||||
child: Text("Target humidity", style: TextStyle(
|
child: Text("Target humidity", style: Theme.of(context).textTheme.body1),
|
||||||
fontSize: Sizes.stateFontSize
|
|
||||||
)),
|
|
||||||
),
|
),
|
||||||
Row(
|
Row(
|
||||||
crossAxisAlignment: CrossAxisAlignment.center,
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
@ -404,57 +405,7 @@ class _ClimateControlWidgetState extends State<ClimateControlWidget> {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
_resetTimer?.cancel();
|
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class TemperatureControlWidget extends StatelessWidget {
|
|
||||||
final double value;
|
|
||||||
final double fontSize;
|
|
||||||
final Color fontColor;
|
|
||||||
final onInc;
|
|
||||||
final onDec;
|
|
||||||
|
|
||||||
TemperatureControlWidget(
|
|
||||||
{Key key,
|
|
||||||
@required this.value,
|
|
||||||
@required this.onInc,
|
|
||||||
@required this.onDec,
|
|
||||||
this.fontSize,
|
|
||||||
this.fontColor})
|
|
||||||
: super(key: key);
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return Row(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.center,
|
|
||||||
children: <Widget>[
|
|
||||||
Text(
|
|
||||||
"$value",
|
|
||||||
style: TextStyle(
|
|
||||||
fontSize: fontSize ?? 24.0,
|
|
||||||
color: fontColor ?? Colors.black
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Column(
|
|
||||||
children: <Widget>[
|
|
||||||
IconButton(
|
|
||||||
icon: Icon(MaterialDesignIcons.getIconDataFromIconName(
|
|
||||||
'mdi:chevron-up')),
|
|
||||||
iconSize: 30.0,
|
|
||||||
onPressed: () => onInc(),
|
|
||||||
),
|
|
||||||
IconButton(
|
|
||||||
icon: Icon(MaterialDesignIcons.getIconDataFromIconName(
|
|
||||||
'mdi:chevron-down')),
|
|
||||||
iconSize: 30.0,
|
|
||||||
onPressed: () => onDec(),
|
|
||||||
)
|
|
||||||
],
|
|
||||||
)
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
@ -33,23 +33,16 @@ class ClimateStateWidget extends StatelessWidget {
|
|||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
Text("$displayState",
|
Text("$displayState",
|
||||||
textAlign: TextAlign.right,
|
textAlign: TextAlign.right,
|
||||||
style: new TextStyle(
|
style: Theme.of(context).textTheme.body2),
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
fontSize: Sizes.stateFontSize,
|
|
||||||
)),
|
|
||||||
Text(" $targetTemp",
|
Text(" $targetTemp",
|
||||||
textAlign: TextAlign.right,
|
textAlign: TextAlign.right,
|
||||||
style: new TextStyle(
|
style: Theme.of(context).textTheme.body1)
|
||||||
fontSize: Sizes.stateFontSize,
|
|
||||||
))
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
entity.currentTemperature != null ?
|
entity.currentTemperature != null ?
|
||||||
Text("Currently: ${entity.currentTemperature}",
|
Text("Currently: ${entity.currentTemperature}",
|
||||||
textAlign: TextAlign.right,
|
textAlign: TextAlign.right,
|
||||||
style: new TextStyle(
|
style: Theme.of(context).textTheme.subtitle
|
||||||
fontSize: Sizes.stateFontSize,
|
|
||||||
color: Colors.black45)
|
|
||||||
) :
|
) :
|
||||||
Container(height: 0.0,)
|
Container(height: 0.0,)
|
||||||
],
|
],
|
||||||
|
@ -1,12 +1,10 @@
|
|||||||
part of '../../main.dart';
|
part of '../../../main.dart';
|
||||||
|
|
||||||
class ModeSelectorWidget extends StatelessWidget {
|
class ModeSelectorWidget extends StatelessWidget {
|
||||||
|
|
||||||
final String caption;
|
final String caption;
|
||||||
final List<String> options;
|
final List options;
|
||||||
final String value;
|
final String value;
|
||||||
final double captionFontSize;
|
|
||||||
final double valueFontSize;
|
|
||||||
final onChange;
|
final onChange;
|
||||||
final EdgeInsets padding;
|
final EdgeInsets padding;
|
||||||
|
|
||||||
@ -16,8 +14,6 @@ class ModeSelectorWidget extends StatelessWidget {
|
|||||||
@required this.options,
|
@required this.options,
|
||||||
this.value,
|
this.value,
|
||||||
@required this.onChange,
|
@required this.onChange,
|
||||||
this.captionFontSize,
|
|
||||||
this.valueFontSize,
|
|
||||||
this.padding: const EdgeInsets.fromLTRB(Sizes.leftWidgetPadding, Sizes.rowPadding, Sizes.rightWidgetPadding, 0.0),
|
this.padding: const EdgeInsets.fromLTRB(Sizes.leftWidgetPadding, Sizes.rowPadding, Sizes.rightWidgetPadding, 0.0),
|
||||||
}) : super(key: key);
|
}) : super(key: key);
|
||||||
|
|
||||||
@ -28,9 +24,7 @@ class ModeSelectorWidget extends StatelessWidget {
|
|||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
Text("$caption", style: TextStyle(
|
Text("$caption", style: Theme.of(context).textTheme.body1),
|
||||||
fontSize: captionFontSize ?? Sizes.stateFontSize
|
|
||||||
)),
|
|
||||||
Row(
|
Row(
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
Expanded(
|
Expanded(
|
||||||
@ -40,15 +34,12 @@ class ModeSelectorWidget extends StatelessWidget {
|
|||||||
value: value,
|
value: value,
|
||||||
iconSize: 30.0,
|
iconSize: 30.0,
|
||||||
isExpanded: true,
|
isExpanded: true,
|
||||||
style: TextStyle(
|
style: Theme.of(context).textTheme.title,
|
||||||
fontSize: valueFontSize ?? Sizes.largeFontSize,
|
|
||||||
color: Colors.black,
|
|
||||||
),
|
|
||||||
hint: Text("Select ${caption.toLowerCase()}"),
|
hint: Text("Select ${caption.toLowerCase()}"),
|
||||||
items: options.map((String value) {
|
items: options.map((value) {
|
||||||
return new DropdownMenuItem<String>(
|
return new DropdownMenuItem<String>(
|
||||||
value: value,
|
value: '$value',
|
||||||
child: Text(value),
|
child: Text('$value'),
|
||||||
);
|
);
|
||||||
}).toList(),
|
}).toList(),
|
||||||
onChanged: (mode) => onChange(mode),
|
onChanged: (mode) => onChange(mode),
|
@ -1,10 +1,9 @@
|
|||||||
part of '../../main.dart';
|
part of '../../../main.dart';
|
||||||
|
|
||||||
class ModeSwitchWidget extends StatelessWidget {
|
class ModeSwitchWidget extends StatelessWidget {
|
||||||
|
|
||||||
final String caption;
|
final String caption;
|
||||||
final onChange;
|
final onChange;
|
||||||
final double captionFontSize;
|
|
||||||
final bool value;
|
final bool value;
|
||||||
final bool expanded;
|
final bool expanded;
|
||||||
final EdgeInsets padding;
|
final EdgeInsets padding;
|
||||||
@ -13,7 +12,6 @@ class ModeSwitchWidget extends StatelessWidget {
|
|||||||
Key key,
|
Key key,
|
||||||
@required this.caption,
|
@required this.caption,
|
||||||
@required this.onChange,
|
@required this.onChange,
|
||||||
this.captionFontSize,
|
|
||||||
this.value,
|
this.value,
|
||||||
this.expanded: true,
|
this.expanded: true,
|
||||||
this.padding: const EdgeInsets.only(left: Sizes.leftWidgetPadding, right: Sizes.rightWidgetPadding)
|
this.padding: const EdgeInsets.only(left: Sizes.leftWidgetPadding, right: Sizes.rightWidgetPadding)
|
||||||
@ -25,7 +23,7 @@ class ModeSwitchWidget extends StatelessWidget {
|
|||||||
padding: this.padding,
|
padding: this.padding,
|
||||||
child: Row(
|
child: Row(
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
_buildCaption(),
|
_buildCaption(context),
|
||||||
Switch(
|
Switch(
|
||||||
onChanged: (value) => onChange(value),
|
onChanged: (value) => onChange(value),
|
||||||
value: value ?? false,
|
value: value ?? false,
|
||||||
@ -35,12 +33,10 @@ class ModeSwitchWidget extends StatelessWidget {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildCaption() {
|
Widget _buildCaption(BuildContext context) {
|
||||||
Widget captionWidget = Text(
|
Widget captionWidget = Text(
|
||||||
"$caption",
|
"$caption",
|
||||||
style: TextStyle(
|
style: Theme.of(context).textTheme.body1,
|
||||||
fontSize: captionFontSize ?? Sizes.stateFontSize
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
if (expanded) {
|
if (expanded) {
|
||||||
return Expanded(
|
return Expanded(
|
47
lib/entities/climate/widgets/temperature_control_widget.dart
Normal file
47
lib/entities/climate/widgets/temperature_control_widget.dart
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
part of '../../../main.dart';
|
||||||
|
|
||||||
|
class TemperatureControlWidget extends StatelessWidget {
|
||||||
|
final double value;
|
||||||
|
final bool active;
|
||||||
|
final onInc;
|
||||||
|
final onDec;
|
||||||
|
|
||||||
|
TemperatureControlWidget(
|
||||||
|
{Key key,
|
||||||
|
@required this.value,
|
||||||
|
@required this.onInc,
|
||||||
|
@required this.onDec,
|
||||||
|
//this.fontSize,
|
||||||
|
this.active: false
|
||||||
|
})
|
||||||
|
: super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Row(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
|
children: <Widget>[
|
||||||
|
Text(
|
||||||
|
"$value",
|
||||||
|
style: active ? Theme.of(context).textTheme.display2 : Theme.of(context).textTheme.display1,
|
||||||
|
),
|
||||||
|
Column(
|
||||||
|
children: <Widget>[
|
||||||
|
IconButton(
|
||||||
|
icon: Icon(MaterialDesignIcons.getIconDataFromIconName(
|
||||||
|
'mdi:chevron-up')),
|
||||||
|
iconSize: 30.0,
|
||||||
|
onPressed: () => onInc(),
|
||||||
|
),
|
||||||
|
IconButton(
|
||||||
|
icon: Icon(MaterialDesignIcons.getIconDataFromIconName(
|
||||||
|
'mdi:chevron-down')),
|
||||||
|
iconSize: 30.0,
|
||||||
|
onPressed: () => onDec(),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
)
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -18,7 +18,7 @@ class _CoverControlWidgetState extends State<CoverControlWidget> {
|
|||||||
setState(() {
|
setState(() {
|
||||||
_tmpPosition = position.roundToDouble();
|
_tmpPosition = position.roundToDouble();
|
||||||
_changedHere = true;
|
_changedHere = true;
|
||||||
eventBus.fire(new ServiceCallEvent(entity.domain, "set_cover_position", entity.entityId,{"position": _tmpPosition.round()}));
|
ConnectionManager().callService(domain: entity.domain, service: "set_cover_position", entityId: entity.entityId, data: {"position": _tmpPosition.round()});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -26,7 +26,7 @@ class _CoverControlWidgetState extends State<CoverControlWidget> {
|
|||||||
setState(() {
|
setState(() {
|
||||||
_tmpTiltPosition = position.roundToDouble();
|
_tmpTiltPosition = position.roundToDouble();
|
||||||
_changedHere = true;
|
_changedHere = true;
|
||||||
eventBus.fire(new ServiceCallEvent(entity.domain, "set_cover_tilt_position", entity.entityId,{"tilt_position": _tmpTiltPosition.round()}));
|
ConnectionManager().callService(domain: entity.domain, service: "set_cover_tilt_position", entityId: entity.entityId, data: {"tilt_position": _tmpTiltPosition.round()});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -64,9 +64,7 @@ class _CoverControlWidgetState extends State<CoverControlWidget> {
|
|||||||
Padding(
|
Padding(
|
||||||
padding: EdgeInsets.fromLTRB(
|
padding: EdgeInsets.fromLTRB(
|
||||||
0.0, Sizes.rowPadding, 0.0, Sizes.rowPadding),
|
0.0, Sizes.rowPadding, 0.0, Sizes.rowPadding),
|
||||||
child: Text("Position", style: TextStyle(
|
child: Text("Position"),
|
||||||
fontSize: Sizes.stateFontSize
|
|
||||||
)),
|
|
||||||
),
|
),
|
||||||
Slider(
|
Slider(
|
||||||
value: _tmpPosition,
|
value: _tmpPosition,
|
||||||
@ -118,9 +116,7 @@ class _CoverControlWidgetState extends State<CoverControlWidget> {
|
|||||||
controls.insert(0, Padding(
|
controls.insert(0, Padding(
|
||||||
padding: EdgeInsets.fromLTRB(
|
padding: EdgeInsets.fromLTRB(
|
||||||
0.0, Sizes.rowPadding, 0.0, Sizes.rowPadding),
|
0.0, Sizes.rowPadding, 0.0, Sizes.rowPadding),
|
||||||
child: Text("Tilt position", style: TextStyle(
|
child: Text("Tilt position"),
|
||||||
fontSize: Sizes.stateFontSize
|
|
||||||
)),
|
|
||||||
));
|
));
|
||||||
return Column(
|
return Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
@ -135,18 +131,18 @@ class _CoverControlWidgetState extends State<CoverControlWidget> {
|
|||||||
|
|
||||||
class CoverTiltControlsWidget extends StatelessWidget {
|
class CoverTiltControlsWidget extends StatelessWidget {
|
||||||
void _open(CoverEntity entity) {
|
void _open(CoverEntity entity) {
|
||||||
eventBus.fire(new ServiceCallEvent(
|
ConnectionManager().callService(
|
||||||
entity.domain, "open_cover_tilt", entity.entityId, null));
|
domain: entity.domain, service: "open_cover_tilt", entityId: entity.entityId);
|
||||||
}
|
}
|
||||||
|
|
||||||
void _close(CoverEntity entity) {
|
void _close(CoverEntity entity) {
|
||||||
eventBus.fire(new ServiceCallEvent(
|
ConnectionManager().callService(
|
||||||
entity.domain, "close_cover_tilt", entity.entityId, null));
|
domain: entity.domain, service: "close_cover_tilt", entityId: entity.entityId);
|
||||||
}
|
}
|
||||||
|
|
||||||
void _stop(CoverEntity entity) {
|
void _stop(CoverEntity entity) {
|
||||||
eventBus.fire(new ServiceCallEvent(
|
ConnectionManager().callService(
|
||||||
entity.domain, "stop_cover_tilt", entity.entityId, null));
|
domain: entity.domain, service: "stop_cover_tilt", entityId: entity.entityId);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
@ -2,18 +2,27 @@ part of '../../../main.dart';
|
|||||||
|
|
||||||
class CoverStateWidget extends StatelessWidget {
|
class CoverStateWidget extends StatelessWidget {
|
||||||
void _open(CoverEntity entity) {
|
void _open(CoverEntity entity) {
|
||||||
eventBus.fire(new ServiceCallEvent(
|
ConnectionManager().callService(
|
||||||
entity.domain, "open_cover", entity.entityId, null));
|
domain: entity.domain,
|
||||||
|
service: "open_cover",
|
||||||
|
entityId: entity.entityId
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
void _close(CoverEntity entity) {
|
void _close(CoverEntity entity) {
|
||||||
eventBus.fire(new ServiceCallEvent(
|
ConnectionManager().callService(
|
||||||
entity.domain, "close_cover", entity.entityId, null));
|
domain: entity.domain,
|
||||||
|
service: "close_cover",
|
||||||
|
entityId: entity.entityId
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
void _stop(CoverEntity entity) {
|
void _stop(CoverEntity entity) {
|
||||||
eventBus.fire(new ServiceCallEvent(
|
ConnectionManager().callService(
|
||||||
entity.domain, "stop_cover", entity.entityId, null));
|
domain: entity.domain,
|
||||||
|
service: "stop_cover",
|
||||||
|
entityId: entity.entityId
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
@ -35,8 +35,7 @@ class DateTimeEntity extends Entity {
|
|||||||
return formattedState;
|
return formattedState;
|
||||||
}
|
}
|
||||||
|
|
||||||
void setNewState(newValue) {
|
void setNewState(Map newValue) {
|
||||||
eventBus
|
ConnectionManager().callService(domain: domain, service: "set_datetime", entityId: entityId, data: newValue);
|
||||||
.fire(new ServiceCallEvent(domain, "set_datetime", entityId, newValue));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -9,10 +9,8 @@ class DateTimeStateWidget extends StatelessWidget {
|
|||||||
padding: EdgeInsets.fromLTRB(0.0, 0.0, Sizes.rightWidgetPadding, 0.0),
|
padding: EdgeInsets.fromLTRB(0.0, 0.0, Sizes.rightWidgetPadding, 0.0),
|
||||||
child: GestureDetector(
|
child: GestureDetector(
|
||||||
child: Text("${entity.formattedState}",
|
child: Text("${entity.formattedState}",
|
||||||
textAlign: TextAlign.right,
|
textAlign: TextAlign.right
|
||||||
style: new TextStyle(
|
),
|
||||||
fontSize: Sizes.stateFontSize,
|
|
||||||
)),
|
|
||||||
onTap: () => _handleStateTap(context, entity),
|
onTap: () => _handleStateTap(context, entity),
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
73
lib/entities/default_entity_container.widget.dart
Normal file
73
lib/entities/default_entity_container.widget.dart
Normal file
@ -0,0 +1,73 @@
|
|||||||
|
part of '../main.dart';
|
||||||
|
|
||||||
|
class DefaultEntityContainer extends StatelessWidget {
|
||||||
|
DefaultEntityContainer({
|
||||||
|
Key key,
|
||||||
|
@required this.state
|
||||||
|
}) : super(key: key);
|
||||||
|
|
||||||
|
final Widget state;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final EntityModel entityModel = EntityModel.of(context);
|
||||||
|
if (entityModel.entityWrapper.entity.statelessType == StatelessEntityType.missed) {
|
||||||
|
return MissedEntityWidget();
|
||||||
|
}
|
||||||
|
if (entityModel.entityWrapper.entity.statelessType == StatelessEntityType.divider) {
|
||||||
|
return Divider();
|
||||||
|
}
|
||||||
|
if (entityModel.entityWrapper.entity.statelessType == StatelessEntityType.section) {
|
||||||
|
return Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: <Widget>[
|
||||||
|
Divider(),
|
||||||
|
Text(
|
||||||
|
"${entityModel.entityWrapper.entity.displayName}",
|
||||||
|
style: HAClientTheme().getLinkTextStyle(context).copyWith(
|
||||||
|
decoration: TextDecoration.none
|
||||||
|
)
|
||||||
|
)
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
Widget result = Row(
|
||||||
|
mainAxisSize: MainAxisSize.max,
|
||||||
|
children: <Widget>[
|
||||||
|
EntityIcon(),
|
||||||
|
|
||||||
|
Flexible(
|
||||||
|
fit: FlexFit.tight,
|
||||||
|
flex: 3,
|
||||||
|
child: EntityName(
|
||||||
|
padding: EdgeInsets.fromLTRB(10.0, 2.0, 10.0, 2.0),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
state
|
||||||
|
],
|
||||||
|
);
|
||||||
|
if (entityModel.handleTap) {
|
||||||
|
return InkWell(
|
||||||
|
onLongPress: () {
|
||||||
|
if (entityModel.handleTap) {
|
||||||
|
entityModel.entityWrapper.handleHold();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onTap: () {
|
||||||
|
if (entityModel.handleTap) {
|
||||||
|
entityModel.entityWrapper.handleTap();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onDoubleTap: () {
|
||||||
|
if (entityModel.handleTap) {
|
||||||
|
entityModel.entityWrapper.handleDoubleTap();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
child: result,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,13 +1,6 @@
|
|||||||
part of '../main.dart';
|
part of '../main.dart';
|
||||||
|
|
||||||
class StatelessEntityType {
|
enum StatelessEntityType {none, missed, ghost, divider, section, callService, webLink}
|
||||||
static const NONE = 0;
|
|
||||||
static const MISSED = 1;
|
|
||||||
static const DIVIDER = 2;
|
|
||||||
static const SECTION = 3;
|
|
||||||
static const CALL_SERVICE = 4;
|
|
||||||
static const WEBLINK = 5;
|
|
||||||
}
|
|
||||||
|
|
||||||
class Entity {
|
class Entity {
|
||||||
|
|
||||||
@ -76,8 +69,8 @@ class Entity {
|
|||||||
String entityPicture;
|
String entityPicture;
|
||||||
String state;
|
String state;
|
||||||
String displayState;
|
String displayState;
|
||||||
DateTime _lastUpdated;
|
DateTime lastUpdatedTimestamp;
|
||||||
int statelessType = 0;
|
StatelessEntityType statelessType = StatelessEntityType.none;
|
||||||
|
|
||||||
List<Entity> childEntities = [];
|
List<Entity> childEntities = [];
|
||||||
String deviceClass;
|
String deviceClass;
|
||||||
@ -120,31 +113,36 @@ class Entity {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Entity.missed(String entityId) {
|
Entity.missed(String entityId) {
|
||||||
statelessType = StatelessEntityType.MISSED;
|
statelessType = StatelessEntityType.missed;
|
||||||
attributes = {"hidden": false};
|
attributes = {"hidden": false};
|
||||||
this.entityId = entityId;
|
this.entityId = entityId;
|
||||||
}
|
}
|
||||||
|
|
||||||
Entity.divider() {
|
Entity.divider() {
|
||||||
statelessType = StatelessEntityType.DIVIDER;
|
statelessType = StatelessEntityType.divider;
|
||||||
attributes = {"hidden": false};
|
attributes = {"hidden": false};
|
||||||
}
|
}
|
||||||
|
|
||||||
Entity.section(String label) {
|
Entity.section(String label) {
|
||||||
statelessType = StatelessEntityType.SECTION;
|
statelessType = StatelessEntityType.section;
|
||||||
attributes = {"hidden": false, "friendly_name": "$label"};
|
attributes = {"hidden": false, "friendly_name": "$label"};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Entity.ghost(String name, String icon) {
|
||||||
|
statelessType = StatelessEntityType.ghost;
|
||||||
|
attributes = {"icon": icon, "hidden": false, "friendly_name": name};
|
||||||
|
}
|
||||||
|
|
||||||
Entity.callService({String icon, String name, String service, String actionName}) {
|
Entity.callService({String icon, String name, String service, String actionName}) {
|
||||||
statelessType = StatelessEntityType.CALL_SERVICE;
|
statelessType = StatelessEntityType.callService;
|
||||||
entityId = service;
|
entityId = service;
|
||||||
displayState = actionName?.toUpperCase() ?? "RUN";
|
displayState = actionName?.toUpperCase() ?? "RUN";
|
||||||
attributes = {"hidden": false, "friendly_name": "$name", "icon": "$icon"};
|
attributes = {"hidden": false, "friendly_name": "$name", "icon": "$icon"};
|
||||||
}
|
}
|
||||||
|
|
||||||
Entity.weblink({String url, String name, String icon}) {
|
Entity.weblink({String url, String name, String icon}) {
|
||||||
statelessType = StatelessEntityType.WEBLINK;
|
statelessType = StatelessEntityType.webLink;
|
||||||
entityId = "custom.custom"; //TODO wtf??
|
entityId = "custom.custom";
|
||||||
attributes = {"hidden": false, "friendly_name": "${name ?? url}", "icon": "${icon ?? 'mdi:link'}"};
|
attributes = {"hidden": false, "friendly_name": "${name ?? url}", "icon": "${icon ?? 'mdi:link'}"};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -153,9 +151,9 @@ class Entity {
|
|||||||
domain = rawData["entity_id"].split(".")[0];
|
domain = rawData["entity_id"].split(".")[0];
|
||||||
entityId = rawData["entity_id"];
|
entityId = rawData["entity_id"];
|
||||||
deviceClass = attributes["device_class"];
|
deviceClass = attributes["device_class"];
|
||||||
state = rawData["state"];
|
state = rawData["state"] is bool ? (rawData["state"] ? EntityState.on : EntityState.off) : rawData["state"];
|
||||||
displayState = Entity.StateByDeviceClass["$deviceClass.$state"] ?? (state.toLowerCase() == 'unknown' ? '-' : state);
|
displayState = Entity.StateByDeviceClass["$deviceClass.$state"] ?? (state.toLowerCase() == 'unknown' ? '-' : state);
|
||||||
_lastUpdated = DateTime.tryParse(rawData["last_updated"]);
|
lastUpdatedTimestamp = DateTime.tryParse(rawData["last_updated"]);
|
||||||
entityPicture = _getEntityPictureUrl(webHost);
|
entityPicture = _getEntityPictureUrl(webHost);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -211,31 +209,6 @@ class Entity {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget buildEntityPageWidget(BuildContext context) {
|
|
||||||
return EntityModel(
|
|
||||||
entityWrapper: EntityWrapper(entity: this),
|
|
||||||
child: EntityPageContainer(children: <Widget>[
|
|
||||||
Padding(
|
|
||||||
padding: EdgeInsets.only(top: Sizes.rowPadding, left: Sizes.leftWidgetPadding),
|
|
||||||
child: DefaultEntityContainer(state: _buildStatePartForPage(context)),
|
|
||||||
),
|
|
||||||
LastUpdatedWidget(),
|
|
||||||
Divider(),
|
|
||||||
_buildAdditionalControlsForPage(context),
|
|
||||||
Divider(),
|
|
||||||
buildHistoryWidget(),
|
|
||||||
EntityAttributesList()
|
|
||||||
]),
|
|
||||||
handleTap: false,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget buildHistoryWidget() {
|
|
||||||
return EntityHistoryWidget(
|
|
||||||
config: historyConfig,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget buildBadgeWidget(BuildContext context) {
|
Widget buildBadgeWidget(BuildContext context) {
|
||||||
return EntityModel(
|
return EntityModel(
|
||||||
entityWrapper: EntityWrapper(entity: this),
|
entityWrapper: EntityWrapper(entity: this),
|
||||||
@ -246,17 +219,17 @@ class Entity {
|
|||||||
|
|
||||||
String getAttribute(String attributeName) {
|
String getAttribute(String attributeName) {
|
||||||
if (attributes != null) {
|
if (attributes != null) {
|
||||||
return attributes["$attributeName"];
|
return attributes["$attributeName"].toString();
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
String _getLastUpdatedFormatted() {
|
String _getLastUpdatedFormatted() {
|
||||||
if (_lastUpdated == null) {
|
if (lastUpdatedTimestamp == null) {
|
||||||
return "-";
|
return "-";
|
||||||
} else {
|
} else {
|
||||||
DateTime now = DateTime.now();
|
DateTime now = DateTime.now();
|
||||||
Duration d = now.difference(_lastUpdated);
|
Duration d = now.difference(lastUpdatedTimestamp);
|
||||||
String text;
|
String text;
|
||||||
int v;
|
int v;
|
||||||
if (d.inDays == 0) {
|
if (d.inDays == 0) {
|
||||||
|
@ -50,24 +50,29 @@ class EntityIcon extends StatelessWidget {
|
|||||||
iconCode = getDefaultIconByEntityId(data.entity.entityId,
|
iconCode = getDefaultIconByEntityId(data.entity.entityId,
|
||||||
data.entity.deviceClass, data.entity.state); //
|
data.entity.deviceClass, data.entity.state); //
|
||||||
}
|
}
|
||||||
return Padding(
|
return Icon(
|
||||||
padding: EdgeInsets.fromLTRB(6.0, 6.0, 6.0, 6.0),
|
|
||||||
child: Icon(
|
|
||||||
IconData(iconCode, fontFamily: 'Material Design Icons'),
|
IconData(iconCode, fontFamily: 'Material Design Icons'),
|
||||||
size: size,
|
size: size,
|
||||||
color: color,
|
color: color,
|
||||||
)
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final EntityWrapper entityWrapper = EntityModel.of(context).entityWrapper;
|
final EntityWrapper entityWrapper = EntityModel.of(context).entityWrapper;
|
||||||
|
Color iconColor;
|
||||||
|
if (color != null) {
|
||||||
|
iconColor = color;
|
||||||
|
} else if (entityWrapper.stateColor) {
|
||||||
|
iconColor = HAClientTheme().getColorByEntityState(entityWrapper.entity.state, context);
|
||||||
|
} else {
|
||||||
|
iconColor = HAClientTheme().getOffStateColor(context);
|
||||||
|
}
|
||||||
return Padding(
|
return Padding(
|
||||||
padding: padding,
|
padding: padding,
|
||||||
child: buildIcon(
|
child: buildIcon(
|
||||||
entityWrapper,
|
entityWrapper,
|
||||||
color ?? EntityColor.stateColor(entityWrapper.entity.state)
|
iconColor
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
@ -12,11 +12,11 @@ class EntityModel extends InheritedWidget {
|
|||||||
final bool handleTap;
|
final bool handleTap;
|
||||||
|
|
||||||
static EntityModel of(BuildContext context) {
|
static EntityModel of(BuildContext context) {
|
||||||
return context.inheritFromWidgetOfExactType(EntityModel);
|
return context.dependOnInheritedWidgetOfExactType<EntityModel>();
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
bool updateShouldNotify(InheritedWidget oldWidget) {
|
bool updateShouldNotify(EntityModel oldWidget) {
|
||||||
return true;
|
return entityWrapper.entity.lastUpdatedTimestamp != oldWidget.entityWrapper.entity.lastUpdatedTimestamp;
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -5,18 +5,24 @@ class EntityName extends StatelessWidget {
|
|||||||
final EdgeInsetsGeometry padding;
|
final EdgeInsetsGeometry padding;
|
||||||
final TextOverflow textOverflow;
|
final TextOverflow textOverflow;
|
||||||
final bool wordsWrap;
|
final bool wordsWrap;
|
||||||
final double fontSize;
|
|
||||||
final TextAlign textAlign;
|
final TextAlign textAlign;
|
||||||
final int maxLines;
|
final int maxLines;
|
||||||
|
final TextStyle textStyle;
|
||||||
|
|
||||||
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);
|
const EntityName({Key key, this.maxLines, this.padding: const EdgeInsets.only(right: 10.0), this.textOverflow: TextOverflow.ellipsis, this.textStyle, this.wordsWrap: true, this.textAlign: TextAlign.left}) : super(key: key);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final EntityWrapper entityWrapper = EntityModel.of(context).entityWrapper;
|
final EntityWrapper entityWrapper = EntityModel.of(context).entityWrapper;
|
||||||
TextStyle textStyle = TextStyle(fontSize: fontSize);
|
TextStyle tStyle;
|
||||||
if (entityWrapper.entity.statelessType == StatelessEntityType.WEBLINK) {
|
if (textStyle == null) {
|
||||||
textStyle = textStyle.apply(color: Colors.blue, decoration: TextDecoration.underline);
|
if (entityWrapper.entity.statelessType == StatelessEntityType.webLink) {
|
||||||
|
tStyle = HAClientTheme().getLinkTextStyle(context);
|
||||||
|
} else {
|
||||||
|
tStyle = Theme.of(context).textTheme.body1;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
tStyle = textStyle;
|
||||||
}
|
}
|
||||||
return Padding(
|
return Padding(
|
||||||
padding: padding,
|
padding: padding,
|
||||||
@ -25,7 +31,7 @@ class EntityName extends StatelessWidget {
|
|||||||
overflow: textOverflow,
|
overflow: textOverflow,
|
||||||
softWrap: wordsWrap,
|
softWrap: wordsWrap,
|
||||||
maxLines: maxLines,
|
maxLines: maxLines,
|
||||||
style: textStyle,
|
style: tStyle,
|
||||||
textAlign: textAlign,
|
textAlign: textAlign,
|
||||||
),
|
),
|
||||||
);
|
);
|
66
lib/entities/entity_page_layout.widget.dart
Normal file
66
lib/entities/entity_page_layout.widget.dart
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
part of '../main.dart';
|
||||||
|
|
||||||
|
class EntityPageLayout extends StatelessWidget {
|
||||||
|
|
||||||
|
final bool showClose;
|
||||||
|
final Entity entity;
|
||||||
|
|
||||||
|
EntityPageLayout({Key key, this.showClose: false, this.entity}) : super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return EntityModel(
|
||||||
|
entityWrapper: EntityWrapper(entity: entity),
|
||||||
|
child: ListView(
|
||||||
|
padding: EdgeInsets.all(0),
|
||||||
|
children: <Widget>[
|
||||||
|
showClose ?
|
||||||
|
Container(
|
||||||
|
color: Theme.of(context).primaryColor,
|
||||||
|
height: 40,
|
||||||
|
child: Row(
|
||||||
|
children: <Widget>[
|
||||||
|
Expanded(
|
||||||
|
child: Padding(
|
||||||
|
padding: EdgeInsets.only(left: 8),
|
||||||
|
child: Text(
|
||||||
|
entity.displayName,
|
||||||
|
style: Theme.of(context).primaryTextTheme.headline
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
IconButton(
|
||||||
|
padding: EdgeInsets.all(0),
|
||||||
|
icon: Icon(Icons.close),
|
||||||
|
color: Theme.of(context).primaryTextTheme.headline.color,
|
||||||
|
iconSize: 36.0,
|
||||||
|
onPressed: () {
|
||||||
|
eventBus.fire(ShowEntityPageEvent());
|
||||||
|
},
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
) :
|
||||||
|
Container(height: 0, width: 0,),
|
||||||
|
Padding(
|
||||||
|
padding: EdgeInsets.only(top: Sizes.rowPadding, left: Sizes.leftWidgetPadding),
|
||||||
|
child: DefaultEntityContainer(state: entity._buildStatePartForPage(context)),
|
||||||
|
),
|
||||||
|
LastUpdatedWidget(),
|
||||||
|
Divider(),
|
||||||
|
entity._buildAdditionalControlsForPage(context),
|
||||||
|
Divider(),
|
||||||
|
SpoilerCard(
|
||||||
|
title: "State history",
|
||||||
|
body: EntityHistoryWidget(),
|
||||||
|
),
|
||||||
|
SpoilerCard(
|
||||||
|
title: "Attributes",
|
||||||
|
body: EntityAttributesList(),
|
||||||
|
),
|
||||||
|
]
|
||||||
|
),
|
||||||
|
handleTap: false,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
71
lib/entities/entity_picture.widget.dart
Normal file
71
lib/entities/entity_picture.widget.dart
Normal file
@ -0,0 +1,71 @@
|
|||||||
|
part of '../main.dart';
|
||||||
|
|
||||||
|
class EntityPicture extends StatelessWidget {
|
||||||
|
|
||||||
|
final EdgeInsetsGeometry padding;
|
||||||
|
final BoxFit fit;
|
||||||
|
|
||||||
|
const EntityPicture({Key key, this.padding: const EdgeInsets.all(0.0), this.fit: BoxFit.cover}) : super(key: key);
|
||||||
|
|
||||||
|
int getDefaultIconByEntityId(String entityId, String deviceClass, String state) {
|
||||||
|
String domain = entityId.split(".")[0];
|
||||||
|
String iconNameByDomain = MaterialDesignIcons.defaultIconsByDomains["$domain.$state"] ?? MaterialDesignIcons.defaultIconsByDomains["$domain"];
|
||||||
|
String iconNameByDeviceClass;
|
||||||
|
if (deviceClass != null) {
|
||||||
|
iconNameByDeviceClass = MaterialDesignIcons.defaultIconsByDeviceClass["$domain.$deviceClass.$state"] ?? MaterialDesignIcons.defaultIconsByDeviceClass["$domain.$deviceClass"];
|
||||||
|
}
|
||||||
|
String iconName = iconNameByDeviceClass ?? iconNameByDomain;
|
||||||
|
if (iconName != null) {
|
||||||
|
return MaterialDesignIcons.iconsDataMap[iconName] ?? 0;
|
||||||
|
} else {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget buildIcon(EntityWrapper data, BuildContext context) {
|
||||||
|
if (data == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
String iconName = data.icon;
|
||||||
|
int iconCode = 0;
|
||||||
|
if (iconName.length > 0) {
|
||||||
|
iconCode = MaterialDesignIcons.getIconCodeByIconName(iconName);
|
||||||
|
} else {
|
||||||
|
iconCode = getDefaultIconByEntityId(data.entity.entityId,
|
||||||
|
data.entity.deviceClass, data.entity.state); //
|
||||||
|
}
|
||||||
|
Widget iconPicture = Container(
|
||||||
|
child: Center(
|
||||||
|
child: Icon(
|
||||||
|
IconData(iconCode, fontFamily: 'Material Design Icons'),
|
||||||
|
size: Sizes.largeIconSize,
|
||||||
|
color: HAClientTheme().getOffStateColor(context),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
if (data.entityPicture != null) {
|
||||||
|
return CachedNetworkImage(
|
||||||
|
imageUrl: data.entityPicture,
|
||||||
|
fit: this.fit,
|
||||||
|
errorWidget: (context, _, __) => iconPicture,
|
||||||
|
placeholder: (context, _) => iconPicture,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return iconPicture;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final EntityWrapper entityWrapper = EntityModel.of(context).entityWrapper;
|
||||||
|
return Padding(
|
||||||
|
padding: padding,
|
||||||
|
child: buildIcon(
|
||||||
|
entityWrapper,
|
||||||
|
context
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -2,25 +2,28 @@ part of '../main.dart';
|
|||||||
|
|
||||||
class EntityWrapper {
|
class EntityWrapper {
|
||||||
|
|
||||||
String displayName;
|
String overrideName;
|
||||||
String icon;
|
final String overrideIcon;
|
||||||
String entityPicture;
|
final bool stateColor;
|
||||||
EntityUIAction uiAction;
|
EntityUIAction uiAction;
|
||||||
Entity entity;
|
Entity entity;
|
||||||
|
String unitOfMeasurementOverride;
|
||||||
|
final List stateFilter;
|
||||||
|
|
||||||
|
String get icon => overrideIcon ?? entity.icon;
|
||||||
|
String get entityPicture => entity.entityPicture;
|
||||||
|
String get displayName => overrideName ?? entity.displayName;
|
||||||
|
String get unitOfMeasurement => unitOfMeasurementOverride ?? entity.unitOfMeasurement;
|
||||||
|
|
||||||
EntityWrapper({
|
EntityWrapper({
|
||||||
this.entity,
|
this.entity,
|
||||||
String icon,
|
this.overrideIcon,
|
||||||
String displayName,
|
this.overrideName,
|
||||||
this.uiAction
|
this.stateColor: true,
|
||||||
|
this.uiAction,
|
||||||
|
this.stateFilter
|
||||||
}) {
|
}) {
|
||||||
if (entity.statelessType == StatelessEntityType.NONE || entity.statelessType == StatelessEntityType.CALL_SERVICE || entity.statelessType == StatelessEntityType.WEBLINK) {
|
if (entity.statelessType == StatelessEntityType.ghost || entity.statelessType == StatelessEntityType.none || entity.statelessType == StatelessEntityType.callService || entity.statelessType == StatelessEntityType.webLink) {
|
||||||
this.icon = icon ?? entity.icon;
|
|
||||||
if (icon == null) {
|
|
||||||
entityPicture = entity.entityPicture;
|
|
||||||
}
|
|
||||||
this.displayName = displayName ?? entity.displayName;
|
|
||||||
if (uiAction == null) {
|
if (uiAction == null) {
|
||||||
uiAction = EntityUIAction();
|
uiAction = EntityUIAction();
|
||||||
}
|
}
|
||||||
@ -30,17 +33,17 @@ class EntityWrapper {
|
|||||||
void handleTap() {
|
void handleTap() {
|
||||||
switch (uiAction.tapAction) {
|
switch (uiAction.tapAction) {
|
||||||
case EntityUIAction.toggle: {
|
case EntityUIAction.toggle: {
|
||||||
eventBus.fire(
|
ConnectionManager().callService(domain: "homeassistant", service: "toggle", entityId: entity.entityId);
|
||||||
ServiceCallEvent("homeassistant", "toggle", entity.entityId, null));
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
case EntityUIAction.callService: {
|
case EntityUIAction.callService: {
|
||||||
if (uiAction.tapService != null) {
|
if (uiAction.tapService != null) {
|
||||||
eventBus.fire(
|
ConnectionManager().callService(
|
||||||
ServiceCallEvent(uiAction.tapService.split(".")[0],
|
domain: uiAction.tapService.split(".")[0],
|
||||||
uiAction.tapService.split(".")[1], null,
|
service: uiAction.tapService.split(".")[1],
|
||||||
uiAction.tapServiceData));
|
data: uiAction.tapServiceData
|
||||||
|
);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@ -51,16 +54,16 @@ class EntityWrapper {
|
|||||||
|
|
||||||
case EntityUIAction.moreInfo: {
|
case EntityUIAction.moreInfo: {
|
||||||
eventBus.fire(
|
eventBus.fire(
|
||||||
new ShowEntityPageEvent(entity));
|
new ShowEntityPageEvent(entity: entity));
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
case EntityUIAction.navigate: {
|
case EntityUIAction.navigate: {
|
||||||
if (uiAction.tapService.startsWith("/")) {
|
if (uiAction.tapService != null && uiAction.tapService.startsWith("/")) {
|
||||||
//TODO handle local urls
|
//TODO handle local urls
|
||||||
Logger.w("Local urls is not supported yet");
|
Logger.w("Local urls is not supported yet");
|
||||||
} else {
|
} else {
|
||||||
HAUtils.launchURL(uiAction.tapService);
|
Launcher.launchURL(uiAction.tapService);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@ -74,33 +77,73 @@ class EntityWrapper {
|
|||||||
void handleHold() {
|
void handleHold() {
|
||||||
switch (uiAction.holdAction) {
|
switch (uiAction.holdAction) {
|
||||||
case EntityUIAction.toggle: {
|
case EntityUIAction.toggle: {
|
||||||
eventBus.fire(
|
ConnectionManager().callService(domain: "homeassistant", service: "toggle", entityId: entity.entityId);
|
||||||
ServiceCallEvent("homeassistant", "toggle", entity.entityId, null));
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
case EntityUIAction.callService: {
|
case EntityUIAction.callService: {
|
||||||
if (uiAction.holdService != null) {
|
if (uiAction.holdService != null) {
|
||||||
eventBus.fire(
|
ConnectionManager().callService(
|
||||||
ServiceCallEvent(uiAction.holdService.split(".")[0],
|
domain: uiAction.holdService.split(".")[0],
|
||||||
uiAction.holdService.split(".")[1], null,
|
service: uiAction.holdService.split(".")[1],
|
||||||
uiAction.holdServiceData));
|
data: uiAction.holdServiceData
|
||||||
|
);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
case EntityUIAction.moreInfo: {
|
case EntityUIAction.moreInfo: {
|
||||||
eventBus.fire(
|
eventBus.fire(
|
||||||
new ShowEntityPageEvent(entity));
|
new ShowEntityPageEvent(entity: entity));
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
case EntityUIAction.navigate: {
|
case EntityUIAction.navigate: {
|
||||||
if (uiAction.holdService.startsWith("/")) {
|
if (uiAction.holdService != null && uiAction.holdService.startsWith("/")) {
|
||||||
//TODO handle local urls
|
//TODO handle local urls
|
||||||
Logger.w("Local urls is not supported yet");
|
Logger.w("Local urls is not supported yet");
|
||||||
} else {
|
} else {
|
||||||
HAUtils.launchURL(uiAction.holdService);
|
Launcher.launchURL(uiAction.holdService);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
default: {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void handleDoubleTap() {
|
||||||
|
switch (uiAction.doubleTapAction) {
|
||||||
|
case EntityUIAction.toggle: {
|
||||||
|
ConnectionManager().callService(domain: "homeassistant", service: "toggle", entityId: entity.entityId);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case EntityUIAction.callService: {
|
||||||
|
if (uiAction.doubleTapService != null) {
|
||||||
|
ConnectionManager().callService(
|
||||||
|
domain: uiAction.doubleTapService.split(".")[0],
|
||||||
|
service: uiAction.doubleTapService.split(".")[1],
|
||||||
|
data: uiAction.doubleTapServiceData
|
||||||
|
);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case EntityUIAction.moreInfo: {
|
||||||
|
eventBus.fire(
|
||||||
|
new ShowEntityPageEvent(entity: entity));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case EntityUIAction.navigate: {
|
||||||
|
if (uiAction.doubleTapService != null && uiAction.doubleTapService.startsWith("/")) {
|
||||||
|
//TODO handle local urls
|
||||||
|
Logger.w("Local urls is not supported yet");
|
||||||
|
} else {
|
||||||
|
Launcher.launchURL(uiAction.doubleTapService);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
@ -24,9 +24,12 @@ class _FanControlsWidgetState extends State<FanControlsWidget> {
|
|||||||
setState(() {
|
setState(() {
|
||||||
_tmpOscillate = oscillate;
|
_tmpOscillate = oscillate;
|
||||||
_changedHere = true;
|
_changedHere = true;
|
||||||
eventBus.fire(new ServiceCallEvent(
|
ConnectionManager().callService(
|
||||||
"fan", "oscillate", entity.entityId,
|
domain: "fan",
|
||||||
{"oscillating": oscillate}));
|
service: "oscillate",
|
||||||
|
entityId: entity.entityId,
|
||||||
|
data: {"oscillating": oscillate}
|
||||||
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -34,9 +37,12 @@ class _FanControlsWidgetState extends State<FanControlsWidget> {
|
|||||||
setState(() {
|
setState(() {
|
||||||
_tmpDirectionForward = forward;
|
_tmpDirectionForward = forward;
|
||||||
_changedHere = true;
|
_changedHere = true;
|
||||||
eventBus.fire(new ServiceCallEvent(
|
ConnectionManager().callService(
|
||||||
"fan", "set_direction", entity.entityId,
|
domain: "fan",
|
||||||
{"direction": forward ? "forward" : "reverse"}));
|
service: "set_direction",
|
||||||
|
entityId: entity.entityId,
|
||||||
|
data: {"direction": forward ? "forward" : "reverse"}
|
||||||
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -44,9 +50,12 @@ class _FanControlsWidgetState extends State<FanControlsWidget> {
|
|||||||
setState(() {
|
setState(() {
|
||||||
_tmpSpeed = value;
|
_tmpSpeed = value;
|
||||||
_changedHere = true;
|
_changedHere = true;
|
||||||
eventBus.fire(new ServiceCallEvent(
|
ConnectionManager().callService(
|
||||||
"fan", "set_speed", entity.entityId,
|
domain: "fan",
|
||||||
{"speed": value}));
|
service: "set_speed",
|
||||||
|
entityId: entity.entityId,
|
||||||
|
data: {"speed": value}
|
||||||
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
part of '../../main.dart';
|
part of '../main.dart';
|
||||||
|
|
||||||
class FlatServiceButton extends StatelessWidget {
|
class FlatServiceButton extends StatelessWidget {
|
||||||
|
|
||||||
@ -6,7 +6,6 @@ class FlatServiceButton extends StatelessWidget {
|
|||||||
final String serviceName;
|
final String serviceName;
|
||||||
final String entityId;
|
final String entityId;
|
||||||
final String text;
|
final String text;
|
||||||
final double fontSize;
|
|
||||||
|
|
||||||
FlatServiceButton({
|
FlatServiceButton({
|
||||||
Key key,
|
Key key,
|
||||||
@ -14,17 +13,16 @@ class FlatServiceButton extends StatelessWidget {
|
|||||||
@required this.serviceName,
|
@required this.serviceName,
|
||||||
@required this.entityId,
|
@required this.entityId,
|
||||||
@required this.text,
|
@required this.text,
|
||||||
this.fontSize: Sizes.stateFontSize
|
|
||||||
}) : super(key: key);
|
}) : super(key: key);
|
||||||
|
|
||||||
void _setNewState() {
|
void _setNewState() {
|
||||||
eventBus.fire(new ServiceCallEvent(serviceDomain, serviceName, entityId, null));
|
ConnectionManager().callService(domain: serviceDomain, service: serviceName, entityId: entityId);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return SizedBox(
|
return SizedBox(
|
||||||
height: fontSize*2.5,
|
height: Theme.of(context).textTheme.subhead.fontSize*2.5,
|
||||||
child: FlatButton(
|
child: FlatButton(
|
||||||
onPressed: (() {
|
onPressed: (() {
|
||||||
_setNewState();
|
_setNewState();
|
||||||
@ -32,8 +30,7 @@ class FlatServiceButton extends StatelessWidget {
|
|||||||
child: Text(
|
child: Text(
|
||||||
text,
|
text,
|
||||||
textAlign: TextAlign.right,
|
textAlign: TextAlign.right,
|
||||||
style:
|
style: HAClientTheme().getActionTextStyle(context),
|
||||||
new TextStyle(fontSize: fontSize, color: Colors.blue),
|
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
);
|
);
|
@ -1,4 +1,4 @@
|
|||||||
part of '../../main.dart';
|
part of '../../../main.dart';
|
||||||
|
|
||||||
class LightColorPicker extends StatefulWidget {
|
class LightColorPicker extends StatefulWidget {
|
||||||
|
|
||||||
@ -27,7 +27,6 @@ class LightColorPickerState extends State<LightColorPicker> {
|
|||||||
List<Widget> colorRows = [];
|
List<Widget> colorRows = [];
|
||||||
Border border;
|
Border border;
|
||||||
bool isSomethingSelected = false;
|
bool isSomethingSelected = false;
|
||||||
Logger.d("Current colotfor picker: [${widget.color.hue}, ${widget.color.saturation}]");
|
|
||||||
for (double saturation = 1.0; saturation >= (0.0 + widget.saturationStep); saturation = double.parse((saturation - widget.saturationStep).toStringAsFixed(2))) {
|
for (double saturation = 1.0; saturation >= (0.0 + widget.saturationStep); saturation = double.parse((saturation - widget.saturationStep).toStringAsFixed(2))) {
|
||||||
List<Widget> rowChildren = [];
|
List<Widget> rowChildren = [];
|
||||||
//Logger.d("$saturation");
|
//Logger.d("$saturation");
|
@ -28,9 +28,12 @@ class _LightControlsWidgetState extends State<LightControlsWidget> {
|
|||||||
setState(() {
|
setState(() {
|
||||||
_tmpBrightness = value.round();
|
_tmpBrightness = value.round();
|
||||||
_changedHere = true;
|
_changedHere = true;
|
||||||
eventBus.fire(new ServiceCallEvent(
|
ConnectionManager().callService(
|
||||||
entity.domain, "turn_on", entity.entityId,
|
domain: entity.domain,
|
||||||
{"brightness": _tmpBrightness}));
|
service: "turn_on",
|
||||||
|
entityId: entity.entityId,
|
||||||
|
data: {"brightness": _tmpBrightness}
|
||||||
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -38,9 +41,12 @@ class _LightControlsWidgetState extends State<LightControlsWidget> {
|
|||||||
setState(() {
|
setState(() {
|
||||||
_tmpWhiteValue = value.round();
|
_tmpWhiteValue = value.round();
|
||||||
_changedHere = true;
|
_changedHere = true;
|
||||||
eventBus.fire(new ServiceCallEvent(
|
ConnectionManager().callService(
|
||||||
entity.domain, "turn_on", entity.entityId,
|
domain: entity.domain,
|
||||||
{"white_value": _tmpWhiteValue}));
|
service: "turn_on",
|
||||||
|
entityId: entity.entityId,
|
||||||
|
data: {"white_value": _tmpWhiteValue}
|
||||||
|
);
|
||||||
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -49,9 +55,12 @@ class _LightControlsWidgetState extends State<LightControlsWidget> {
|
|||||||
setState(() {
|
setState(() {
|
||||||
_tmpColorTemp = value.round();
|
_tmpColorTemp = value.round();
|
||||||
_changedHere = true;
|
_changedHere = true;
|
||||||
eventBus.fire(new ServiceCallEvent(
|
ConnectionManager().callService(
|
||||||
entity.domain, "turn_on", entity.entityId,
|
domain: entity.domain,
|
||||||
{"color_temp": _tmpColorTemp}));
|
service: "turn_on",
|
||||||
|
entityId: entity.entityId,
|
||||||
|
data: {"color_temp": _tmpColorTemp}
|
||||||
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -59,10 +68,12 @@ class _LightControlsWidgetState extends State<LightControlsWidget> {
|
|||||||
setState(() {
|
setState(() {
|
||||||
_tmpColor = color;
|
_tmpColor = color;
|
||||||
_changedHere = true;
|
_changedHere = true;
|
||||||
Logger.d( "HS Color: [${color.hue}, ${color.saturation}]");
|
ConnectionManager().callService(
|
||||||
eventBus.fire(new ServiceCallEvent(
|
domain: entity.domain,
|
||||||
entity.domain, "turn_on", entity.entityId,
|
service: "turn_on",
|
||||||
{"hs_color": [color.hue, color.saturation*100]}));
|
entityId: entity.entityId,
|
||||||
|
data: {"hs_color": [color.hue, color.saturation*100]}
|
||||||
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -71,9 +82,12 @@ class _LightControlsWidgetState extends State<LightControlsWidget> {
|
|||||||
_tmpEffect = value;
|
_tmpEffect = value;
|
||||||
_changedHere = true;
|
_changedHere = true;
|
||||||
if (_tmpEffect != null) {
|
if (_tmpEffect != null) {
|
||||||
eventBus.fire(new ServiceCallEvent(
|
ConnectionManager().callService(
|
||||||
entity.domain, "turn_on", entity.entityId,
|
domain: entity.domain,
|
||||||
{"effect": "$value"}));
|
service: "turn_on",
|
||||||
|
entityId: entity.entityId,
|
||||||
|
data: {"effect": "$value"}
|
||||||
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -100,7 +114,19 @@ class _LightControlsWidgetState extends State<LightControlsWidget> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildBrightnessControl(LightEntity entity) {
|
Widget _buildBrightnessControl(LightEntity entity) {
|
||||||
if ((entity.supportBrightness) && (_tmpBrightness != null)) {
|
if (entity.supportBrightness) {
|
||||||
|
double val;
|
||||||
|
if (_tmpBrightness != null) {
|
||||||
|
if (_tmpBrightness > 255) {
|
||||||
|
val = 255;
|
||||||
|
} else if (_tmpBrightness < 1) {
|
||||||
|
val = 1;
|
||||||
|
} else {
|
||||||
|
val = _tmpBrightness.toDouble();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
val = 1;
|
||||||
|
}
|
||||||
return UniversalSlider(
|
return UniversalSlider(
|
||||||
onChanged: (value) {
|
onChanged: (value) {
|
||||||
setState(() {
|
setState(() {
|
||||||
@ -111,7 +137,7 @@ class _LightControlsWidgetState extends State<LightControlsWidget> {
|
|||||||
min: 1.0,
|
min: 1.0,
|
||||||
max: 255.0,
|
max: 255.0,
|
||||||
onChangeEnd: (value) => _setBrightness(entity, value),
|
onChangeEnd: (value) => _setBrightness(entity, value),
|
||||||
value: _tmpBrightness == null ? 1.0 : _tmpBrightness.toDouble(),
|
value: val,
|
||||||
leading: Icon(Icons.brightness_5),
|
leading: Icon(Icons.brightness_5),
|
||||||
title: "Brightness",
|
title: "Brightness",
|
||||||
);
|
);
|
||||||
@ -143,10 +169,22 @@ class _LightControlsWidgetState extends State<LightControlsWidget> {
|
|||||||
|
|
||||||
Widget _buildColorTempControl(LightEntity entity) {
|
Widget _buildColorTempControl(LightEntity entity) {
|
||||||
if (entity.supportColorTemp) {
|
if (entity.supportColorTemp) {
|
||||||
|
double val;
|
||||||
|
if (_tmpColorTemp != null) {
|
||||||
|
if (_tmpColorTemp > entity.maxMireds) {
|
||||||
|
val = entity.maxMireds;
|
||||||
|
} else if (_tmpColorTemp < entity.minMireds) {
|
||||||
|
val = entity.minMireds;
|
||||||
|
} else {
|
||||||
|
val = _tmpColorTemp.toDouble();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
val = entity.minMireds;
|
||||||
|
}
|
||||||
return UniversalSlider(
|
return UniversalSlider(
|
||||||
title: "Color temperature",
|
title: "Color temperature",
|
||||||
leading: Text("Cold", style: TextStyle(color: Colors.lightBlue),),
|
leading: Text("Cold", style: Theme.of(context).textTheme.body1.copyWith(color: Colors.lightBlue)),
|
||||||
value: _tmpColorTemp == null ? entity.maxMireds : _tmpColorTemp.toDouble(),
|
value: val,
|
||||||
onChangeEnd: (value) => _setColorTemp(entity, value),
|
onChangeEnd: (value) => _setColorTemp(entity, value),
|
||||||
max: entity.maxMireds,
|
max: entity.maxMireds,
|
||||||
min: entity.minMireds,
|
min: entity.minMireds,
|
||||||
@ -156,7 +194,7 @@ class _LightControlsWidgetState extends State<LightControlsWidget> {
|
|||||||
_tmpColorTemp = value.round();
|
_tmpColorTemp = value.round();
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
closing: Text("Warm", style: TextStyle(color: Colors.amberAccent),),
|
closing: Text("Warm", style: Theme.of(context).textTheme.body1.copyWith(color: Colors.amberAccent),),
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
return Container(width: 0.0, height: 0.0);
|
return Container(width: 0.0, height: 0.0);
|
||||||
@ -186,7 +224,7 @@ class _LightControlsWidgetState extends State<LightControlsWidget> {
|
|||||||
},
|
},
|
||||||
),
|
),
|
||||||
FlatButton(
|
FlatButton(
|
||||||
color: savedColor?.toColor() ?? Colors.transparent,
|
color: savedColor?.toColor() ?? Theme.of(context).backgroundColor,
|
||||||
child: Text('Paste color'),
|
child: Text('Paste color'),
|
||||||
onPressed: savedColor == null ? null : () {
|
onPressed: savedColor == null ? null : () {
|
||||||
_setColor(entity, savedColor);
|
_setColor(entity, savedColor);
|
||||||
@ -203,10 +241,14 @@ class _LightControlsWidgetState extends State<LightControlsWidget> {
|
|||||||
|
|
||||||
Widget _buildEffectControl(LightEntity entity) {
|
Widget _buildEffectControl(LightEntity entity) {
|
||||||
if ((entity.supportEffect) && (entity.effectList != null)) {
|
if ((entity.supportEffect) && (entity.effectList != null)) {
|
||||||
|
List<String> list = List.from(entity.effectList);
|
||||||
|
if (_tmpEffect!= null && !list.contains(_tmpEffect)) {
|
||||||
|
list.insert(0, _tmpEffect);
|
||||||
|
}
|
||||||
return ModeSelectorWidget(
|
return ModeSelectorWidget(
|
||||||
onChange: (effect) => _setEffect(entity, effect),
|
onChange: (effect) => _setEffect(entity, effect),
|
||||||
caption: "Effect",
|
caption: "Effect",
|
||||||
options: entity.effectList,
|
options: list,
|
||||||
value: _tmpEffect
|
value: _tmpEffect
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
|
@ -7,11 +7,11 @@ class LockStateWidget extends StatelessWidget {
|
|||||||
const LockStateWidget({Key key, this.assumedState: false}) : super(key: key);
|
const LockStateWidget({Key key, this.assumedState: false}) : super(key: key);
|
||||||
|
|
||||||
void _lock(Entity entity) {
|
void _lock(Entity entity) {
|
||||||
eventBus.fire(new ServiceCallEvent("lock", "lock", entity.entityId, null));
|
ConnectionManager().callService(domain: "lock", service: "lock", entityId: entity.entityId);
|
||||||
}
|
}
|
||||||
|
|
||||||
void _unlock(Entity entity) {
|
void _unlock(Entity entity) {
|
||||||
eventBus.fire(new ServiceCallEvent("lock", "unlock", entity.entityId, null));
|
ConnectionManager().callService(domain: "lock", service: "unlock", entityId: entity.entityId);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -28,8 +28,7 @@ class LockStateWidget extends StatelessWidget {
|
|||||||
onPressed: () => _unlock(entity),
|
onPressed: () => _unlock(entity),
|
||||||
child: Text("UNLOCK",
|
child: Text("UNLOCK",
|
||||||
textAlign: TextAlign.right,
|
textAlign: TextAlign.right,
|
||||||
style:
|
style: HAClientTheme().getActionTextStyle(context)
|
||||||
new TextStyle(fontSize: Sizes.stateFontSize, color: Colors.blue),
|
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
@ -39,8 +38,7 @@ class LockStateWidget extends StatelessWidget {
|
|||||||
onPressed: () => _lock(entity),
|
onPressed: () => _lock(entity),
|
||||||
child: Text("LOCK",
|
child: Text("LOCK",
|
||||||
textAlign: TextAlign.right,
|
textAlign: TextAlign.right,
|
||||||
style:
|
style: HAClientTheme().getActionTextStyle(context),
|
||||||
new TextStyle(fontSize: Sizes.stateFontSize, color: Colors.blue),
|
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
@ -56,8 +54,7 @@ class LockStateWidget extends StatelessWidget {
|
|||||||
child: Text(
|
child: Text(
|
||||||
entity.isLocked ? "UNLOCK" : "LOCK",
|
entity.isLocked ? "UNLOCK" : "LOCK",
|
||||||
textAlign: TextAlign.right,
|
textAlign: TextAlign.right,
|
||||||
style:
|
style: HAClientTheme().getActionTextStyle(context),
|
||||||
new TextStyle(fontSize: Sizes.stateFontSize, color: Colors.blue),
|
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
@ -74,10 +74,34 @@ class MediaPlayerEntity extends Entity {
|
|||||||
|
|
||||||
List<String> get soundModeList => getStringListAttributeValue("sound_mode_list");
|
List<String> get soundModeList => getStringListAttributeValue("sound_mode_list");
|
||||||
List<String> get sourceList => getStringListAttributeValue("source_list");
|
List<String> get sourceList => getStringListAttributeValue("source_list");
|
||||||
|
DateTime get positionLastUpdated => DateTime.tryParse("${attributes["media_position_updated_at"]}")?.toLocal();
|
||||||
|
int get durationSeconds => _getIntAttributeValue("media_duration");
|
||||||
|
int get positionSeconds => _getIntAttributeValue("media_position");
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget _buildAdditionalControlsForPage(BuildContext context) {
|
Widget _buildAdditionalControlsForPage(BuildContext context) {
|
||||||
return MediaPlayerControls();
|
return MediaPlayerControls();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool canCalculateActualPosition() {
|
||||||
|
return positionLastUpdated != null && durationSeconds != null && positionSeconds != null && durationSeconds > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
double getActualPosition() {
|
||||||
|
double result = 0;
|
||||||
|
Duration durationD;
|
||||||
|
Duration positionD;
|
||||||
|
durationD = Duration(seconds: durationSeconds);
|
||||||
|
positionD = Duration(
|
||||||
|
seconds: positionSeconds);
|
||||||
|
result = positionD.inSeconds.toDouble();
|
||||||
|
int differenceInSeconds = DateTime
|
||||||
|
.now()
|
||||||
|
.difference(positionLastUpdated)
|
||||||
|
.inSeconds;
|
||||||
|
result = ((result + differenceInSeconds) <= durationD.inSeconds) ? (result + differenceInSeconds) : durationD.inSeconds.toDouble();
|
||||||
|
return result;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
@ -0,0 +1,46 @@
|
|||||||
|
part of '../../../main.dart';
|
||||||
|
|
||||||
|
class MediaPlayerProgressBar extends StatefulWidget {
|
||||||
|
@override
|
||||||
|
_MediaPlayerProgressBarState createState() => _MediaPlayerProgressBarState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _MediaPlayerProgressBarState extends State<MediaPlayerProgressBar> {
|
||||||
|
|
||||||
|
Timer _timer;
|
||||||
|
|
||||||
|
@override
|
||||||
|
initState() {
|
||||||
|
super.initState();
|
||||||
|
_timer = Timer.periodic(Duration(seconds: 1), (_) {
|
||||||
|
setState(() {
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final EntityModel entityModel = EntityModel.of(context);
|
||||||
|
final MediaPlayerEntity entity = entityModel.entityWrapper.entity;
|
||||||
|
double progress = 0;
|
||||||
|
int currentPosition;
|
||||||
|
if (entity.canCalculateActualPosition()) {
|
||||||
|
currentPosition = entity.getActualPosition().toInt();
|
||||||
|
if (currentPosition > 0) {
|
||||||
|
progress = (currentPosition <= entity.durationSeconds) ? currentPosition / entity.durationSeconds : 100;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return LinearProgressIndicator(
|
||||||
|
value: progress,
|
||||||
|
backgroundColor: Colors.black45,
|
||||||
|
valueColor: AlwaysStoppedAnimation<Color>(HAClientTheme().getOnStateColor(context)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_timer?.cancel();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,135 @@
|
|||||||
|
part of '../../../main.dart';
|
||||||
|
|
||||||
|
class MediaPlayerSeekBar extends StatefulWidget {
|
||||||
|
@override
|
||||||
|
_MediaPlayerSeekBarState createState() => _MediaPlayerSeekBarState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _MediaPlayerSeekBarState extends State<MediaPlayerSeekBar> {
|
||||||
|
|
||||||
|
Timer _timer;
|
||||||
|
bool _seekStarted = false;
|
||||||
|
bool _changedHere = false;
|
||||||
|
double _currentPosition = 0;
|
||||||
|
int _savedPosition = 0;
|
||||||
|
|
||||||
|
@override
|
||||||
|
initState() {
|
||||||
|
super.initState();
|
||||||
|
_timer = Timer.periodic(Duration(seconds: 1), (_) {
|
||||||
|
if (!_seekStarted && !_changedHere) {
|
||||||
|
setState(() {});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final EntityModel entityModel = EntityModel.of(context);
|
||||||
|
final MediaPlayerEntity entity = entityModel.entityWrapper.entity;
|
||||||
|
|
||||||
|
if (entity.canCalculateActualPosition() && entity.state != EntityState.idle) {
|
||||||
|
if (HomeAssistant().sendToPlayerId == entity.entityId && HomeAssistant().savedPlayerPosition != null) {
|
||||||
|
_savedPosition = HomeAssistant().savedPlayerPosition;
|
||||||
|
HomeAssistant().savedPlayerPosition = null;
|
||||||
|
HomeAssistant().sendToPlayerId = null;
|
||||||
|
}
|
||||||
|
if (entity.state == EntityState.playing && !_seekStarted &&
|
||||||
|
!_changedHere) {
|
||||||
|
_currentPosition = entity.getActualPosition();
|
||||||
|
} else if (entity.state == EntityState.paused) {
|
||||||
|
_currentPosition = entity.positionSeconds.toDouble();
|
||||||
|
} else if (_changedHere) {
|
||||||
|
_changedHere = false;
|
||||||
|
}
|
||||||
|
List<Widget> buttons = [];
|
||||||
|
if (_savedPosition > 0) {
|
||||||
|
buttons.add(
|
||||||
|
RaisedButton(
|
||||||
|
child: Text("Jump to ${Duration(seconds: _savedPosition).toString().split('.')[0]}"),
|
||||||
|
color: Theme.of(context).accentColor,
|
||||||
|
onPressed: () {
|
||||||
|
ConnectionManager().callService(
|
||||||
|
domain: "media_player",
|
||||||
|
service: "media_seek",
|
||||||
|
entityId: entity.entityId,
|
||||||
|
data: {"seek_position": _savedPosition}
|
||||||
|
);
|
||||||
|
setState(() {
|
||||||
|
_savedPosition = 0;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return Padding(
|
||||||
|
padding: EdgeInsets.fromLTRB(Sizes.leftWidgetPadding, 20, Sizes.rightWidgetPadding, 0),
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: <Widget>[
|
||||||
|
Row(
|
||||||
|
mainAxisSize: MainAxisSize.max,
|
||||||
|
children: <Widget>[
|
||||||
|
Text("00:00"),
|
||||||
|
Expanded(
|
||||||
|
child: Text(
|
||||||
|
"${Duration(seconds: _currentPosition.toInt()).toString().split(".")[0]}",
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
style: Theme.of(context).textTheme.title.copyWith(
|
||||||
|
color: Colors.blue
|
||||||
|
)
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Text("${Duration(seconds: entity.durationSeconds).toString().split(".")[0]}")
|
||||||
|
],
|
||||||
|
),
|
||||||
|
Container(height: 10,),
|
||||||
|
Slider(
|
||||||
|
min: 0,
|
||||||
|
activeColor: Theme.of(context).accentColor,
|
||||||
|
max: entity.durationSeconds.toDouble(),
|
||||||
|
value: _currentPosition,
|
||||||
|
onChangeStart: (val) {
|
||||||
|
_seekStarted = true;
|
||||||
|
},
|
||||||
|
onChanged: (val) {
|
||||||
|
setState(() {
|
||||||
|
_currentPosition = val;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
onChangeEnd: (val) {
|
||||||
|
_seekStarted = false;
|
||||||
|
Timer(Duration(milliseconds: 500), () {
|
||||||
|
if (!_seekStarted) {
|
||||||
|
ConnectionManager().callService(
|
||||||
|
domain: "media_player",
|
||||||
|
service: "media_seek",
|
||||||
|
entityId: entity.entityId,
|
||||||
|
data: {"seek_position": val}
|
||||||
|
);
|
||||||
|
setState(() {
|
||||||
|
_changedHere = true;
|
||||||
|
_currentPosition = val;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
),
|
||||||
|
ButtonBar(
|
||||||
|
children: buttons,
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return Container(width: 0, height: 0,);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_timer?.cancel();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -12,21 +12,21 @@ class MediaPlayerWidget extends StatelessWidget {
|
|||||||
Stack(
|
Stack(
|
||||||
alignment: AlignmentDirectional.topEnd,
|
alignment: AlignmentDirectional.topEnd,
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
_buildImage(entity),
|
_buildImage(entity, context),
|
||||||
Positioned(
|
Positioned(
|
||||||
bottom: 0.0,
|
bottom: 0.0,
|
||||||
left: 0.0,
|
left: 0.0,
|
||||||
right: 0.0,
|
right: 0.0,
|
||||||
child: Container(
|
child: Container(
|
||||||
color: Colors.black45,
|
color: Colors.black45,
|
||||||
child: _buildState(entity),
|
child: _buildState(entity, context),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
Positioned(
|
Positioned(
|
||||||
bottom: 0.0,
|
bottom: 0.0,
|
||||||
left: 0.0,
|
left: 0.0,
|
||||||
right: 0.0,
|
right: 0.0,
|
||||||
child: MediaPlayerProgressWidget()
|
child: MediaPlayerProgressBar()
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@ -35,12 +35,9 @@ class MediaPlayerWidget extends StatelessWidget {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildState(MediaPlayerEntity entity) {
|
Widget _buildState(MediaPlayerEntity entity, BuildContext context) {
|
||||||
TextStyle style = TextStyle(
|
TextStyle style = Theme.of(context).textTheme.body1.copyWith(
|
||||||
fontSize: 14.0,
|
color: Colors.white
|
||||||
color: Colors.white,
|
|
||||||
fontWeight: FontWeight.normal,
|
|
||||||
height: 1.2
|
|
||||||
);
|
);
|
||||||
List<Widget> states = [];
|
List<Widget> states = [];
|
||||||
states.add(Text("${entity.displayName}", style: style));
|
states.add(Text("${entity.displayName}", style: style));
|
||||||
@ -71,7 +68,7 @@ class MediaPlayerWidget extends StatelessWidget {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildImage(MediaPlayerEntity entity) {
|
Widget _buildImage(MediaPlayerEntity entity, BuildContext context) {
|
||||||
String state = entity.state;
|
String state = entity.state;
|
||||||
if (entity.entityPicture != null && state != EntityState.off && state != EntityState.unavailable && state != EntityState.idle) {
|
if (entity.entityPicture != null && state != EntityState.off && state != EntityState.unavailable && state != EntityState.idle) {
|
||||||
return Container(
|
return Container(
|
||||||
@ -97,7 +94,7 @@ class MediaPlayerWidget extends StatelessWidget {
|
|||||||
Icon(
|
Icon(
|
||||||
MaterialDesignIcons.getIconDataFromIconName("mdi:movie"),
|
MaterialDesignIcons.getIconDataFromIconName("mdi:movie"),
|
||||||
size: 150.0,
|
size: 150.0,
|
||||||
color: EntityColor.stateColor("$state"),
|
color: HAClientTheme().getColorByEntityState("$state", context),
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
@ -118,26 +115,28 @@ class MediaPlayerPlaybackControls extends StatelessWidget {
|
|||||||
|
|
||||||
|
|
||||||
void _setPower(MediaPlayerEntity entity) {
|
void _setPower(MediaPlayerEntity entity) {
|
||||||
if (entity.state != EntityState.unavailable && entity.state != EntityState.unknown) {
|
|
||||||
if (entity.state == EntityState.off) {
|
if (entity.state == EntityState.off) {
|
||||||
Logger.d("${entity.entityId} turn_on");
|
ConnectionManager().callService(
|
||||||
eventBus.fire(new ServiceCallEvent(
|
domain: entity.domain,
|
||||||
entity.domain, "turn_on", entity.entityId,
|
service: "turn_on",
|
||||||
null));
|
entityId: entity.entityId
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
Logger.d("${entity.entityId} turn_off");
|
ConnectionManager().callService(
|
||||||
eventBus.fire(new ServiceCallEvent(
|
domain: entity.domain,
|
||||||
entity.domain, "turn_off", entity.entityId,
|
service: "turn_off",
|
||||||
null));
|
entityId: entity.entityId
|
||||||
}
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void _callAction(MediaPlayerEntity entity, String action) {
|
void _callAction(MediaPlayerEntity entity, String action) {
|
||||||
Logger.d("${entity.entityId} $action");
|
Logger.d("${entity.entityId} $action");
|
||||||
eventBus.fire(new ServiceCallEvent(
|
ConnectionManager().callService(
|
||||||
entity.domain, "$action", entity.entityId,
|
domain: entity.domain,
|
||||||
null));
|
service: "$action",
|
||||||
|
entityId: entity.entityId
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -229,7 +228,7 @@ class MediaPlayerPlaybackControls extends StatelessWidget {
|
|||||||
IconButton(
|
IconButton(
|
||||||
icon: Icon(MaterialDesignIcons.getIconDataFromIconName(
|
icon: Icon(MaterialDesignIcons.getIconDataFromIconName(
|
||||||
"mdi:dots-vertical")),
|
"mdi:dots-vertical")),
|
||||||
onPressed: () => eventBus.fire(new ShowEntityPageEvent(entity))
|
onPressed: () => eventBus.fire(new ShowEntityPageEvent(entity: entity))
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
} else if (entity.supportStop && entity.state != EntityState.off && entity.state != EntityState.unavailable) {
|
} else if (entity.supportStop && entity.state != EntityState.off && entity.state != EntityState.unavailable) {
|
||||||
@ -264,27 +263,50 @@ class _MediaPlayerControlsState extends State<MediaPlayerControls> {
|
|||||||
setState(() {
|
setState(() {
|
||||||
_changedHere = true;
|
_changedHere = true;
|
||||||
_newVolumeLevel = value;
|
_newVolumeLevel = value;
|
||||||
eventBus.fire(ServiceCallEvent("media_player", "volume_set", entityId, {"volume_level": value}));
|
ConnectionManager().callService(
|
||||||
|
domain: "media_player",
|
||||||
|
service: "volume_set",
|
||||||
|
entityId: entityId,
|
||||||
|
data: {"volume_level": value}
|
||||||
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
void _setVolumeMute(bool isMuted, String entityId) {
|
void _setVolumeMute(bool isMuted, String entityId) {
|
||||||
eventBus.fire(ServiceCallEvent("media_player", "volume_mute", entityId, {"is_volume_muted": isMuted}));
|
ConnectionManager().callService(
|
||||||
|
domain: "media_player",
|
||||||
|
service: "volume_mute",
|
||||||
|
entityId: entityId,
|
||||||
|
data: {"is_volume_muted": isMuted}
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
void _setVolumeUp(String entityId) {
|
void _setVolumeUp(String entityId) {
|
||||||
eventBus.fire(ServiceCallEvent("media_player", "volume_up", entityId, null));
|
ConnectionManager().callService(
|
||||||
|
domain: "media_player",
|
||||||
|
service: "volume_up",
|
||||||
|
entityId: entityId
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
void _setVolumeDown(String entityId) {
|
void _setVolumeDown(String entityId) {
|
||||||
eventBus.fire(ServiceCallEvent("media_player", "volume_down", entityId, null));
|
ConnectionManager().callService(
|
||||||
|
domain: "media_player",
|
||||||
|
service: "volume_down",
|
||||||
|
entityId: entityId
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
void _setSoundMode(String value, String entityId) {
|
void _setSoundMode(String value, String entityId) {
|
||||||
setState(() {
|
setState(() {
|
||||||
_newSoundMode = value;
|
_newSoundMode = value;
|
||||||
_changedHere = true;
|
_changedHere = true;
|
||||||
eventBus.fire(ServiceCallEvent("media_player", "select_sound_mode", entityId, {"sound_mode": "$value"}));
|
ConnectionManager().callService(
|
||||||
|
domain: "media_player",
|
||||||
|
service: "select_sound_mode",
|
||||||
|
entityId: entityId,
|
||||||
|
data: {"sound_mode": "$value"}
|
||||||
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -292,7 +314,12 @@ class _MediaPlayerControlsState extends State<MediaPlayerControls> {
|
|||||||
setState(() {
|
setState(() {
|
||||||
_newSource = source;
|
_newSource = source;
|
||||||
_changedHere = true;
|
_changedHere = true;
|
||||||
eventBus.fire(ServiceCallEvent("media_player", "select_source", entityId, {"source": "$source"}));
|
ConnectionManager().callService(
|
||||||
|
domain: "media_player",
|
||||||
|
service: "select_source",
|
||||||
|
entityId: entityId,
|
||||||
|
data: {"source": "$source"}
|
||||||
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -305,6 +332,11 @@ class _MediaPlayerControlsState extends State<MediaPlayerControls> {
|
|||||||
)
|
)
|
||||||
];
|
];
|
||||||
if (entity.state != EntityState.off && entity.state != EntityState.unknown && entity.state != EntityState.unavailable) {
|
if (entity.state != EntityState.off && entity.state != EntityState.unknown && entity.state != EntityState.unavailable) {
|
||||||
|
if (entity.supportSeek) {
|
||||||
|
children.add(MediaPlayerSeekBar());
|
||||||
|
} else {
|
||||||
|
children.add(MediaPlayerProgressBar());
|
||||||
|
}
|
||||||
Widget muteWidget;
|
Widget muteWidget;
|
||||||
Widget volumeStepWidget;
|
Widget volumeStepWidget;
|
||||||
if (entity.supportVolumeMute || entity.attributes["is_volume_muted"] != null) {
|
if (entity.supportVolumeMute || entity.attributes["is_volume_muted"] != null) {
|
||||||
@ -321,13 +353,13 @@ class _MediaPlayerControlsState extends State<MediaPlayerControls> {
|
|||||||
volumeStepWidget = Row(
|
volumeStepWidget = Row(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
IconButton(
|
|
||||||
icon: Icon(MaterialDesignIcons.getIconDataFromIconName("mdi:plus")),
|
|
||||||
onPressed: () => _setVolumeUp(entity.entityId)
|
|
||||||
),
|
|
||||||
IconButton(
|
IconButton(
|
||||||
icon: Icon(MaterialDesignIcons.getIconDataFromIconName("mdi:minus")),
|
icon: Icon(MaterialDesignIcons.getIconDataFromIconName("mdi:minus")),
|
||||||
onPressed: () => _setVolumeDown(entity.entityId)
|
onPressed: () => _setVolumeDown(entity.entityId)
|
||||||
|
),
|
||||||
|
IconButton(
|
||||||
|
icon: Icon(MaterialDesignIcons.getIconDataFromIconName("mdi:plus")),
|
||||||
|
onPressed: () => _setVolumeUp(entity.entityId)
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
@ -398,69 +430,47 @@ class _MediaPlayerControlsState extends State<MediaPlayerControls> {
|
|||||||
)
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
if (entity.state == EntityState.playing || entity.state == EntityState.paused) {
|
||||||
|
children.add(
|
||||||
|
ButtonBar(
|
||||||
|
children: <Widget>[
|
||||||
|
RaisedButton(
|
||||||
|
child: Text("Duplicate to"),
|
||||||
|
color: Colors.blue,
|
||||||
|
textColor: Colors.white,
|
||||||
|
onPressed: () => _duplicateTo(entity),
|
||||||
|
),
|
||||||
|
RaisedButton(
|
||||||
|
child: Text("Switch to"),
|
||||||
|
color: Colors.blue,
|
||||||
|
textColor: Colors.white,
|
||||||
|
onPressed: () => _switchTo(entity),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return Column(
|
return Column(
|
||||||
children: children,
|
children: children,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
void _duplicateTo(entity) {
|
||||||
|
if (entity.canCalculateActualPosition()) {
|
||||||
class MediaPlayerProgressWidget extends StatefulWidget {
|
HomeAssistant().savedPlayerPosition = entity.getActualPosition().toInt();
|
||||||
@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 {
|
} else {
|
||||||
_timer?.cancel();
|
HomeAssistant().savedPlayerPosition = 0;
|
||||||
}
|
}
|
||||||
progress = currentPosition / duration.inSeconds;
|
Navigator.of(context).pushNamed("/play-media", arguments: {
|
||||||
return LinearProgressIndicator(
|
"url": entity.attributes["media_content_id"],
|
||||||
value: progress,
|
"type": entity.attributes["media_content_type"]
|
||||||
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 _switchTo(entity) {
|
||||||
void dispose() {
|
HomeAssistant().sendFromPlayerId = entity.entityId;
|
||||||
_timer?.cancel();
|
_duplicateTo(entity);
|
||||||
super.dispose();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
@ -11,8 +11,12 @@ class SelectStateWidget extends StatefulWidget {
|
|||||||
class _SelectStateWidgetState extends State<SelectStateWidget> {
|
class _SelectStateWidgetState extends State<SelectStateWidget> {
|
||||||
|
|
||||||
void setNewState(domain, entityId, newValue) {
|
void setNewState(domain, entityId, newValue) {
|
||||||
eventBus.fire(new ServiceCallEvent(domain, "select_option", entityId,
|
ConnectionManager().callService(
|
||||||
{"option": "$newValue"}));
|
domain: domain,
|
||||||
|
service: "select_option",
|
||||||
|
entityId: entityId,
|
||||||
|
data: {"option": "$newValue"}
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
part of '../../main.dart';
|
part of '../main.dart';
|
||||||
|
|
||||||
class SimpleEntityState extends StatelessWidget {
|
class SimpleEntityState extends StatelessWidget {
|
||||||
|
|
||||||
@ -7,8 +7,10 @@ class SimpleEntityState extends StatelessWidget {
|
|||||||
final EdgeInsetsGeometry padding;
|
final EdgeInsetsGeometry padding;
|
||||||
final int maxLines;
|
final int maxLines;
|
||||||
final String customValue;
|
final String customValue;
|
||||||
|
final TextStyle textStyle;
|
||||||
|
//final bool bold;
|
||||||
|
|
||||||
const SimpleEntityState({Key key, this.maxLines: 10, this.expanded: true, this.textAlign: TextAlign.right, this.padding: const EdgeInsets.fromLTRB(0.0, 0.0, Sizes.rightWidgetPadding, 0.0), this.customValue}) : super(key: key);
|
const SimpleEntityState({Key key,/*this.bold: false,*/ this.maxLines: 10, this.expanded: true, this.textAlign: TextAlign.right, this.textStyle, this.padding: const EdgeInsets.fromLTRB(0.0, 0.0, Sizes.rightWidgetPadding, 0.0), this.customValue}) : super(key: key);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
@ -20,24 +22,31 @@ class SimpleEntityState extends StatelessWidget {
|
|||||||
} else {
|
} else {
|
||||||
state = customValue;
|
state = customValue;
|
||||||
}
|
}
|
||||||
TextStyle textStyle = TextStyle(
|
TextStyle tStyle;
|
||||||
fontSize: Sizes.stateFontSize,
|
if (textStyle != null) {
|
||||||
|
tStyle = textStyle;
|
||||||
|
} else if (entityModel.entityWrapper.entity.statelessType == StatelessEntityType.callService) {
|
||||||
|
tStyle = Theme.of(context).textTheme.subhead.copyWith(
|
||||||
|
color: Colors.blue
|
||||||
);
|
);
|
||||||
if (entityModel.entityWrapper.entity.statelessType == StatelessEntityType.CALL_SERVICE) {
|
} else {
|
||||||
textStyle = textStyle.apply(color: Colors.blue);
|
tStyle = Theme.of(context).textTheme.body1;
|
||||||
}
|
}
|
||||||
|
/*if (this.bold) {
|
||||||
|
textStyle = textStyle.apply(fontWeightDelta: 100);
|
||||||
|
}*/
|
||||||
while (state.contains(" ")){
|
while (state.contains(" ")){
|
||||||
state = state.replaceAll(" ", " ");
|
state = state.replaceAll(" ", " ");
|
||||||
}
|
}
|
||||||
Widget result = Padding(
|
Widget result = Padding(
|
||||||
padding: padding,
|
padding: padding,
|
||||||
child: Text(
|
child: Text(
|
||||||
"$state ${entityModel.entityWrapper.entity.unitOfMeasurement}",
|
"$state ${entityModel.entityWrapper.unitOfMeasurement}",
|
||||||
textAlign: textAlign,
|
textAlign: textAlign,
|
||||||
maxLines: maxLines,
|
maxLines: maxLines,
|
||||||
overflow: TextOverflow.ellipsis,
|
overflow: TextOverflow.ellipsis,
|
||||||
softWrap: true,
|
softWrap: true,
|
||||||
style: textStyle
|
style: tStyle
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
if (expanded) {
|
if (expanded) {
|
@ -18,8 +18,12 @@ class _SliderControlsWidgetState extends State<SliderControlsWidget> {
|
|||||||
_newValue = newValue;
|
_newValue = newValue;
|
||||||
_changedHere = true;
|
_changedHere = true;
|
||||||
});
|
});
|
||||||
eventBus.fire(new ServiceCallEvent(domain, "set_value", entityId,
|
ConnectionManager().callService(
|
||||||
{"value": "${newValue.toString()}"}));
|
domain: domain,
|
||||||
|
service: "set_value",
|
||||||
|
entityId: entityId,
|
||||||
|
data: {"value": "${newValue.toString()}"}
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -58,8 +62,7 @@ class _SliderControlsWidgetState extends State<SliderControlsWidget> {
|
|||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
Text(
|
Text(
|
||||||
"$_newValue",
|
"$_newValue",
|
||||||
style: TextStyle(
|
style: Theme.of(context).textTheme.display1.copyWith(
|
||||||
fontSize: Sizes.largeFontSize,
|
|
||||||
color: Colors.blue
|
color: Colors.blue
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
@ -38,8 +38,11 @@ class _SwitchStateWidgetState extends State<SwitchStateWidget> {
|
|||||||
} else {
|
} else {
|
||||||
domain = entity.domain;
|
domain = entity.domain;
|
||||||
}
|
}
|
||||||
eventBus.fire(new ServiceCallEvent(
|
ConnectionManager().callService(
|
||||||
domain, (newValue as bool) ? "turn_on" : "turn_off", entity.entityId, null));
|
domain: domain,
|
||||||
|
service: (newValue as bool) ? "turn_on" : "turn_off",
|
||||||
|
entityId: entity.entityId
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
@ -26,8 +26,12 @@ class _TextInputStateWidgetState extends State<TextInputStateWidget> {
|
|||||||
|
|
||||||
void setNewState(newValue, domain, entityId) {
|
void setNewState(newValue, domain, entityId) {
|
||||||
if (validate(newValue, _minLength, _maxLength)) {
|
if (validate(newValue, _minLength, _maxLength)) {
|
||||||
eventBus.fire(new ServiceCallEvent(domain, "set_value", entityId,
|
ConnectionManager().callService(
|
||||||
{"value": "$newValue"}));
|
domain: domain,
|
||||||
|
service: "set_value",
|
||||||
|
entityId: entityId,
|
||||||
|
data: {"value": "$newValue"}
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
setState(() {
|
setState(() {
|
||||||
_tmpValue = _entityState;
|
_tmpValue = _entityState;
|
||||||
@ -73,13 +77,7 @@ class _TextInputStateWidgetState extends State<TextInputStateWidget> {
|
|||||||
child: TextField(
|
child: TextField(
|
||||||
focusNode: _focusNode,
|
focusNode: _focusNode,
|
||||||
obscureText: entity.isPasswordField,
|
obscureText: entity.isPasswordField,
|
||||||
controller: new TextEditingController.fromValue(
|
controller: TextEditingController.fromValue(TextEditingValue(text: _tmpValue)),
|
||||||
new TextEditingValue(
|
|
||||||
text: _tmpValue,
|
|
||||||
selection:
|
|
||||||
new TextSelection.collapsed(offset: _tmpValue.length)
|
|
||||||
)
|
|
||||||
),
|
|
||||||
onChanged: (value) {
|
onChanged: (value) {
|
||||||
_tmpValue = value;
|
_tmpValue = value;
|
||||||
}),
|
}),
|
||||||
|
@ -27,7 +27,7 @@ class _TimerStateState extends State<TimerState> {
|
|||||||
try {
|
try {
|
||||||
int passed = DateTime
|
int passed = DateTime
|
||||||
.now()
|
.now()
|
||||||
.difference(entity._lastUpdated)
|
.difference(entity.lastUpdatedTimestamp)
|
||||||
.inSeconds;
|
.inSeconds;
|
||||||
remaining = Duration(seconds: entity.duration.inSeconds - passed);
|
remaining = Duration(seconds: entity.duration.inSeconds - passed);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
90
lib/entities/universal_slider.widget.dart
Normal file
90
lib/entities/universal_slider.widget.dart
Normal file
@ -0,0 +1,90 @@
|
|||||||
|
part of '../main.dart';
|
||||||
|
|
||||||
|
class UniversalSlider extends StatefulWidget {
|
||||||
|
|
||||||
|
final onChanged;
|
||||||
|
final onChangeEnd;
|
||||||
|
final Widget leading;
|
||||||
|
final Widget closing;
|
||||||
|
final String title;
|
||||||
|
final double min;
|
||||||
|
final double max;
|
||||||
|
final double value;
|
||||||
|
final EdgeInsets padding;
|
||||||
|
|
||||||
|
const UniversalSlider({Key key, this.onChanged, this.onChangeEnd, this.leading, this.closing, this.title, this.min, this.max, this.value, this.padding: const EdgeInsets.fromLTRB(Sizes.leftWidgetPadding, Sizes.rowPadding, Sizes.rightWidgetPadding, 0.0)}) : super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<StatefulWidget> createState() {
|
||||||
|
return UniversalSliderState();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
class UniversalSliderState extends State<UniversalSlider> {
|
||||||
|
|
||||||
|
double _value;
|
||||||
|
bool _changeStarted = false;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
_value = widget.value;
|
||||||
|
super.initState();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
List <Widget> row = [];
|
||||||
|
if (widget.leading != null) {
|
||||||
|
row.add(widget.leading);
|
||||||
|
}
|
||||||
|
row.add(
|
||||||
|
Flexible(
|
||||||
|
child: Slider(
|
||||||
|
value: _value,
|
||||||
|
min: widget.min,
|
||||||
|
max: widget.max,
|
||||||
|
onChangeStart: (_) {
|
||||||
|
_changeStarted = true;
|
||||||
|
},
|
||||||
|
onChanged: (value) {
|
||||||
|
setState(() {
|
||||||
|
_value = value;
|
||||||
|
});
|
||||||
|
widget.onChanged(value);
|
||||||
|
},
|
||||||
|
onChangeEnd: (value) {
|
||||||
|
_changeStarted = false;
|
||||||
|
Timer(Duration(milliseconds: 500), () {
|
||||||
|
if (!_changeStarted) {
|
||||||
|
setState(() {
|
||||||
|
_value = value;
|
||||||
|
});
|
||||||
|
widget.onChangeEnd(value);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
),
|
||||||
|
)
|
||||||
|
);
|
||||||
|
if (widget.closing != null) {
|
||||||
|
row.add(widget.closing);
|
||||||
|
}
|
||||||
|
return Padding(
|
||||||
|
padding: widget.padding,
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: <Widget>[
|
||||||
|
Container(height: Sizes.rowPadding,),
|
||||||
|
Text('${widget.title}'),
|
||||||
|
Container(height: Sizes.rowPadding,),
|
||||||
|
Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: row,
|
||||||
|
),
|
||||||
|
Container(height: Sizes.rowPadding,)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
98
lib/entities/vacuum/vacuum_entity.class.dart
Normal file
98
lib/entities/vacuum/vacuum_entity.class.dart
Normal file
@ -0,0 +1,98 @@
|
|||||||
|
part of '../../main.dart';
|
||||||
|
|
||||||
|
class VacuumEntity extends Entity {
|
||||||
|
|
||||||
|
static const SUPPORT_TURN_ON = 1;
|
||||||
|
static const SUPPORT_TURN_OFF = 2;
|
||||||
|
static const SUPPORT_PAUSE = 4;
|
||||||
|
static const SUPPORT_STOP = 8;
|
||||||
|
static const SUPPORT_RETURN_HOME = 16;
|
||||||
|
static const SUPPORT_FAN_SPEED = 32;
|
||||||
|
static const SUPPORT_BATTERY = 64;
|
||||||
|
static const SUPPORT_STATUS = 128;
|
||||||
|
static const SUPPORT_SEND_COMMAND = 256;
|
||||||
|
static const SUPPORT_LOCATE = 512;
|
||||||
|
static const SUPPORT_CLEAN_SPOT = 1024;
|
||||||
|
static const SUPPORT_MAP = 2048;
|
||||||
|
static const SUPPORT_STATE = 4096;
|
||||||
|
static const SUPPORT_START = 8192;
|
||||||
|
|
||||||
|
VacuumEntity(Map rawData, String webHost) : super(rawData, webHost);
|
||||||
|
|
||||||
|
bool get supportTurnOn => ((supportedFeatures &
|
||||||
|
VacuumEntity.SUPPORT_TURN_ON) ==
|
||||||
|
VacuumEntity.SUPPORT_TURN_ON);
|
||||||
|
bool get supportTurnOff => ((supportedFeatures &
|
||||||
|
VacuumEntity.SUPPORT_TURN_OFF) ==
|
||||||
|
VacuumEntity.SUPPORT_TURN_OFF);
|
||||||
|
bool get supportPause => ((supportedFeatures &
|
||||||
|
VacuumEntity.SUPPORT_PAUSE) ==
|
||||||
|
VacuumEntity.SUPPORT_PAUSE);
|
||||||
|
bool get supportStop => ((supportedFeatures &
|
||||||
|
VacuumEntity.SUPPORT_STOP) ==
|
||||||
|
VacuumEntity.SUPPORT_STOP);
|
||||||
|
bool get supportReturnHome => ((supportedFeatures &
|
||||||
|
VacuumEntity.SUPPORT_RETURN_HOME) ==
|
||||||
|
VacuumEntity.SUPPORT_RETURN_HOME);
|
||||||
|
bool get supportFanSpeed => ((supportedFeatures &
|
||||||
|
VacuumEntity.SUPPORT_FAN_SPEED) ==
|
||||||
|
VacuumEntity.SUPPORT_FAN_SPEED);
|
||||||
|
bool get supportBattery => ((supportedFeatures &
|
||||||
|
VacuumEntity.SUPPORT_BATTERY) ==
|
||||||
|
VacuumEntity.SUPPORT_BATTERY);
|
||||||
|
bool get supportStatus => ((supportedFeatures &
|
||||||
|
VacuumEntity.SUPPORT_STATUS) ==
|
||||||
|
VacuumEntity.SUPPORT_STATUS);
|
||||||
|
bool get supportSendCommand => ((supportedFeatures &
|
||||||
|
VacuumEntity.SUPPORT_SEND_COMMAND) ==
|
||||||
|
VacuumEntity.SUPPORT_SEND_COMMAND);
|
||||||
|
bool get supportLocate => ((supportedFeatures &
|
||||||
|
VacuumEntity.SUPPORT_LOCATE) ==
|
||||||
|
VacuumEntity.SUPPORT_LOCATE);
|
||||||
|
bool get supportCleanSpot => ((supportedFeatures &
|
||||||
|
VacuumEntity.SUPPORT_CLEAN_SPOT) ==
|
||||||
|
VacuumEntity.SUPPORT_CLEAN_SPOT);
|
||||||
|
bool get supportMap => ((supportedFeatures &
|
||||||
|
VacuumEntity.SUPPORT_MAP) ==
|
||||||
|
VacuumEntity.SUPPORT_MAP);
|
||||||
|
bool get supportState => ((supportedFeatures &
|
||||||
|
VacuumEntity.SUPPORT_STATE) ==
|
||||||
|
VacuumEntity.SUPPORT_STATE);
|
||||||
|
bool get supportStart => ((supportedFeatures &
|
||||||
|
VacuumEntity.SUPPORT_START) ==
|
||||||
|
VacuumEntity.SUPPORT_START);
|
||||||
|
|
||||||
|
List<String> get fanSpeedList => getStringListAttributeValue("fan_speed_list");
|
||||||
|
String get fanSpeed => getAttribute("fan_speed");
|
||||||
|
String get status => getAttribute("status");
|
||||||
|
int get batteryLevel => _getIntAttributeValue("battery_level");
|
||||||
|
String get batteryIcon => getAttribute("battery_icon");
|
||||||
|
double get cleanedArea => _getDoubleAttributeValue("cleaned_area");
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget _buildStatePart(BuildContext context) {
|
||||||
|
if (supportTurnOn || supportTurnOff) {
|
||||||
|
return SwitchStateWidget(
|
||||||
|
domainForService: "vacuum",
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return SimpleEntityState();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget _buildStatePartForPage(BuildContext context) {
|
||||||
|
return EntityModel(
|
||||||
|
entityWrapper: EntityWrapper(
|
||||||
|
entity: this
|
||||||
|
),
|
||||||
|
child: VacuumStateButton(),
|
||||||
|
handleTap: false,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget _buildAdditionalControlsForPage(BuildContext context) {
|
||||||
|
return VacuumControls();
|
||||||
|
}
|
||||||
|
}
|
229
lib/entities/vacuum/widgets/vacuum_controls.dart
Normal file
229
lib/entities/vacuum/widgets/vacuum_controls.dart
Normal file
@ -0,0 +1,229 @@
|
|||||||
|
part of '../../../main.dart';
|
||||||
|
|
||||||
|
class VacuumControls extends StatelessWidget {
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
VacuumEntity entity = EntityModel.of(context).entityWrapper.entity;
|
||||||
|
return Padding(
|
||||||
|
padding: EdgeInsets.only(left: Sizes.leftWidgetPadding, right: Sizes.rightWidgetPadding),
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: <Widget>[
|
||||||
|
_buildStatusAndBattery(entity, context),
|
||||||
|
_buildCommands(entity),
|
||||||
|
_buildFanSpeed(entity),
|
||||||
|
_buildAdditionalInfo(entity)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildStatusAndBattery(VacuumEntity entity, BuildContext context) {
|
||||||
|
List<Widget> result = [];
|
||||||
|
if (entity.supportStatus) {
|
||||||
|
result.addAll(
|
||||||
|
<Widget>[
|
||||||
|
Text("Status:"),
|
||||||
|
Container(width: 6,),
|
||||||
|
Expanded(
|
||||||
|
//flex: 1,
|
||||||
|
child: Text(
|
||||||
|
"${entity.status}",
|
||||||
|
maxLines: 1,
|
||||||
|
softWrap: true,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
style: Theme.of(context).textTheme.body2,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (entity.supportBattery && entity.batteryLevel != null) {
|
||||||
|
String iconName = entity.batteryIcon ?? "mdi:battery";
|
||||||
|
int batteryLevel = entity.batteryLevel ?? 100;
|
||||||
|
result.addAll(<Widget>[
|
||||||
|
Icon(MaterialDesignIcons.getIconDataFromIconName(iconName)),
|
||||||
|
Container(width: 6,),
|
||||||
|
Text("$batteryLevel %")
|
||||||
|
]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result.isEmpty) {
|
||||||
|
return Container(width: 0, height: 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Padding(
|
||||||
|
padding: EdgeInsets.only(bottom: Sizes.doubleRowPadding),
|
||||||
|
child: Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.start,
|
||||||
|
mainAxisSize: MainAxisSize.max,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
|
children: result,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildCommands(VacuumEntity entity) {
|
||||||
|
List<Widget> commandButtons = [];
|
||||||
|
double iconSize = 32;
|
||||||
|
if (entity.supportStart) {
|
||||||
|
commandButtons.add(
|
||||||
|
IconButton(
|
||||||
|
icon: Icon(MaterialDesignIcons.getIconDataFromIconName("mdi:play")),
|
||||||
|
iconSize: iconSize,
|
||||||
|
onPressed: () => ConnectionManager().callService(
|
||||||
|
domain: "vacuum",
|
||||||
|
entityId: entity.entityId,
|
||||||
|
service: "start"
|
||||||
|
),
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (entity.supportPause && !entity.supportStart) {
|
||||||
|
commandButtons.add(
|
||||||
|
IconButton(
|
||||||
|
icon: Icon(MaterialDesignIcons.getIconDataFromIconName("mdi:play-pause")),
|
||||||
|
iconSize: iconSize,
|
||||||
|
onPressed: () => ConnectionManager().callService(
|
||||||
|
domain: "vacuum",
|
||||||
|
entityId: entity.entityId,
|
||||||
|
service: "start_pause"
|
||||||
|
),
|
||||||
|
)
|
||||||
|
);
|
||||||
|
} else if (entity.supportPause) {
|
||||||
|
commandButtons.add(
|
||||||
|
IconButton(
|
||||||
|
icon: Icon(MaterialDesignIcons.getIconDataFromIconName("mdi:pause")),
|
||||||
|
iconSize: iconSize,
|
||||||
|
onPressed: () => ConnectionManager().callService(
|
||||||
|
domain: "vacuum",
|
||||||
|
entityId: entity.entityId,
|
||||||
|
service: "pause"
|
||||||
|
),
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (entity.supportStop) {
|
||||||
|
commandButtons.add(
|
||||||
|
IconButton(
|
||||||
|
icon: Icon(MaterialDesignIcons.getIconDataFromIconName("mdi:stop")),
|
||||||
|
iconSize: iconSize,
|
||||||
|
onPressed: () => ConnectionManager().callService(
|
||||||
|
domain: "vacuum",
|
||||||
|
entityId: entity.entityId,
|
||||||
|
service: "stop"
|
||||||
|
),
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (entity.supportCleanSpot) {
|
||||||
|
commandButtons.add(
|
||||||
|
IconButton(
|
||||||
|
icon: Icon(MaterialDesignIcons.getIconDataFromIconName("mdi:broom")),
|
||||||
|
iconSize: iconSize,
|
||||||
|
onPressed: () => ConnectionManager().callService(
|
||||||
|
domain: "vacuum",
|
||||||
|
entityId: entity.entityId,
|
||||||
|
service: "clean_spot"
|
||||||
|
),
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (entity.supportLocate) {
|
||||||
|
commandButtons.add(
|
||||||
|
IconButton(
|
||||||
|
icon: Icon(MaterialDesignIcons.getIconDataFromIconName("mdi:map-marker")),
|
||||||
|
iconSize: iconSize,
|
||||||
|
onPressed: () => ConnectionManager().callService(
|
||||||
|
domain: "vacuum",
|
||||||
|
entityId: entity.entityId,
|
||||||
|
service: "locate"
|
||||||
|
),
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (entity.supportReturnHome) {
|
||||||
|
commandButtons.add(
|
||||||
|
IconButton(
|
||||||
|
icon: Icon(MaterialDesignIcons.getIconDataFromIconName("mdi:home-map-marker")),
|
||||||
|
iconSize: iconSize,
|
||||||
|
onPressed: () => ConnectionManager().callService(
|
||||||
|
domain: "vacuum",
|
||||||
|
entityId: entity.entityId,
|
||||||
|
service: "return_to_base"
|
||||||
|
),
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (commandButtons.isEmpty) {
|
||||||
|
return Container(width: 0, height: 0,);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Padding(
|
||||||
|
padding: EdgeInsets.only(bottom: Sizes.doubleRowPadding),
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: <Widget>[
|
||||||
|
Text("Vacuum cleaner commands:"),
|
||||||
|
Container(height: Sizes.rowPadding,),
|
||||||
|
Row(
|
||||||
|
mainAxisSize: MainAxisSize.max,
|
||||||
|
children: commandButtons.map((button) => Expanded(
|
||||||
|
child: button,
|
||||||
|
)).toList(),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildFanSpeed(VacuumEntity entity) {
|
||||||
|
if (entity.supportFanSpeed) {
|
||||||
|
return Padding(
|
||||||
|
padding: EdgeInsets.only(bottom: Sizes.doubleRowPadding),
|
||||||
|
child: ModeSelectorWidget(
|
||||||
|
caption: "Fan speed",
|
||||||
|
options: entity.fanSpeedList,
|
||||||
|
value: entity.fanSpeed,
|
||||||
|
onChange: (val) => ConnectionManager().callService(
|
||||||
|
domain: "vacuum",
|
||||||
|
entityId: entity.entityId,
|
||||||
|
service: "set_fan_speed",
|
||||||
|
data: {"fan_speed": val}
|
||||||
|
)
|
||||||
|
),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return Container(width: 0, height: 0,);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildAdditionalInfo(VacuumEntity entity) {
|
||||||
|
List<Widget> rows = [];
|
||||||
|
if (entity.cleanedArea != null) {
|
||||||
|
rows.add(
|
||||||
|
Text("Cleaned area: ${entity.cleanedArea}")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (rows.isEmpty) {
|
||||||
|
return Container(width: 0, height: 0,);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Padding(
|
||||||
|
padding: EdgeInsets.only(bottom: Sizes.doubleRowPadding),
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: rows,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
37
lib/entities/vacuum/widgets/vacuum_state_button.dart
Normal file
37
lib/entities/vacuum/widgets/vacuum_state_button.dart
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
part of '../../../main.dart';
|
||||||
|
|
||||||
|
class VacuumStateButton extends StatelessWidget {
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
Widget result;
|
||||||
|
VacuumEntity entity = EntityModel.of(context).entityWrapper.entity;
|
||||||
|
if (entity.supportTurnOn && entity.supportTurnOff) {
|
||||||
|
result = FlatServiceButton(
|
||||||
|
serviceDomain: "vacuum",
|
||||||
|
serviceName: entity.state == EntityState.on ? "turn_off" : "turn_on",
|
||||||
|
entityId: entity.entityId,
|
||||||
|
text: entity.state == EntityState.on ? "TURN OFF" : "TURN ON"
|
||||||
|
);
|
||||||
|
} else if (entity.supportStart && (entity.state == EntityState.docked || entity.state == EntityState.idle)) {
|
||||||
|
result = FlatServiceButton(
|
||||||
|
serviceDomain: "vacuum",
|
||||||
|
serviceName: "start",
|
||||||
|
entityId: entity.entityId,
|
||||||
|
text: "START CLEANING"
|
||||||
|
);
|
||||||
|
} else if (entity.supportReturnHome && entity.state == EntityState.cleaning) {
|
||||||
|
result = FlatServiceButton(
|
||||||
|
serviceDomain: "vacuum",
|
||||||
|
serviceName: "return_to_base",
|
||||||
|
entityId: entity.entityId,
|
||||||
|
text: "RETURN TO DOCK"
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
result = Text(entity.state.toUpperCase(), style: Theme.of(context).textTheme.subhead);
|
||||||
|
}
|
||||||
|
return Padding(
|
||||||
|
padding: EdgeInsets.only(right: 15),
|
||||||
|
child: result,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -101,6 +101,9 @@ class EntityCollection {
|
|||||||
case "timer": {
|
case "timer": {
|
||||||
return TimerEntity(rawEntityData, homeAssistantWebHost);
|
return TimerEntity(rawEntityData, homeAssistantWebHost);
|
||||||
}
|
}
|
||||||
|
case "vacuum": {
|
||||||
|
return VacuumEntity(rawEntityData, homeAssistantWebHost);
|
||||||
|
}
|
||||||
default: {
|
default: {
|
||||||
return Entity(rawEntityData, homeAssistantWebHost);
|
return Entity(rawEntityData, homeAssistantWebHost);
|
||||||
}
|
}
|
||||||
@ -149,32 +152,12 @@ class EntityCollection {
|
|||||||
return _allEntities[entityId] != null;
|
return _allEntities[entityId] != null;
|
||||||
}
|
}
|
||||||
|
|
||||||
List<Entity> filterEntitiesForDefaultView() {
|
List<Entity> getByDomains({List<String> includeDomains: const [], List<String> excludeDomains: const [], List<String> stateFiler}) {
|
||||||
List<Entity> result = [];
|
return _allEntities.values.where((entity) {
|
||||||
List<Entity> groups = [];
|
return
|
||||||
List<Entity> nonGroupEntities = [];
|
(excludeDomains.isEmpty || !excludeDomains.contains(entity.domain)) &&
|
||||||
_allEntities.forEach((id, entity){
|
(includeDomains.isEmpty || includeDomains.contains(entity.domain)) &&
|
||||||
if (entity.isGroup && (entity.attributes['auto'] == null || (entity.attributes['auto'] && !entity.isHidden)) && (!entity.isView)) {
|
((stateFiler != null && stateFiler.contains(entity.state)) || stateFiler == null);
|
||||||
groups.add(entity);
|
}).toList();
|
||||||
}
|
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -1,50 +0,0 @@
|
|||||||
part of '../main.dart';
|
|
||||||
|
|
||||||
class ButtonEntityContainer extends StatelessWidget {
|
|
||||||
|
|
||||||
ButtonEntityContainer({
|
|
||||||
Key key,
|
|
||||||
}) : super(key: key);
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
final EntityWrapper entityWrapper = EntityModel.of(context).entityWrapper;
|
|
||||||
if (entityWrapper.entity.statelessType == StatelessEntityType.MISSED) {
|
|
||||||
return MissedEntityWidget();
|
|
||||||
}
|
|
||||||
if (entityWrapper.entity.statelessType > StatelessEntityType.MISSED) {
|
|
||||||
return Container(width: 0.0, height: 0.0,);
|
|
||||||
}
|
|
||||||
return InkWell(
|
|
||||||
onTap: () => entityWrapper.handleTap(),
|
|
||||||
onLongPress: () => entityWrapper.handleHold(),
|
|
||||||
child: Column(
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
children: <Widget>[
|
|
||||||
FractionallySizedBox(
|
|
||||||
widthFactor: 0.4,
|
|
||||||
child: FittedBox(
|
|
||||||
fit: BoxFit.fitHeight,
|
|
||||||
child: EntityIcon(
|
|
||||||
padding: EdgeInsets.fromLTRB(2.0, 6.0, 2.0, 2.0),
|
|
||||||
size: Sizes.iconSize,
|
|
||||||
)
|
|
||||||
),
|
|
||||||
),
|
|
||||||
_buildName()
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildName() {
|
|
||||||
return EntityName(
|
|
||||||
padding: EdgeInsets.fromLTRB(Sizes.buttonPadding, 0.0, Sizes.buttonPadding, Sizes.rowPadding),
|
|
||||||
textOverflow: TextOverflow.ellipsis,
|
|
||||||
maxLines: 3,
|
|
||||||
wordsWrap: true,
|
|
||||||
textAlign: TextAlign.center,
|
|
||||||
fontSize: Sizes.nameFontSize,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,70 +0,0 @@
|
|||||||
part of '../../main.dart';
|
|
||||||
|
|
||||||
class CameraStreamView extends StatefulWidget {
|
|
||||||
|
|
||||||
CameraStreamView({Key key}) : super(key: key);
|
|
||||||
|
|
||||||
@override
|
|
||||||
_CameraStreamViewState createState() => _CameraStreamViewState();
|
|
||||||
}
|
|
||||||
|
|
||||||
class _CameraStreamViewState extends State<CameraStreamView> {
|
|
||||||
|
|
||||||
@override
|
|
||||||
void initState() {
|
|
||||||
super.initState();
|
|
||||||
}
|
|
||||||
|
|
||||||
CameraEntity _entity;
|
|
||||||
bool started = false;
|
|
||||||
String streamUrl = "";
|
|
||||||
|
|
||||||
launchStream() {
|
|
||||||
Navigator.push(
|
|
||||||
context,
|
|
||||||
MaterialPageRoute(
|
|
||||||
builder: (context) => WebviewScaffold(
|
|
||||||
url: "$streamUrl",
|
|
||||||
withZoom: true,
|
|
||||||
appBar: new AppBar(
|
|
||||||
leading: IconButton(
|
|
||||||
icon: Icon(Icons.close),
|
|
||||||
onPressed: () => Navigator.pop(context)
|
|
||||||
),
|
|
||||||
title: new Text("${_entity.displayName}"),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
if (!started) {
|
|
||||||
_entity = EntityModel
|
|
||||||
.of(context)
|
|
||||||
.entityWrapper
|
|
||||||
.entity;
|
|
||||||
started = true;
|
|
||||||
}
|
|
||||||
streamUrl = '${Connection().httpWebHost}/api/camera_proxy_stream/${_entity
|
|
||||||
.entityId}?token=${_entity.attributes['access_token']}';
|
|
||||||
return Column(
|
|
||||||
children: <Widget>[
|
|
||||||
Container(
|
|
||||||
padding: const EdgeInsets.all(20.0),
|
|
||||||
child: IconButton(
|
|
||||||
icon: Icon(MaterialDesignIcons.getIconDataFromIconName("mdi:monitor-screenshot"), color: Colors.amber),
|
|
||||||
iconSize: 50.0,
|
|
||||||
onPressed: () => launchStream(),
|
|
||||||
)
|
|
||||||
)
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void dispose() {
|
|
||||||
super.dispose();
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,58 +0,0 @@
|
|||||||
part of '../../main.dart';
|
|
||||||
|
|
||||||
class UniversalSlider extends StatelessWidget {
|
|
||||||
|
|
||||||
final onChanged;
|
|
||||||
final onChangeEnd;
|
|
||||||
final Widget leading;
|
|
||||||
final Widget closing;
|
|
||||||
final String title;
|
|
||||||
final double min;
|
|
||||||
final double max;
|
|
||||||
final double value;
|
|
||||||
final EdgeInsets padding;
|
|
||||||
|
|
||||||
const UniversalSlider({Key key, this.onChanged, this.onChangeEnd, this.leading, this.closing, this.title, this.min, this.max, this.value, this.padding: const EdgeInsets.fromLTRB(Sizes.leftWidgetPadding, Sizes.rowPadding, Sizes.rightWidgetPadding, 0.0)}) : super(key: key);
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
List <Widget> row = [];
|
|
||||||
if (leading != null) {
|
|
||||||
row.add(leading);
|
|
||||||
}
|
|
||||||
row.add(
|
|
||||||
Flexible(
|
|
||||||
child: Slider(
|
|
||||||
value: value,
|
|
||||||
min: min,
|
|
||||||
max: max,
|
|
||||||
onChanged: (value) => onChanged(value),
|
|
||||||
onChangeEnd: (value) => onChangeEnd(value),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
);
|
|
||||||
if (closing != null) {
|
|
||||||
row.add(closing);
|
|
||||||
}
|
|
||||||
return Padding(
|
|
||||||
padding: padding,
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: <Widget>[
|
|
||||||
Container(height: Sizes.rowPadding,),
|
|
||||||
Text(
|
|
||||||
"$title",
|
|
||||||
style: TextStyle(fontSize: Sizes.stateFontSize),
|
|
||||||
),
|
|
||||||
Container(height: Sizes.rowPadding,),
|
|
||||||
Row(
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
children: row,
|
|
||||||
),
|
|
||||||
Container(height: Sizes.rowPadding,)
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -1,65 +0,0 @@
|
|||||||
part of '../main.dart';
|
|
||||||
|
|
||||||
class DefaultEntityContainer extends StatelessWidget {
|
|
||||||
DefaultEntityContainer({
|
|
||||||
Key key,
|
|
||||||
@required this.state
|
|
||||||
}) : super(key: key);
|
|
||||||
|
|
||||||
final Widget state;
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
final EntityModel entityModel = EntityModel.of(context);
|
|
||||||
if (entityModel.entityWrapper.entity.statelessType == StatelessEntityType.MISSED) {
|
|
||||||
return MissedEntityWidget();
|
|
||||||
}
|
|
||||||
if (entityModel.entityWrapper.entity.statelessType == StatelessEntityType.DIVIDER) {
|
|
||||||
return Divider(
|
|
||||||
color: Colors.black45,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if (entityModel.entityWrapper.entity.statelessType == StatelessEntityType.SECTION) {
|
|
||||||
return Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
children: <Widget>[
|
|
||||||
Divider(
|
|
||||||
color: Colors.black45,
|
|
||||||
),
|
|
||||||
Text(
|
|
||||||
"${entityModel.entityWrapper.entity.displayName}",
|
|
||||||
style: TextStyle(color: Colors.blue),
|
|
||||||
)
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return InkWell(
|
|
||||||
onLongPress: () {
|
|
||||||
if (entityModel.handleTap) {
|
|
||||||
entityModel.entityWrapper.handleHold();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
onTap: () {
|
|
||||||
if (entityModel.handleTap) {
|
|
||||||
entityModel.entityWrapper.handleTap();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
child: Row(
|
|
||||||
mainAxisSize: MainAxisSize.max,
|
|
||||||
children: <Widget>[
|
|
||||||
EntityIcon(),
|
|
||||||
|
|
||||||
Flexible(
|
|
||||||
fit: FlexFit.tight,
|
|
||||||
flex: 3,
|
|
||||||
child: EntityName(
|
|
||||||
padding: EdgeInsets.fromLTRB(10.0, 2.0, 10.0, 2.0),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
state
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,74 +0,0 @@
|
|||||||
part of '../main.dart';
|
|
||||||
|
|
||||||
class EntityColor {
|
|
||||||
|
|
||||||
static const defaultStateColor = Color.fromRGBO(68, 115, 158, 1.0);
|
|
||||||
|
|
||||||
static const badgeColors = {
|
|
||||||
"default": Color.fromRGBO(223, 76, 30, 1.0),
|
|
||||||
"binary_sensor": Color.fromRGBO(3, 155, 229, 1.0)
|
|
||||||
};
|
|
||||||
|
|
||||||
static const _stateColors = {
|
|
||||||
EntityState.on: Colors.amber,
|
|
||||||
"auto": Colors.amber,
|
|
||||||
EntityState.active: Colors.amber,
|
|
||||||
EntityState.playing: Colors.amber,
|
|
||||||
"above_horizon": Colors.amber,
|
|
||||||
EntityState.home: Colors.amber,
|
|
||||||
EntityState.open: Colors.amber,
|
|
||||||
EntityState.off: defaultStateColor,
|
|
||||||
EntityState.closed: defaultStateColor,
|
|
||||||
"below_horizon": defaultStateColor,
|
|
||||||
"default": defaultStateColor,
|
|
||||||
EntityState.idle: defaultStateColor,
|
|
||||||
"heat": Colors.redAccent,
|
|
||||||
"cool": Colors.lightBlue,
|
|
||||||
EntityState.unavailable: Colors.black26,
|
|
||||||
EntityState.unknown: Colors.black26,
|
|
||||||
EntityState.alarm_disarmed: Colors.green,
|
|
||||||
EntityState.alarm_armed_away: Colors.redAccent,
|
|
||||||
EntityState.alarm_armed_custom_bypass: Colors.redAccent,
|
|
||||||
EntityState.alarm_armed_home: Colors.redAccent,
|
|
||||||
EntityState.alarm_armed_night: Colors.redAccent,
|
|
||||||
EntityState.alarm_triggered: Colors.redAccent,
|
|
||||||
EntityState.alarm_arming: Colors.amber,
|
|
||||||
EntityState.alarm_disarming: Colors.amber,
|
|
||||||
EntityState.alarm_pending: Colors.amber,
|
|
||||||
};
|
|
||||||
|
|
||||||
static Color stateColor(String state) {
|
|
||||||
return _stateColors[state] ?? _stateColors["default"];
|
|
||||||
}
|
|
||||||
|
|
||||||
static charts.Color chartHistoryStateColor(String state, int id) {
|
|
||||||
Color c = _stateColors[state];
|
|
||||||
if (c != null) {
|
|
||||||
return charts.Color(
|
|
||||||
r: c.red,
|
|
||||||
g: c.green,
|
|
||||||
b: c.blue,
|
|
||||||
a: c.alpha
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
double r = id.toDouble() % 10;
|
|
||||||
return charts.MaterialPalette.getOrderedPalettes(10)[r.round()].shadeDefault;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static Color historyStateColor(String state, int id) {
|
|
||||||
Color c = _stateColors[state];
|
|
||||||
if (c != null) {
|
|
||||||
return c;
|
|
||||||
} else {
|
|
||||||
if (id > -1) {
|
|
||||||
double r = id.toDouble() % 10;
|
|
||||||
charts.Color c1 = charts.MaterialPalette.getOrderedPalettes(10)[r.round()].shadeDefault;
|
|
||||||
return Color.fromARGB(c1.a, c1.r, c1.g, c1.b);
|
|
||||||
} else {
|
|
||||||
return _stateColors[EntityState.on];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -1,14 +0,0 @@
|
|||||||
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,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,90 +0,0 @@
|
|||||||
part of '../main.dart';
|
|
||||||
|
|
||||||
class GlanceEntityContainer extends StatelessWidget {
|
|
||||||
|
|
||||||
final bool showName;
|
|
||||||
final bool showState;
|
|
||||||
final bool nameInTheBottom;
|
|
||||||
final double iconSize;
|
|
||||||
final double nameFontSize;
|
|
||||||
final bool wordsWrapInName;
|
|
||||||
|
|
||||||
GlanceEntityContainer({
|
|
||||||
Key key,
|
|
||||||
@required this.showName,
|
|
||||||
@required this.showState,
|
|
||||||
this.nameInTheBottom: false,
|
|
||||||
this.iconSize: Sizes.iconSize,
|
|
||||||
this.nameFontSize: Sizes.smallFontSize,
|
|
||||||
this.wordsWrapInName: false
|
|
||||||
}) : super(key: key);
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
final EntityWrapper entityWrapper = EntityModel.of(context).entityWrapper;
|
|
||||||
if (entityWrapper.entity.statelessType == StatelessEntityType.MISSED) {
|
|
||||||
return MissedEntityWidget();
|
|
||||||
}
|
|
||||||
if (entityWrapper.entity.statelessType > StatelessEntityType.MISSED) {
|
|
||||||
return Container(width: 0.0, height: 0.0,);
|
|
||||||
}
|
|
||||||
List<Widget> result = [];
|
|
||||||
if (!nameInTheBottom) {
|
|
||||||
if (showName) {
|
|
||||||
result.add(_buildName());
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if (showState) {
|
|
||||||
result.add(_buildState());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
result.add(
|
|
||||||
EntityIcon(
|
|
||||||
padding: EdgeInsets.all(0.0),
|
|
||||||
size: iconSize,
|
|
||||||
)
|
|
||||||
);
|
|
||||||
if (!nameInTheBottom) {
|
|
||||||
if (showState) {
|
|
||||||
result.add(_buildState());
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
result.add(_buildName());
|
|
||||||
}
|
|
||||||
|
|
||||||
return Center(
|
|
||||||
child: InkResponse(
|
|
||||||
child: ConstrainedBox(
|
|
||||||
constraints: BoxConstraints(minWidth: Sizes.iconSize * 2),
|
|
||||||
child: Column(
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
//mainAxisAlignment: MainAxisAlignment.start,
|
|
||||||
//crossAxisAlignment: CrossAxisAlignment.center,
|
|
||||||
children: result,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
onTap: () => entityWrapper.handleTap(),
|
|
||||||
onLongPress: () => entityWrapper.handleHold(),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildName() {
|
|
||||||
return EntityName(
|
|
||||||
padding: EdgeInsets.only(bottom: Sizes.rowPadding),
|
|
||||||
textOverflow: TextOverflow.ellipsis,
|
|
||||||
wordsWrap: wordsWrapInName,
|
|
||||||
textAlign: TextAlign.center,
|
|
||||||
fontSize: nameFontSize,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildState() {
|
|
||||||
return SimpleEntityState(
|
|
||||||
textAlign: TextAlign.center,
|
|
||||||
expanded: false,
|
|
||||||
maxLines: 1,
|
|
||||||
padding: EdgeInsets.only(top: Sizes.rowPadding),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
@ -2,6 +2,8 @@ part of 'main.dart';
|
|||||||
|
|
||||||
class HomeAssistant {
|
class HomeAssistant {
|
||||||
|
|
||||||
|
static const DEFAULT_DASHBOARD = 'lovelace';
|
||||||
|
|
||||||
static final HomeAssistant _instance = HomeAssistant._internal();
|
static final HomeAssistant _instance = HomeAssistant._internal();
|
||||||
|
|
||||||
factory HomeAssistant() {
|
factory HomeAssistant() {
|
||||||
@ -12,63 +14,72 @@ class HomeAssistant {
|
|||||||
HomeAssistantUI ui;
|
HomeAssistantUI ui;
|
||||||
Map _instanceConfig = {};
|
Map _instanceConfig = {};
|
||||||
String _userName;
|
String _userName;
|
||||||
|
String _lovelaceDashbordUrl;
|
||||||
HSVColor savedColor;
|
HSVColor savedColor;
|
||||||
|
int savedPlayerPosition;
|
||||||
|
String sendToPlayerId;
|
||||||
|
String sendFromPlayerId;
|
||||||
|
Map services;
|
||||||
|
bool autoUi = false;
|
||||||
|
|
||||||
String fcmToken;
|
String fcmToken;
|
||||||
|
|
||||||
Map _rawLovelaceData;
|
Map _rawLovelaceData;
|
||||||
|
var _rawStates;
|
||||||
|
var _rawUserInfo;
|
||||||
|
var _rawPanels;
|
||||||
|
|
||||||
|
set lovelaceDashboardUrl(String val) => _lovelaceDashbordUrl = val;
|
||||||
|
|
||||||
List<Panel> panels = [];
|
List<Panel> panels = [];
|
||||||
|
|
||||||
Duration fetchTimeout = Duration(seconds: 30);
|
Duration fetchTimeout = Duration(seconds: 30);
|
||||||
|
|
||||||
String get locationName {
|
String get locationName {
|
||||||
if (Connection().useLovelace) {
|
if (!autoUi) {
|
||||||
return ui?.title ?? "";
|
return ui?.title ?? "Home";
|
||||||
} else {
|
} else {
|
||||||
return _instanceConfig["location_name"] ?? "";
|
return _instanceConfig["location_name"] ?? "Home";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
String get userName => _userName ?? locationName;
|
String get userName => _userName ?? locationName;
|
||||||
String get userAvatarText => userName.length > 0 ? userName[0] : "";
|
String get userAvatarText => userName.length > 0 ? userName[0] : "";
|
||||||
bool get isNoEntities => entities == null || entities.isEmpty;
|
bool get isNoEntities => entities == null || entities.isEmpty;
|
||||||
bool get isNoViews => ui == null || ui.isEmpty;
|
bool get isNoViews => ui == null || ui.isEmpty;
|
||||||
bool get isMobileAppEnabled => _instanceConfig["components"] != null && (_instanceConfig["components"] as List).contains("mobile_app");
|
|
||||||
|
|
||||||
HomeAssistant._internal() {
|
HomeAssistant._internal() {
|
||||||
Connection().onStateChangeCallback = _handleEntityStateChange;
|
ConnectionManager().onStateChangeCallback = _handleEntityStateChange;
|
||||||
Device().loadDeviceInfo();
|
ConnectionManager().onLovelaceUpdatedCallback = _handleLovelaceUpdate;
|
||||||
|
DeviceInfoManager().loadDeviceInfo();
|
||||||
}
|
}
|
||||||
|
|
||||||
Completer _fetchCompleter;
|
Completer _fetchCompleter;
|
||||||
|
|
||||||
Future fetchData() {
|
Future fetchData(bool uiOnly) {
|
||||||
if (_fetchCompleter != null && !_fetchCompleter.isCompleted) {
|
if (_fetchCompleter != null && !_fetchCompleter.isCompleted) {
|
||||||
Logger.w("Previous data fetch is not completed yet");
|
Logger.w("Previous data fetch is not completed yet");
|
||||||
return _fetchCompleter.future;
|
return _fetchCompleter.future;
|
||||||
}
|
}
|
||||||
if (entities == null) entities = EntityCollection(Connection().httpWebHost);
|
|
||||||
_fetchCompleter = Completer();
|
_fetchCompleter = Completer();
|
||||||
List<Future> futures = [];
|
List<Future> futures = [];
|
||||||
futures.add(_getStates());
|
if (!uiOnly) {
|
||||||
if (Connection().useLovelace) {
|
if (entities == null) entities = EntityCollection(ConnectionManager().httpWebHost);
|
||||||
futures.add(_getLovelace());
|
futures.add(_getStates(null));
|
||||||
|
futures.add(_getConfig(null));
|
||||||
|
futures.add(_getUserInfo(null));
|
||||||
|
futures.add(_getPanels(null));
|
||||||
|
futures.add(_getServices(null));
|
||||||
|
}
|
||||||
|
if (!autoUi) {
|
||||||
|
futures.add(_getLovelace(null));
|
||||||
}
|
}
|
||||||
futures.add(_getConfig());
|
|
||||||
futures.add(_getServices());
|
|
||||||
futures.add(_getUserInfo());
|
|
||||||
futures.add(_getPanels());
|
|
||||||
futures.add(Connection().sendSocketMessage(
|
|
||||||
type: "subscribe_events",
|
|
||||||
additionalData: {"event_type": "state_changed"},
|
|
||||||
));
|
|
||||||
Future.wait(futures).then((_) {
|
Future.wait(futures).then((_) {
|
||||||
if (isMobileAppEnabled) {
|
if (isComponentEnabled('mobile_app')) {
|
||||||
_createUI();
|
_createUI();
|
||||||
_fetchCompleter.complete();
|
_fetchCompleter.complete();
|
||||||
checkAppRegistration();
|
if (!uiOnly) MobileAppIntegrationManager.checkAppRegistration();
|
||||||
} else {
|
} else {
|
||||||
_fetchCompleter.completeError(HAError("Mobile app component not found", actions: [HAErrorAction.tryAgain(), HAErrorAction(type: HAErrorActionType.URL ,title: "Help",url: "http://ha-client.homemade.systems/docs#mobile-app")]));
|
_fetchCompleter.completeError(HAError("Mobile app component not found", actions: [HAErrorAction.tryAgain(), HAErrorAction(type: HAErrorActionType.URL ,title: "Help",url: "http://ha-client.app/docs#mobile-app-integration")]));
|
||||||
}
|
}
|
||||||
}).catchError((e) {
|
}).catchError((e) {
|
||||||
_fetchCompleter.completeError(e);
|
_fetchCompleter.completeError(e);
|
||||||
@ -76,191 +87,237 @@ class HomeAssistant {
|
|||||||
return _fetchCompleter.future;
|
return _fetchCompleter.future;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> fetchDataFromCache() async {
|
||||||
|
Logger.d('Loading cached data');
|
||||||
|
SharedPreferences prefs = await SharedPreferences.getInstance();
|
||||||
|
bool cached = prefs.getBool('cached');
|
||||||
|
if (cached != null && cached) {
|
||||||
|
if (entities == null) entities = EntityCollection(ConnectionManager().httpWebHost);
|
||||||
|
try {
|
||||||
|
_getStates(prefs);
|
||||||
|
if (!autoUi) {
|
||||||
|
_getLovelace(prefs);
|
||||||
|
}
|
||||||
|
_getConfig(prefs);
|
||||||
|
_getUserInfo(prefs);
|
||||||
|
_getPanels(prefs);
|
||||||
|
_getServices(prefs);
|
||||||
|
if (isComponentEnabled('mobile_app')) {
|
||||||
|
_createUI();
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
Logger.d('Didnt get cached data: $e');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void saveCache() async {
|
||||||
|
Logger.d('Saving data to cache...');
|
||||||
|
SharedPreferences prefs = await SharedPreferences.getInstance();
|
||||||
|
try {
|
||||||
|
await prefs.setString('cached_states', json.encode(_rawStates));
|
||||||
|
await prefs.setString('cached_lovelace', json.encode(_rawLovelaceData));
|
||||||
|
await prefs.setString('cached_user', json.encode(_rawUserInfo));
|
||||||
|
await prefs.setString('cached_config', json.encode(_instanceConfig));
|
||||||
|
await prefs.setString('cached_panels', json.encode(_rawPanels));
|
||||||
|
await prefs.setString('cached_services', json.encode(services));
|
||||||
|
await prefs.setBool('cached', true);
|
||||||
|
} catch (e) {
|
||||||
|
await prefs.setBool('cached', false);
|
||||||
|
Logger.e('Error saving cache: $e');
|
||||||
|
}
|
||||||
|
Logger.d('Done saving cache');
|
||||||
|
}
|
||||||
|
|
||||||
Future logout() async {
|
Future logout() async {
|
||||||
Logger.d("Logging out...");
|
Logger.d("Logging out...");
|
||||||
await Connection().logout().then((_) {
|
await ConnectionManager().logout().then((_) {
|
||||||
ui?.clear();
|
ui?.clear();
|
||||||
entities?.clear();
|
entities?.clear();
|
||||||
panels?.clear();
|
panels?.clear();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
Map _getAppRegistrationData() {
|
Future _getConfig(SharedPreferences sharedPrefs) async {
|
||||||
return {
|
if (sharedPrefs != null) {
|
||||||
"app_version": "$appVersion",
|
try {
|
||||||
"device_name": "$userName's ${Device().model}",
|
var data = json.decode(sharedPrefs.getString('cached_config'));
|
||||||
"manufacturer": Device().manufacturer,
|
_parseConfig(data);
|
||||||
"model": Device().model,
|
} catch (e) {
|
||||||
"os_version": Device().osVersion,
|
throw HAError("Error getting config: $e");
|
||||||
"app_data": {
|
|
||||||
"push_token": "$fcmToken",
|
|
||||||
"push_url": "https://us-central1-ha-client-c73c4.cloudfunctions.net/sendPushNotification"
|
|
||||||
}
|
}
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
Future checkAppRegistration({bool forceRegister: false, bool showOkDialog: false}) {
|
|
||||||
Completer completer = Completer();
|
|
||||||
if (Connection().webhookId == null || forceRegister) {
|
|
||||||
Logger.d("Mobile app was not registered yet or need to be reseted. Registering...");
|
|
||||||
var registrationData = _getAppRegistrationData();
|
|
||||||
registrationData.addAll({
|
|
||||||
"app_id": "ha_client",
|
|
||||||
"app_name": "$appName",
|
|
||||||
"os_name": Device().osName,
|
|
||||||
"supports_encryption": false,
|
|
||||||
});
|
|
||||||
Connection().sendHTTPPost(
|
|
||||||
endPoint: "/api/mobile_app/registrations",
|
|
||||||
includeAuthHeader: true,
|
|
||||||
data: json.encode(registrationData)
|
|
||||||
).then((response) {
|
|
||||||
Logger.d("Processing registration responce...");
|
|
||||||
var responseObject = json.decode(response);
|
|
||||||
SharedPreferences.getInstance().then((prefs) {
|
|
||||||
prefs.setString("app-webhook-id", responseObject["webhook_id"]);
|
|
||||||
Connection().webhookId = responseObject["webhook_id"];
|
|
||||||
completer.complete();
|
|
||||||
eventBus.fire(ShowPopupDialogEvent(
|
|
||||||
title: "Mobile app Integration was created",
|
|
||||||
body: "HA Client was registered as MobileApp in your Home Assistant. To start using notifications you need to restart your Home Assistant",
|
|
||||||
positiveText: "Restart now",
|
|
||||||
negativeText: "Later",
|
|
||||||
onPositive: () {
|
|
||||||
Connection().callService(domain: "homeassistant", service: "restart", entityId: null);
|
|
||||||
},
|
|
||||||
));
|
|
||||||
});
|
|
||||||
}).catchError((e) {
|
|
||||||
completer.complete();
|
|
||||||
Logger.e("Error registering the app: ${e.toString()}");
|
|
||||||
});
|
|
||||||
return completer.future;
|
|
||||||
} else {
|
} else {
|
||||||
Logger.d("App was previously registered. Checking...");
|
await ConnectionManager().sendSocketMessage(type: "get_config").then((data) => _parseConfig(data)).catchError((e) {
|
||||||
var updateData = {
|
throw HAError("Error getting config: $e");
|
||||||
"type": "update_registration",
|
|
||||||
"data": _getAppRegistrationData()
|
|
||||||
};
|
|
||||||
Connection().sendHTTPPost(
|
|
||||||
endPoint: "/api/webhook/${Connection().webhookId}",
|
|
||||||
includeAuthHeader: false,
|
|
||||||
data: json.encode(updateData)
|
|
||||||
).then((response) {
|
|
||||||
if (response == null || response.isEmpty) {
|
|
||||||
Logger.d("No registration data in response. MobileApp integration was removed");
|
|
||||||
_askToRegisterApp();
|
|
||||||
} else {
|
|
||||||
Logger.d("App registration works fine");
|
|
||||||
if (showOkDialog) {
|
|
||||||
eventBus.fire(ShowPopupDialogEvent(
|
|
||||||
title: "All good",
|
|
||||||
body: "HA Client integration with your Home Assistant server works fine",
|
|
||||||
positiveText: "Nice!",
|
|
||||||
negativeText: "Ok"
|
|
||||||
));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
completer.complete();
|
|
||||||
}).catchError((e) {
|
|
||||||
if (e['code'] != null && e['code'] == 410) {
|
|
||||||
Logger.e("MobileApp integration was removed");
|
|
||||||
_askToRegisterApp();
|
|
||||||
} else {
|
|
||||||
Logger.e("Error updating app registration: ${e.toString()}");
|
|
||||||
eventBus.fire(ShowPopupDialogEvent(
|
|
||||||
title: "App integration is not working properly",
|
|
||||||
body: "Something wrong with HA Client integration on your Home Assistant server. Please report this issue.",
|
|
||||||
positiveText: "Report to GitHub",
|
|
||||||
negativeText: "Report to Discord",
|
|
||||||
onPositive: () {
|
|
||||||
HAUtils.launchURL("https://github.com/estevez-dev/ha_client/issues/new");
|
|
||||||
},
|
|
||||||
onNegative: () {
|
|
||||||
HAUtils.launchURL("https://discord.gg/AUzEvwn");
|
|
||||||
},
|
|
||||||
));
|
|
||||||
}
|
|
||||||
completer.complete();
|
|
||||||
});
|
});
|
||||||
return completer.future;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void _askToRegisterApp() {
|
void _parseConfig(data) {
|
||||||
eventBus.fire(ShowPopupDialogEvent(
|
|
||||||
title: "App integration was removed",
|
|
||||||
body: "Looks like app integration was removed from your Home Assistant. HA Client needs to be registered on your Home Assistant server to make it possible to use notifications and other useful stuff.",
|
|
||||||
positiveText: "Register now",
|
|
||||||
negativeText: "Cancel",
|
|
||||||
onPositive: () {
|
|
||||||
SharedPreferences.getInstance().then((prefs) {
|
|
||||||
prefs.remove("app-webhook-id");
|
|
||||||
Connection().webhookId = null;
|
|
||||||
HomeAssistant().checkAppRegistration();
|
|
||||||
});
|
|
||||||
},
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
Future _getConfig() async {
|
|
||||||
await Connection().sendSocketMessage(type: "get_config").then((data) {
|
|
||||||
_instanceConfig = Map.from(data);
|
_instanceConfig = Map.from(data);
|
||||||
}).catchError((e) {
|
Logger.d('stream: ${_instanceConfig['components'].contains('stream')}');
|
||||||
throw HAError("Error getting config: ${e}");
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Future _getStates() async {
|
Future _getStates(SharedPreferences sharedPrefs) async {
|
||||||
await Connection().sendSocketMessage(type: "get_states").then(
|
if (sharedPrefs != null) {
|
||||||
(data) => entities.parse(data)
|
try {
|
||||||
|
var data = json.decode(sharedPrefs.getString('cached_states'));
|
||||||
|
_parseStates(data);
|
||||||
|
} catch (e) {
|
||||||
|
throw HAError("Error getting states: $e");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
await ConnectionManager().sendSocketMessage(type: "get_states").then(
|
||||||
|
(data) => _parseStates(data)
|
||||||
).catchError((e) {
|
).catchError((e) {
|
||||||
throw HAError("Error getting states: $e");
|
throw HAError("Error getting states: $e");
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
Future _getLovelace() async {
|
|
||||||
await Connection().sendSocketMessage(type: "lovelace/config").then((data) => _rawLovelaceData = data).catchError((e) {
|
|
||||||
throw HAError("Error getting lovelace config: $e");
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Future _getUserInfo() async {
|
void _parseStates(data) {
|
||||||
_userName = null;
|
_rawStates = data;
|
||||||
await Connection().sendSocketMessage(type: "auth/current_user").then((data) => _userName = data["name"]).catchError((e) {
|
entities.parse(data);
|
||||||
Logger.w("Can't get user info: ${e}");
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Future _getServices() async {
|
Future _getLovelace(SharedPreferences sharedPrefs) {
|
||||||
await Connection().sendSocketMessage(type: "get_services").then((data) => Logger.d("Services received")).catchError((e) {
|
if (sharedPrefs != null) {
|
||||||
Logger.w("Can't get services: ${e}");
|
try {
|
||||||
});
|
var data = json.decode(sharedPrefs.getString('cached_lovelace'));
|
||||||
|
_rawLovelaceData = data;
|
||||||
|
} catch (e) {
|
||||||
|
autoUi = true;
|
||||||
}
|
}
|
||||||
|
return Future.value();
|
||||||
Future _getPanels() async {
|
} else {
|
||||||
panels.clear();
|
Completer completer = Completer();
|
||||||
await Connection().sendSocketMessage(type: "get_panels").then((data) {
|
var additionalData;
|
||||||
data.forEach((k,v) {
|
if (_lovelaceDashbordUrl != HomeAssistant.DEFAULT_DASHBOARD) {
|
||||||
String title = v['title'] == null ? "${k[0].toUpperCase()}${k.substring(1)}" : "${v['title'][0].toUpperCase()}${v['title'].substring(1)}";
|
additionalData = {
|
||||||
panels.add(Panel(
|
'url_path': _lovelaceDashbordUrl
|
||||||
id: k,
|
};
|
||||||
type: v["component_name"],
|
}
|
||||||
title: title,
|
ConnectionManager().sendSocketMessage(
|
||||||
urlPath: v["url_path"],
|
type: 'lovelace/config',
|
||||||
config: v["config"],
|
additionalData: additionalData
|
||||||
icon: v["icon"]
|
).then((data) {
|
||||||
)
|
_rawLovelaceData = data;
|
||||||
);
|
completer.complete();
|
||||||
});
|
|
||||||
}).catchError((e) {
|
}).catchError((e) {
|
||||||
|
if ("$e" == "config_not_found") {
|
||||||
|
autoUi = true;
|
||||||
|
_rawLovelaceData = null;
|
||||||
|
completer.complete();
|
||||||
|
} else {
|
||||||
|
completer.completeError(HAError("Error getting lovelace config: $e"));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return completer.future;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future _getServices(SharedPreferences prefs) async {
|
||||||
|
if (prefs != null) {
|
||||||
|
try {
|
||||||
|
var data = json.decode(prefs.getString('cached_services'));
|
||||||
|
_parseServices(data);
|
||||||
|
} catch (e) {
|
||||||
|
Logger.w("Can't get services: $e");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
await ConnectionManager().sendSocketMessage(type: "get_services").then((data) => _parseServices(data)).catchError((e) {
|
||||||
|
Logger.w("Can't get services: $e");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void _parseServices(data) {
|
||||||
|
services = data;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future _getUserInfo(SharedPreferences sharedPrefs) async {
|
||||||
|
_userName = null;
|
||||||
|
await ConnectionManager().sendSocketMessage(type: "auth/current_user").then((data) => _parseUserInfo(data)).catchError((e) {
|
||||||
|
Logger.w("Can't get user info: $e");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void _parseUserInfo(data) {
|
||||||
|
_rawUserInfo = data;
|
||||||
|
_userName = data["name"];
|
||||||
|
}
|
||||||
|
|
||||||
|
Future _getPanels(SharedPreferences sharedPrefs) async {
|
||||||
|
if (sharedPrefs != null) {
|
||||||
|
try {
|
||||||
|
var data = json.decode(sharedPrefs.getString('cached_panels'));
|
||||||
|
_parsePanels(data);
|
||||||
|
} catch (e) {
|
||||||
|
throw HAError("Error getting panels list: $e");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
await ConnectionManager().sendSocketMessage(type: "get_panels").then((data) => _parsePanels(data)).catchError((e) {
|
||||||
throw HAError("Error getting panels list: $e");
|
throw HAError("Error getting panels list: $e");
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _parsePanels(data) {
|
||||||
|
_rawPanels = data;
|
||||||
|
panels.clear();
|
||||||
|
List<Panel> dashboards = [];
|
||||||
|
data.forEach((k,v) {
|
||||||
|
String title = v['title'] == null ? "${k[0].toUpperCase()}${k.substring(1)}" : "${v['title'][0].toUpperCase()}${v['title'].substring(1)}";
|
||||||
|
if (v['component_name'] != null && v['component_name'] == 'lovelace') {
|
||||||
|
dashboards.add(
|
||||||
|
Panel(
|
||||||
|
id: k,
|
||||||
|
componentName: v['component_name'],
|
||||||
|
title: title,
|
||||||
|
urlPath: v['url_path'],
|
||||||
|
config: v['config'],
|
||||||
|
icon: (v['icon'] == null || v['icon'] == 'hass:view-dashboard') ? 'mdi:view-dashboard' : v['icon']
|
||||||
|
)
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
panels.add(
|
||||||
|
Panel(
|
||||||
|
id: k,
|
||||||
|
componentName: v['component_name'],
|
||||||
|
title: title,
|
||||||
|
urlPath: v['url_path'],
|
||||||
|
config: v['config'],
|
||||||
|
icon: v['icon']
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
panels.insertAll(0, dashboards);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future getCameraStream(String entityId) {
|
||||||
|
Completer completer = Completer();
|
||||||
|
|
||||||
|
ConnectionManager().sendSocketMessage(type: "camera/stream", additionalData: {"entity_id": entityId}).then((data) {
|
||||||
|
completer.complete(data);
|
||||||
|
}).catchError((e) {
|
||||||
|
completer.completeError(e);
|
||||||
|
});
|
||||||
|
return completer.future;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool isComponentEnabled(String name) {
|
||||||
|
return _instanceConfig["components"] != null && (_instanceConfig["components"] as List).contains("$name");
|
||||||
|
}
|
||||||
|
|
||||||
|
void _handleLovelaceUpdate() {
|
||||||
|
if (_fetchCompleter != null && _fetchCompleter.isCompleted) {
|
||||||
|
eventBus.fire(new LovelaceChangedEvent());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void _handleEntityStateChange(Map eventData) {
|
void _handleEntityStateChange(Map eventData) {
|
||||||
//TheLogger.debug( "New state for ${eventData['entity_id']}");
|
//TheLogger.debug( "New state for ${eventData['entity_id']}");
|
||||||
if (_fetchCompleter.isCompleted) {
|
if (_fetchCompleter != null && _fetchCompleter.isCompleted) {
|
||||||
Map data = Map.from(eventData);
|
Map data = Map.from(eventData);
|
||||||
eventBus.fire(new StateChangedEvent(
|
eventBus.fire(new StateChangedEvent(
|
||||||
entityId: data["entity_id"],
|
entityId: data["entity_id"],
|
||||||
@ -269,196 +326,26 @@ class HomeAssistant {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void _parseLovelace() {
|
bool isServiceExist(String service) {
|
||||||
Logger.d("--Title: ${_rawLovelaceData["title"]}");
|
return services != null &&
|
||||||
ui.title = _rawLovelaceData["title"];
|
services.isNotEmpty &&
|
||||||
int viewCounter = 0;
|
services.containsKey(service);
|
||||||
Logger.d("--Views count: ${_rawLovelaceData['views'].length}");
|
|
||||||
_rawLovelaceData["views"].forEach((rawView){
|
|
||||||
Logger.d("----view id: ${rawView['id']}");
|
|
||||||
HAView view = HAView(
|
|
||||||
count: viewCounter,
|
|
||||||
id: "${rawView['id']}",
|
|
||||||
name: rawView['title'],
|
|
||||||
iconName: rawView['icon']
|
|
||||||
);
|
|
||||||
|
|
||||||
if (rawView['badges'] != null && rawView['badges'] is List) {
|
|
||||||
rawView['badges'].forEach((entity) {
|
|
||||||
if (entities.isExist(entity)) {
|
|
||||||
Entity e = entities.get(entity);
|
|
||||||
view.badges.add(e);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
view.cards.addAll(_createLovelaceCards(rawView["cards"] ?? []));
|
|
||||||
ui.views.add(
|
|
||||||
view
|
|
||||||
);
|
|
||||||
viewCounter += 1;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
List<HACard> _createLovelaceCards(List rawCards) {
|
|
||||||
List<HACard> result = [];
|
|
||||||
rawCards.forEach((rawCard){
|
|
||||||
try {
|
|
||||||
//bool isThereCardOptionsInside = rawCard["card"] != null;
|
|
||||||
var rawCardInfo = rawCard["card"] ?? rawCard;
|
|
||||||
HACard card = HACard(
|
|
||||||
id: "card",
|
|
||||||
name: rawCardInfo["title"] ?? rawCardInfo["name"],
|
|
||||||
type: rawCardInfo['type'],
|
|
||||||
columnsCount: rawCardInfo['columns'] ?? 4,
|
|
||||||
showName: rawCardInfo['show_name'] ?? true,
|
|
||||||
showState: rawCardInfo['show_state'] ?? true,
|
|
||||||
showEmpty: rawCardInfo['show_empty'] ?? true,
|
|
||||||
stateFilter: rawCardInfo['state_filter'] ?? [],
|
|
||||||
states: rawCardInfo['states'],
|
|
||||||
conditions: rawCard['conditions'] ?? [],
|
|
||||||
content: rawCardInfo['content']
|
|
||||||
);
|
|
||||||
if (rawCardInfo["cards"] != null) {
|
|
||||||
card.childCards = _createLovelaceCards(rawCardInfo["cards"]);
|
|
||||||
}
|
|
||||||
rawCardInfo["entities"]?.forEach((rawEntity) {
|
|
||||||
if (rawEntity is String) {
|
|
||||||
if (entities.isExist(rawEntity)) {
|
|
||||||
card.entities.add(EntityWrapper(entity: entities.get(rawEntity)));
|
|
||||||
} else {
|
|
||||||
card.entities.add(EntityWrapper(entity: Entity.missed(rawEntity)));
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if (rawEntity["type"] == "divider") {
|
|
||||||
card.entities.add(EntityWrapper(entity: Entity.divider()));
|
|
||||||
} else if (rawEntity["type"] == "section") {
|
|
||||||
card.entities.add(EntityWrapper(entity: Entity.section(rawEntity["label"] ?? "")));
|
|
||||||
} else if (rawEntity["type"] == "call-service") {
|
|
||||||
Map uiActionData = {
|
|
||||||
"tap_action": {
|
|
||||||
"action": EntityUIAction.callService,
|
|
||||||
"service": rawEntity["service"],
|
|
||||||
"service_data": rawEntity["service_data"]
|
|
||||||
},
|
|
||||||
"hold_action": EntityUIAction.none
|
|
||||||
};
|
|
||||||
card.entities.add(EntityWrapper(
|
|
||||||
entity: Entity.callService(
|
|
||||||
icon: rawEntity["icon"],
|
|
||||||
name: rawEntity["name"],
|
|
||||||
service: rawEntity["service"],
|
|
||||||
actionName: rawEntity["action_name"]
|
|
||||||
),
|
|
||||||
uiAction: EntityUIAction(rawEntityData: uiActionData)
|
|
||||||
)
|
|
||||||
);
|
|
||||||
} else if (rawEntity["type"] == "weblink") {
|
|
||||||
Map uiActionData = {
|
|
||||||
"tap_action": {
|
|
||||||
"action": EntityUIAction.navigate,
|
|
||||||
"service": rawEntity["url"]
|
|
||||||
},
|
|
||||||
"hold_action": EntityUIAction.none
|
|
||||||
};
|
|
||||||
card.entities.add(EntityWrapper(
|
|
||||||
entity: Entity.weblink(
|
|
||||||
icon: rawEntity["icon"],
|
|
||||||
name: rawEntity["name"],
|
|
||||||
url: rawEntity["url"]
|
|
||||||
),
|
|
||||||
uiAction: EntityUIAction(rawEntityData: uiActionData)
|
|
||||||
)
|
|
||||||
);
|
|
||||||
} else if (entities.isExist(rawEntity["entity"])) {
|
|
||||||
Entity e = entities.get(rawEntity["entity"]);
|
|
||||||
card.entities.add(
|
|
||||||
EntityWrapper(
|
|
||||||
entity: e,
|
|
||||||
displayName: rawEntity["name"],
|
|
||||||
icon: rawEntity["icon"],
|
|
||||||
uiAction: EntityUIAction(rawEntityData: rawEntity)
|
|
||||||
)
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
card.entities.add(EntityWrapper(entity: Entity.missed(rawEntity["entity"])));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
if (rawCardInfo["entity"] != null) {
|
|
||||||
var en = rawCardInfo["entity"];
|
|
||||||
if (en is String) {
|
|
||||||
if (entities.isExist(en)) {
|
|
||||||
Entity e = entities.get(en);
|
|
||||||
card.linkedEntityWrapper = EntityWrapper(
|
|
||||||
entity: e,
|
|
||||||
icon: rawCardInfo["icon"],
|
|
||||||
displayName: rawCardInfo["name"],
|
|
||||||
uiAction: EntityUIAction(rawEntityData: rawCard)
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
card.linkedEntityWrapper = EntityWrapper(entity: Entity.missed(en));
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if (entities.isExist(en["entity"])) {
|
|
||||||
Entity e = entities.get(en["entity"]);
|
|
||||||
card.linkedEntityWrapper = EntityWrapper(
|
|
||||||
entity: e,
|
|
||||||
icon: en["icon"],
|
|
||||||
displayName: en["name"],
|
|
||||||
uiAction: EntityUIAction(rawEntityData: rawCard)
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
card.linkedEntityWrapper = EntityWrapper(entity: Entity.missed(en["entity"]));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
result.add(card);
|
|
||||||
} catch (e) {
|
|
||||||
Logger.e("There was an error parsing card: ${e.toString()}");
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return result;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void _createUI() {
|
void _createUI() {
|
||||||
ui = HomeAssistantUI();
|
|
||||||
if ((Connection().useLovelace) && (_rawLovelaceData != null)) {
|
|
||||||
Logger.d("Creating Lovelace UI");
|
Logger.d("Creating Lovelace UI");
|
||||||
_parseLovelace();
|
ui = HomeAssistantUI(rawLovelaceConfig: _rawLovelaceData);
|
||||||
} else {
|
/*if (isServiceExist('zha_map')) {
|
||||||
Logger.d("Creating group-based UI");
|
panels.add(
|
||||||
int viewCounter = 0;
|
Panel(
|
||||||
if (!entities.hasDefaultView) {
|
id: 'haclient_zha',
|
||||||
HAView view = HAView(
|
componentName: 'haclient_zha',
|
||||||
count: viewCounter,
|
title: 'ZHA',
|
||||||
id: "group.default_view",
|
urlPath: '/haclient_zha',
|
||||||
name: "Home",
|
icon: 'mdi:zigbee'
|
||||||
childEntities: entities.filterEntitiesForDefaultView()
|
)
|
||||||
);
|
);
|
||||||
ui.views.add(
|
}*/
|
||||||
view
|
|
||||||
);
|
|
||||||
viewCounter += 1;
|
|
||||||
}
|
|
||||||
entities.viewEntities.forEach((viewEntity) {
|
|
||||||
HAView view = HAView(
|
|
||||||
count: viewCounter,
|
|
||||||
id: viewEntity.entityId,
|
|
||||||
name: viewEntity.displayName,
|
|
||||||
childEntities: viewEntity.childEntities
|
|
||||||
);
|
|
||||||
view.linkedEntity = viewEntity;
|
|
||||||
ui.views.add(
|
|
||||||
view
|
|
||||||
);
|
|
||||||
viewCounter += 1;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget buildViews(BuildContext context, TabController tabController) {
|
|
||||||
return ui.build(context, tabController);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
990
lib/main.dart
990
lib/main.dart
File diff suppressed because it is too large
Load Diff
@ -1,4 +1,4 @@
|
|||||||
part of 'main.dart';
|
part of '../main.dart';
|
||||||
|
|
||||||
class AuthManager {
|
class AuthManager {
|
||||||
|
|
||||||
@ -10,31 +10,31 @@ class AuthManager {
|
|||||||
|
|
||||||
AuthManager._internal();
|
AuthManager._internal();
|
||||||
|
|
||||||
Future getTempToken({String oauthUrl}) {
|
Future start({String oauthUrl}) {
|
||||||
Completer completer = Completer();
|
Completer completer = Completer();
|
||||||
final flutterWebviewPlugin = new FlutterWebviewPlugin();
|
final flutterWebviewPlugin = new standaloneWebview.FlutterWebviewPlugin();
|
||||||
flutterWebviewPlugin.onUrlChanged.listen((String url) {
|
flutterWebviewPlugin.onUrlChanged.listen((String url) {
|
||||||
if (url.startsWith("http://ha-client.homemade.systems/service/auth_callback.html")) {
|
if (url.startsWith("https://ha-client.app/service/auth_callback.html")) {
|
||||||
|
Logger.d("url=$url");
|
||||||
String authCode = url.split("=")[1];
|
String authCode = url.split("=")[1];
|
||||||
|
Logger.d("authCode=$authCode");
|
||||||
Logger.d("We have auth code. Getting temporary access token...");
|
Logger.d("We have auth code. Getting temporary access token...");
|
||||||
Connection().sendHTTPPost(
|
ConnectionManager().sendHTTPPost(
|
||||||
endPoint: "/auth/token",
|
endPoint: "/auth/token",
|
||||||
contentType: "application/x-www-form-urlencoded",
|
contentType: "application/x-www-form-urlencoded",
|
||||||
includeAuthHeader: false,
|
includeAuthHeader: false,
|
||||||
data: "grant_type=authorization_code&code=$authCode&client_id=${Uri.encodeComponent('http://ha-client.homemade.systems/')}"
|
data: "grant_type=authorization_code&code=$authCode&client_id=${Uri.encodeComponent('https://ha-client.app')}"
|
||||||
).then((response) {
|
).then((response) {
|
||||||
Logger.d("Got temp token");
|
Logger.d("Got temp token");
|
||||||
String tempToken = json.decode(response)['access_token'];
|
String tempToken = json.decode(response)['access_token'];
|
||||||
Logger.d("Closing webview...");
|
Logger.d("Closing webview...");
|
||||||
//flutterWebviewPlugin.close();
|
|
||||||
eventBus.fire(StartAuthEvent(oauthUrl, false));
|
eventBus.fire(StartAuthEvent(oauthUrl, false));
|
||||||
completer.complete(tempToken);
|
completer.complete(tempToken);
|
||||||
}).catchError((e) {
|
}).catchError((e) {
|
||||||
//flutterWebviewPlugin.close();
|
|
||||||
Logger.e("Error getting temp token: ${e.toString()}");
|
Logger.e("Error getting temp token: ${e.toString()}");
|
||||||
eventBus.fire(StartAuthEvent(oauthUrl, false));
|
eventBus.fire(StartAuthEvent(oauthUrl, false));
|
||||||
completer.completeError(HAError("Error getting temp token"));
|
completer.completeError(HAError("Error getting temp token"));
|
||||||
});
|
}).whenComplete(() => flutterWebviewPlugin.close());
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
Logger.d("Launching OAuth");
|
Logger.d("Launching OAuth");
|
@ -1,14 +1,14 @@
|
|||||||
part of 'main.dart';
|
part of '../main.dart';
|
||||||
|
|
||||||
class Connection {
|
class ConnectionManager {
|
||||||
|
|
||||||
static final Connection _instance = Connection._internal();
|
static final ConnectionManager _instance = ConnectionManager._internal();
|
||||||
|
|
||||||
factory Connection() {
|
factory ConnectionManager() {
|
||||||
return _instance;
|
return _instance;
|
||||||
}
|
}
|
||||||
|
|
||||||
Connection._internal();
|
ConnectionManager._internal();
|
||||||
|
|
||||||
String _domain;
|
String _domain;
|
||||||
String _port;
|
String _port;
|
||||||
@ -19,8 +19,8 @@ class Connection {
|
|||||||
String _tempToken;
|
String _tempToken;
|
||||||
String oauthUrl;
|
String oauthUrl;
|
||||||
String webhookId;
|
String webhookId;
|
||||||
bool useLovelace = true;
|
|
||||||
bool settingsLoaded = false;
|
bool settingsLoaded = false;
|
||||||
|
int appIntegrationVersion;
|
||||||
bool get isAuthenticated => _token != null;
|
bool get isAuthenticated => _token != null;
|
||||||
StreamSubscription _socketSubscription;
|
StreamSubscription _socketSubscription;
|
||||||
Duration connectTimeout = Duration(seconds: 15);
|
Duration connectTimeout = Duration(seconds: 15);
|
||||||
@ -28,6 +28,7 @@ class Connection {
|
|||||||
bool isConnected = false;
|
bool isConnected = false;
|
||||||
|
|
||||||
var onStateChangeCallback;
|
var onStateChangeCallback;
|
||||||
|
var onLovelaceUpdatedCallback;
|
||||||
|
|
||||||
IOWebSocketChannel _socket;
|
IOWebSocketChannel _socket;
|
||||||
|
|
||||||
@ -38,12 +39,12 @@ class Connection {
|
|||||||
Completer completer = Completer();
|
Completer completer = Completer();
|
||||||
bool stopInit = false;
|
bool stopInit = false;
|
||||||
if (loadSettings) {
|
if (loadSettings) {
|
||||||
Logger.e("Loading settings...");
|
Logger.d("Loading settings...");
|
||||||
SharedPreferences prefs = await SharedPreferences.getInstance();
|
SharedPreferences prefs = await SharedPreferences.getInstance();
|
||||||
useLovelace = prefs.getBool('use-lovelace') ?? true;
|
|
||||||
_domain = prefs.getString('hassio-domain');
|
_domain = prefs.getString('hassio-domain');
|
||||||
_port = prefs.getString('hassio-port');
|
_port = prefs.getString('hassio-port');
|
||||||
webhookId = prefs.getString('app-webhook-id');
|
webhookId = prefs.getString('app-webhook-id');
|
||||||
|
appIntegrationVersion = prefs.getInt('app-integration-version') ?? 0;
|
||||||
displayHostname = "$_domain:$_port";
|
displayHostname = "$_domain:$_port";
|
||||||
_webSocketAPIEndpoint =
|
_webSocketAPIEndpoint =
|
||||||
"${prefs.getString('hassio-protocol')}://$_domain:$_port/api/websocket";
|
"${prefs.getString('hassio-protocol')}://$_domain:$_port/api/websocket";
|
||||||
@ -54,21 +55,20 @@ class Connection {
|
|||||||
completer.completeError(HAError.checkConnectionSettings());
|
completer.completeError(HAError.checkConnectionSettings());
|
||||||
stopInit = true;
|
stopInit = true;
|
||||||
} else {
|
} else {
|
||||||
//_token = prefs.getString('hassio-token');
|
|
||||||
final storage = new FlutterSecureStorage();
|
final storage = new FlutterSecureStorage();
|
||||||
try {
|
try {
|
||||||
_token = await storage.read(key: "hacl_llt");
|
_token = await storage.read(key: "hacl_llt");
|
||||||
Logger.e("Long-lived token read successful");
|
Logger.e("Long-lived token read successful");
|
||||||
} catch (e) {
|
|
||||||
Logger.e("Cannt read secure storage. Need to relogin.");
|
|
||||||
_token = null;
|
|
||||||
await storage.delete(key: "hacl_llt");
|
|
||||||
}
|
|
||||||
oauthUrl = "$httpWebHost/auth/authorize?client_id=${Uri.encodeComponent(
|
oauthUrl = "$httpWebHost/auth/authorize?client_id=${Uri.encodeComponent(
|
||||||
'http://ha-client.homemade.systems/')}&redirect_uri=${Uri
|
'https://ha-client.app')}&redirect_uri=${Uri
|
||||||
.encodeComponent(
|
.encodeComponent(
|
||||||
'http://ha-client.homemade.systems/service/auth_callback.html')}";
|
'https://ha-client.app/service/auth_callback.html')}";
|
||||||
settingsLoaded = true;
|
settingsLoaded = true;
|
||||||
|
} catch (e) {
|
||||||
|
completer.completeError(HAError("Error reading login details", actions: [HAErrorAction.tryAgain(type: HAErrorActionType.FULL_RELOAD), HAErrorAction.loginAgain()]));
|
||||||
|
Logger.e("Cannt read secure storage. Need to relogin.");
|
||||||
|
stopInit = true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if ((_domain == null) || (_port == null) ||
|
if ((_domain == null) || (_port == null) ||
|
||||||
@ -80,7 +80,7 @@ class Connection {
|
|||||||
|
|
||||||
if (!stopInit) {
|
if (!stopInit) {
|
||||||
if (_token == null) {
|
if (_token == null) {
|
||||||
AuthManager().getTempToken(
|
AuthManager().start(
|
||||||
oauthUrl: oauthUrl
|
oauthUrl: oauthUrl
|
||||||
).then((token) {
|
).then((token) {
|
||||||
Logger.d("Token from AuthManager recived");
|
Logger.d("Token from AuthManager recived");
|
||||||
@ -99,14 +99,23 @@ class Connection {
|
|||||||
|
|
||||||
void _doConnect({Completer completer, bool forceReconnect}) {
|
void _doConnect({Completer completer, bool forceReconnect}) {
|
||||||
if (forceReconnect || !isConnected) {
|
if (forceReconnect || !isConnected) {
|
||||||
_connect().timeout(connectTimeout, onTimeout: () {
|
_disconnect().then((_){
|
||||||
_disconnect().then((_) {
|
_connect().timeout(connectTimeout).then((_) {
|
||||||
completer?.completeError(HAError("Connection timeout"));
|
|
||||||
});
|
|
||||||
}).then((_) {
|
|
||||||
completer?.complete();
|
completer?.complete();
|
||||||
}).catchError((e) {
|
}).catchError((e) {
|
||||||
|
_disconnect().then((_) {
|
||||||
|
if (e is TimeoutException) {
|
||||||
|
if (connecting != null && !connecting.isCompleted) {
|
||||||
|
connecting.completeError(HAError("Connection timeout"));
|
||||||
|
}
|
||||||
|
completer?.completeError(HAError("Connection timeout"));
|
||||||
|
} else if (e is HAError) {
|
||||||
completer?.completeError(e);
|
completer?.completeError(e);
|
||||||
|
} else {
|
||||||
|
completer?.completeError(HAError("${e.toString()}"));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
completer?.complete();
|
completer?.complete();
|
||||||
@ -123,6 +132,7 @@ class Connection {
|
|||||||
connecting = Completer();
|
connecting = Completer();
|
||||||
_disconnect().then((_) {
|
_disconnect().then((_) {
|
||||||
Logger.d("Socket connecting...");
|
Logger.d("Socket connecting...");
|
||||||
|
try {
|
||||||
_socket = IOWebSocketChannel.connect(
|
_socket = IOWebSocketChannel.connect(
|
||||||
_webSocketAPIEndpoint, pingInterval: Duration(seconds: 15));
|
_webSocketAPIEndpoint, pingInterval: Duration(seconds: 15));
|
||||||
_socketSubscription = _socket.stream.listen(
|
_socketSubscription = _socket.stream.listen(
|
||||||
@ -139,18 +149,26 @@ class Connection {
|
|||||||
});
|
});
|
||||||
} else if (data["type"] == "auth_ok") {
|
} else if (data["type"] == "auth_ok") {
|
||||||
Logger.d("[Received] <== ${data.toString()}");
|
Logger.d("[Received] <== ${data.toString()}");
|
||||||
|
Logger.d("[Connection] Subscribing to events");
|
||||||
|
sendSocketMessage(
|
||||||
|
type: "subscribe_events",
|
||||||
|
additionalData: {"event_type": "lovelace_updated"},
|
||||||
|
);
|
||||||
|
sendSocketMessage(
|
||||||
|
type: "subscribe_events",
|
||||||
|
additionalData: {"event_type": "state_changed"},
|
||||||
|
).whenComplete((){
|
||||||
_messageResolver["auth"]?.complete();
|
_messageResolver["auth"]?.complete();
|
||||||
_messageResolver.remove("auth");
|
_messageResolver.remove("auth");
|
||||||
if (_token != null) {
|
if (_token != null) {
|
||||||
if (!connecting.isCompleted) connecting.complete();
|
if (!connecting.isCompleted) connecting.complete();
|
||||||
}
|
}
|
||||||
|
});
|
||||||
} else if (data["type"] == "auth_invalid") {
|
} else if (data["type"] == "auth_invalid") {
|
||||||
Logger.d("[Received] <== ${data.toString()}");
|
Logger.d("[Received] <== ${data.toString()}");
|
||||||
_messageResolver["auth"]?.completeError(HAError("${data["message"]}", actions: [HAErrorAction.loginAgain()]));
|
_messageResolver["auth"]?.completeError(HAError("${data["message"]}", actions: [HAErrorAction.loginAgain()]));
|
||||||
_messageResolver.remove("auth");
|
_messageResolver.remove("auth");
|
||||||
logout().then((_) {
|
if (!connecting.isCompleted) connecting.completeError(HAError("${data["message"]}", actions: [HAErrorAction.tryAgain(title: "Retry"), HAErrorAction.loginAgain(title: "Relogin")]));
|
||||||
if (!connecting.isCompleted) connecting.completeError(HAError("${data["message"]}", actions: [HAErrorAction.loginAgain()]));
|
|
||||||
});
|
|
||||||
} else {
|
} else {
|
||||||
_handleMessage(data);
|
_handleMessage(data);
|
||||||
}
|
}
|
||||||
@ -159,13 +177,14 @@ class Connection {
|
|||||||
onDone: () => _handleSocketClose(connecting),
|
onDone: () => _handleSocketClose(connecting),
|
||||||
onError: (e) => _handleSocketError(e, connecting)
|
onError: (e) => _handleSocketError(e, connecting)
|
||||||
);
|
);
|
||||||
|
} catch(exeption) {
|
||||||
|
connecting.completeError(HAError("${exeption.toString()}"));
|
||||||
|
}
|
||||||
});
|
});
|
||||||
return connecting.future;
|
return connecting.future;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
Future _disconnect() {
|
Future _disconnect() {
|
||||||
Completer completer = Completer();
|
Completer completer = Completer();
|
||||||
if (!isConnected) {
|
if (!isConnected) {
|
||||||
@ -189,21 +208,22 @@ class Connection {
|
|||||||
_handleMessage(data) {
|
_handleMessage(data) {
|
||||||
if (data["type"] == "result") {
|
if (data["type"] == "result") {
|
||||||
if (data["id"] != null && data["success"]) {
|
if (data["id"] != null && data["success"]) {
|
||||||
Logger.d("[Received] <== Request id ${data['id']} was successful");
|
//Logger.d("[Received] <== Request id ${data['id']} was successful");
|
||||||
_messageResolver["${data["id"]}"]?.complete(data["result"]);
|
_messageResolver["${data["id"]}"]?.complete(data["result"]);
|
||||||
} else if (data["id"] != null) {
|
} else if (data["id"] != null) {
|
||||||
Logger.e("[Received] <== Error received on request id ${data['id']}: ${data['error']}");
|
Logger.e("[Received] <== Error received on request id ${data['id']}: ${data['error']}");
|
||||||
_messageResolver["${data["id"]}"]?.completeError("${data['error']["message"]}");
|
_messageResolver["${data["id"]}"]?.completeError("${data["error"]["code"]}");
|
||||||
}
|
}
|
||||||
_messageResolver.remove("${data["id"]}");
|
_messageResolver.remove("${data["id"]}");
|
||||||
} else if (data["type"] == "event") {
|
} else if (data["type"] == "event") {
|
||||||
if ((data["event"] != null) && (data["event"]["event_type"] == "state_changed")) {
|
if (data["event"] != null) {
|
||||||
|
if (data["event"]["event_type"] == "state_changed") {
|
||||||
Logger.d("[Received] <== ${data['type']}.${data["event"]["event_type"]}: ${data["event"]["data"]["entity_id"]}");
|
Logger.d("[Received] <== ${data['type']}.${data["event"]["event_type"]}: ${data["event"]["data"]["entity_id"]}");
|
||||||
onStateChangeCallback(data["event"]["data"]);
|
onStateChangeCallback(data["event"]["data"]);
|
||||||
} else if (data["event"] != null) {
|
} else if (data["event"]["event_type"] == "lovelace_updated") {
|
||||||
Logger.w("Unhandled event type: ${data["event"]["event_type"]}");
|
Logger.d("[Received] <== ${data['type']}.${data["event"]["event_type"]}: $data");
|
||||||
} else {
|
onLovelaceUpdatedCallback();
|
||||||
Logger.e("Event is null: $data");
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
Logger.d("[Received unhandled] <== ${data.toString()}");
|
Logger.d("[Received unhandled] <== ${data.toString()}");
|
||||||
@ -212,38 +232,24 @@ class Connection {
|
|||||||
|
|
||||||
void _handleSocketClose(Completer connectionCompleter) {
|
void _handleSocketClose(Completer connectionCompleter) {
|
||||||
Logger.d("Socket disconnected.");
|
Logger.d("Socket disconnected.");
|
||||||
|
_disconnect().then((_) {
|
||||||
if (!connectionCompleter.isCompleted) {
|
if (!connectionCompleter.isCompleted) {
|
||||||
isConnected = false;
|
isConnected = false;
|
||||||
connectionCompleter.completeError(HAError("Disconnected", actions: [HAErrorAction.reconnect()]));
|
connectionCompleter.completeError(HAError("Disconnected", actions: [HAErrorAction.reconnect()]));
|
||||||
} else {
|
}
|
||||||
_disconnect().then((_) {
|
|
||||||
Timer(Duration(seconds: 5), () {
|
|
||||||
Logger.d("Trying to reconnect...");
|
|
||||||
_connect().catchError((e) {
|
|
||||||
isConnected = false;
|
|
||||||
eventBus.fire(ShowErrorEvent(HAError("Unable to connect to Home Assistant")));
|
eventBus.fire(ShowErrorEvent(HAError("Unable to connect to Home Assistant")));
|
||||||
});
|
});
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void _handleSocketError(e, Completer connectionCompleter) {
|
void _handleSocketError(e, Completer connectionCompleter) {
|
||||||
Logger.e("Socket stream Error: $e");
|
Logger.e("Socket stream Error: $e");
|
||||||
|
_disconnect().then((_) {
|
||||||
if (!connectionCompleter.isCompleted) {
|
if (!connectionCompleter.isCompleted) {
|
||||||
isConnected = false;
|
isConnected = false;
|
||||||
connectionCompleter.completeError(HAError("Unable to connect to Home Assistant"));
|
connectionCompleter.completeError(HAError("Disconnected", actions: [HAErrorAction.reconnect()]));
|
||||||
} else {
|
}
|
||||||
_disconnect().then((_) {
|
|
||||||
Timer(Duration(seconds: 5), () {
|
|
||||||
Logger.d("Trying to reconnect...");
|
|
||||||
_connect().catchError((e) {
|
|
||||||
isConnected = false;
|
|
||||||
eventBus.fire(ShowErrorEvent(HAError("Unable to connect to Home Assistant")));
|
eventBus.fire(ShowErrorEvent(HAError("Unable to connect to Home Assistant")));
|
||||||
});
|
});
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Future _authenticate() {
|
Future _authenticate() {
|
||||||
@ -281,6 +287,7 @@ class Connection {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future logout() {
|
Future logout() {
|
||||||
|
Logger.d("Logging out");
|
||||||
Completer completer = Completer();
|
Completer completer = Completer();
|
||||||
_disconnect().whenComplete(() {
|
_disconnect().whenComplete(() {
|
||||||
_token = null;
|
_token = null;
|
||||||
@ -309,8 +316,7 @@ class Connection {
|
|||||||
throw e;
|
throw e;
|
||||||
});
|
});
|
||||||
}).catchError((e) {
|
}).catchError((e) {
|
||||||
logout();
|
completer.completeError(HAError("Authentication error: $e", actions: [HAErrorAction.reload(title: "Retry"), HAErrorAction.loginAgain(title: "Relogin")]));
|
||||||
completer.completeError(HAError("Authentication error: $e", actions: [HAErrorAction.loginAgain()]));
|
|
||||||
});
|
});
|
||||||
return completer.future;
|
return completer.future;
|
||||||
}
|
}
|
||||||
@ -332,13 +338,13 @@ class Connection {
|
|||||||
_messageResolver[callbackName] = _completer;
|
_messageResolver[callbackName] = _completer;
|
||||||
String rawMessage = json.encode(dataObject);
|
String rawMessage = json.encode(dataObject);
|
||||||
if (!isConnected) {
|
if (!isConnected) {
|
||||||
_connect().timeout(connectTimeout, onTimeout: (){
|
_connect().timeout(connectTimeout).then((_) {
|
||||||
_completer.completeError(HAError("No connection to Home Assistant", actions: [HAErrorAction.reconnect()]));
|
|
||||||
}).then((_) {
|
|
||||||
Logger.d("[Sending] ==> ${auth ? "type="+dataObject['type'] : rawMessage}");
|
Logger.d("[Sending] ==> ${auth ? "type="+dataObject['type'] : rawMessage}");
|
||||||
_socket.sink.add(rawMessage);
|
_socket.sink.add(rawMessage);
|
||||||
}).catchError((e) {
|
}).catchError((e) {
|
||||||
_completer.completeError(e);
|
if (!_completer.isCompleted) {
|
||||||
|
_completer.completeError(HAError("No connection to Home Assistant", actions: [HAErrorAction.reconnect()]));
|
||||||
|
}
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
Logger.d("[Sending] ==> ${auth ? "type="+dataObject['type'] : rawMessage}");
|
Logger.d("[Sending] ==> ${auth ? "type="+dataObject['type'] : rawMessage}");
|
||||||
@ -351,18 +357,29 @@ class Connection {
|
|||||||
_currentMessageId += 1;
|
_currentMessageId += 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
Future callService({String domain, String service, String entityId, Map additionalServiceData}) {
|
Future callService({@required String domain, @required String service, entityId, Map data}) {
|
||||||
|
eventBus.fire(NotifyServiceCallEvent(domain, service, entityId));
|
||||||
|
Logger.d("Service call: $domain.$service, $entityId, $data");
|
||||||
|
Completer completer = Completer();
|
||||||
Map serviceData = {};
|
Map serviceData = {};
|
||||||
if (entityId != null) {
|
if (entityId != null) {
|
||||||
serviceData["entity_id"] = entityId;
|
serviceData["entity_id"] = entityId;
|
||||||
}
|
}
|
||||||
if (additionalServiceData != null && additionalServiceData.isNotEmpty) {
|
if (data != null && data.isNotEmpty) {
|
||||||
serviceData.addAll(additionalServiceData);
|
serviceData.addAll(data);
|
||||||
}
|
}
|
||||||
if (serviceData.isNotEmpty)
|
if (serviceData.isNotEmpty)
|
||||||
return sendSocketMessage(type: "call_service", additionalData: {"domain": domain, "service": service, "service_data": serviceData});
|
sendHTTPPost(
|
||||||
|
endPoint: "/api/services/$domain/$service",
|
||||||
|
data: json.encode(serviceData)
|
||||||
|
).then((data) => completer.complete(data)).catchError((e) => completer.completeError(HAError(e.toString())));
|
||||||
|
//return sendSocketMessage(type: "call_service", additionalData: {"domain": domain, "service": service, "service_data": serviceData});
|
||||||
else
|
else
|
||||||
return sendSocketMessage(type: "call_service", additionalData: {"domain": domain, "service": service});
|
sendHTTPPost(
|
||||||
|
endPoint: "/api/services/$domain/$service"
|
||||||
|
).then((data) => completer.complete(data)).catchError((e) => completer.completeError(HAError(e.toString())));
|
||||||
|
//return sendSocketMessage(type: "call_service", additionalData: {"domain": domain, "service": service});
|
||||||
|
return completer.future;
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<List> getHistory(String entityId) async {
|
Future<List> getHistory(String entityId) async {
|
||||||
@ -401,11 +418,12 @@ class Connection {
|
|||||||
headers: headers,
|
headers: headers,
|
||||||
body: data
|
body: data
|
||||||
).then((response) {
|
).then((response) {
|
||||||
Logger.d("[Received] <== HTTP ${response.statusCode}");
|
|
||||||
if (response.statusCode >= 200 && response.statusCode < 300 ) {
|
if (response.statusCode >= 200 && response.statusCode < 300 ) {
|
||||||
|
Logger.d("[Received] <== HTTP ${response.statusCode}");
|
||||||
completer.complete(response.body);
|
completer.complete(response.body);
|
||||||
} else {
|
} else {
|
||||||
completer.completeError({"code": response.statusCode, "message": "${response.body}"});
|
Logger.d("[Received] <== HTTP ${response.statusCode}: ${response.body}");
|
||||||
|
completer.completeError(response);
|
||||||
}
|
}
|
||||||
}).catchError((e) {
|
}).catchError((e) {
|
||||||
completer.completeError(e);
|
completer.completeError(e);
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user