Compare commits
265 Commits
0.6.5
...
beta/0.8.0
Author | SHA1 | Date | |
---|---|---|---|
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 |
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.
|
7
.gitignore
vendored
7
.gitignore
vendored
@ -9,6 +9,13 @@ build/
|
||||
.flutter-plugins
|
||||
|
||||
.idea/
|
||||
.vscode/
|
||||
.theia/
|
||||
.project/
|
||||
.settings/
|
||||
|
||||
flutter_export_environment.sh
|
||||
.flutter-plugins-dependencies
|
||||
|
||||
key.properties
|
||||
premium_features_manager.class.dart
|
||||
|
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:$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
13
README.md
13
README.md
@ -1,9 +1,16 @@
|
||||
# HA Client
|
||||
## 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.
|
||||
|
||||
Download the app from [Google Play](https://play.google.com/apps/testing/com.keyboardcrumbs.haclient)
|
||||
|
||||
Discuss it in [Home Assistant community](https://community.home-assistant.io/t/alpha-testing-ha-client-native-android-client-for-home-assistant/69912) or on [Discord server](https://discord.gg/AUzEvwn)
|
||||
Discuss it on [Spectrum.chat](https://spectrum.chat/ha-client) or at [Home Assistant community](https://community.home-assistant.io/c/mobile-apps/ha-client-android)
|
||||
|
||||
[](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
|
||||
/captures
|
||||
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 {
|
||||
if (!System.getenv()["CI"]) {
|
||||
debug {
|
||||
keyAlias keystoreProperties['debugKeyAlias']
|
||||
keyPassword keystoreProperties['debugKeyPassword']
|
||||
storeFile file(keystoreProperties['debugStoreFile'])
|
||||
storePassword keystoreProperties['debugStorePassword']
|
||||
}
|
||||
}
|
||||
release {
|
||||
keyAlias keystoreProperties['keyAlias']
|
||||
keyPassword keystoreProperties['keyPassword']
|
||||
@ -70,10 +78,11 @@ flutter {
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation 'com.google.firebase:firebase-core:16.0.8'
|
||||
implementation 'com.google.firebase:firebase-analytics:17.2.2'
|
||||
testImplementation 'junit:junit:4.12'
|
||||
androidTestImplementation 'com.android.support.test:runner:1.0.2'
|
||||
androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2'
|
||||
}
|
||||
|
||||
apply plugin: 'io.fabric'
|
||||
apply plugin: 'com.google.gms.google-services'
|
||||
|
@ -14,6 +14,30 @@
|
||||
}
|
||||
},
|
||||
"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_type": 3
|
||||
@ -25,15 +49,13 @@
|
||||
}
|
||||
],
|
||||
"services": {
|
||||
"analytics_service": {
|
||||
"status": 1
|
||||
},
|
||||
"appinvite_service": {
|
||||
"status": 1,
|
||||
"other_platform_oauth_client": []
|
||||
},
|
||||
"ads_service": {
|
||||
"status": 2
|
||||
"other_platform_oauth_client": [
|
||||
{
|
||||
"client_id": "441874387819-id0hqsfprj3b5kc312faqv3lmdfpm7l8.apps.googleusercontent.com",
|
||||
"client_type": 3
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,16 +1,15 @@
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="com.keyboardcrumbs.hassclient">
|
||||
|
||||
<!-- The INTERNET permission is required for development. Specifically,
|
||||
flutter needs it to communicate with the running application
|
||||
to allow setting breakpoints, to provide hot reload, etc.
|
||||
-->
|
||||
<uses-feature android:name="android.hardware.touchscreen"
|
||||
android:required="false" />
|
||||
<uses-permission android:name="android.permission.INTERNET"/>
|
||||
<uses-permission android:name="android.permission.VIBRATE" />
|
||||
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
|
||||
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
|
||||
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
|
||||
<uses-permission android:name="android.permission.WAKE_LOCK"/>
|
||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
|
||||
|
||||
|
||||
<!-- io.flutter.app.FlutterApplication is an android.app.Application that
|
||||
calls FlutterMain.startInitialization(this); in its onCreate method.
|
||||
@ -18,11 +17,14 @@
|
||||
additional functionality it is fine to subclass or reimplement
|
||||
FlutterApplication and put your custom class here. -->
|
||||
<application
|
||||
android:name=".Application"
|
||||
android:label="HA Client"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:usesCleartextTraffic="true">
|
||||
|
||||
<meta-data
|
||||
android:name="flutterEmbedding"
|
||||
android:value="2" />
|
||||
|
||||
<meta-data
|
||||
android:name="com.google.firebase.messaging.default_notification_channel_id"
|
||||
android:value="ha_notify" />
|
||||
@ -34,13 +36,12 @@
|
||||
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|locale|layoutDirection|fontScale|screenLayout|density"
|
||||
android:hardwareAccelerated="true"
|
||||
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
|
||||
android:name="io.flutter.app.android.SplashScreenUntilFirstFrame"
|
||||
android:value="true" />-->
|
||||
android:name="io.flutter.embedding.android.SplashScreenDrawable"
|
||||
android:resource="@drawable/launch_background" />
|
||||
<meta-data
|
||||
android:name="io.flutter.embedding.android.NormalTheme"
|
||||
android:resource="@style/NormalTheme" />
|
||||
<intent-filter>
|
||||
<action android:name="FLUTTER_NOTIFICATION_CLICK" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
|
@ -1,18 +0,0 @@
|
||||
package com.keyboardcrumbs.hassclient;
|
||||
|
||||
import io.flutter.app.FlutterApplication;
|
||||
import io.flutter.plugin.common.PluginRegistry;
|
||||
import io.flutter.plugin.common.PluginRegistry.PluginRegistrantCallback;
|
||||
import io.flutter.plugins.GeneratedPluginRegistrant;
|
||||
|
||||
public class Application extends FlutterApplication implements PluginRegistrantCallback {
|
||||
@Override
|
||||
public void onCreate() {
|
||||
super.onCreate();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void registerWith(PluginRegistry registry) {
|
||||
GeneratedPluginRegistrant.registerWith(registry);
|
||||
}
|
||||
}
|
@ -1,13 +1,15 @@
|
||||
package com.keyboardcrumbs.hassclient;
|
||||
|
||||
import android.os.Bundle;
|
||||
import io.flutter.app.FlutterActivity;
|
||||
import androidx.annotation.NonNull;
|
||||
import io.flutter.embedding.android.FlutterActivity;
|
||||
import io.flutter.embedding.engine.FlutterEngine;
|
||||
import io.flutter.plugins.GeneratedPluginRegistrant;
|
||||
|
||||
public class MainActivity extends FlutterActivity {
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
GeneratedPluginRegistrant.registerWith(this);
|
||||
public void configureFlutterEngine(@NonNull FlutterEngine flutterEngine) {
|
||||
GeneratedPluginRegistrant.registerWith(flutterEngine);
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -5,4 +5,7 @@
|
||||
Flutter draws its first frame -->
|
||||
<item name="android:windowBackground">@drawable/launch_background</item>
|
||||
</style>
|
||||
<style name="NormalTheme" parent="@android:style/Theme.Black.NoTitleBar">
|
||||
<item name="android:windowBackground">@drawable/launch_background</item>
|
||||
</style>
|
||||
</resources>
|
||||
|
@ -2,11 +2,15 @@ buildscript {
|
||||
repositories {
|
||||
google()
|
||||
jcenter()
|
||||
maven {
|
||||
url 'https://maven.fabric.io/public'
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
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 {
|
||||
google()
|
||||
jcenter()
|
||||
maven {
|
||||
url 'https://maven.fabric.io/public'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -3,3 +3,4 @@ org.gradle.daemon=true
|
||||
org.gradle.caching=true
|
||||
android.useAndroidX=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'
|
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>
|
@ -14,3 +14,22 @@ window.externalApp.getExternalAuth = function(options) {
|
||||
}, 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');
|
||||
}
|
||||
};
|
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.
123
lib/cards/card.class.dart
Normal file
123
lib/cards/card.class.dart
Normal file
@ -0,0 +1,123 @@
|
||||
part of '../main.dart';
|
||||
|
||||
class HACard {
|
||||
List<EntityWrapper> entities = [];
|
||||
List<HACard> childCards = [];
|
||||
EntityWrapper linkedEntityWrapper;
|
||||
String name;
|
||||
String id;
|
||||
String type;
|
||||
bool showName;
|
||||
bool showState;
|
||||
bool showEmpty;
|
||||
bool showHeaderToggle;
|
||||
int columnsCount;
|
||||
List stateFilter;
|
||||
List states;
|
||||
List conditions;
|
||||
String content;
|
||||
String unit;
|
||||
int min;
|
||||
int max;
|
||||
Map severity;
|
||||
|
||||
HACard({
|
||||
this.name,
|
||||
this.id,
|
||||
this.linkedEntityWrapper,
|
||||
this.columnsCount: 4,
|
||||
this.showName: true,
|
||||
this.showHeaderToggle: true,
|
||||
this.showState: true,
|
||||
this.stateFilter: const [],
|
||||
this.showEmpty: true,
|
||||
this.content,
|
||||
this.states,
|
||||
this.conditions: const [],
|
||||
this.unit,
|
||||
this.min,
|
||||
this.max,
|
||||
this.severity,
|
||||
@required this.type
|
||||
}) {
|
||||
if (this.columnsCount <= 0) {
|
||||
this.columnsCount = 4;
|
||||
}
|
||||
}
|
||||
|
||||
List<EntityWrapper> getEntitiesToShow() {
|
||||
return entities.where((entityWrapper) {
|
||||
if (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();
|
||||
}
|
||||
|
||||
Widget build(BuildContext context) {
|
||||
return CardWidget(
|
||||
card: this,
|
||||
);
|
||||
}
|
||||
|
||||
}
|
@ -25,14 +25,15 @@ class CardWidget extends StatelessWidget {
|
||||
}
|
||||
|
||||
if (card.conditions.isNotEmpty) {
|
||||
bool showCardByConditions = false;
|
||||
bool showCardByConditions = true;
|
||||
for (var condition in card.conditions) {
|
||||
Entity conditionEntity = HomeAssistant().entities.get(condition['entity']);
|
||||
if (conditionEntity != null &&
|
||||
(condition['state'] != null && conditionEntity.state == condition['state']) ||
|
||||
(condition['state_not'] != null && conditionEntity.state != condition['state_not'])
|
||||
((condition['state'] != null && conditionEntity.state != condition['state']) ||
|
||||
(condition['state_not'] != null && conditionEntity.state == condition['state_not']))
|
||||
) {
|
||||
showCardByConditions = true;
|
||||
showCardByConditions = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!showCardByConditions) {
|
||||
@ -42,31 +43,39 @@ class CardWidget extends StatelessWidget {
|
||||
|
||||
switch (card.type) {
|
||||
|
||||
case CardType.entities: {
|
||||
case CardType.ENTITIES: {
|
||||
return _buildEntitiesCard(context);
|
||||
}
|
||||
|
||||
case CardType.glance: {
|
||||
case CardType.GLANCE: {
|
||||
return _buildGlanceCard(context);
|
||||
}
|
||||
|
||||
case CardType.mediaControl: {
|
||||
case CardType.MEDIA_CONTROL: {
|
||||
return _buildMediaControlsCard(context);
|
||||
}
|
||||
|
||||
case CardType.entityButton: {
|
||||
case CardType.ENTITY_BUTTON: {
|
||||
return _buildEntityButtonCard(context);
|
||||
}
|
||||
|
||||
case CardType.markdown: {
|
||||
case CardType.GAUGE: {
|
||||
return _buildGaugeCard(context);
|
||||
}
|
||||
|
||||
/* case CardType.LIGHT: {
|
||||
return _buildLightCard(context);
|
||||
}*/
|
||||
|
||||
case CardType.MARKDOWN: {
|
||||
return _buildMarkdownCard(context);
|
||||
}
|
||||
|
||||
case CardType.alarmPanel: {
|
||||
case CardType.ALARM_PANEL: {
|
||||
return _buildAlarmPanelCard(context);
|
||||
}
|
||||
|
||||
case CardType.horizontalStack: {
|
||||
case CardType.HORIZONTAL_STACK: {
|
||||
if (card.childCards.isNotEmpty) {
|
||||
List<Widget> children = [];
|
||||
card.childCards.forEach((card) {
|
||||
@ -89,7 +98,7 @@ class CardWidget extends StatelessWidget {
|
||||
return Container(height: 0.0, width: 0.0,);
|
||||
}
|
||||
|
||||
case CardType.verticalStack: {
|
||||
case CardType.VERTICAL_STACK: {
|
||||
if (card.childCards.isNotEmpty) {
|
||||
List<Widget> children = [];
|
||||
card.childCards.forEach((card) {
|
||||
@ -123,9 +132,34 @@ class CardWidget extends StatelessWidget {
|
||||
return Container(height: 0.0, width: 0.0,);
|
||||
}
|
||||
List<Widget> body = [];
|
||||
body.add(CardHeaderWidget(name: card.name));
|
||||
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.name,
|
||||
trailing: headerSwitch
|
||||
)
|
||||
);
|
||||
entitiesToShow.forEach((EntityWrapper entity) {
|
||||
if (!entity.entity.isHidden) {
|
||||
body.add(
|
||||
Padding(
|
||||
padding: EdgeInsets.fromLTRB(0.0, 4.0, 0.0, 4.0),
|
||||
@ -135,7 +169,6 @@ class CardWidget extends StatelessWidget {
|
||||
child: entity.entity.buildDefaultWidget(context)
|
||||
),
|
||||
));
|
||||
}
|
||||
});
|
||||
return Card(
|
||||
child: Padding(
|
||||
@ -150,7 +183,7 @@ class CardWidget extends StatelessWidget {
|
||||
return Container(height: 0.0, width: 0.0,);
|
||||
}
|
||||
List<Widget> body = [];
|
||||
body.add(CardHeaderWidget(name: card.name));
|
||||
body.add(CardHeader(name: card.name));
|
||||
body.add(MarkdownBody(data: card.content));
|
||||
return Card(
|
||||
child: Padding(
|
||||
@ -162,7 +195,7 @@ class CardWidget extends StatelessWidget {
|
||||
|
||||
Widget _buildAlarmPanelCard(BuildContext context) {
|
||||
List<Widget> body = [];
|
||||
body.add(CardHeaderWidget(
|
||||
body.add(CardHeader(
|
||||
name: card.name ?? "",
|
||||
subtitle: Text("${card.linkedEntityWrapper.entity.displayState}",
|
||||
style: TextStyle(
|
||||
@ -183,7 +216,7 @@ class CardWidget extends StatelessWidget {
|
||||
alignment: Alignment.centerRight,
|
||||
icon: Icon(MaterialDesignIcons.getIconDataFromIconName(
|
||||
"mdi:dots-vertical")),
|
||||
onPressed: () => eventBus.fire(new ShowEntityPageEvent(card.linkedEntityWrapper.entity))
|
||||
onPressed: () => eventBus.fire(new ShowEntityPageEvent(entity: card.linkedEntityWrapper.entity))
|
||||
)
|
||||
)
|
||||
]
|
||||
@ -214,18 +247,26 @@ class CardWidget extends StatelessWidget {
|
||||
return Container(height: 0.0, width: 0.0,);
|
||||
}
|
||||
List<Widget> rows = [];
|
||||
rows.add(CardHeaderWidget(name: card.name));
|
||||
rows.add(CardHeader(name: card.name));
|
||||
|
||||
List<Widget> result = [];
|
||||
int columnsCount = entitiesToShow.length >= card.columnsCount ? card.columnsCount : entitiesToShow.length;
|
||||
|
||||
rows.add(
|
||||
Padding(
|
||||
padding: EdgeInsets.only(bottom: Sizes.rowPadding, top: Sizes.rowPadding),
|
||||
child: FractionallySizedBox(
|
||||
widthFactor: 1,
|
||||
child: LayoutBuilder(
|
||||
builder: (BuildContext context, BoxConstraints constraints) {
|
||||
List<Widget> buttons = [];
|
||||
double buttonWidth = constraints.maxWidth / columnsCount;
|
||||
entitiesToShow.forEach((EntityWrapper entity) {
|
||||
result.add(
|
||||
FractionallySizedBox(
|
||||
widthFactor: 1/columnsCount,
|
||||
buttons.add(
|
||||
SizedBox(
|
||||
width: buttonWidth,
|
||||
child: EntityModel(
|
||||
entityWrapper: entity,
|
||||
child: GlanceEntityContainer(
|
||||
child: GlanceCardEntityContainer(
|
||||
showName: card.showName,
|
||||
showState: card.showState,
|
||||
),
|
||||
@ -234,19 +275,23 @@ class CardWidget extends StatelessWidget {
|
||||
)
|
||||
);
|
||||
});
|
||||
rows.add(
|
||||
Padding(
|
||||
padding: EdgeInsets.fromLTRB(0.0, Sizes.rowPadding, 0.0, 2*Sizes.rowPadding),
|
||||
child: Wrap(
|
||||
//alignment: WrapAlignment.spaceAround,
|
||||
runSpacing: Sizes.rowPadding*2,
|
||||
children: result,
|
||||
return Wrap(
|
||||
//spacing: 5.0,
|
||||
//alignment: WrapAlignment.spaceEvenly,
|
||||
runSpacing: Sizes.doubleRowPadding,
|
||||
children: buttons,
|
||||
);
|
||||
}
|
||||
),
|
||||
),
|
||||
)
|
||||
);
|
||||
|
||||
return Card(
|
||||
child: new Column(mainAxisSize: MainAxisSize.min, children: rows)
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: rows
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@ -261,12 +306,48 @@ class CardWidget extends StatelessWidget {
|
||||
}
|
||||
|
||||
Widget _buildEntityButtonCard(BuildContext context) {
|
||||
card.linkedEntityWrapper.displayName = card.name?.toUpperCase() ??
|
||||
card.linkedEntityWrapper.overrideName = card.name?.toUpperCase() ??
|
||||
card.linkedEntityWrapper.displayName.toUpperCase();
|
||||
return Card(
|
||||
child: EntityModel(
|
||||
entityWrapper: card.linkedEntityWrapper,
|
||||
child: ButtonEntityContainer(),
|
||||
child: EntityButtonCardBody(
|
||||
showName: card.showName,
|
||||
),
|
||||
handleTap: true
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildGaugeCard(BuildContext context) {
|
||||
card.linkedEntityWrapper.overrideName = card.name ??
|
||||
card.linkedEntityWrapper.displayName;
|
||||
card.linkedEntityWrapper.unitOfMeasurementOverride = card.unit ??
|
||||
card.linkedEntityWrapper.unitOfMeasurement;
|
||||
return Card(
|
||||
child: EntityModel(
|
||||
entityWrapper: card.linkedEntityWrapper,
|
||||
child: GaugeCardBody(
|
||||
min: card.min,
|
||||
max: card.max,
|
||||
severity: card.severity,
|
||||
),
|
||||
handleTap: true
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildLightCard(BuildContext context) {
|
||||
card.linkedEntityWrapper.overrideName = card.name ??
|
||||
card.linkedEntityWrapper.displayName;
|
||||
return Card(
|
||||
child: EntityModel(
|
||||
entityWrapper: card.linkedEntityWrapper,
|
||||
child: LightCardBody(
|
||||
min: card.min,
|
||||
max: card.max,
|
||||
severity: card.severity,
|
||||
),
|
||||
handleTap: true
|
||||
)
|
||||
);
|
||||
@ -274,7 +355,11 @@ class CardWidget extends StatelessWidget {
|
||||
|
||||
Widget _buildUnsupportedCard(BuildContext context) {
|
||||
List<Widget> body = [];
|
||||
body.add(CardHeaderWidget(name: card.name ?? ""));
|
||||
body.add(
|
||||
CardHeader(
|
||||
name: card.name ?? ""
|
||||
)
|
||||
);
|
||||
List<Widget> result = [];
|
||||
if (card.linkedEntityWrapper != null) {
|
||||
result.addAll(<Widget>[
|
@ -1,12 +1,12 @@
|
||||
part of '../main.dart';
|
||||
part of '../../main.dart';
|
||||
|
||||
class CardHeaderWidget extends StatelessWidget {
|
||||
class CardHeader extends StatelessWidget {
|
||||
|
||||
final String name;
|
||||
final Widget trailing;
|
||||
final Widget subtitle;
|
||||
|
||||
const CardHeaderWidget({Key key, this.name, this.trailing, this.subtitle}) : super(key: key);
|
||||
const CardHeader({Key key, this.name, this.trailing, this.subtitle}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
57
lib/cards/widgets/entity_button_card_body.widget.dart
Normal file
57
lib/cards/widgets/entity_button_card_body.widget.dart
Normal file
@ -0,0 +1,57 @@
|
||||
part of '../../main.dart';
|
||||
|
||||
class EntityButtonCardBody extends StatelessWidget {
|
||||
|
||||
final bool showName;
|
||||
|
||||
EntityButtonCardBody({
|
||||
Key key, this.showName: true,
|
||||
}) : 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(),
|
||||
onDoubleTap: () => entityWrapper.handleDoubleTap(),
|
||||
child: FractionallySizedBox(
|
||||
widthFactor: 1,
|
||||
child: Column(
|
||||
children: <Widget>[
|
||||
LayoutBuilder(
|
||||
builder: (BuildContext context, BoxConstraints constraints) {
|
||||
return EntityIcon(
|
||||
padding: EdgeInsets.fromLTRB(2.0, 6.0, 2.0, 2.0),
|
||||
size: constraints.maxWidth / 2.5,
|
||||
);
|
||||
}
|
||||
),
|
||||
_buildName()
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildName() {
|
||||
if (showName) {
|
||||
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,
|
||||
);
|
||||
}
|
||||
return Container(width: 0, height: 0);
|
||||
}
|
||||
}
|
154
lib/cards/widgets/gauge_card_body.dart
Normal file
154
lib/cards/widgets/gauge_card_body.dart
Normal file
@ -0,0 +1,154 @@
|
||||
part of '../../main.dart';
|
||||
|
||||
class GaugeCardBody extends StatefulWidget {
|
||||
|
||||
final int min;
|
||||
final int max;
|
||||
final Map severity;
|
||||
|
||||
GaugeCardBody({Key key, this.min, this.max, this.severity}) : super(key: key);
|
||||
|
||||
@override
|
||||
_GaugeCardBodyState createState() => _GaugeCardBodyState();
|
||||
}
|
||||
|
||||
class _GaugeCardBodyState extends State<GaugeCardBody> {
|
||||
|
||||
List<charts.Series> seriesList;
|
||||
|
||||
List<charts.Series<GaugeSegment, String>> _createData(double value) {
|
||||
double fixedValue;
|
||||
if (value > widget.max) {
|
||||
fixedValue = widget.max.toDouble();
|
||||
} else if (value < widget.min) {
|
||||
fixedValue = widget.min.toDouble();
|
||||
} else {
|
||||
fixedValue = value;
|
||||
}
|
||||
double toShow = ((fixedValue - widget.min) / (widget.max - widget.min)) * 100;
|
||||
Color mainColor;
|
||||
if (widget.severity != null) {
|
||||
if (widget.severity["red"] is int && fixedValue >= widget.severity["red"]) {
|
||||
mainColor = Colors.red;
|
||||
} else if (widget.severity["yellow"] is int && fixedValue >= widget.severity["yellow"]) {
|
||||
mainColor = Colors.amber;
|
||||
} else {
|
||||
mainColor = Colors.green;
|
||||
}
|
||||
} else {
|
||||
mainColor = Colors.green;
|
||||
}
|
||||
final data = [
|
||||
GaugeSegment('Main', toShow, mainColor),
|
||||
GaugeSegment('Rest', 100 - toShow, Colors.black45),
|
||||
];
|
||||
|
||||
return [
|
||||
charts.Series<GaugeSegment, String>(
|
||||
id: 'Segments',
|
||||
domainFn: (GaugeSegment segment, _) => segment.segment,
|
||||
measureFn: (GaugeSegment segment, _) => segment.value,
|
||||
colorFn: (GaugeSegment segment, _) => segment.color,
|
||||
// Set a label accessor to control the text of the arc label.
|
||||
labelAccessorFn: (GaugeSegment segment, _) =>
|
||||
segment.segment == 'Main' ? '${segment.value}' : null,
|
||||
data: data,
|
||||
)
|
||||
];
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
EntityWrapper entityWrapper = EntityModel.of(context).entityWrapper;
|
||||
|
||||
return InkWell(
|
||||
onTap: () => entityWrapper.handleTap(),
|
||||
onLongPress: () => entityWrapper.handleHold(),
|
||||
onDoubleTap: () => entityWrapper.handleDoubleTap(),
|
||||
child: AspectRatio(
|
||||
aspectRatio: 1.5,
|
||||
child: Stack(
|
||||
fit: StackFit.expand,
|
||||
overflow: Overflow.clip,
|
||||
children: [
|
||||
LayoutBuilder(
|
||||
builder: (BuildContext context, BoxConstraints constraints) {
|
||||
double verticalOffset;
|
||||
if(constraints.maxWidth > 150.0) {
|
||||
verticalOffset = 0.2;
|
||||
} else if (constraints.maxWidth > 100.0) {
|
||||
verticalOffset = 0.3;
|
||||
} else {
|
||||
verticalOffset = 0.3;
|
||||
}
|
||||
return FractionallySizedBox(
|
||||
heightFactor: 2,
|
||||
widthFactor: 1,
|
||||
alignment: FractionalOffset(0,verticalOffset),
|
||||
child: charts.PieChart(
|
||||
_createData(entityWrapper.entity.doubleState),
|
||||
animate: false,
|
||||
defaultRenderer: charts.ArcRendererConfig(
|
||||
arcRatio: 0.4,
|
||||
startAngle: pi,
|
||||
arcLength: pi,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
),
|
||||
Align(
|
||||
alignment: Alignment.bottomCenter,
|
||||
child: LayoutBuilder(
|
||||
builder: (BuildContext context, BoxConstraints constraints) {
|
||||
double fontSize = constraints.maxHeight / 7;
|
||||
return Padding(
|
||||
padding: EdgeInsets.only(bottom: 2*fontSize),
|
||||
child: SimpleEntityState(
|
||||
//textAlign: TextAlign.center,
|
||||
expanded: false,
|
||||
maxLines: 1,
|
||||
bold: true,
|
||||
textAlign: TextAlign.center,
|
||||
padding: EdgeInsets.all(0.0),
|
||||
fontSize: fontSize,
|
||||
//padding: EdgeInsets.only(top: Sizes.rowPadding),
|
||||
),
|
||||
);
|
||||
}
|
||||
),
|
||||
),
|
||||
Align(
|
||||
alignment: Alignment.bottomCenter,
|
||||
child: LayoutBuilder(
|
||||
builder: (BuildContext context, BoxConstraints constraints) {
|
||||
double fontSize = constraints.maxHeight / 7;
|
||||
return Padding(
|
||||
padding: EdgeInsets.only(bottom: fontSize),
|
||||
child: EntityName(
|
||||
fontSize: fontSize,
|
||||
maxLines: 1,
|
||||
padding: EdgeInsets.all(0.0),
|
||||
textAlign: TextAlign.center,
|
||||
textOverflow: TextOverflow.ellipsis,
|
||||
),
|
||||
);
|
||||
}
|
||||
),
|
||||
)
|
||||
]
|
||||
)
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class GaugeSegment {
|
||||
final String segment;
|
||||
final double value;
|
||||
final charts.Color color;
|
||||
|
||||
GaugeSegment(this.segment, this.value, Color color)
|
||||
: this.color = charts.Color(
|
||||
r: color.red, g: color.green, b: color.blue, a: color.alpha);
|
||||
}
|
@ -1,6 +1,6 @@
|
||||
part of '../main.dart';
|
||||
part of '../../main.dart';
|
||||
|
||||
class GlanceEntityContainer extends StatelessWidget {
|
||||
class GlanceCardEntityContainer extends StatelessWidget {
|
||||
|
||||
final bool showName;
|
||||
final bool showState;
|
||||
@ -9,7 +9,7 @@ class GlanceEntityContainer extends StatelessWidget {
|
||||
final double nameFontSize;
|
||||
final bool wordsWrapInName;
|
||||
|
||||
GlanceEntityContainer({
|
||||
GlanceCardEntityContainer({
|
||||
Key key,
|
||||
@required this.showName,
|
||||
@required this.showState,
|
||||
@ -54,17 +54,13 @@ class GlanceEntityContainer extends StatelessWidget {
|
||||
|
||||
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(),
|
||||
onDoubleTap: () => entityWrapper.handleDoubleTap(),
|
||||
),
|
||||
);
|
||||
}
|
91
lib/cards/widgets/light_card_body.dart
Normal file
91
lib/cards/widgets/light_card_body.dart
Normal file
@ -0,0 +1,91 @@
|
||||
part of '../../main.dart';
|
||||
|
||||
class LightCardBody extends StatefulWidget {
|
||||
|
||||
final int min;
|
||||
final int max;
|
||||
final Map severity;
|
||||
|
||||
LightCardBody({Key key, this.min, this.max, this.severity}) : super(key: key);
|
||||
|
||||
@override
|
||||
_LightCardBodyState createState() => _LightCardBodyState();
|
||||
}
|
||||
|
||||
class _LightCardBodyState extends State<LightCardBody> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
EntityWrapper entityWrapper = EntityModel.of(context).entityWrapper;
|
||||
LightEntity entity = entityWrapper.entity;
|
||||
Logger.d("Light brightness: ${entity.brightness}");
|
||||
|
||||
return FractionallySizedBox(
|
||||
widthFactor: 0.5,
|
||||
child: Container(
|
||||
//color: Colors.redAccent,
|
||||
child: SingleCircularSlider(
|
||||
255,
|
||||
entity.brightness ?? 0,
|
||||
baseColor: Colors.white,
|
||||
handlerColor: Colors.blue[200],
|
||||
selectionColor: Colors.blue[100],
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
return InkWell(
|
||||
onTap: () => entityWrapper.handleTap(),
|
||||
onLongPress: () => entityWrapper.handleHold(),
|
||||
onDoubleTap: () => entityWrapper.handleDoubleTap(),
|
||||
child: AspectRatio(
|
||||
aspectRatio: 1.5,
|
||||
child: Stack(
|
||||
fit: StackFit.expand,
|
||||
overflow: Overflow.clip,
|
||||
children: [
|
||||
Align(
|
||||
alignment: Alignment.bottomCenter,
|
||||
child: LayoutBuilder(
|
||||
builder: (BuildContext context, BoxConstraints constraints) {
|
||||
double fontSize = constraints.maxHeight / 7;
|
||||
return Padding(
|
||||
padding: EdgeInsets.only(bottom: 2*fontSize),
|
||||
child: SimpleEntityState(
|
||||
//textAlign: TextAlign.center,
|
||||
expanded: false,
|
||||
maxLines: 1,
|
||||
bold: true,
|
||||
textAlign: TextAlign.center,
|
||||
padding: EdgeInsets.all(0.0),
|
||||
fontSize: fontSize,
|
||||
//padding: EdgeInsets.only(top: Sizes.rowPadding),
|
||||
),
|
||||
);
|
||||
}
|
||||
),
|
||||
),
|
||||
Align(
|
||||
alignment: Alignment.bottomCenter,
|
||||
child: LayoutBuilder(
|
||||
builder: (BuildContext context, BoxConstraints constraints) {
|
||||
double fontSize = constraints.maxHeight / 7;
|
||||
return Padding(
|
||||
padding: EdgeInsets.only(bottom: fontSize),
|
||||
child: EntityName(
|
||||
fontSize: fontSize,
|
||||
maxLines: 1,
|
||||
padding: EdgeInsets.all(0.0),
|
||||
textAlign: TextAlign.center,
|
||||
textOverflow: TextOverflow.ellipsis,
|
||||
),
|
||||
);
|
||||
}
|
||||
),
|
||||
)
|
||||
]
|
||||
)
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -29,6 +29,11 @@ class EntityState {
|
||||
static const ok = 'ok';
|
||||
static const problem = 'problem';
|
||||
static const active = 'active';
|
||||
static const cleaning = 'cleaning';
|
||||
static const docked = 'docked';
|
||||
static const returning = 'returning';
|
||||
static const error = 'error';
|
||||
|
||||
}
|
||||
|
||||
class EntityUIAction {
|
||||
@ -46,6 +51,10 @@ class EntityUIAction {
|
||||
String holdNavigationPath;
|
||||
String holdService;
|
||||
Map<String, dynamic> holdServiceData;
|
||||
String doubleTapAction = EntityUIAction.none;
|
||||
String doubleTapNavigationPath;
|
||||
String doubleTapService;
|
||||
Map<String, dynamic> doubleTapServiceData;
|
||||
|
||||
EntityUIAction({rawEntityData}) {
|
||||
if (rawEntityData != null) {
|
||||
@ -71,29 +80,61 @@ class EntityUIAction {
|
||||
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 {
|
||||
static const horizontalStack = "horizontal-stack";
|
||||
static const verticalStack = "vertical-stack";
|
||||
static const entities = "entities";
|
||||
static const glance = "glance";
|
||||
static const mediaControl = "media-control";
|
||||
static const weatherForecast = "weather-forecast";
|
||||
static const thermostat = "thermostat";
|
||||
static const sensor = "sensor";
|
||||
static const plantStatus = "plant-status";
|
||||
static const pictureEntity = "picture-entity";
|
||||
static const pictureElements = "picture-elements";
|
||||
static const picture = "picture";
|
||||
static const map = "map";
|
||||
static const iframe = "iframe";
|
||||
static const gauge = "gauge";
|
||||
static const entityButton = "entity-button";
|
||||
static const conditional = "conditional";
|
||||
static const alarmPanel = "alarm-panel";
|
||||
static const markdown = "markdown";
|
||||
static const HORIZONTAL_STACK = "horizontal-stack";
|
||||
static const VERTICAL_STACK = "vertical-stack";
|
||||
static const ENTITIES = "entities";
|
||||
static const GLANCE = "glance";
|
||||
static const MEDIA_CONTROL = "media-control";
|
||||
static const WEATHER_FORECAST = "weather-forecast";
|
||||
static const THERMOSTAT = "thermostat";
|
||||
static const SENSOR = "sensor";
|
||||
static const PLANT_STATUS = "plant-status";
|
||||
static const PICTURE_ENTITY = "picture-entity";
|
||||
static const PICTURE_ELEMENTS = "picture-elements";
|
||||
static const PICTURE = "picture";
|
||||
static const MAP = "map";
|
||||
static const IFRAME = "iframe";
|
||||
static const GAUGE = "gauge";
|
||||
static const ENTITY_BUTTON = "entity-button";
|
||||
static const CONDITIONAL = "conditional";
|
||||
static const ALARM_PANEL = "alarm-panel";
|
||||
static const MARKDOWN = "markdown";
|
||||
static const LIGHT = "light";
|
||||
}
|
||||
|
||||
class Sizes {
|
||||
static const rightWidgetPadding = 10.0;
|
||||
static const leftWidgetPadding = 10.0;
|
||||
static const buttonPadding = 4.0;
|
||||
static const extendedWidgetHeight = 50.0;
|
||||
static const iconSize = 28.0;
|
||||
static const largeIconSize = 46.0;
|
||||
static const stateFontSize = 15.0;
|
||||
static const nameFontSize = 15.0;
|
||||
static const smallFontSize = 14.0;
|
||||
static const largeFontSize = 24.0;
|
||||
static const inputWidth = 160.0;
|
||||
static const rowPadding = 10.0;
|
||||
static const doubleRowPadding = rowPadding*2;
|
||||
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) {
|
||||
eventBus.fire(new ServiceCallEvent(
|
||||
entity.domain, service, entity.entityId,
|
||||
{"code": "$code"}));
|
||||
ConnectionManager().callService(
|
||||
domain: entity.domain,
|
||||
service: service,
|
||||
entityId: entity.entityId,
|
||||
data: {"code": "$code"}
|
||||
);
|
||||
setState(() {
|
||||
code = "";
|
||||
});
|
||||
@ -58,7 +61,11 @@ class _AlarmControlPanelControlsWidgetWidgetState extends State<AlarmControlPane
|
||||
FlatButton(
|
||||
child: new Text("Yes"),
|
||||
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();
|
||||
},
|
||||
),
|
||||
|
@ -1,4 +1,4 @@
|
||||
part of '../../main.dart';
|
||||
part of '../main.dart';
|
||||
|
||||
class BadgeWidget extends StatelessWidget {
|
||||
@override
|
||||
@ -57,7 +57,7 @@ class BadgeWidget extends StatelessWidget {
|
||||
} else if (entityModel.entityWrapper.entity.displayState.length <= 10) {
|
||||
stateFontSize = 8.0;
|
||||
}
|
||||
onBadgeTextValue = entityModel.entityWrapper.entity.unitOfMeasurement;
|
||||
onBadgeTextValue = entityModel.entityWrapper.unitOfMeasurement;
|
||||
badgeIcon = Center(
|
||||
child: Text(
|
||||
"${entityModel.entityWrapper.entity.displayState}",
|
||||
@ -140,6 +140,6 @@ class BadgeWidget extends StatelessWidget {
|
||||
],
|
||||
),
|
||||
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 {
|
||||
|
||||
static const SUPPORT_ON_OFF = 1;
|
||||
static const SUPPORT_STREAM = 2;
|
||||
|
||||
CameraEntity(Map rawData, String webHost) : super(rawData, webHost);
|
||||
|
||||
bool get supportOnOff => ((supportedFeatures &
|
||||
CameraEntity.SUPPORT_ON_OFF) ==
|
||||
CameraEntity.SUPPORT_ON_OFF);
|
||||
bool get supportStream => ((supportedFeatures &
|
||||
CameraEntity.SUPPORT_STREAM) ==
|
||||
CameraEntity.SUPPORT_STREAM);
|
||||
|
||||
@override
|
||||
Widget _buildAdditionalControlsForPage(BuildContext context) {
|
||||
|
246
lib/entities/camera/widgets/camera_stream_view.dart
Normal file
246
lib/entities/camera/widgets/camera_stream_view.dart
Normal file
@ -0,0 +1,246 @@
|
||||
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 = "";
|
||||
VideoPlayerController _videoPlayerController;
|
||||
Timer _monitorTimer;
|
||||
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().getCameraStream(_entity.entityId)
|
||||
.then((data) {
|
||||
if (_videoPlayerController != null) {
|
||||
_videoPlayerController.dispose().then((_) => createPlayer(data));
|
||||
} else {
|
||||
createPlayer(data);
|
||||
}
|
||||
})
|
||||
.catchError((e) {
|
||||
_loading.completeError(e);
|
||||
Logger.e("[Camera Player] $e");
|
||||
});
|
||||
} else {
|
||||
_streamUrl = '${ConnectionManager().httpWebHost}/api/camera_proxy_stream/${_entity
|
||||
.entityId}?token=${_entity.attributes['access_token']}';
|
||||
_jsMessageChannelName = 'HA_${_entity.entityId.replaceAll('.', '_')}';
|
||||
rootBundle.loadString('assets/html/cameraView.html').then((file) {
|
||||
_webViewHtml = Uri.dataFromString(
|
||||
file.replaceFirst('{{stream_url}}', _streamUrl).replaceFirst('{{message_channel}}', _jsMessageChannelName),
|
||||
mimeType: 'text/html',
|
||||
encoding: Encoding.getByName('utf-8')
|
||||
).toString();
|
||||
_loading.complete();
|
||||
});
|
||||
}
|
||||
return _loading.future;
|
||||
}
|
||||
|
||||
void createPlayer(data) {
|
||||
_videoPlayerController = VideoPlayerController.network("${ConnectionManager().httpWebHost}${data["url"]}");
|
||||
_videoPlayerController.initialize().then((_) {
|
||||
setState((){
|
||||
_aspectRatio = _videoPlayerController.value.aspectRatio;
|
||||
});
|
||||
_loading.complete();
|
||||
autoPlay();
|
||||
startMonitor();
|
||||
}).catchError((e) {
|
||||
_loading.completeError(e);
|
||||
Logger.e("[Camera Player] Error player init. Retrying");
|
||||
_loadResources();
|
||||
});
|
||||
}
|
||||
|
||||
void autoPlay() {
|
||||
if (!_videoPlayerController.value.isPlaying) {
|
||||
_videoPlayerController.play();
|
||||
}
|
||||
}
|
||||
|
||||
void startMonitor() {
|
||||
_monitorTimer?.cancel();
|
||||
_monitorTimer = Timer.periodic(Duration(milliseconds: 500), (timer) {
|
||||
if (_videoPlayerController.value.hasError) {
|
||||
timer.cancel();
|
||||
setState(() {
|
||||
_isLoaded = false;
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Widget _buildScreen() {
|
||||
Widget screenWidget;
|
||||
if (!_isLoaded) {
|
||||
screenWidget = Center(
|
||||
child: EntityPicture(
|
||||
fit: BoxFit.contain,
|
||||
)
|
||||
);
|
||||
} else if (_entity.supportStream) {
|
||||
if (_videoPlayerController.value.initialized) {
|
||||
screenWidget = VideoPlayer(_videoPlayerController);
|
||||
} else {
|
||||
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) {
|
||||
setState((){
|
||||
_aspectRatio = double.tryParse(message.message) ?? 1.33;
|
||||
});
|
||||
})
|
||||
)
|
||||
}
|
||||
);
|
||||
}
|
||||
return AspectRatio(
|
||||
aspectRatio: _aspectRatio,
|
||||
child: screenWidget
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildControls() {
|
||||
Widget playControl;
|
||||
if (_entity.supportStream) {
|
||||
playControl = Center(
|
||||
child: IconButton(
|
||||
icon: Icon((_videoPlayerController != null && _videoPlayerController.value.isPlaying) ? Icons.pause_circle_outline : Icons.play_circle_outline),
|
||||
iconSize: 60,
|
||||
color: Colors.amberAccent,
|
||||
onPressed: (_videoPlayerController == null || _videoPlayerController.value.hasError || !_isLoaded) ? null :
|
||||
() {
|
||||
setState(() {
|
||||
if (_videoPlayerController != null && _videoPlayerController.value.isPlaying) {
|
||||
_videoPlayerController.pause();
|
||||
} else {
|
||||
_videoPlayerController.play();
|
||||
}
|
||||
});
|
||||
},
|
||||
),
|
||||
);
|
||||
} else {
|
||||
playControl = Container();
|
||||
}
|
||||
return Row(
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: <Widget>[
|
||||
IconButton(
|
||||
icon: Icon(Icons.refresh),
|
||||
iconSize: 40,
|
||||
color: Colors.amberAccent,
|
||||
onPressed: _isLoaded ? () {
|
||||
setState(() {
|
||||
_isLoaded = false;
|
||||
});
|
||||
} : null,
|
||||
),
|
||||
Expanded(
|
||||
child: playControl,
|
||||
),
|
||||
IconButton(
|
||||
icon: Icon(Icons.fullscreen),
|
||||
iconSize: 40,
|
||||
color: Colors.amberAccent,
|
||||
onPressed: _isLoaded ? () {
|
||||
_videoPlayerController?.pause();
|
||||
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() {
|
||||
_monitorTimer?.cancel();
|
||||
_videoPlayerController?.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
}
|
@ -10,9 +10,8 @@ class ClimateControlWidget extends StatefulWidget {
|
||||
|
||||
class _ClimateControlWidgetState extends State<ClimateControlWidget> {
|
||||
|
||||
bool _showPending = false;
|
||||
bool _temperaturePending = false;
|
||||
bool _changedHere = false;
|
||||
Timer _resetTimer;
|
||||
Timer _tempThrottleTimer;
|
||||
Timer _targetTempThrottleTimer;
|
||||
double _tmpTemperature = 0.0;
|
||||
@ -27,9 +26,11 @@ class _ClimateControlWidgetState extends State<ClimateControlWidget> {
|
||||
bool _tmpAuxHeat = false;
|
||||
|
||||
void _resetVars(ClimateEntity entity) {
|
||||
if (!_temperaturePending) {
|
||||
_tmpTemperature = entity.temperature;
|
||||
_tmpTargetHigh = entity.targetHigh;
|
||||
_tmpTargetLow = entity.targetLow;
|
||||
}
|
||||
_tmpHVACMode = entity.state;
|
||||
_tmpFanMode = entity.fanMode;
|
||||
_tmpSwingMode = entity.swingMode;
|
||||
@ -38,7 +39,6 @@ class _ClimateControlWidgetState extends State<ClimateControlWidget> {
|
||||
_tmpAuxHeat = entity.auxHeat;
|
||||
_tmpTargetHumidity = entity.targetHumidity;
|
||||
|
||||
_showPending = false;
|
||||
_changedHere = false;
|
||||
}
|
||||
|
||||
@ -73,36 +73,44 @@ class _ClimateControlWidgetState extends State<ClimateControlWidget> {
|
||||
}
|
||||
|
||||
void _setTemperature(ClimateEntity entity) {
|
||||
if (_tempThrottleTimer!=null) {
|
||||
_tempThrottleTimer.cancel();
|
||||
}
|
||||
_tempThrottleTimer?.cancel();
|
||||
setState(() {
|
||||
_changedHere = true;
|
||||
_temperaturePending = true;
|
||||
_tmpTemperature = double.parse(_tmpTemperature.toStringAsFixed(1));
|
||||
});
|
||||
_tempThrottleTimer = Timer(Duration(seconds: 2), () {
|
||||
setState(() {
|
||||
_changedHere = true;
|
||||
eventBus.fire(new ServiceCallEvent(entity.domain, "set_temperature", entity.entityId,{"temperature": "${_tmpTemperature.toStringAsFixed(1)}"}));
|
||||
_resetStateTimer(entity);
|
||||
_temperaturePending = false;
|
||||
ConnectionManager().callService(
|
||||
domain: entity.domain,
|
||||
service: "set_temperature",
|
||||
entityId: entity.entityId,
|
||||
data: {"temperature": "${_tmpTemperature.toStringAsFixed(1)}"}
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
void _setTargetTemp(ClimateEntity entity) {
|
||||
if (_targetTempThrottleTimer!=null) {
|
||||
_targetTempThrottleTimer.cancel();
|
||||
}
|
||||
_targetTempThrottleTimer?.cancel();
|
||||
setState(() {
|
||||
_changedHere = true;
|
||||
_temperaturePending = true;
|
||||
_tmpTargetLow = double.parse(_tmpTargetLow.toStringAsFixed(1));
|
||||
_tmpTargetHigh = double.parse(_tmpTargetHigh.toStringAsFixed(1));
|
||||
});
|
||||
_targetTempThrottleTimer = Timer(Duration(seconds: 2), () {
|
||||
setState(() {
|
||||
_changedHere = true;
|
||||
eventBus.fire(new ServiceCallEvent(entity.domain, "set_temperature", entity.entityId,{"target_temp_high": "${_tmpTargetHigh.toStringAsFixed(1)}", "target_temp_low": "${_tmpTargetLow.toStringAsFixed(1)}"}));
|
||||
_resetStateTimer(entity);
|
||||
_temperaturePending = false;
|
||||
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(() {
|
||||
_tmpTargetHumidity = value.roundToDouble();
|
||||
_changedHere = true;
|
||||
eventBus.fire(new ServiceCallEvent(entity.domain, "set_humidity", entity.entityId,{"humidity": "$_tmpTargetHumidity"}));
|
||||
_resetStateTimer(entity);
|
||||
ConnectionManager().callService(
|
||||
domain: entity.domain,
|
||||
service: "set_humidity",
|
||||
entityId: entity.entityId,
|
||||
data: {"humidity": "$_tmpTargetHumidity"}
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
@ -120,8 +132,12 @@ class _ClimateControlWidgetState extends State<ClimateControlWidget> {
|
||||
setState(() {
|
||||
_tmpHVACMode = value;
|
||||
_changedHere = true;
|
||||
eventBus.fire(new ServiceCallEvent(entity.domain, "set_hvac_mode", entity.entityId,{"hvac_mode": "$_tmpHVACMode"}));
|
||||
_resetStateTimer(entity);
|
||||
ConnectionManager().callService(
|
||||
domain: entity.domain,
|
||||
service: "set_hvac_mode",
|
||||
entityId: entity.entityId,
|
||||
data: {"hvac_mode": "$_tmpHVACMode"}
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
@ -129,8 +145,12 @@ class _ClimateControlWidgetState extends State<ClimateControlWidget> {
|
||||
setState(() {
|
||||
_tmpSwingMode = value;
|
||||
_changedHere = true;
|
||||
eventBus.fire(new ServiceCallEvent(entity.domain, "set_swing_mode", entity.entityId,{"swing_mode": "$_tmpSwingMode"}));
|
||||
_resetStateTimer(entity);
|
||||
ConnectionManager().callService(
|
||||
domain: entity.domain,
|
||||
service: "set_swing_mode",
|
||||
entityId: entity.entityId,
|
||||
data: {"swing_mode": "$_tmpSwingMode"}
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
@ -138,8 +158,7 @@ class _ClimateControlWidgetState extends State<ClimateControlWidget> {
|
||||
setState(() {
|
||||
_tmpFanMode = value;
|
||||
_changedHere = true;
|
||||
eventBus.fire(new ServiceCallEvent(entity.domain, "set_fan_mode", entity.entityId,{"fan_mode": "$_tmpFanMode"}));
|
||||
_resetStateTimer(entity);
|
||||
ConnectionManager().callService(domain: entity.domain, service: "set_fan_mode", entityId: entity.entityId, data: {"fan_mode": "$_tmpFanMode"});
|
||||
});
|
||||
}
|
||||
|
||||
@ -147,8 +166,7 @@ class _ClimateControlWidgetState extends State<ClimateControlWidget> {
|
||||
setState(() {
|
||||
_tmpPresetMode = value;
|
||||
_changedHere = true;
|
||||
eventBus.fire(new ServiceCallEvent(entity.domain, "set_preset_mode", entity.entityId,{"preset_mode": "$_tmpPresetMode"}));
|
||||
_resetStateTimer(entity);
|
||||
ConnectionManager().callService(domain: entity.domain, service: "set_preset_mode", entityId: entity.entityId, data: {"preset_mode": "$_tmpPresetMode"});
|
||||
});
|
||||
}
|
||||
|
||||
@ -165,18 +183,7 @@ class _ClimateControlWidgetState extends State<ClimateControlWidget> {
|
||||
setState(() {
|
||||
_tmpAuxHeat = value;
|
||||
_changedHere = true;
|
||||
eventBus.fire(new ServiceCallEvent(entity.domain, "set_aux_heat", entity.entityId, {"aux_heat": "$_tmpAuxHeat"}));
|
||||
_resetStateTimer(entity);
|
||||
});
|
||||
}
|
||||
|
||||
void _resetStateTimer(ClimateEntity entity) {
|
||||
if (_resetTimer!=null) {
|
||||
_resetTimer.cancel();
|
||||
}
|
||||
_resetTimer = Timer(Duration(seconds: 3), () {
|
||||
setState(() {});
|
||||
_resetVars(entity);
|
||||
ConnectionManager().callService(domain: entity.domain, service: "set_aux_heat", entityId: entity.entityId, data: {"aux_heat": "$_tmpAuxHeat"});
|
||||
});
|
||||
}
|
||||
|
||||
@ -184,11 +191,11 @@ class _ClimateControlWidgetState extends State<ClimateControlWidget> {
|
||||
Widget build(BuildContext context) {
|
||||
final entityModel = EntityModel.of(context);
|
||||
final ClimateEntity entity = entityModel.entityWrapper.entity;
|
||||
Logger.d("[Climate widget build] changed here = $_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;
|
||||
} else {
|
||||
_resetTimer?.cancel();
|
||||
_resetVars(entity);
|
||||
}
|
||||
return Padding(
|
||||
@ -296,7 +303,7 @@ class _ClimateControlWidgetState extends State<ClimateControlWidget> {
|
||||
)),
|
||||
TemperatureControlWidget(
|
||||
value: _tmpTemperature,
|
||||
fontColor: _showPending ? Colors.red : Colors.black,
|
||||
fontColor: _temperaturePending ? Colors.red : Colors.black,
|
||||
onDec: () => _temperatureDown(entity),
|
||||
onInc: () => _temperatureUp(entity),
|
||||
)
|
||||
@ -313,7 +320,7 @@ class _ClimateControlWidgetState extends State<ClimateControlWidget> {
|
||||
controls.addAll(<Widget>[
|
||||
TemperatureControlWidget(
|
||||
value: _tmpTargetLow,
|
||||
fontColor: _showPending ? Colors.red : Colors.black,
|
||||
fontColor: _temperaturePending ? Colors.red : Colors.black,
|
||||
onDec: () => _targetLowDown(entity),
|
||||
onInc: () => _targetLowUp(entity),
|
||||
),
|
||||
@ -326,7 +333,7 @@ class _ClimateControlWidgetState extends State<ClimateControlWidget> {
|
||||
controls.add(
|
||||
TemperatureControlWidget(
|
||||
value: _tmpTargetHigh,
|
||||
fontColor: _showPending ? Colors.red : Colors.black,
|
||||
fontColor: _temperaturePending ? Colors.red : Colors.black,
|
||||
onDec: () => _targetHighDown(entity),
|
||||
onInc: () => _targetHighUp(entity),
|
||||
)
|
||||
@ -404,7 +411,6 @@ class _ClimateControlWidgetState extends State<ClimateControlWidget> {
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_resetTimer?.cancel();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
|
@ -1,9 +1,9 @@
|
||||
part of '../../main.dart';
|
||||
part of '../../../main.dart';
|
||||
|
||||
class ModeSelectorWidget extends StatelessWidget {
|
||||
|
||||
final String caption;
|
||||
final List<String> options;
|
||||
final List options;
|
||||
final String value;
|
||||
final double captionFontSize;
|
||||
final double valueFontSize;
|
||||
@ -45,10 +45,10 @@ class ModeSelectorWidget extends StatelessWidget {
|
||||
color: Colors.black,
|
||||
),
|
||||
hint: Text("Select ${caption.toLowerCase()}"),
|
||||
items: options.map((String value) {
|
||||
items: options.map((value) {
|
||||
return new DropdownMenuItem<String>(
|
||||
value: value,
|
||||
child: Text(value),
|
||||
value: '$value',
|
||||
child: Text('$value'),
|
||||
);
|
||||
}).toList(),
|
||||
onChanged: (mode) => onChange(mode),
|
@ -1,4 +1,4 @@
|
||||
part of '../../main.dart';
|
||||
part of '../../../main.dart';
|
||||
|
||||
class ModeSwitchWidget extends StatelessWidget {
|
||||
|
@ -18,7 +18,7 @@ class _CoverControlWidgetState extends State<CoverControlWidget> {
|
||||
setState(() {
|
||||
_tmpPosition = position.roundToDouble();
|
||||
_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(() {
|
||||
_tmpTiltPosition = position.roundToDouble();
|
||||
_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()});
|
||||
});
|
||||
}
|
||||
|
||||
@ -135,18 +135,18 @@ class _CoverControlWidgetState extends State<CoverControlWidget> {
|
||||
|
||||
class CoverTiltControlsWidget extends StatelessWidget {
|
||||
void _open(CoverEntity entity) {
|
||||
eventBus.fire(new ServiceCallEvent(
|
||||
entity.domain, "open_cover_tilt", entity.entityId, null));
|
||||
ConnectionManager().callService(
|
||||
domain: entity.domain, service: "open_cover_tilt", entityId: entity.entityId);
|
||||
}
|
||||
|
||||
void _close(CoverEntity entity) {
|
||||
eventBus.fire(new ServiceCallEvent(
|
||||
entity.domain, "close_cover_tilt", entity.entityId, null));
|
||||
ConnectionManager().callService(
|
||||
domain: entity.domain, service: "close_cover_tilt", entityId: entity.entityId);
|
||||
}
|
||||
|
||||
void _stop(CoverEntity entity) {
|
||||
eventBus.fire(new ServiceCallEvent(
|
||||
entity.domain, "stop_cover_tilt", entity.entityId, null));
|
||||
ConnectionManager().callService(
|
||||
domain: entity.domain, service: "stop_cover_tilt", entityId: entity.entityId);
|
||||
}
|
||||
|
||||
@override
|
||||
|
@ -2,18 +2,27 @@ part of '../../../main.dart';
|
||||
|
||||
class CoverStateWidget extends StatelessWidget {
|
||||
void _open(CoverEntity entity) {
|
||||
eventBus.fire(new ServiceCallEvent(
|
||||
entity.domain, "open_cover", entity.entityId, null));
|
||||
ConnectionManager().callService(
|
||||
domain: entity.domain,
|
||||
service: "open_cover",
|
||||
entityId: entity.entityId
|
||||
);
|
||||
}
|
||||
|
||||
void _close(CoverEntity entity) {
|
||||
eventBus.fire(new ServiceCallEvent(
|
||||
entity.domain, "close_cover", entity.entityId, null));
|
||||
ConnectionManager().callService(
|
||||
domain: entity.domain,
|
||||
service: "close_cover",
|
||||
entityId: entity.entityId
|
||||
);
|
||||
}
|
||||
|
||||
void _stop(CoverEntity entity) {
|
||||
eventBus.fire(new ServiceCallEvent(
|
||||
entity.domain, "stop_cover", entity.entityId, null));
|
||||
ConnectionManager().callService(
|
||||
domain: entity.domain,
|
||||
service: "stop_cover",
|
||||
entityId: entity.entityId
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
|
@ -35,8 +35,7 @@ class DateTimeEntity extends Entity {
|
||||
return formattedState;
|
||||
}
|
||||
|
||||
void setNewState(newValue) {
|
||||
eventBus
|
||||
.fire(new ServiceCallEvent(domain, "set_datetime", entityId, newValue));
|
||||
void setNewState(Map newValue) {
|
||||
ConnectionManager().callService(domain: domain, service: "set_datetime", entityId: entityId, data: newValue);
|
||||
}
|
||||
}
|
@ -34,18 +34,7 @@ class DefaultEntityContainer extends StatelessWidget {
|
||||
],
|
||||
);
|
||||
}
|
||||
return InkWell(
|
||||
onLongPress: () {
|
||||
if (entityModel.handleTap) {
|
||||
entityModel.entityWrapper.handleHold();
|
||||
}
|
||||
},
|
||||
onTap: () {
|
||||
if (entityModel.handleTap) {
|
||||
entityModel.entityWrapper.handleTap();
|
||||
}
|
||||
},
|
||||
child: Row(
|
||||
Widget result = Row(
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
children: <Widget>[
|
||||
EntityIcon(),
|
||||
@ -59,7 +48,28 @@ class DefaultEntityContainer extends StatelessWidget {
|
||||
),
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
@ -153,7 +153,7 @@ class Entity {
|
||||
domain = rawData["entity_id"].split(".")[0];
|
||||
entityId = rawData["entity_id"];
|
||||
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);
|
||||
_lastUpdated = DateTime.tryParse(rawData["last_updated"]);
|
||||
entityPicture = _getEntityPictureUrl(webHost);
|
||||
@ -211,31 +211,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) {
|
||||
return EntityModel(
|
||||
entityWrapper: EntityWrapper(entity: this),
|
||||
@ -246,7 +221,7 @@ class Entity {
|
||||
|
||||
String getAttribute(String attributeName) {
|
||||
if (attributes != null) {
|
||||
return attributes["$attributeName"];
|
||||
return attributes["$attributeName"].toString();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
@ -14,9 +14,12 @@ class EntityColor {
|
||||
"auto": Colors.amber,
|
||||
EntityState.active: Colors.amber,
|
||||
EntityState.playing: Colors.amber,
|
||||
EntityState.paused: Colors.amber,
|
||||
"above_horizon": Colors.amber,
|
||||
EntityState.home: Colors.amber,
|
||||
EntityState.open: Colors.amber,
|
||||
EntityState.cleaning: Colors.amber,
|
||||
EntityState.returning: Colors.amber,
|
||||
EntityState.off: defaultStateColor,
|
||||
EntityState.closed: defaultStateColor,
|
||||
"below_horizon": defaultStateColor,
|
70
lib/entities/entity_page_layout.widget.dart
Normal file
70
lib/entities/entity_page_layout.widget.dart
Normal file
@ -0,0 +1,70 @@
|
||||
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: Colors.blue[300],
|
||||
height: 40,
|
||||
child: Row(
|
||||
children: <Widget>[
|
||||
Expanded(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.only(left: 8),
|
||||
child: Text(
|
||||
entity.displayName,
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.white,
|
||||
fontSize: 22
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
IconButton(
|
||||
padding: EdgeInsets.all(0),
|
||||
icon: Icon(Icons.close),
|
||||
color: Colors.white,
|
||||
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,
|
||||
);
|
||||
}
|
||||
}
|
70
lib/entities/entity_picture.widget.dart
Normal file
70
lib/entities/entity_picture.widget.dart
Normal file
@ -0,0 +1,70 @@
|
||||
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) {
|
||||
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: EntityColor.defaultStateColor,
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
|
||||
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
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -2,25 +2,26 @@ part of '../main.dart';
|
||||
|
||||
class EntityWrapper {
|
||||
|
||||
String displayName;
|
||||
String icon;
|
||||
String entityPicture;
|
||||
String overrideName;
|
||||
final String overrideIcon;
|
||||
EntityUIAction uiAction;
|
||||
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({
|
||||
this.entity,
|
||||
String icon,
|
||||
String displayName,
|
||||
this.uiAction
|
||||
this.overrideIcon,
|
||||
this.overrideName,
|
||||
this.uiAction,
|
||||
this.stateFilter
|
||||
}) {
|
||||
if (entity.statelessType == StatelessEntityType.NONE || entity.statelessType == StatelessEntityType.CALL_SERVICE || entity.statelessType == StatelessEntityType.WEBLINK) {
|
||||
this.icon = icon ?? entity.icon;
|
||||
if (icon == null) {
|
||||
entityPicture = entity.entityPicture;
|
||||
}
|
||||
this.displayName = displayName ?? entity.displayName;
|
||||
if (uiAction == null) {
|
||||
uiAction = EntityUIAction();
|
||||
}
|
||||
@ -30,17 +31,17 @@ class EntityWrapper {
|
||||
void handleTap() {
|
||||
switch (uiAction.tapAction) {
|
||||
case EntityUIAction.toggle: {
|
||||
eventBus.fire(
|
||||
ServiceCallEvent("homeassistant", "toggle", entity.entityId, null));
|
||||
ConnectionManager().callService(domain: "homeassistant", service: "toggle", entityId: entity.entityId);
|
||||
break;
|
||||
}
|
||||
|
||||
case EntityUIAction.callService: {
|
||||
if (uiAction.tapService != null) {
|
||||
eventBus.fire(
|
||||
ServiceCallEvent(uiAction.tapService.split(".")[0],
|
||||
uiAction.tapService.split(".")[1], null,
|
||||
uiAction.tapServiceData));
|
||||
ConnectionManager().callService(
|
||||
domain: uiAction.tapService.split(".")[0],
|
||||
service: uiAction.tapService.split(".")[1],
|
||||
data: uiAction.tapServiceData
|
||||
);
|
||||
}
|
||||
break;
|
||||
}
|
||||
@ -51,12 +52,12 @@ class EntityWrapper {
|
||||
|
||||
case EntityUIAction.moreInfo: {
|
||||
eventBus.fire(
|
||||
new ShowEntityPageEvent(entity));
|
||||
new ShowEntityPageEvent(entity: entity));
|
||||
break;
|
||||
}
|
||||
|
||||
case EntityUIAction.navigate: {
|
||||
if (uiAction.tapService.startsWith("/")) {
|
||||
if (uiAction.tapService != null && uiAction.tapService.startsWith("/")) {
|
||||
//TODO handle local urls
|
||||
Logger.w("Local urls is not supported yet");
|
||||
} else {
|
||||
@ -74,29 +75,29 @@ class EntityWrapper {
|
||||
void handleHold() {
|
||||
switch (uiAction.holdAction) {
|
||||
case EntityUIAction.toggle: {
|
||||
eventBus.fire(
|
||||
ServiceCallEvent("homeassistant", "toggle", entity.entityId, null));
|
||||
ConnectionManager().callService(domain: "homeassistant", service: "toggle", entityId: entity.entityId);
|
||||
break;
|
||||
}
|
||||
|
||||
case EntityUIAction.callService: {
|
||||
if (uiAction.holdService != null) {
|
||||
eventBus.fire(
|
||||
ServiceCallEvent(uiAction.holdService.split(".")[0],
|
||||
uiAction.holdService.split(".")[1], null,
|
||||
uiAction.holdServiceData));
|
||||
ConnectionManager().callService(
|
||||
domain: uiAction.holdService.split(".")[0],
|
||||
service: uiAction.holdService.split(".")[1],
|
||||
data: uiAction.holdServiceData
|
||||
);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case EntityUIAction.moreInfo: {
|
||||
eventBus.fire(
|
||||
new ShowEntityPageEvent(entity));
|
||||
new ShowEntityPageEvent(entity: entity));
|
||||
break;
|
||||
}
|
||||
|
||||
case EntityUIAction.navigate: {
|
||||
if (uiAction.holdService.startsWith("/")) {
|
||||
if (uiAction.holdService != null && uiAction.holdService.startsWith("/")) {
|
||||
//TODO handle local urls
|
||||
Logger.w("Local urls is not supported yet");
|
||||
} else {
|
||||
@ -111,4 +112,44 @@ class EntityWrapper {
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
default: {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -24,9 +24,12 @@ class _FanControlsWidgetState extends State<FanControlsWidget> {
|
||||
setState(() {
|
||||
_tmpOscillate = oscillate;
|
||||
_changedHere = true;
|
||||
eventBus.fire(new ServiceCallEvent(
|
||||
"fan", "oscillate", entity.entityId,
|
||||
{"oscillating": oscillate}));
|
||||
ConnectionManager().callService(
|
||||
domain: "fan",
|
||||
service: "oscillate",
|
||||
entityId: entity.entityId,
|
||||
data: {"oscillating": oscillate}
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
@ -34,9 +37,12 @@ class _FanControlsWidgetState extends State<FanControlsWidget> {
|
||||
setState(() {
|
||||
_tmpDirectionForward = forward;
|
||||
_changedHere = true;
|
||||
eventBus.fire(new ServiceCallEvent(
|
||||
"fan", "set_direction", entity.entityId,
|
||||
{"direction": forward ? "forward" : "reverse"}));
|
||||
ConnectionManager().callService(
|
||||
domain: "fan",
|
||||
service: "set_direction",
|
||||
entityId: entity.entityId,
|
||||
data: {"direction": forward ? "forward" : "reverse"}
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
@ -44,9 +50,12 @@ class _FanControlsWidgetState extends State<FanControlsWidget> {
|
||||
setState(() {
|
||||
_tmpSpeed = value;
|
||||
_changedHere = true;
|
||||
eventBus.fire(new ServiceCallEvent(
|
||||
"fan", "set_speed", entity.entityId,
|
||||
{"speed": value}));
|
||||
ConnectionManager().callService(
|
||||
domain: "fan",
|
||||
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 {
|
||||
|
||||
@ -18,7 +18,7 @@ class FlatServiceButton extends StatelessWidget {
|
||||
}) : super(key: key);
|
||||
|
||||
void _setNewState() {
|
||||
eventBus.fire(new ServiceCallEvent(serviceDomain, serviceName, entityId, null));
|
||||
ConnectionManager().callService(domain: serviceDomain, service: serviceName, entityId: entityId);
|
||||
}
|
||||
|
||||
@override
|
@ -1,4 +1,4 @@
|
||||
part of '../../main.dart';
|
||||
part of '../../../main.dart';
|
||||
|
||||
class LightColorPicker extends StatefulWidget {
|
||||
|
||||
@ -27,7 +27,6 @@ class LightColorPickerState extends State<LightColorPicker> {
|
||||
List<Widget> colorRows = [];
|
||||
Border border;
|
||||
bool isSomethingSelected = false;
|
||||
Logger.d("Current colotfor picker: [${widget.color.hue}, ${widget.color.saturation}]");
|
||||
for (double saturation = 1.0; saturation >= (0.0 + widget.saturationStep); saturation = double.parse((saturation - widget.saturationStep).toStringAsFixed(2))) {
|
||||
List<Widget> rowChildren = [];
|
||||
//Logger.d("$saturation");
|
@ -28,9 +28,12 @@ class _LightControlsWidgetState extends State<LightControlsWidget> {
|
||||
setState(() {
|
||||
_tmpBrightness = value.round();
|
||||
_changedHere = true;
|
||||
eventBus.fire(new ServiceCallEvent(
|
||||
entity.domain, "turn_on", entity.entityId,
|
||||
{"brightness": _tmpBrightness}));
|
||||
ConnectionManager().callService(
|
||||
domain: entity.domain,
|
||||
service: "turn_on",
|
||||
entityId: entity.entityId,
|
||||
data: {"brightness": _tmpBrightness}
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
@ -38,9 +41,12 @@ class _LightControlsWidgetState extends State<LightControlsWidget> {
|
||||
setState(() {
|
||||
_tmpWhiteValue = value.round();
|
||||
_changedHere = true;
|
||||
eventBus.fire(new ServiceCallEvent(
|
||||
entity.domain, "turn_on", entity.entityId,
|
||||
{"white_value": _tmpWhiteValue}));
|
||||
ConnectionManager().callService(
|
||||
domain: entity.domain,
|
||||
service: "turn_on",
|
||||
entityId: entity.entityId,
|
||||
data: {"white_value": _tmpWhiteValue}
|
||||
);
|
||||
|
||||
});
|
||||
}
|
||||
@ -49,9 +55,12 @@ class _LightControlsWidgetState extends State<LightControlsWidget> {
|
||||
setState(() {
|
||||
_tmpColorTemp = value.round();
|
||||
_changedHere = true;
|
||||
eventBus.fire(new ServiceCallEvent(
|
||||
entity.domain, "turn_on", entity.entityId,
|
||||
{"color_temp": _tmpColorTemp}));
|
||||
ConnectionManager().callService(
|
||||
domain: entity.domain,
|
||||
service: "turn_on",
|
||||
entityId: entity.entityId,
|
||||
data: {"color_temp": _tmpColorTemp}
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
@ -59,10 +68,12 @@ class _LightControlsWidgetState extends State<LightControlsWidget> {
|
||||
setState(() {
|
||||
_tmpColor = color;
|
||||
_changedHere = true;
|
||||
Logger.d( "HS Color: [${color.hue}, ${color.saturation}]");
|
||||
eventBus.fire(new ServiceCallEvent(
|
||||
entity.domain, "turn_on", entity.entityId,
|
||||
{"hs_color": [color.hue, color.saturation*100]}));
|
||||
ConnectionManager().callService(
|
||||
domain: entity.domain,
|
||||
service: "turn_on",
|
||||
entityId: entity.entityId,
|
||||
data: {"hs_color": [color.hue, color.saturation*100]}
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
@ -71,9 +82,12 @@ class _LightControlsWidgetState extends State<LightControlsWidget> {
|
||||
_tmpEffect = value;
|
||||
_changedHere = true;
|
||||
if (_tmpEffect != null) {
|
||||
eventBus.fire(new ServiceCallEvent(
|
||||
entity.domain, "turn_on", entity.entityId,
|
||||
{"effect": "$value"}));
|
||||
ConnectionManager().callService(
|
||||
domain: entity.domain,
|
||||
service: "turn_on",
|
||||
entityId: entity.entityId,
|
||||
data: {"effect": "$value"}
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
@ -100,7 +114,19 @@ class _LightControlsWidgetState extends State<LightControlsWidget> {
|
||||
}
|
||||
|
||||
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(
|
||||
onChanged: (value) {
|
||||
setState(() {
|
||||
@ -111,7 +137,7 @@ class _LightControlsWidgetState extends State<LightControlsWidget> {
|
||||
min: 1.0,
|
||||
max: 255.0,
|
||||
onChangeEnd: (value) => _setBrightness(entity, value),
|
||||
value: _tmpBrightness == null ? 1.0 : _tmpBrightness.toDouble(),
|
||||
value: val,
|
||||
leading: Icon(Icons.brightness_5),
|
||||
title: "Brightness",
|
||||
);
|
||||
@ -143,10 +169,22 @@ class _LightControlsWidgetState extends State<LightControlsWidget> {
|
||||
|
||||
Widget _buildColorTempControl(LightEntity entity) {
|
||||
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(
|
||||
title: "Color temperature",
|
||||
leading: Text("Cold", style: TextStyle(color: Colors.lightBlue),),
|
||||
value: _tmpColorTemp == null ? entity.maxMireds : _tmpColorTemp.toDouble(),
|
||||
value: val,
|
||||
onChangeEnd: (value) => _setColorTemp(entity, value),
|
||||
max: entity.maxMireds,
|
||||
min: entity.minMireds,
|
||||
@ -203,10 +241,14 @@ class _LightControlsWidgetState extends State<LightControlsWidget> {
|
||||
|
||||
Widget _buildEffectControl(LightEntity entity) {
|
||||
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(
|
||||
onChange: (effect) => _setEffect(entity, effect),
|
||||
caption: "Effect",
|
||||
options: entity.effectList,
|
||||
options: list,
|
||||
value: _tmpEffect
|
||||
);
|
||||
} else {
|
||||
|
@ -7,11 +7,11 @@ class LockStateWidget extends StatelessWidget {
|
||||
const LockStateWidget({Key key, this.assumedState: false}) : super(key: key);
|
||||
|
||||
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) {
|
||||
eventBus.fire(new ServiceCallEvent("lock", "unlock", entity.entityId, null));
|
||||
ConnectionManager().callService(domain: "lock", service: "unlock", entityId: entity.entityId);
|
||||
}
|
||||
|
||||
@override
|
||||
|
@ -74,10 +74,34 @@ class MediaPlayerEntity extends Entity {
|
||||
|
||||
List<String> get soundModeList => getStringListAttributeValue("sound_mode_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
|
||||
Widget _buildAdditionalControlsForPage(BuildContext context) {
|
||||
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>(EntityColor.stateColor(EntityState.on)),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_timer?.cancel();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,137 @@
|
||||
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;
|
||||
|
||||
final TextStyle _seekTextStyle = TextStyle(
|
||||
fontSize: 20,
|
||||
color: Colors.blue,
|
||||
fontWeight: FontWeight.bold
|
||||
);
|
||||
|
||||
@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: Colors.orange,
|
||||
focusColor: Colors.white,
|
||||
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: _seekTextStyle),
|
||||
),
|
||||
Text("${Duration(seconds: entity.durationSeconds).toString().split(".")[0]}")
|
||||
],
|
||||
),
|
||||
Container(height: 10,),
|
||||
Slider(
|
||||
min: 0,
|
||||
activeColor: Colors.amber,
|
||||
inactiveColor: Colors.black26,
|
||||
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();
|
||||
}
|
||||
|
||||
}
|
@ -26,7 +26,7 @@ class MediaPlayerWidget extends StatelessWidget {
|
||||
bottom: 0.0,
|
||||
left: 0.0,
|
||||
right: 0.0,
|
||||
child: MediaPlayerProgressWidget()
|
||||
child: MediaPlayerProgressBar()
|
||||
)
|
||||
],
|
||||
),
|
||||
@ -118,26 +118,28 @@ class MediaPlayerPlaybackControls extends StatelessWidget {
|
||||
|
||||
|
||||
void _setPower(MediaPlayerEntity entity) {
|
||||
if (entity.state != EntityState.unavailable && entity.state != EntityState.unknown) {
|
||||
if (entity.state == EntityState.off) {
|
||||
Logger.d("${entity.entityId} turn_on");
|
||||
eventBus.fire(new ServiceCallEvent(
|
||||
entity.domain, "turn_on", entity.entityId,
|
||||
null));
|
||||
ConnectionManager().callService(
|
||||
domain: entity.domain,
|
||||
service: "turn_on",
|
||||
entityId: entity.entityId
|
||||
);
|
||||
} else {
|
||||
Logger.d("${entity.entityId} turn_off");
|
||||
eventBus.fire(new ServiceCallEvent(
|
||||
entity.domain, "turn_off", entity.entityId,
|
||||
null));
|
||||
}
|
||||
ConnectionManager().callService(
|
||||
domain: entity.domain,
|
||||
service: "turn_off",
|
||||
entityId: entity.entityId
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
void _callAction(MediaPlayerEntity entity, String action) {
|
||||
Logger.d("${entity.entityId} $action");
|
||||
eventBus.fire(new ServiceCallEvent(
|
||||
entity.domain, "$action", entity.entityId,
|
||||
null));
|
||||
ConnectionManager().callService(
|
||||
domain: entity.domain,
|
||||
service: "$action",
|
||||
entityId: entity.entityId
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
@ -229,7 +231,7 @@ class MediaPlayerPlaybackControls extends StatelessWidget {
|
||||
IconButton(
|
||||
icon: Icon(MaterialDesignIcons.getIconDataFromIconName(
|
||||
"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) {
|
||||
@ -264,27 +266,50 @@ class _MediaPlayerControlsState extends State<MediaPlayerControls> {
|
||||
setState(() {
|
||||
_changedHere = true;
|
||||
_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) {
|
||||
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) {
|
||||
eventBus.fire(ServiceCallEvent("media_player", "volume_up", entityId, null));
|
||||
ConnectionManager().callService(
|
||||
domain: "media_player",
|
||||
service: "volume_up",
|
||||
entityId: 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) {
|
||||
setState(() {
|
||||
_newSoundMode = value;
|
||||
_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 +317,12 @@ class _MediaPlayerControlsState extends State<MediaPlayerControls> {
|
||||
setState(() {
|
||||
_newSource = source;
|
||||
_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 +335,11 @@ class _MediaPlayerControlsState extends State<MediaPlayerControls> {
|
||||
)
|
||||
];
|
||||
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 volumeStepWidget;
|
||||
if (entity.supportVolumeMute || entity.attributes["is_volume_muted"] != null) {
|
||||
@ -321,13 +356,13 @@ class _MediaPlayerControlsState extends State<MediaPlayerControls> {
|
||||
volumeStepWidget = Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: <Widget>[
|
||||
IconButton(
|
||||
icon: Icon(MaterialDesignIcons.getIconDataFromIconName("mdi:plus")),
|
||||
onPressed: () => _setVolumeUp(entity.entityId)
|
||||
),
|
||||
IconButton(
|
||||
icon: Icon(MaterialDesignIcons.getIconDataFromIconName("mdi:minus")),
|
||||
onPressed: () => _setVolumeDown(entity.entityId)
|
||||
),
|
||||
IconButton(
|
||||
icon: Icon(MaterialDesignIcons.getIconDataFromIconName("mdi:plus")),
|
||||
onPressed: () => _setVolumeUp(entity.entityId)
|
||||
)
|
||||
],
|
||||
);
|
||||
@ -398,69 +433,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(
|
||||
children: children,
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class MediaPlayerProgressWidget extends StatefulWidget {
|
||||
@override
|
||||
_MediaPlayerProgressWidgetState createState() => _MediaPlayerProgressWidgetState();
|
||||
}
|
||||
|
||||
class _MediaPlayerProgressWidgetState extends State<MediaPlayerProgressWidget> {
|
||||
|
||||
Timer _timer;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final EntityModel entityModel = EntityModel.of(context);
|
||||
final MediaPlayerEntity entity = entityModel.entityWrapper.entity;
|
||||
double progress;
|
||||
try {
|
||||
DateTime lastUpdated = DateTime.parse(
|
||||
entity.attributes["media_position_updated_at"]).toLocal();
|
||||
Duration duration = Duration(seconds: entity._getIntAttributeValue("media_duration") ?? 1);
|
||||
Duration position = Duration(seconds: entity._getIntAttributeValue("media_position") ?? 0);
|
||||
int currentPosition = position.inSeconds;
|
||||
if (entity.state == EntityState.playing) {
|
||||
_timer?.cancel();
|
||||
_timer = Timer(Duration(seconds: 1), () {
|
||||
setState(() {
|
||||
});
|
||||
});
|
||||
int differenceInSeconds = DateTime
|
||||
.now()
|
||||
.difference(lastUpdated)
|
||||
.inSeconds;
|
||||
currentPosition = currentPosition + differenceInSeconds;
|
||||
void _duplicateTo(entity) {
|
||||
if (entity.canCalculateActualPosition()) {
|
||||
HomeAssistant().savedPlayerPosition = entity.getActualPosition().toInt();
|
||||
} else {
|
||||
_timer?.cancel();
|
||||
HomeAssistant().savedPlayerPosition = 0;
|
||||
}
|
||||
progress = currentPosition / duration.inSeconds;
|
||||
return LinearProgressIndicator(
|
||||
value: progress,
|
||||
backgroundColor: Colors.black45,
|
||||
valueColor: AlwaysStoppedAnimation<Color>(EntityColor.stateColor(EntityState.on)),
|
||||
);
|
||||
} catch (e) {
|
||||
_timer?.cancel();
|
||||
progress = 0.0;
|
||||
}
|
||||
return LinearProgressIndicator(
|
||||
value: progress,
|
||||
backgroundColor: Colors.black45,
|
||||
);
|
||||
Navigator.of(context).pushNamed("/play-media", arguments: {
|
||||
"url": entity.attributes["media_content_id"],
|
||||
"type": entity.attributes["media_content_type"]
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_timer?.cancel();
|
||||
super.dispose();
|
||||
void _switchTo(entity) {
|
||||
HomeAssistant().sendFromPlayerId = entity.entityId;
|
||||
_duplicateTo(entity);
|
||||
}
|
||||
|
||||
}
|
@ -11,8 +11,12 @@ class SelectStateWidget extends StatefulWidget {
|
||||
class _SelectStateWidgetState extends State<SelectStateWidget> {
|
||||
|
||||
void setNewState(domain, entityId, newValue) {
|
||||
eventBus.fire(new ServiceCallEvent(domain, "select_option", entityId,
|
||||
{"option": "$newValue"}));
|
||||
ConnectionManager().callService(
|
||||
domain: domain,
|
||||
service: "select_option",
|
||||
entityId: entityId,
|
||||
data: {"option": "$newValue"}
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
|
@ -1,4 +1,4 @@
|
||||
part of '../../main.dart';
|
||||
part of '../main.dart';
|
||||
|
||||
class SimpleEntityState extends StatelessWidget {
|
||||
|
||||
@ -7,8 +7,10 @@ class SimpleEntityState extends StatelessWidget {
|
||||
final EdgeInsetsGeometry padding;
|
||||
final int maxLines;
|
||||
final String customValue;
|
||||
final double fontSize;
|
||||
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.fontSize: Sizes.stateFontSize, this.expanded: true, this.textAlign: TextAlign.right, this.padding: const EdgeInsets.fromLTRB(0.0, 0.0, Sizes.rightWidgetPadding, 0.0), this.customValue}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
@ -21,18 +23,22 @@ class SimpleEntityState extends StatelessWidget {
|
||||
state = customValue;
|
||||
}
|
||||
TextStyle textStyle = TextStyle(
|
||||
fontSize: Sizes.stateFontSize,
|
||||
fontSize: this.fontSize,
|
||||
fontWeight: FontWeight.normal
|
||||
);
|
||||
if (entityModel.entityWrapper.entity.statelessType == StatelessEntityType.CALL_SERVICE) {
|
||||
textStyle = textStyle.apply(color: Colors.blue);
|
||||
}
|
||||
if (this.bold) {
|
||||
textStyle = textStyle.apply(fontWeightDelta: 100);
|
||||
}
|
||||
while (state.contains(" ")){
|
||||
state = state.replaceAll(" ", " ");
|
||||
}
|
||||
Widget result = Padding(
|
||||
padding: padding,
|
||||
child: Text(
|
||||
"$state ${entityModel.entityWrapper.entity.unitOfMeasurement}",
|
||||
"$state ${entityModel.entityWrapper.unitOfMeasurement}",
|
||||
textAlign: textAlign,
|
||||
maxLines: maxLines,
|
||||
overflow: TextOverflow.ellipsis,
|
@ -18,8 +18,12 @@ class _SliderControlsWidgetState extends State<SliderControlsWidget> {
|
||||
_newValue = newValue;
|
||||
_changedHere = true;
|
||||
});
|
||||
eventBus.fire(new ServiceCallEvent(domain, "set_value", entityId,
|
||||
{"value": "${newValue.toString()}"}));
|
||||
ConnectionManager().callService(
|
||||
domain: domain,
|
||||
service: "set_value",
|
||||
entityId: entityId,
|
||||
data: {"value": "${newValue.toString()}"}
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
|
@ -38,8 +38,11 @@ class _SwitchStateWidgetState extends State<SwitchStateWidget> {
|
||||
} else {
|
||||
domain = entity.domain;
|
||||
}
|
||||
eventBus.fire(new ServiceCallEvent(
|
||||
domain, (newValue as bool) ? "turn_on" : "turn_off", entity.entityId, null));
|
||||
ConnectionManager().callService(
|
||||
domain: domain,
|
||||
service: (newValue as bool) ? "turn_on" : "turn_off",
|
||||
entityId: entity.entityId
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
|
@ -26,8 +26,12 @@ class _TextInputStateWidgetState extends State<TextInputStateWidget> {
|
||||
|
||||
void setNewState(newValue, domain, entityId) {
|
||||
if (validate(newValue, _minLength, _maxLength)) {
|
||||
eventBus.fire(new ServiceCallEvent(domain, "set_value", entityId,
|
||||
{"value": "$newValue"}));
|
||||
ConnectionManager().callService(
|
||||
domain: domain,
|
||||
service: "set_value",
|
||||
entityId: entityId,
|
||||
data: {"value": "$newValue"}
|
||||
);
|
||||
} else {
|
||||
setState(() {
|
||||
_tmpValue = _entityState;
|
||||
|
@ -1,4 +1,4 @@
|
||||
part of '../../main.dart';
|
||||
part of '../main.dart';
|
||||
|
||||
class UniversalSlider extends StatelessWidget {
|
||||
|
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();
|
||||
}
|
||||
}
|
232
lib/entities/vacuum/widgets/vacuum_controls.dart
Normal file
232
lib/entities/vacuum/widgets/vacuum_controls.dart
Normal file
@ -0,0 +1,232 @@
|
||||
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),
|
||||
_buildCommands(entity),
|
||||
_buildFanSpeed(entity),
|
||||
_buildAdditionalInfo(entity)
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildStatusAndBattery(VacuumEntity entity) {
|
||||
List<Widget> result = [];
|
||||
if (entity.supportStatus) {
|
||||
result.addAll(
|
||||
<Widget>[
|
||||
Text("Status:", style: TextStyle(fontSize: Sizes.stateFontSize),),
|
||||
Container(width: 6,),
|
||||
Expanded(
|
||||
//flex: 1,
|
||||
child: Text(
|
||||
"${entity.status}",
|
||||
maxLines: 1,
|
||||
softWrap: true,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(
|
||||
fontSize: Sizes.stateFontSize,
|
||||
fontWeight: FontWeight.bold
|
||||
),
|
||||
),
|
||||
),
|
||||
]
|
||||
);
|
||||
}
|
||||
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 %", style: TextStyle(fontSize: Sizes.stateFontSize))
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
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:", style: TextStyle(fontSize: Sizes.stateFontSize)),
|
||||
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,
|
||||
),
|
||||
);
|
||||
|
||||
}
|
||||
}
|
40
lib/entities/vacuum/widgets/vacuum_state_button.dart
Normal file
40
lib/entities/vacuum/widgets/vacuum_state_button.dart
Normal file
@ -0,0 +1,40 @@
|
||||
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: TextStyle(
|
||||
fontSize: 16,
|
||||
color: Colors.grey
|
||||
));
|
||||
}
|
||||
return Padding(
|
||||
padding: EdgeInsets.only(right: 15),
|
||||
child: result,
|
||||
);
|
||||
}
|
||||
}
|
@ -101,6 +101,9 @@ class EntityCollection {
|
||||
case "timer": {
|
||||
return TimerEntity(rawEntityData, homeAssistantWebHost);
|
||||
}
|
||||
case "vacuum": {
|
||||
return VacuumEntity(rawEntityData, homeAssistantWebHost);
|
||||
}
|
||||
default: {
|
||||
return Entity(rawEntityData, homeAssistantWebHost);
|
||||
}
|
||||
@ -149,32 +152,12 @@ class EntityCollection {
|
||||
return _allEntities[entityId] != null;
|
||||
}
|
||||
|
||||
List<Entity> filterEntitiesForDefaultView() {
|
||||
List<Entity> result = [];
|
||||
List<Entity> groups = [];
|
||||
List<Entity> nonGroupEntities = [];
|
||||
_allEntities.forEach((id, entity){
|
||||
if (entity.isGroup && (entity.attributes['auto'] == null || (entity.attributes['auto'] && !entity.isHidden)) && (!entity.isView)) {
|
||||
groups.add(entity);
|
||||
}
|
||||
if (!entity.isGroup) {
|
||||
nonGroupEntities.add(entity);
|
||||
}
|
||||
});
|
||||
|
||||
nonGroupEntities.forEach((entity) {
|
||||
bool foundInGroup = false;
|
||||
groups.forEach((groupEntity) {
|
||||
if (groupEntity.childEntityIds.contains(entity.entityId)) {
|
||||
foundInGroup = true;
|
||||
}
|
||||
});
|
||||
if (!foundInGroup) {
|
||||
result.add(entity);
|
||||
}
|
||||
});
|
||||
result.insertAll(0, groups);
|
||||
|
||||
return result;
|
||||
List<Entity> getByDomains({List<String> includeDomains: const [], List<String> excludeDomains: const [], List<String> stateFiler}) {
|
||||
return _allEntities.values.where((entity) {
|
||||
return
|
||||
(excludeDomains.isEmpty || !excludeDomains.contains(entity.domain)) &&
|
||||
(includeDomains.isEmpty || includeDomains.contains(entity.domain)) &&
|
||||
((stateFiler != null && stateFiler.contains(entity.state)) || stateFiler == null);
|
||||
}).toList();
|
||||
}
|
||||
}
|
@ -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 = '${ConnectionManager().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,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,
|
||||
);
|
||||
}
|
||||
}
|
@ -2,6 +2,8 @@ part of 'main.dart';
|
||||
|
||||
class HomeAssistant {
|
||||
|
||||
static const DEFAULT_DASHBOARD = 'lovelace';
|
||||
|
||||
static final HomeAssistant _instance = HomeAssistant._internal();
|
||||
|
||||
factory HomeAssistant() {
|
||||
@ -12,21 +14,32 @@ class HomeAssistant {
|
||||
HomeAssistantUI ui;
|
||||
Map _instanceConfig = {};
|
||||
String _userName;
|
||||
String _lovelaceDashbordUrl;
|
||||
HSVColor savedColor;
|
||||
int savedPlayerPosition;
|
||||
String sendToPlayerId;
|
||||
String sendFromPlayerId;
|
||||
Map services;
|
||||
bool autoUi = false;
|
||||
|
||||
String fcmToken;
|
||||
|
||||
Map _rawLovelaceData;
|
||||
var _rawStates;
|
||||
var _rawUserInfo;
|
||||
var _rawPanels;
|
||||
|
||||
set lovelaceDashboardUrl(String val) => _lovelaceDashbordUrl = val;
|
||||
|
||||
List<Panel> panels = [];
|
||||
|
||||
Duration fetchTimeout = Duration(seconds: 30);
|
||||
|
||||
String get locationName {
|
||||
if (ConnectionManager().useLovelace) {
|
||||
return ui?.title ?? "";
|
||||
if (!autoUi) {
|
||||
return ui?.title ?? "Home";
|
||||
} else {
|
||||
return _instanceConfig["location_name"] ?? "";
|
||||
return _instanceConfig["location_name"] ?? "Home";
|
||||
}
|
||||
}
|
||||
String get userName => _userName ?? locationName;
|
||||
@ -37,38 +50,37 @@ class HomeAssistant {
|
||||
|
||||
HomeAssistant._internal() {
|
||||
ConnectionManager().onStateChangeCallback = _handleEntityStateChange;
|
||||
ConnectionManager().onLovelaceUpdatedCallback = _handleLovelaceUpdate;
|
||||
DeviceInfoManager().loadDeviceInfo();
|
||||
}
|
||||
|
||||
Completer _fetchCompleter;
|
||||
|
||||
Future fetchData() {
|
||||
Future fetchData(bool uiOnly) {
|
||||
if (_fetchCompleter != null && !_fetchCompleter.isCompleted) {
|
||||
Logger.w("Previous data fetch is not completed yet");
|
||||
return _fetchCompleter.future;
|
||||
}
|
||||
if (entities == null) entities = EntityCollection(ConnectionManager().httpWebHost);
|
||||
_fetchCompleter = Completer();
|
||||
List<Future> futures = [];
|
||||
futures.add(_getStates());
|
||||
if (ConnectionManager().useLovelace) {
|
||||
futures.add(_getLovelace());
|
||||
if (!uiOnly) {
|
||||
if (entities == null) entities = EntityCollection(ConnectionManager().httpWebHost);
|
||||
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(ConnectionManager().sendSocketMessage(
|
||||
type: "subscribe_events",
|
||||
additionalData: {"event_type": "state_changed"},
|
||||
));
|
||||
Future.wait(futures).then((_) {
|
||||
if (isMobileAppEnabled) {
|
||||
_createUI();
|
||||
_fetchCompleter.complete();
|
||||
MobileAppIntegrationManager.checkAppRegistration();
|
||||
if (!uiOnly) MobileAppIntegrationManager.checkAppRegistration();
|
||||
} 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-integration")]));
|
||||
_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) {
|
||||
_fetchCompleter.completeError(e);
|
||||
@ -76,6 +88,48 @@ class HomeAssistant {
|
||||
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 (isMobileAppEnabled) {
|
||||
_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 {
|
||||
Logger.d("Logging out...");
|
||||
await ConnectionManager().logout().then((_) {
|
||||
@ -85,64 +139,177 @@ class HomeAssistant {
|
||||
});
|
||||
}
|
||||
|
||||
Future _getConfig() async {
|
||||
await ConnectionManager().sendSocketMessage(type: "get_config").then((data) {
|
||||
_instanceConfig = Map.from(data);
|
||||
}).catchError((e) {
|
||||
throw HAError("Error getting config: ${e}");
|
||||
Future _getConfig(SharedPreferences sharedPrefs) async {
|
||||
if (sharedPrefs != null) {
|
||||
try {
|
||||
var data = json.decode(sharedPrefs.getString('cached_config'));
|
||||
_parseConfig(data);
|
||||
} catch (e) {
|
||||
throw HAError("Error getting config: $e");
|
||||
}
|
||||
} else {
|
||||
await ConnectionManager().sendSocketMessage(type: "get_config").then((data) => _parseConfig(data)).catchError((e) {
|
||||
throw HAError("Error getting config: $e");
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Future _getStates() async {
|
||||
void _parseConfig(data) {
|
||||
_instanceConfig = Map.from(data);
|
||||
}
|
||||
|
||||
Future _getStates(SharedPreferences sharedPrefs) async {
|
||||
if (sharedPrefs != null) {
|
||||
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) => entities.parse(data)
|
||||
(data) => _parseStates(data)
|
||||
).catchError((e) {
|
||||
throw HAError("Error getting states: $e");
|
||||
});
|
||||
}
|
||||
|
||||
Future _getLovelace() async {
|
||||
await ConnectionManager().sendSocketMessage(type: "lovelace/config").then((data) => _rawLovelaceData = data).catchError((e) {
|
||||
throw HAError("Error getting lovelace config: $e");
|
||||
});
|
||||
}
|
||||
|
||||
Future _getUserInfo() async {
|
||||
_userName = null;
|
||||
await ConnectionManager().sendSocketMessage(type: "auth/current_user").then((data) => _userName = data["name"]).catchError((e) {
|
||||
Logger.w("Can't get user info: ${e}");
|
||||
});
|
||||
void _parseStates(data) {
|
||||
_rawStates = data;
|
||||
entities.parse(data);
|
||||
}
|
||||
|
||||
Future _getServices() async {
|
||||
await ConnectionManager().sendSocketMessage(type: "get_services").then((data) => Logger.d("Services received")).catchError((e) {
|
||||
Logger.w("Can't get services: ${e}");
|
||||
});
|
||||
Future _getLovelace(SharedPreferences sharedPrefs) {
|
||||
if (sharedPrefs != null) {
|
||||
try {
|
||||
var data = json.decode(sharedPrefs.getString('cached_lovelace'));
|
||||
_rawLovelaceData = data;
|
||||
} catch (e) {
|
||||
autoUi = true;
|
||||
}
|
||||
|
||||
Future _getPanels() async {
|
||||
panels.clear();
|
||||
await ConnectionManager().sendSocketMessage(type: "get_panels").then((data) {
|
||||
data.forEach((k,v) {
|
||||
String title = v['title'] == null ? "${k[0].toUpperCase()}${k.substring(1)}" : "${v['title'][0].toUpperCase()}${v['title'].substring(1)}";
|
||||
panels.add(Panel(
|
||||
id: k,
|
||||
type: v["component_name"],
|
||||
title: title,
|
||||
urlPath: v["url_path"],
|
||||
config: v["config"],
|
||||
icon: v["icon"]
|
||||
)
|
||||
);
|
||||
});
|
||||
return Future.value();
|
||||
} else {
|
||||
Completer completer = Completer();
|
||||
ConnectionManager().sendSocketMessage(
|
||||
type: 'lovelace/config',
|
||||
additionalData: {
|
||||
'url_path': _lovelaceDashbordUrl == HomeAssistant.DEFAULT_DASHBOARD ? null : _lovelaceDashbordUrl
|
||||
}
|
||||
).then((data) {
|
||||
_rawLovelaceData = data;
|
||||
completer.complete();
|
||||
}).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");
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
void _handleLovelaceUpdate() {
|
||||
if (_fetchCompleter != null && _fetchCompleter.isCompleted) {
|
||||
eventBus.fire(new LovelaceChangedEvent());
|
||||
}
|
||||
}
|
||||
|
||||
void _handleEntityStateChange(Map eventData) {
|
||||
//TheLogger.debug( "New state for ${eventData['entity_id']}");
|
||||
if (_fetchCompleter.isCompleted) {
|
||||
if (_fetchCompleter != null && _fetchCompleter.isCompleted) {
|
||||
Map data = Map.from(eventData);
|
||||
eventBus.fire(new StateChangedEvent(
|
||||
entityId: data["entity_id"],
|
||||
@ -151,196 +318,26 @@ class HomeAssistant {
|
||||
}
|
||||
}
|
||||
|
||||
void _parseLovelace() {
|
||||
Logger.d("--Title: ${_rawLovelaceData["title"]}");
|
||||
ui.title = _rawLovelaceData["title"];
|
||||
int viewCounter = 0;
|
||||
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'] ?? CardType.entities,
|
||||
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;
|
||||
bool isServiceExist(String service) {
|
||||
return services != null &&
|
||||
services.isNotEmpty &&
|
||||
services.containsKey(service);
|
||||
}
|
||||
|
||||
void _createUI() {
|
||||
ui = HomeAssistantUI();
|
||||
if ((ConnectionManager().useLovelace) && (_rawLovelaceData != null)) {
|
||||
Logger.d("Creating Lovelace UI");
|
||||
_parseLovelace();
|
||||
} else {
|
||||
Logger.d("Creating group-based UI");
|
||||
int viewCounter = 0;
|
||||
if (!entities.hasDefaultView) {
|
||||
HAView view = HAView(
|
||||
count: viewCounter,
|
||||
id: "group.default_view",
|
||||
name: "Home",
|
||||
childEntities: entities.filterEntitiesForDefaultView()
|
||||
ui = HomeAssistantUI(rawLovelaceConfig: _rawLovelaceData);
|
||||
if (isServiceExist('zha_map')) {
|
||||
panels.add(
|
||||
Panel(
|
||||
id: 'haclient_zha',
|
||||
componentName: 'haclient_zha',
|
||||
title: 'ZHA',
|
||||
urlPath: '/haclient_zha',
|
||||
icon: 'mdi:zigbee'
|
||||
)
|
||||
);
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
|
212
lib/main.dart
212
lib/main.dart
@ -1,5 +1,8 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:async';
|
||||
import 'dart:math';
|
||||
import 'dart:io';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/rendering.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
@ -7,6 +10,7 @@ import 'package:web_socket_channel/io.dart';
|
||||
import 'package:event_bus/event_bus.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:cached_network_image/cached_network_image.dart';
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
import 'package:url_launcher/url_launcher.dart' as urlLauncher;
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:date_format/date_format.dart';
|
||||
@ -16,11 +20,22 @@ import 'package:progress_indicators/progress_indicators.dart';
|
||||
import 'package:flutter_markdown/flutter_markdown.dart';
|
||||
import 'package:flutter_custom_tabs/flutter_custom_tabs.dart';
|
||||
import 'package:firebase_messaging/firebase_messaging.dart';
|
||||
import 'package:flutter_webview_plugin/flutter_webview_plugin.dart';
|
||||
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
||||
import 'package:device_info/device_info.dart';
|
||||
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
|
||||
import 'package:in_app_purchase/in_app_purchase.dart';
|
||||
import 'plugins/circular_slider/single_circular_slider.dart';
|
||||
import 'plugins/dynamic_multi_column_layout.dart';
|
||||
import 'plugins/spoiler_card.dart';
|
||||
import 'package:workmanager/workmanager.dart' as workManager;
|
||||
import 'package:geolocator/geolocator.dart';
|
||||
import 'package:battery/battery.dart';
|
||||
import 'package:firebase_crashlytics/firebase_crashlytics.dart';
|
||||
import 'package:flutter_webview_plugin/flutter_webview_plugin.dart' as standaloneWebview;
|
||||
import 'package:webview_flutter/webview_flutter.dart';
|
||||
import 'package:video_player/video_player.dart';
|
||||
|
||||
import 'utils/logger.dart';
|
||||
|
||||
part 'const.dart';
|
||||
part 'utils/launcher.dart';
|
||||
@ -45,35 +60,35 @@ part 'entities/fan/fan_entity.class.dart';
|
||||
part 'entities/automation/automation_entity.class.dart';
|
||||
part 'entities/camera/camera_entity.class.dart';
|
||||
part 'entities/alarm_control_panel/alarm_control_panel_entity.class.dart';
|
||||
part 'entity_widgets/common/badge.dart';
|
||||
part 'entity_widgets/model_widgets.dart';
|
||||
part 'entity_widgets/default_entity_container.dart';
|
||||
part 'entity_widgets/missed_entity.dart';
|
||||
part 'entity_widgets/glance_entity_container.dart';
|
||||
part 'entity_widgets/button_entity_container.dart';
|
||||
part 'entity_widgets/common/entity_attributes_list.dart';
|
||||
part 'entity_widgets/entity_icon.dart';
|
||||
part 'entity_widgets/entity_name.dart';
|
||||
part 'entity_widgets/common/last_updated.dart';
|
||||
part 'entity_widgets/common/mode_swicth.dart';
|
||||
part 'entity_widgets/common/mode_selector.dart';
|
||||
part 'entity_widgets/common/universal_slider.dart';
|
||||
part 'entity_widgets/common/flat_service_button.dart';
|
||||
part 'entity_widgets/common/light_color_picker.dart';
|
||||
part 'entity_widgets/common/camera_stream_view.dart';
|
||||
part 'entity_widgets/entity_colors.class.dart';
|
||||
part 'entity_widgets/entity_page_container.dart';
|
||||
part 'entity_widgets/history_chart/entity_history.dart';
|
||||
part 'entity_widgets/history_chart/simple_state_history_chart.dart';
|
||||
part 'entity_widgets/history_chart/numeric_state_history_chart.dart';
|
||||
part 'entity_widgets/history_chart/combined_history_chart.dart';
|
||||
part 'entity_widgets/history_chart/history_control_widget.dart';
|
||||
part 'entity_widgets/history_chart/entity_history_moment.dart';
|
||||
part 'entities/badge.widget.dart';
|
||||
part 'entities/entity_model.widget.dart';
|
||||
part 'entities/default_entity_container.widget.dart';
|
||||
part 'entities/missed_entity.widget.dart';
|
||||
part 'cards/widgets/glance_card_entity_container.dart';
|
||||
part 'cards/widgets/entity_button_card_body.widget.dart';
|
||||
part 'pages/widgets/entity_attributes_list.dart';
|
||||
part 'entities/entity_icon.widget.dart';
|
||||
part 'entities/entity_name.widget.dart';
|
||||
part 'pages/widgets/last_updated.dart';
|
||||
part 'entities/climate/widgets/mode_swicth.dart';
|
||||
part 'entities/climate/widgets/mode_selector.dart';
|
||||
part 'entities/universal_slider.widget.dart';
|
||||
part 'entities/flat_service_button.widget.dart';
|
||||
part 'entities/light/widgets/light_color_picker.dart';
|
||||
part 'entities/camera/widgets/camera_stream_view.dart';
|
||||
part 'entities/entity_colors.class.dart';
|
||||
part 'plugins/history_chart/entity_history.dart';
|
||||
part 'plugins/history_chart/simple_state_history_chart.dart';
|
||||
part 'plugins/history_chart/numeric_state_history_chart.dart';
|
||||
part 'plugins/history_chart/combined_history_chart.dart';
|
||||
part 'plugins/history_chart/history_control_widget.dart';
|
||||
part 'plugins/history_chart/entity_history_moment.dart';
|
||||
part 'entities/switch/widget/switch_state.dart';
|
||||
part 'entities/slider/widgets/slider_controls.dart';
|
||||
part 'entities/text/widgets/text_input_state.dart';
|
||||
part 'entities/select/widgets/select_state.dart';
|
||||
part 'entity_widgets/common/simple_state.dart';
|
||||
part 'entities/simple_state.widget.dart';
|
||||
part 'entities/entity_picture.widget.dart';
|
||||
part 'entities/timer/widgets/timer_state.dart';
|
||||
part 'entities/climate/widgets/climate_state.widget.dart';
|
||||
part 'entities/cover/widgets/cover_state.dart';
|
||||
@ -86,17 +101,22 @@ part 'entities/light/widgets/light_controls.dart';
|
||||
part 'entities/media_player/widgets/media_player_widgets.dart';
|
||||
part 'entities/fan/widgets/fan_controls.dart';
|
||||
part 'entities/alarm_control_panel/widgets/alarm_control_panel_controls.widget.dart';
|
||||
part 'entities/vacuum/vacuum_entity.class.dart';
|
||||
part 'entities/vacuum/widgets/vacuum_controls.dart';
|
||||
part 'entities/vacuum/widgets/vacuum_state_button.dart';
|
||||
part 'pages/settings.page.dart';
|
||||
part 'pages/purchase.page.dart';
|
||||
part 'pages/widgets/product_purchase.widget.dart';
|
||||
part 'pages/widgets/page_loading_indicator.dart';
|
||||
part 'pages/widgets/page_loading_error.dart';
|
||||
part 'pages/panel.page.dart';
|
||||
part 'pages/main.page.dart';
|
||||
part 'pages/main/main.page.dart';
|
||||
part 'pages/integration_settings.page.dart';
|
||||
part 'pages/zha_page.dart';
|
||||
part 'home_assistant.class.dart';
|
||||
part 'pages/log.page.dart';
|
||||
part 'pages/entity.page.dart';
|
||||
part 'mdi.class.dart';
|
||||
part 'utils/mdi.class.dart';
|
||||
part 'entity_collection.class.dart';
|
||||
part 'managers/auth_manager.class.dart';
|
||||
part 'managers/location_manager.class.dart';
|
||||
@ -104,51 +124,102 @@ part 'managers/mobile_app_integration_manager.class.dart';
|
||||
part 'managers/connection_manager.class.dart';
|
||||
part 'managers/device_info_manager.class.dart';
|
||||
part 'managers/startup_user_messages_manager.class.dart';
|
||||
part 'ui_class/ui.dart';
|
||||
part 'ui_class/view.class.dart';
|
||||
part 'ui_class/card.class.dart';
|
||||
part 'ui_class/sizes_class.dart';
|
||||
part 'ui_class/panel_class.dart';
|
||||
part 'ui_widgets/view.dart';
|
||||
part 'ui_widgets/card_widget.dart';
|
||||
part 'ui_widgets/card_header_widget.dart';
|
||||
part 'ui.dart';
|
||||
part 'view.class.dart';
|
||||
part 'cards/card.class.dart';
|
||||
part 'panels/panel_class.dart';
|
||||
part 'viewWidget.widget.dart';
|
||||
part 'cards/card_widget.dart';
|
||||
part 'cards/widgets/card_header.widget.dart';
|
||||
part 'panels/config_panel_widget.dart';
|
||||
part 'panels/widgets/link_to_web_config.dart';
|
||||
part 'utils/logger.dart';
|
||||
part 'types/ha_error.dart';
|
||||
part 'types/event_bus_events.dart';
|
||||
|
||||
part 'cards/widgets/gauge_card_body.dart';
|
||||
part 'cards/widgets/light_card_body.dart';
|
||||
part 'pages/play_media.page.dart';
|
||||
part 'entities/entity_page_layout.widget.dart';
|
||||
part 'entities/media_player/widgets/media_player_seek_bar.widget.dart';
|
||||
part 'entities/media_player/widgets/media_player_progress_bar.widget.dart';
|
||||
part 'pages/whats_new.page.dart';
|
||||
part 'pages/fullscreen.page.dart';
|
||||
|
||||
EventBus eventBus = new EventBus();
|
||||
final FirebaseMessaging _firebaseMessaging = FirebaseMessaging();
|
||||
FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin = new FlutterLocalNotificationsPlugin();
|
||||
const String appName = "HA Client";
|
||||
const appVersion = "0.6.5";
|
||||
const appVersionNumber = "0.8.0";
|
||||
const appVersionAdd = "";
|
||||
const appVersion = "$appVersionNumber$appVersionAdd";
|
||||
|
||||
Future<void> _reportError(dynamic error, dynamic stackTrace) async {
|
||||
// Print the exception to the console.
|
||||
if (Logger.isInDebugMode) {
|
||||
Logger.e('Caught error: $error');
|
||||
Logger.p(stackTrace);
|
||||
}
|
||||
Crashlytics.instance.recordError(error, stackTrace);
|
||||
|
||||
}
|
||||
|
||||
void main() async {
|
||||
FlutterError.onError = (errorDetails) {
|
||||
Logger.e( "${errorDetails.exception}");
|
||||
Crashlytics.instance.enableInDevMode = false;
|
||||
|
||||
FlutterError.onError = (FlutterErrorDetails details) {
|
||||
Logger.e(" Caut Flutter runtime error: ${details.exception}");
|
||||
if (Logger.isInDebugMode) {
|
||||
FlutterError.dumpErrorToConsole(errorDetails);
|
||||
FlutterError.dumpErrorToConsole(details);
|
||||
}
|
||||
Crashlytics.instance.recordFlutterError(details);
|
||||
};
|
||||
|
||||
runZoned(() {
|
||||
//AndroidAlarmManager.initialize().then((_) {
|
||||
runApp(new HAClientApp());
|
||||
// print("Running MAIN isolate ${Isolate.current.hashCode}");
|
||||
//});
|
||||
|
||||
}, onError: (error, stack) {
|
||||
Logger.e("$error");
|
||||
Logger.e("$stack");
|
||||
if (Logger.isInDebugMode) {
|
||||
debugPrint("$stack");
|
||||
}
|
||||
_reportError(error, stack);
|
||||
});
|
||||
}
|
||||
|
||||
class HAClientApp extends StatelessWidget {
|
||||
class HAClientApp extends StatefulWidget {
|
||||
|
||||
@override
|
||||
_HAClientAppState createState() => new _HAClientAppState();
|
||||
|
||||
}
|
||||
|
||||
class _HAClientAppState extends State<HAClientApp> {
|
||||
StreamSubscription<List<PurchaseDetails>> _subscription;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
InAppPurchaseConnection.enablePendingPurchases();
|
||||
final Stream purchaseUpdates =
|
||||
InAppPurchaseConnection.instance.purchaseUpdatedStream;
|
||||
_subscription = purchaseUpdates.listen((purchases) {
|
||||
_handlePurchaseUpdates(purchases);
|
||||
});
|
||||
workManager.Workmanager.initialize(
|
||||
updateDeviceLocationIsolate,
|
||||
isInDebugMode: false
|
||||
);
|
||||
super.initState();
|
||||
}
|
||||
|
||||
void _handlePurchaseUpdates(purchase) {
|
||||
if (purchase is List<PurchaseDetails>) {
|
||||
if (purchase[0].status == PurchaseStatus.purchased) {
|
||||
eventBus.fire(ShowPopupMessageEvent(
|
||||
title: "Thanks a lot!",
|
||||
body: "Thank you for supporting HA Client development!",
|
||||
buttonText: "Ok"
|
||||
));
|
||||
} else {
|
||||
Logger.d("Purchase change handler: ${purchase[0].status}");
|
||||
}
|
||||
} else {
|
||||
Logger.e("Something wrong with purchase handling. Got: $purchase");
|
||||
}
|
||||
}
|
||||
|
||||
// This widget is the root of your application.
|
||||
@override
|
||||
@ -162,14 +233,31 @@ class HAClientApp extends StatelessWidget {
|
||||
routes: {
|
||||
"/": (context) => MainPage(title: 'HA Client'),
|
||||
"/connection-settings": (context) => ConnectionSettingsPage(title: "Settings"),
|
||||
"/integration-settings": (context) => IntegrationSettingsPage(title: "Integration settings"),
|
||||
"/putchase": (context) => PurchasePage(title: "Support app development"),
|
||||
"/play-media": (context) => PlayMediaPage(
|
||||
mediaUrl: "${ModalRoute.of(context).settings.arguments != null ? (ModalRoute.of(context).settings.arguments as Map)['url'] : ''}",
|
||||
mediaType: "${ModalRoute.of(context).settings.arguments != null ? (ModalRoute.of(context).settings.arguments as Map)['type'] ?? '' : ''}",
|
||||
),
|
||||
"/log-view": (context) => LogViewPage(title: "Log"),
|
||||
"/login": (context) => WebviewScaffold(
|
||||
"/webview": (context) => standaloneWebview.WebviewScaffold(
|
||||
url: "${(ModalRoute.of(context).settings.arguments as Map)['url']}",
|
||||
appBar: new AppBar(
|
||||
leading: IconButton(
|
||||
icon: Icon(Icons.arrow_back),
|
||||
onPressed: () => Navigator.of(context).pop()
|
||||
),
|
||||
title: new Text("${(ModalRoute.of(context).settings.arguments as Map)['title']}"),
|
||||
),
|
||||
),
|
||||
"/whats-new": (context) => WhatsNewPage(),
|
||||
"/haclient_zha": (context) => ZhaPage(),
|
||||
"/auth": (context) => new standaloneWebview.WebviewScaffold(
|
||||
url: "${ConnectionManager().oauthUrl}",
|
||||
appBar: new AppBar(
|
||||
leading: IconButton(
|
||||
icon: Icon(Icons.help),
|
||||
onPressed: () => Launcher.launchURLInCustomTab(context: context, url: "http://ha-client.homemade.systems/docs#authentication")
|
||||
onPressed: () => Launcher.launchURLInCustomTab(context: context, url: "http://ha-client.app/docs#authentication")
|
||||
),
|
||||
title: new Text("Login with HA"),
|
||||
actions: <Widget>[
|
||||
@ -181,18 +269,14 @@ class HAClientApp extends StatelessWidget {
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
"/webview": (context) => WebviewScaffold(
|
||||
url: "${(ModalRoute.of(context).settings.arguments as Map)['url']}",
|
||||
appBar: new AppBar(
|
||||
leading: IconButton(
|
||||
icon: Icon(Icons.arrow_back),
|
||||
onPressed: () => Navigator.of(context).pop()
|
||||
),
|
||||
title: new Text("${(ModalRoute.of(context).settings.arguments as Map)['title']}"),
|
||||
),
|
||||
)
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_subscription.cancel();
|
||||
super.dispose();
|
||||
}
|
||||
}
|
@ -10,31 +10,31 @@ class AuthManager {
|
||||
|
||||
AuthManager._internal();
|
||||
|
||||
Future getTempToken({String oauthUrl}) {
|
||||
Future start({String oauthUrl}) {
|
||||
Completer completer = Completer();
|
||||
final flutterWebviewPlugin = new FlutterWebviewPlugin();
|
||||
final flutterWebviewPlugin = new standaloneWebview.FlutterWebviewPlugin();
|
||||
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];
|
||||
Logger.d("authCode=$authCode");
|
||||
Logger.d("We have auth code. Getting temporary access token...");
|
||||
ConnectionManager().sendHTTPPost(
|
||||
endPoint: "/auth/token",
|
||||
contentType: "application/x-www-form-urlencoded",
|
||||
includeAuthHeader: false,
|
||||
data: "grant_type=authorization_code&code=$authCode&client_id=${Uri.encodeComponent('http://ha-client.homemade.systems/')}"
|
||||
data: "grant_type=authorization_code&code=$authCode&client_id=${Uri.encodeComponent('https://ha-client.app')}"
|
||||
).then((response) {
|
||||
Logger.d("Got temp token");
|
||||
String tempToken = json.decode(response)['access_token'];
|
||||
Logger.d("Closing webview...");
|
||||
//flutterWebviewPlugin.close();
|
||||
eventBus.fire(StartAuthEvent(oauthUrl, false));
|
||||
completer.complete(tempToken);
|
||||
}).catchError((e) {
|
||||
//flutterWebviewPlugin.close();
|
||||
Logger.e("Error getting temp token: ${e.toString()}");
|
||||
eventBus.fire(StartAuthEvent(oauthUrl, false));
|
||||
completer.completeError(HAError("Error getting temp token"));
|
||||
});
|
||||
}).whenComplete(() => flutterWebviewPlugin.close());
|
||||
}
|
||||
});
|
||||
Logger.d("Launching OAuth");
|
||||
|
@ -19,7 +19,6 @@ class ConnectionManager {
|
||||
String _tempToken;
|
||||
String oauthUrl;
|
||||
String webhookId;
|
||||
bool useLovelace = true;
|
||||
bool settingsLoaded = false;
|
||||
bool get isAuthenticated => _token != null;
|
||||
StreamSubscription _socketSubscription;
|
||||
@ -28,6 +27,7 @@ class ConnectionManager {
|
||||
bool isConnected = false;
|
||||
|
||||
var onStateChangeCallback;
|
||||
var onLovelaceUpdatedCallback;
|
||||
|
||||
IOWebSocketChannel _socket;
|
||||
|
||||
@ -38,9 +38,8 @@ class ConnectionManager {
|
||||
Completer completer = Completer();
|
||||
bool stopInit = false;
|
||||
if (loadSettings) {
|
||||
Logger.e("Loading settings...");
|
||||
Logger.d("Loading settings...");
|
||||
SharedPreferences prefs = await SharedPreferences.getInstance();
|
||||
useLovelace = prefs.getBool('use-lovelace') ?? true;
|
||||
_domain = prefs.getString('hassio-domain');
|
||||
_port = prefs.getString('hassio-port');
|
||||
webhookId = prefs.getString('app-webhook-id');
|
||||
@ -59,9 +58,9 @@ class ConnectionManager {
|
||||
_token = await storage.read(key: "hacl_llt");
|
||||
Logger.e("Long-lived token read successful");
|
||||
oauthUrl = "$httpWebHost/auth/authorize?client_id=${Uri.encodeComponent(
|
||||
'http://ha-client.homemade.systems/')}&redirect_uri=${Uri
|
||||
'https://ha-client.app')}&redirect_uri=${Uri
|
||||
.encodeComponent(
|
||||
'http://ha-client.homemade.systems/service/auth_callback.html')}";
|
||||
'https://ha-client.app/service/auth_callback.html')}";
|
||||
settingsLoaded = true;
|
||||
} catch (e) {
|
||||
completer.completeError(HAError("Error reading login details", actions: [HAErrorAction.tryAgain(type: HAErrorActionType.FULL_RELOAD), HAErrorAction.loginAgain()]));
|
||||
@ -79,7 +78,7 @@ class ConnectionManager {
|
||||
|
||||
if (!stopInit) {
|
||||
if (_token == null) {
|
||||
AuthManager().getTempToken(
|
||||
AuthManager().start(
|
||||
oauthUrl: oauthUrl
|
||||
).then((token) {
|
||||
Logger.d("Token from AuthManager recived");
|
||||
@ -98,14 +97,23 @@ class ConnectionManager {
|
||||
|
||||
void _doConnect({Completer completer, bool forceReconnect}) {
|
||||
if (forceReconnect || !isConnected) {
|
||||
_connect().timeout(connectTimeout, onTimeout: () {
|
||||
_disconnect().then((_){
|
||||
completer?.completeError(HAError("Connection timeout"));
|
||||
});
|
||||
}).then((_) {
|
||||
_connect().timeout(connectTimeout).then((_) {
|
||||
completer?.complete();
|
||||
}).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);
|
||||
} else {
|
||||
completer?.completeError(HAError("${e.toString()}"));
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
} else {
|
||||
completer?.complete();
|
||||
@ -122,6 +130,7 @@ class ConnectionManager {
|
||||
connecting = Completer();
|
||||
_disconnect().then((_) {
|
||||
Logger.d("Socket connecting...");
|
||||
try {
|
||||
_socket = IOWebSocketChannel.connect(
|
||||
_webSocketAPIEndpoint, pingInterval: Duration(seconds: 15));
|
||||
_socketSubscription = _socket.stream.listen(
|
||||
@ -138,11 +147,21 @@ class ConnectionManager {
|
||||
});
|
||||
} else if (data["type"] == "auth_ok") {
|
||||
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.remove("auth");
|
||||
if (_token != null) {
|
||||
if (!connecting.isCompleted) connecting.complete();
|
||||
}
|
||||
});
|
||||
} else if (data["type"] == "auth_invalid") {
|
||||
Logger.d("[Received] <== ${data.toString()}");
|
||||
_messageResolver["auth"]?.completeError(HAError("${data["message"]}", actions: [HAErrorAction.loginAgain()]));
|
||||
@ -156,13 +175,14 @@ class ConnectionManager {
|
||||
onDone: () => _handleSocketClose(connecting),
|
||||
onError: (e) => _handleSocketError(e, connecting)
|
||||
);
|
||||
} catch(exeption) {
|
||||
connecting.completeError(HAError("${exeption.toString()}"));
|
||||
}
|
||||
});
|
||||
return connecting.future;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
Future _disconnect() {
|
||||
Completer completer = Completer();
|
||||
if (!isConnected) {
|
||||
@ -186,14 +206,25 @@ class ConnectionManager {
|
||||
_handleMessage(data) {
|
||||
if (data["type"] == "result") {
|
||||
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"]);
|
||||
} else if (data["id"] != null) {
|
||||
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"]}");
|
||||
} else if (data["type"] == "event") {
|
||||
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"]}");
|
||||
onStateChangeCallback(data["event"]["data"]);
|
||||
} else if (data["event"]["event_type"] == "lovelace_updated") {
|
||||
Logger.d("[Received] <== ${data['type']}.${data["event"]["event_type"]}: $data");
|
||||
onLovelaceUpdatedCallback();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if ((data["event"] != null) && (data["event"]["event_type"] == "state_changed")) {
|
||||
Logger.d("[Received] <== ${data['type']}.${data["event"]["event_type"]}: ${data["event"]["data"]["entity_id"]}");
|
||||
onStateChangeCallback(data["event"]["data"]);
|
||||
@ -209,38 +240,24 @@ class ConnectionManager {
|
||||
|
||||
void _handleSocketClose(Completer connectionCompleter) {
|
||||
Logger.d("Socket disconnected.");
|
||||
_disconnect().then((_) {
|
||||
if (!connectionCompleter.isCompleted) {
|
||||
isConnected = false;
|
||||
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")));
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
void _handleSocketError(e, Completer connectionCompleter) {
|
||||
Logger.e("Socket stream Error: $e");
|
||||
_disconnect().then((_) {
|
||||
if (!connectionCompleter.isCompleted) {
|
||||
isConnected = false;
|
||||
connectionCompleter.completeError(HAError("Unable to connect to Home Assistant"));
|
||||
} else {
|
||||
_disconnect().then((_) {
|
||||
Timer(Duration(seconds: 5), () {
|
||||
Logger.d("Trying to reconnect...");
|
||||
_connect().catchError((e) {
|
||||
isConnected = false;
|
||||
connectionCompleter.completeError(HAError("Disconnected", actions: [HAErrorAction.reconnect()]));
|
||||
}
|
||||
eventBus.fire(ShowErrorEvent(HAError("Unable to connect to Home Assistant")));
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Future _authenticate() {
|
||||
@ -329,13 +346,13 @@ class ConnectionManager {
|
||||
_messageResolver[callbackName] = _completer;
|
||||
String rawMessage = json.encode(dataObject);
|
||||
if (!isConnected) {
|
||||
_connect().timeout(connectTimeout, onTimeout: (){
|
||||
_completer.completeError(HAError("No connection to Home Assistant", actions: [HAErrorAction.reconnect()]));
|
||||
}).then((_) {
|
||||
_connect().timeout(connectTimeout).then((_) {
|
||||
Logger.d("[Sending] ==> ${auth ? "type="+dataObject['type'] : rawMessage}");
|
||||
_socket.sink.add(rawMessage);
|
||||
}).catchError((e) {
|
||||
_completer.completeError(e);
|
||||
if (!_completer.isCompleted) {
|
||||
_completer.completeError(HAError("No connection to Home Assistant", actions: [HAErrorAction.reconnect()]));
|
||||
}
|
||||
});
|
||||
} else {
|
||||
Logger.d("[Sending] ==> ${auth ? "type="+dataObject['type'] : rawMessage}");
|
||||
@ -348,18 +365,29 @@ class ConnectionManager {
|
||||
_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 = {};
|
||||
if (entityId != null) {
|
||||
serviceData["entity_id"] = entityId;
|
||||
}
|
||||
if (additionalServiceData != null && additionalServiceData.isNotEmpty) {
|
||||
serviceData.addAll(additionalServiceData);
|
||||
if (data != null && data.isNotEmpty) {
|
||||
serviceData.addAll(data);
|
||||
}
|
||||
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
|
||||
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 {
|
||||
@ -398,11 +426,12 @@ class ConnectionManager {
|
||||
headers: headers,
|
||||
body: data
|
||||
).then((response) {
|
||||
Logger.d("[Received] <== HTTP ${response.statusCode}");
|
||||
if (response.statusCode >= 200 && response.statusCode < 300 ) {
|
||||
Logger.d("[Received] <== HTTP ${response.statusCode}");
|
||||
completer.complete(response.body);
|
||||
} else {
|
||||
completer.completeError({"code": response.statusCode, "message": "${response.body}"});
|
||||
Logger.d("[Received] <== HTTP ${response.statusCode}: ${response.body}");
|
||||
completer.completeError(response);
|
||||
}
|
||||
}).catchError((e) {
|
||||
completer.completeError(e);
|
||||
|
@ -2,4 +2,236 @@ part of '../main.dart';
|
||||
|
||||
class LocationManager {
|
||||
|
||||
static final LocationManager _instance = LocationManager
|
||||
._internal();
|
||||
|
||||
factory LocationManager() {
|
||||
return _instance;
|
||||
}
|
||||
|
||||
LocationManager._internal() {
|
||||
init();
|
||||
}
|
||||
|
||||
final int defaultUpdateIntervalMinutes = 20;
|
||||
final String backgroundTaskId = "haclocationtask0";
|
||||
final String backgroundTaskTag = "haclocation";
|
||||
Duration _updateInterval;
|
||||
bool _isRunning;
|
||||
|
||||
void init() async {
|
||||
SharedPreferences prefs = await SharedPreferences.getInstance();
|
||||
await prefs.reload();
|
||||
_updateInterval = Duration(minutes: prefs.getInt("location-interval") ??
|
||||
defaultUpdateIntervalMinutes);
|
||||
_isRunning = prefs.getBool("location-enabled") ?? false;
|
||||
if (_isRunning) {
|
||||
await _startLocationService();
|
||||
}
|
||||
}
|
||||
|
||||
setSettings(bool enabled, int interval) async {
|
||||
SharedPreferences prefs = await SharedPreferences.getInstance();
|
||||
if (interval != _updateInterval.inMinutes) {
|
||||
prefs.setInt("location-interval", interval);
|
||||
_updateInterval = Duration(minutes: interval);
|
||||
if (_isRunning) {
|
||||
Logger.d("Stopping location tracking...");
|
||||
_isRunning = false;
|
||||
await _stopLocationService();
|
||||
}
|
||||
}
|
||||
if (enabled && !_isRunning) {
|
||||
Logger.d("Starting location tracking");
|
||||
SharedPreferences prefs = await SharedPreferences.getInstance();
|
||||
prefs.setBool("location-enabled", enabled);
|
||||
_isRunning = true;
|
||||
await _startLocationService();
|
||||
} else if (!enabled && _isRunning) {
|
||||
Logger.d("Stopping location tracking...");
|
||||
SharedPreferences prefs = await SharedPreferences.getInstance();
|
||||
prefs.setBool("location-enabled", enabled);
|
||||
_isRunning = false;
|
||||
await _stopLocationService();
|
||||
}
|
||||
}
|
||||
|
||||
_startLocationService() async {
|
||||
String webhookId = ConnectionManager().webhookId;
|
||||
String httpWebHost = ConnectionManager().httpWebHost;
|
||||
if (webhookId != null && webhookId.isNotEmpty) {
|
||||
Duration interval;
|
||||
int delayFactor;
|
||||
int taskCount;
|
||||
Logger.d("Starting location update for every ${_updateInterval
|
||||
.inMinutes} minutes...");
|
||||
if (_updateInterval.inMinutes == 10) {
|
||||
interval = Duration(minutes: 20);
|
||||
taskCount = 2;
|
||||
delayFactor = 10;
|
||||
} else if (_updateInterval.inMinutes == 5) {
|
||||
interval = Duration(minutes: 15);
|
||||
taskCount = 3;
|
||||
delayFactor = 5;
|
||||
} else {
|
||||
interval = _updateInterval;
|
||||
taskCount = 1;
|
||||
delayFactor = 0;
|
||||
}
|
||||
for (int i = 1; i <= taskCount; i++) {
|
||||
int delay = i*delayFactor;
|
||||
Logger.d("Scheduling location update task #$i for every ${interval.inMinutes} minutes in $delay minutes...");
|
||||
await workManager.Workmanager.registerPeriodicTask(
|
||||
"$backgroundTaskId$i",
|
||||
"haClientLocationTracking-0$i",
|
||||
tag: backgroundTaskTag,
|
||||
inputData: {
|
||||
"webhookId": webhookId,
|
||||
"httpWebHost": httpWebHost
|
||||
},
|
||||
frequency: interval,
|
||||
initialDelay: Duration(minutes: delay),
|
||||
existingWorkPolicy: workManager.ExistingWorkPolicy.keep,
|
||||
backoffPolicy: workManager.BackoffPolicy.linear,
|
||||
backoffPolicyDelay: interval,
|
||||
constraints: workManager.Constraints(
|
||||
networkType: workManager.NetworkType.connected,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_stopLocationService() async {
|
||||
Logger.d("Canceling previous schedule if any...");
|
||||
await workManager.Workmanager.cancelAll();
|
||||
}
|
||||
|
||||
updateDeviceLocation() async {
|
||||
Logger.d("[Foreground location] Started");
|
||||
Geolocator geolocator = Geolocator();
|
||||
var battery = Battery();
|
||||
String webhookId = ConnectionManager().webhookId;
|
||||
String httpWebHost = ConnectionManager().httpWebHost;
|
||||
if (webhookId != null && webhookId.isNotEmpty) {
|
||||
Logger.d("[Foreground location] Getting battery level...");
|
||||
int batteryLevel = await battery.batteryLevel;
|
||||
Logger.d("[Foreground location] Getting device location...");
|
||||
Position position = await geolocator.getCurrentPosition(
|
||||
desiredAccuracy: LocationAccuracy.high,
|
||||
locationPermissionLevel: GeolocationPermission.locationAlways
|
||||
);
|
||||
if (position != null) {
|
||||
Logger.d("[Foreground location] Location: ${position.latitude} ${position.longitude}. Accuracy: ${position.accuracy}. (${position.timestamp})");
|
||||
String url = "$httpWebHost/api/webhook/$webhookId";
|
||||
Map data = {
|
||||
"type": "update_location",
|
||||
"data": {
|
||||
"gps": [position.latitude, position.longitude],
|
||||
"gps_accuracy": position.accuracy,
|
||||
"battery": batteryLevel ?? 100
|
||||
}
|
||||
};
|
||||
Logger.d("[Foreground location] Sending data home...");
|
||||
var response = await http.post(
|
||||
url,
|
||||
headers: {"Content-Type": "application/json"},
|
||||
body: json.encode(data)
|
||||
);
|
||||
Logger.d("[Foreground location] Got HTTP ${response.statusCode}");
|
||||
} else {
|
||||
Logger.d("[Foreground location] No location. Aborting.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
void updateDeviceLocationIsolate() {
|
||||
workManager.Workmanager.executeTask((backgroundTask, data) async {
|
||||
//print("[Background $backgroundTask] Started");
|
||||
Geolocator geolocator = Geolocator();
|
||||
var battery = Battery();
|
||||
String webhookId = data["webhookId"];
|
||||
String httpWebHost = data["httpWebHost"];
|
||||
//String logData = '==> ${DateTime.now()} [Background $backgroundTask]:';
|
||||
//print("[Background $backgroundTask] Getting path for log file...");
|
||||
//final logFileDirectory = await getExternalStorageDirectory();
|
||||
//print("[Background $backgroundTask] Opening log file...");
|
||||
//File logFile = File('${logFileDirectory.path}/ha-client-background-log.txt');
|
||||
//print("[Background $backgroundTask] Log file path: ${logFile.path}");
|
||||
if (webhookId != null && webhookId.isNotEmpty) {
|
||||
String url = "$httpWebHost/api/webhook/$webhookId";
|
||||
Map<String, String> headers = {};
|
||||
headers["Content-Type"] = "application/json";
|
||||
Map data = {
|
||||
"type": "update_location",
|
||||
"data": {
|
||||
"gps": [],
|
||||
"gps_accuracy": 0,
|
||||
"battery": 100
|
||||
}
|
||||
};
|
||||
//print("[Background $backgroundTask] Getting battery level...");
|
||||
int batteryLevel;
|
||||
try {
|
||||
batteryLevel = await battery.batteryLevel;
|
||||
//print("[Background $backgroundTask] Got battery level: $batteryLevel");
|
||||
} catch(e) {
|
||||
//print("[Background $backgroundTask] Error getting battery level: $e. Setting zero");
|
||||
batteryLevel = 0;
|
||||
//logData += 'Battery: error, $e';
|
||||
}
|
||||
if (batteryLevel != null) {
|
||||
data["data"]["battery"] = batteryLevel;
|
||||
//logData += 'Battery: success, $batteryLevel';
|
||||
}/* else {
|
||||
logData += 'Battery: error, level is null';
|
||||
}*/
|
||||
Position location;
|
||||
try {
|
||||
location = await geolocator.getCurrentPosition(desiredAccuracy: LocationAccuracy.high, locationPermissionLevel: GeolocationPermission.locationAlways);
|
||||
if (location != null && location.latitude != null) {
|
||||
//logData += ' || Location: success, ${location.latitude} ${location.longitude} (${location.timestamp})';
|
||||
data["data"]["gps"] = [location.latitude, location.longitude];
|
||||
data["data"]["gps_accuracy"] = location.accuracy;
|
||||
try {
|
||||
http.Response response = await http.post(
|
||||
url,
|
||||
headers: headers,
|
||||
body: json.encode(data)
|
||||
);
|
||||
/*if (response.statusCode >= 200 && response.statusCode < 300) {
|
||||
logData += ' || Post: success, ${response.statusCode}';
|
||||
} else {
|
||||
logData += ' || Post: error, ${response.statusCode}';
|
||||
}*/
|
||||
} catch(e) {
|
||||
//logData += ' || Post: error, $e';
|
||||
}
|
||||
}/* else {
|
||||
logData += ' || Location: error, location is null';
|
||||
}*/
|
||||
} catch (e) {
|
||||
//print("[Background $backgroundTask] Location error: $e");
|
||||
//logData += ' || Location: error, $e';
|
||||
}
|
||||
}/* else {
|
||||
logData += 'Not configured';
|
||||
}*/
|
||||
//print("[Background $backgroundTask] Writing log data...");
|
||||
/*try {
|
||||
var fileMode;
|
||||
if (logFile.existsSync() && logFile.lengthSync() < 5000000) {
|
||||
fileMode = FileMode.append;
|
||||
} else {
|
||||
fileMode = FileMode.write;
|
||||
}
|
||||
await logFile.writeAsString('$logData\n', mode: fileMode);
|
||||
} catch (e) {
|
||||
print("[Background $backgroundTask] Error writing log: $e");
|
||||
}
|
||||
print("[Background $backgroundTask] Finished.");*/
|
||||
return true;
|
||||
});
|
||||
}
|
@ -4,18 +4,20 @@ class MobileAppIntegrationManager {
|
||||
|
||||
static final _appRegistrationData = {
|
||||
"app_version": "$appVersion",
|
||||
"device_name": "${HomeAssistant().userName}'s ${DeviceInfoManager().model}",
|
||||
"device_name": "",
|
||||
"manufacturer": DeviceInfoManager().manufacturer,
|
||||
"model": DeviceInfoManager().model,
|
||||
"os_version": DeviceInfoManager().osVersion,
|
||||
"app_data": {
|
||||
"push_token": "${HomeAssistant().fcmToken}",
|
||||
"push_token": "",
|
||||
"push_url": "https://us-central1-ha-client-c73c4.cloudfunctions.net/sendPushNotification"
|
||||
}
|
||||
};
|
||||
|
||||
static Future checkAppRegistration({bool forceRegister: false, bool showOkDialog: false}) {
|
||||
Completer completer = Completer();
|
||||
_appRegistrationData["device_name"] = "${HomeAssistant().userName}'s ${DeviceInfoManager().model}";
|
||||
(_appRegistrationData["app_data"] as Map)["push_token"] = "${HomeAssistant().fcmToken}";
|
||||
if (ConnectionManager().webhookId == null || forceRegister) {
|
||||
Logger.d("Mobile app was not registered yet or need to be reseted. Registering...");
|
||||
var registrationData = Map.from(_appRegistrationData);
|
||||
@ -35,6 +37,7 @@ class MobileAppIntegrationManager {
|
||||
SharedPreferences.getInstance().then((prefs) {
|
||||
prefs.setString("app-webhook-id", responseObject["webhook_id"]);
|
||||
ConnectionManager().webhookId = responseObject["webhook_id"];
|
||||
|
||||
completer.complete();
|
||||
eventBus.fire(ShowPopupDialogEvent(
|
||||
title: "Mobile app Integration was created",
|
||||
@ -42,7 +45,7 @@ class MobileAppIntegrationManager {
|
||||
positiveText: "Restart now",
|
||||
negativeText: "Later",
|
||||
onPositive: () {
|
||||
ConnectionManager().callService(domain: "homeassistant", service: "restart", entityId: null);
|
||||
ConnectionManager().callService(domain: "homeassistant", service: "restart");
|
||||
},
|
||||
));
|
||||
});
|
||||
@ -78,7 +81,7 @@ class MobileAppIntegrationManager {
|
||||
}
|
||||
completer.complete();
|
||||
}).catchError((e) {
|
||||
if (e['code'] != null && e['code'] == 410) {
|
||||
if (e is http.Response && e.statusCode == 410) {
|
||||
Logger.e("MobileApp integration was removed");
|
||||
_askToRegisterApp();
|
||||
} else {
|
||||
|
@ -9,16 +9,21 @@ class StartupUserMessagesManager {
|
||||
return _instance;
|
||||
}
|
||||
|
||||
StartupUserMessagesManager._internal() {}
|
||||
StartupUserMessagesManager._internal();
|
||||
|
||||
bool _supportAppDevelopmentMessageShown;
|
||||
bool _whatsNewMessageShown;
|
||||
static final _supportAppDevelopmentMessageKey = "user-message-shown-support-development_3";
|
||||
static final _whatsNewMessageKey = "user-message-shown-whats-new-884";
|
||||
|
||||
void checkMessagesToShow() async {
|
||||
SharedPreferences prefs = await SharedPreferences.getInstance();
|
||||
await prefs.reload();
|
||||
_supportAppDevelopmentMessageShown = prefs.getBool(_supportAppDevelopmentMessageKey) ?? false;
|
||||
if (!_supportAppDevelopmentMessageShown) {
|
||||
_whatsNewMessageShown = prefs.getBool(_whatsNewMessageKey) ?? false;
|
||||
if (!_whatsNewMessageShown) {
|
||||
_showWhatsNewMessage();
|
||||
} else if (!_supportAppDevelopmentMessageShown) {
|
||||
_showSupportAppDevelopmentMessage();
|
||||
}
|
||||
}
|
||||
@ -43,4 +48,11 @@ class StartupUserMessagesManager {
|
||||
));
|
||||
}
|
||||
|
||||
void _showWhatsNewMessage() {
|
||||
SharedPreferences.getInstance().then((prefs) {
|
||||
prefs.setBool(_whatsNewMessageKey, true);
|
||||
eventBus.fire(ShowPageEvent(path: "/whats-new"));
|
||||
});
|
||||
}
|
||||
|
||||
}
|
3861
lib/mdi.class.dart
3861
lib/mdi.class.dart
File diff suppressed because it is too large
Load Diff
@ -10,49 +10,63 @@ class EntityViewPage extends StatefulWidget {
|
||||
}
|
||||
|
||||
class _EntityViewPageState extends State<EntityViewPage> {
|
||||
String _title;
|
||||
StreamSubscription _refreshDataSubscription;
|
||||
StreamSubscription _stateSubscription;
|
||||
Entity entity;
|
||||
Entity forwardToMainPage;
|
||||
bool _popScheduled = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_stateSubscription = eventBus.on<StateChangedEvent>().listen((event) {
|
||||
if (event.entityId == widget.entityId) {
|
||||
Logger.d("State change event handled by entity page: ${event.entityId}");
|
||||
entity = HomeAssistant().entities.get(widget.entityId);
|
||||
Logger.d("[Entity page] State change event handled: ${event.entityId}");
|
||||
setState(() {});
|
||||
}
|
||||
});
|
||||
_refreshDataSubscription = eventBus.on<RefreshDataFinishedEvent>().listen((event) {
|
||||
entity = HomeAssistant().entities.get(widget.entityId);
|
||||
setState(() {});
|
||||
});
|
||||
_prepareData();
|
||||
entity = HomeAssistant().entities.get(widget.entityId);
|
||||
}
|
||||
|
||||
void _prepareData() async {
|
||||
_title = HomeAssistant().entities.get(widget.entityId).displayName;
|
||||
}
|
||||
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
Widget body;
|
||||
if (MediaQuery.of(context).size.width >= Sizes.tabletMinWidth) {
|
||||
if (!_popScheduled) {
|
||||
_popScheduled = true;
|
||||
_popAfterBuild();
|
||||
}
|
||||
body = PageLoadingIndicator();
|
||||
} else {
|
||||
body = EntityPageLayout(entity: entity);
|
||||
}
|
||||
return new Scaffold(
|
||||
appBar: new AppBar(
|
||||
leading: IconButton(icon: Icon(Icons.arrow_back), onPressed: (){
|
||||
Navigator.pop(context);
|
||||
}),
|
||||
// Here we take the value from the MyHomePage object that was created by
|
||||
// the App.build method, and use it to set our appbar title.
|
||||
title: new Text(_title),
|
||||
title: new Text("${entity.displayName}"),
|
||||
),
|
||||
body: HomeAssistant().entities.get(widget.entityId).buildEntityPageWidget(context),
|
||||
body: body,
|
||||
);
|
||||
}
|
||||
|
||||
_popAfterBuild() async {
|
||||
forwardToMainPage = entity;
|
||||
await Future.delayed(Duration(milliseconds: 300));
|
||||
Navigator.of(context).pop();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose(){
|
||||
if (_stateSubscription != null) _stateSubscription.cancel();
|
||||
if (_refreshDataSubscription != null) _refreshDataSubscription.cancel();
|
||||
eventBus.fire(ShowEntityPageEvent(entity: forwardToMainPage));
|
||||
super.dispose();
|
||||
}
|
||||
}
|
18
lib/pages/fullscreen.page.dart
Normal file
18
lib/pages/fullscreen.page.dart
Normal file
@ -0,0 +1,18 @@
|
||||
part of '../main.dart';
|
||||
|
||||
class FullScreenPage extends StatelessWidget {
|
||||
|
||||
final Widget child;
|
||||
|
||||
const FullScreenPage({Key key, this.child}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
color: Colors.black,
|
||||
child: Center(
|
||||
child: this.child,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
200
lib/pages/integration_settings.page.dart
Normal file
200
lib/pages/integration_settings.page.dart
Normal file
@ -0,0 +1,200 @@
|
||||
part of '../main.dart';
|
||||
|
||||
class IntegrationSettingsPage extends StatefulWidget {
|
||||
IntegrationSettingsPage({Key key, this.title}) : super(key: key);
|
||||
|
||||
final String title;
|
||||
|
||||
@override
|
||||
_IntegrationSettingsPageState createState() => new _IntegrationSettingsPageState();
|
||||
}
|
||||
|
||||
class _IntegrationSettingsPageState extends State<IntegrationSettingsPage> {
|
||||
|
||||
int _locationInterval = LocationManager().defaultUpdateIntervalMinutes;
|
||||
bool _locationTrackingEnabled = false;
|
||||
bool _wait = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_loadSettings();
|
||||
|
||||
}
|
||||
|
||||
_loadSettings() async {
|
||||
SharedPreferences prefs = await SharedPreferences.getInstance();
|
||||
await prefs.reload();
|
||||
SharedPreferences.getInstance().then((prefs) {
|
||||
setState(() {
|
||||
_locationTrackingEnabled = prefs.getBool("location-enabled") ?? false;
|
||||
_locationInterval = prefs.getInt("location-interval") ?? LocationManager().defaultUpdateIntervalMinutes;
|
||||
if (_locationInterval % 5 != 0) {
|
||||
_locationInterval = 5 * (_locationInterval ~/ 5);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
void incLocationInterval() {
|
||||
if (_locationInterval < 720) {
|
||||
setState(() {
|
||||
_locationInterval = _locationInterval + 5;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
void decLocationInterval() {
|
||||
if (_locationInterval > 5) {
|
||||
setState(() {
|
||||
_locationInterval = _locationInterval - 5;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
restart() {
|
||||
eventBus.fire(ShowPopupDialogEvent(
|
||||
title: "Are you sure you want to restart Home Assistant?",
|
||||
body: "This will restart your Home Assistant server.",
|
||||
positiveText: "Sure. Make it so",
|
||||
negativeText: "What?? No!",
|
||||
onPositive: () {
|
||||
ConnectionManager().callService(domain: "homeassistant", service: "restart");
|
||||
},
|
||||
));
|
||||
}
|
||||
|
||||
stop() {
|
||||
eventBus.fire(ShowPopupDialogEvent(
|
||||
title: "Are you sure you want to STOP Home Assistant?",
|
||||
body: "This will STOP your Home Assistant server. It means that your web interface as well as HA Client will not work untill you'll find a way to start your server using ssh or something.",
|
||||
positiveText: "Sure. Make it so",
|
||||
negativeText: "What?? No!",
|
||||
onPositive: () {
|
||||
ConnectionManager().callService(domain: "homeassistant", service: "stop");
|
||||
},
|
||||
));
|
||||
}
|
||||
|
||||
updateRegistration() {
|
||||
MobileAppIntegrationManager.checkAppRegistration(showOkDialog: true);
|
||||
}
|
||||
|
||||
resetRegistration() {
|
||||
eventBus.fire(ShowPopupDialogEvent(
|
||||
title: "Waaaait",
|
||||
body: "If you don't whant to have duplicate integrations and entities in your HA for your current device, first you need to remove MobileApp integration from Integration settings in HA and restart server.",
|
||||
positiveText: "Done it already",
|
||||
negativeText: "Ok, I will",
|
||||
onPositive: () {
|
||||
MobileAppIntegrationManager.checkAppRegistration(showOkDialog: true, forceRegister: true);
|
||||
},
|
||||
));
|
||||
}
|
||||
|
||||
_switchLocationTrackingState(bool state) async {
|
||||
if (state) {
|
||||
await LocationManager().updateDeviceLocation();
|
||||
}
|
||||
await LocationManager().setSettings(_locationTrackingEnabled, _locationInterval);
|
||||
setState(() {
|
||||
_wait = false;
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return new Scaffold(
|
||||
appBar: new AppBar(
|
||||
leading: IconButton(icon: Icon(Icons.arrow_back), onPressed: (){
|
||||
Navigator.pop(context);
|
||||
}),
|
||||
title: new Text(widget.title),
|
||||
),
|
||||
body: ListView(
|
||||
scrollDirection: Axis.vertical,
|
||||
padding: const EdgeInsets.all(20.0),
|
||||
children: <Widget>[
|
||||
Text("Location tracking", style: TextStyle(fontSize: Sizes.largeFontSize-2)),
|
||||
Container(height: Sizes.rowPadding,),
|
||||
InkWell(
|
||||
onTap: () => Launcher.launchURLInCustomTab(context: context, url: "http://ha-client.app/docs#location-tracking"),
|
||||
child: Text(
|
||||
"Please read documentation!",
|
||||
style: TextStyle(
|
||||
color: Colors.blue,
|
||||
fontSize: 16,
|
||||
decoration: TextDecoration.underline
|
||||
)
|
||||
),
|
||||
),
|
||||
Container(height: Sizes.rowPadding,),
|
||||
Row(
|
||||
children: <Widget>[
|
||||
Text("Enable device location tracking"),
|
||||
Switch(
|
||||
value: _locationTrackingEnabled,
|
||||
onChanged: _wait ? null : (value) {
|
||||
setState(() {
|
||||
_locationTrackingEnabled = value;
|
||||
_wait = true;
|
||||
});
|
||||
_switchLocationTrackingState(value);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
Container(height: Sizes.rowPadding,),
|
||||
Text("Location update interval in minutes:"),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
children: <Widget>[
|
||||
//Expanded(child: Container(),),
|
||||
FlatButton(
|
||||
padding: EdgeInsets.all(0.0),
|
||||
child: Text("-", style: TextStyle(fontSize: Sizes.largeFontSize)),
|
||||
onPressed: () => decLocationInterval(),
|
||||
),
|
||||
Text("$_locationInterval", style: TextStyle(fontSize: Sizes.largeFontSize)),
|
||||
FlatButton(
|
||||
padding: EdgeInsets.all(0.0),
|
||||
child: Text("+", style: TextStyle(fontSize: Sizes.largeFontSize)),
|
||||
onPressed: () => incLocationInterval(),
|
||||
),
|
||||
],
|
||||
),
|
||||
Divider(),
|
||||
Text("Integration status", style: TextStyle(fontSize: Sizes.largeFontSize-2)),
|
||||
Container(height: Sizes.rowPadding,),
|
||||
Text("${HomeAssistant().userName}'s ${DeviceInfoManager().model}, ${DeviceInfoManager().osName} ${DeviceInfoManager().osVersion}"),
|
||||
Container(height: 6.0,),
|
||||
Text("Here you can manually check if HA Client integration with your Home Assistant works fine. As mobileApp integration in Home Assistant is still in development, this is not 100% correct check."),
|
||||
//Divider(),
|
||||
Row(
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
children: <Widget>[
|
||||
RaisedButton(
|
||||
color: Colors.blue,
|
||||
onPressed: () => updateRegistration(),
|
||||
child: Text("Check integration", style: TextStyle(color: Colors.white))
|
||||
),
|
||||
Container(width: 10.0,),
|
||||
RaisedButton(
|
||||
color: Colors.redAccent,
|
||||
onPressed: () => resetRegistration(),
|
||||
child: Text("Reset integration", style: TextStyle(color: Colors.white))
|
||||
)
|
||||
],
|
||||
),
|
||||
]
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
LocationManager().setSettings(_locationTrackingEnabled, _locationInterval);
|
||||
super.dispose();
|
||||
}
|
||||
}
|
@ -1,4 +1,4 @@
|
||||
part of '../main.dart';
|
||||
part of '../../main.dart';
|
||||
|
||||
class MainPage extends StatefulWidget {
|
||||
MainPage({Key key, this.title}) : super(key: key);
|
||||
@ -11,8 +11,8 @@ class MainPage extends StatefulWidget {
|
||||
|
||||
class _MainPageState extends State<MainPage> with WidgetsBindingObserver, TickerProviderStateMixin {
|
||||
|
||||
StreamSubscription<List<PurchaseDetails>> _subscription;
|
||||
StreamSubscription _stateSubscription;
|
||||
StreamSubscription _lovelaceSubscription;
|
||||
StreamSubscription _settingsSubscription;
|
||||
StreamSubscription _serviceCallSubscription;
|
||||
StreamSubscription _showEntityPageSubscription;
|
||||
@ -25,14 +25,10 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver, Ticker
|
||||
int _previousViewCount;
|
||||
bool _showLoginButton = false;
|
||||
bool _preventAppRefresh = false;
|
||||
Entity _entityToShow;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
final Stream purchaseUpdates =
|
||||
InAppPurchaseConnection.instance.purchaseUpdatedStream;
|
||||
_subscription = purchaseUpdates.listen((purchases) {
|
||||
_handlePurchaseUpdates(purchases);
|
||||
});
|
||||
super.initState();
|
||||
WidgetsBinding.instance.addObserver(this);
|
||||
|
||||
@ -72,8 +68,6 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver, Ticker
|
||||
});
|
||||
|
||||
_fullLoad();
|
||||
|
||||
|
||||
}
|
||||
|
||||
Future onSelectNotification(String payload) async {
|
||||
@ -97,37 +91,42 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver, Ticker
|
||||
);
|
||||
}
|
||||
|
||||
void _fullLoad() async {
|
||||
void _fullLoad() {
|
||||
_showInfoBottomBar(progress: true,);
|
||||
_subscribe().then((_) {
|
||||
ConnectionManager().init(loadSettings: true, forceReconnect: true).then((__){
|
||||
_fetchData();
|
||||
SharedPreferences.getInstance().then((prefs) {
|
||||
HomeAssistant().lovelaceDashboardUrl = prefs.getString('lovelace_dashboard_url') ?? HomeAssistant.DEFAULT_DASHBOARD;
|
||||
_fetchData(useCache: true);
|
||||
LocationManager();
|
||||
StartupUserMessagesManager().checkMessagesToShow();
|
||||
});
|
||||
}, onError: (e) {
|
||||
_setErrorState(e);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
void _quickLoad() {
|
||||
void _quickLoad({bool uiOnly: false}) {
|
||||
_hideBottomBar();
|
||||
_showInfoBottomBar(progress: true,);
|
||||
ConnectionManager().init(loadSettings: false, forceReconnect: false).then((_){
|
||||
_fetchData();
|
||||
//StartupUserMessagesManager().checkMessagesToShow();
|
||||
_fetchData(useCache: false, uiOnly: uiOnly);
|
||||
}, onError: (e) {
|
||||
_setErrorState(e);
|
||||
});
|
||||
}
|
||||
|
||||
_fetchData() async {
|
||||
await HomeAssistant().fetchData().then((_) {
|
||||
_fetchData({useCache: false, uiOnly: false}) async {
|
||||
if (useCache && !uiOnly) {
|
||||
HomeAssistant().fetchDataFromCache().then((_) {
|
||||
setState((){});
|
||||
});
|
||||
}
|
||||
await HomeAssistant().fetchData(uiOnly).then((_) {
|
||||
_hideBottomBar();
|
||||
int currentViewCount = HomeAssistant().ui?.views?.length ?? 0;
|
||||
if (_previousViewCount != currentViewCount) {
|
||||
Logger.d("Views count changed ($_previousViewCount->$currentViewCount). Creating new tabs controller.");
|
||||
_viewsTabController = TabController(vsync: this, length: currentViewCount);
|
||||
_previousViewCount = currentViewCount;
|
||||
if (_entityToShow != null) {
|
||||
_entityToShow = HomeAssistant().entities.get(_entityToShow.entityId);
|
||||
}
|
||||
}).catchError((e) {
|
||||
if (e is HAError) {
|
||||
@ -144,40 +143,32 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver, Ticker
|
||||
Logger.d("$state");
|
||||
if (state == AppLifecycleState.resumed && ConnectionManager().settingsLoaded && !_preventAppRefresh) {
|
||||
_quickLoad();
|
||||
}
|
||||
}
|
||||
|
||||
void _handlePurchaseUpdates(purchase) {
|
||||
if (purchase is List<PurchaseDetails>) {
|
||||
if (purchase[0].status == PurchaseStatus.purchased) {
|
||||
eventBus.fire(ShowPopupMessageEvent(
|
||||
title: "Thanks a lot!",
|
||||
body: "Thank you for supporting HA Client development!",
|
||||
buttonText: "Ok"
|
||||
));
|
||||
} else {
|
||||
Logger.d("Purchase change handler: ${purchase[0].status}");
|
||||
}
|
||||
} else {
|
||||
Logger.e("Something wrong with purchase handling. Got: $purchase");
|
||||
} else if (state == AppLifecycleState.paused && ConnectionManager().settingsLoaded && !_preventAppRefresh) {
|
||||
HomeAssistant().saveCache();
|
||||
}
|
||||
}
|
||||
|
||||
Future _subscribe() {
|
||||
Completer completer = Completer();
|
||||
|
||||
if (_stateSubscription == null) {
|
||||
_stateSubscription = eventBus.on<StateChangedEvent>().listen((event) {
|
||||
if (event.needToRebuildUI) {
|
||||
Logger.d("New entity. Need to rebuild UI");
|
||||
Logger.d("Need to rebuild UI");
|
||||
_quickLoad();
|
||||
} else {
|
||||
setState(() {});
|
||||
}
|
||||
});
|
||||
}
|
||||
if (_lovelaceSubscription == null) {
|
||||
_lovelaceSubscription = eventBus.on<LovelaceChangedEvent>().listen((event) {
|
||||
_quickLoad();
|
||||
});
|
||||
}
|
||||
if (_reloadUISubscription == null) {
|
||||
_reloadUISubscription = eventBus.on<ReloadUIEvent>().listen((event){
|
||||
_quickLoad();
|
||||
_quickLoad(uiOnly: true);
|
||||
});
|
||||
}
|
||||
if (_showPopupDialogSubscription == null) {
|
||||
@ -205,16 +196,15 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver, Ticker
|
||||
}
|
||||
if (_serviceCallSubscription == null) {
|
||||
_serviceCallSubscription =
|
||||
eventBus.on<ServiceCallEvent>().listen((event) {
|
||||
_callService(event.domain, event.service, event.entityId,
|
||||
event.additionalParams);
|
||||
eventBus.on<NotifyServiceCallEvent>().listen((event) {
|
||||
_notifyServiceCalled(event.domain, event.service, event.entityId);
|
||||
});
|
||||
}
|
||||
|
||||
if (_showEntityPageSubscription == null) {
|
||||
_showEntityPageSubscription =
|
||||
eventBus.on<ShowEntityPageEvent>().listen((event) {
|
||||
_showEntityPage(event.entity.entityId);
|
||||
_showEntityPage(event.entity?.entityId);
|
||||
});
|
||||
}
|
||||
|
||||
@ -254,7 +244,7 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver, Ticker
|
||||
|
||||
void _showOAuth() {
|
||||
_preventAppRefresh = true;
|
||||
Navigator.of(context).pushNamed('/login');
|
||||
Navigator.of(context).pushNamed("/auth", arguments: {"url": ConnectionManager().oauthUrl});
|
||||
}
|
||||
|
||||
_setErrorState(HAError e) {
|
||||
@ -304,22 +294,28 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver, Ticker
|
||||
);
|
||||
}
|
||||
|
||||
//TODO remove this shit
|
||||
void _callService(String domain, String service, String entityId, Map additionalParams) {
|
||||
void _notifyServiceCalled(String domain, String service, entityId) {
|
||||
_showInfoBottomBar(
|
||||
message: "Calling $domain.$service",
|
||||
duration: Duration(seconds: 3)
|
||||
duration: Duration(seconds: 4)
|
||||
);
|
||||
ConnectionManager().callService(domain: domain, service: service, entityId: entityId, additionalServiceData: additionalParams).catchError((e) => _setErrorState(e));
|
||||
}
|
||||
|
||||
void _showEntityPage(String entityId) {
|
||||
setState(() {
|
||||
_entityToShow = HomeAssistant().entities?.get(entityId);
|
||||
if (_entityToShow != null) {
|
||||
_mainScrollController?.jumpTo(0);
|
||||
}
|
||||
});
|
||||
/*if (_entityToShow!= null && MediaQuery.of(context).size.width < Sizes.tabletMinWidth) {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => EntityViewPage(entityId: entityId),
|
||||
)
|
||||
);
|
||||
}*/
|
||||
}
|
||||
|
||||
void _showPage(String path, bool goBackFirst) {
|
||||
@ -349,25 +345,7 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver, Ticker
|
||||
menuItems.add(
|
||||
UserAccountsDrawerHeader(
|
||||
accountName: Text(HomeAssistant().userName),
|
||||
accountEmail: Text(ConnectionManager().displayHostname ?? "Not configured"),
|
||||
onDetailsPressed: () {
|
||||
final flutterWebViewPlugin = new FlutterWebviewPlugin();
|
||||
flutterWebViewPlugin.onStateChanged.listen((viewState) async {
|
||||
if (viewState.type == WebViewState.startLoad) {
|
||||
Logger.d("[WebView] Injecting external auth JS");
|
||||
rootBundle.loadString('assets/js/externalAuth.js').then((js){
|
||||
flutterWebViewPlugin.evalJavascript(js.replaceFirst("[token]", ConnectionManager()._token));
|
||||
});
|
||||
}
|
||||
});
|
||||
Navigator.of(context).pushNamed(
|
||||
"/webview",
|
||||
arguments: {
|
||||
"url": "${ConnectionManager().httpWebHost}/profile?external_auth=1",
|
||||
"title": "Profile"
|
||||
}
|
||||
);
|
||||
},
|
||||
accountEmail: Text(HomeAssistant().locationName ?? ""),
|
||||
currentAccountPicture: CircleAvatar(
|
||||
child: Text(
|
||||
HomeAssistant().userAvatarText,
|
||||
@ -404,12 +382,20 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver, Ticker
|
||||
menuItems.addAll([
|
||||
Divider(),
|
||||
ListTile(
|
||||
leading: Icon(MaterialDesignIcons.getIconDataFromIconName("mdi:login-variant")),
|
||||
leading: Icon(MaterialDesignIcons.getIconDataFromIconName("mdi:server-network")),
|
||||
title: Text("Connection settings"),
|
||||
onTap: () {
|
||||
Navigator.of(context).pop();
|
||||
Navigator.of(context).pushNamed('/connection-settings');
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
leading: Icon(MaterialDesignIcons.getIconDataFromIconName("mdi:cellphone-settings-variant")),
|
||||
title: Text("Integration settings"),
|
||||
onTap: () {
|
||||
Navigator.of(context).pop();
|
||||
Navigator.of(context).pushNamed('/integration-settings');
|
||||
},
|
||||
)
|
||||
]);
|
||||
menuItems.addAll([
|
||||
@ -445,26 +431,33 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver, Ticker
|
||||
title: Text("Help"),
|
||||
onTap: () {
|
||||
Navigator.of(context).pop();
|
||||
Launcher.launchURL("http://ha-client.homemade.systems/docs");
|
||||
Launcher.launchURL("http://ha-client.app/docs");
|
||||
},
|
||||
),
|
||||
new ListTile(
|
||||
leading: Icon(MaterialDesignIcons.getIconDataFromIconName("mdi:discord")),
|
||||
title: Text("Join Discord channel"),
|
||||
leading: Icon(MaterialDesignIcons.getIconDataFromIconName("mdi:forum")),
|
||||
title: Text("Contacts/Discussion"),
|
||||
onTap: () {
|
||||
Navigator.of(context).pop();
|
||||
Launcher.launchURL("https://discord.gg/AUzEvwn");
|
||||
Launcher.launchURL("https://spectrum.chat/ha-client");
|
||||
},
|
||||
),
|
||||
new ListTile(
|
||||
title: Text("What's new?"),
|
||||
onTap: () {
|
||||
Navigator.of(context).pop();
|
||||
Navigator.of(context).pushNamed('/whats-new');
|
||||
}
|
||||
),
|
||||
new AboutListTile(
|
||||
aboutBoxChildren: <Widget>[
|
||||
GestureDetector(
|
||||
onTap: () {
|
||||
Navigator.of(context).pop();
|
||||
Launcher.launchURL("http://ha-client.homemade.systems/");
|
||||
Launcher.launchURL("http://ha-client.app/");
|
||||
},
|
||||
child: Text(
|
||||
"ha-client.homemade.systems",
|
||||
"ha-client.app",
|
||||
style: TextStyle(
|
||||
color: Colors.blue,
|
||||
decoration: TextDecoration.underline
|
||||
@ -477,7 +470,7 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver, Ticker
|
||||
GestureDetector(
|
||||
onTap: () {
|
||||
Navigator.of(context).pop();
|
||||
Launcher.launchURLInCustomTab(context: context, url: "http://ha-client.homemade.systems/terms_and_conditions");
|
||||
Launcher.launchURLInCustomTab(context: context, url: "http://ha-client.app/terms_and_conditions");
|
||||
},
|
||||
child: Text(
|
||||
"Terms and Conditions",
|
||||
@ -493,7 +486,7 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver, Ticker
|
||||
GestureDetector(
|
||||
onTap: () {
|
||||
Navigator.of(context).pop();
|
||||
Launcher.launchURLInCustomTab(context: context, url: "http://ha-client.homemade.systems/privacy_policy");
|
||||
Launcher.launchURLInCustomTab(context: context, url: "http://ha-client.app/privacy_policy");
|
||||
},
|
||||
child: Text(
|
||||
"Privacy Policy",
|
||||
@ -622,34 +615,131 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver, Ticker
|
||||
}
|
||||
|
||||
final GlobalKey<ScaffoldState> _scaffoldKey = new GlobalKey<ScaffoldState>();
|
||||
final ScrollController _mainScrollController = ScrollController();
|
||||
|
||||
Widget _buildScaffoldBody(bool empty) {
|
||||
List<PopupMenuItem<String>> popupMenuItems = [];
|
||||
List<PopupMenuItem<String>> serviceMenuItems = [];
|
||||
List<PopupMenuItem<String>> mediaMenuItems = [];
|
||||
|
||||
popupMenuItems.add(PopupMenuItem<String>(
|
||||
int currentViewCount = HomeAssistant().ui?.views?.length ?? 0;
|
||||
if (_previousViewCount != currentViewCount) {
|
||||
Logger.d("Views count changed ($_previousViewCount->$currentViewCount). Creating new tabs controller.");
|
||||
_viewsTabController = TabController(vsync: this, length: currentViewCount);
|
||||
_previousViewCount = currentViewCount;
|
||||
}
|
||||
|
||||
serviceMenuItems.add(PopupMenuItem<String>(
|
||||
child: new Text("Reload"),
|
||||
value: "reload",
|
||||
));
|
||||
List<Widget> emptyBody = [
|
||||
Text("."),
|
||||
];
|
||||
if (ConnectionManager().isAuthenticated) {
|
||||
_showLoginButton = false;
|
||||
popupMenuItems.add(
|
||||
serviceMenuItems.add(
|
||||
PopupMenuItem<String>(
|
||||
child: new Text("Logout"),
|
||||
value: "logout",
|
||||
));
|
||||
}
|
||||
Widget mediaMenuIcon;
|
||||
int playersCount = 0;
|
||||
if (!empty && !HomeAssistant().entities.isEmpty) {
|
||||
List<Entity> activePlayers = HomeAssistant().entities.getByDomains(includeDomains: ["media_player"], stateFiler: [EntityState.paused, EntityState.playing, EntityState.idle]);
|
||||
playersCount = activePlayers.length;
|
||||
mediaMenuItems.addAll(
|
||||
activePlayers.map((entity) => PopupMenuItem<String>(
|
||||
child: Text(
|
||||
"${entity.displayName}",
|
||||
style: TextStyle(
|
||||
color: EntityColor.stateColor(entity.state)
|
||||
),
|
||||
),
|
||||
value: "${entity.entityId}",
|
||||
)).toList()
|
||||
);
|
||||
}
|
||||
mediaMenuItems.addAll([
|
||||
PopupMenuItem<String>(
|
||||
child: new Text("Play media..."),
|
||||
value: "play_media",
|
||||
)
|
||||
]);
|
||||
if (playersCount > 0) {
|
||||
mediaMenuIcon = Stack(
|
||||
overflow: Overflow.visible,
|
||||
children: <Widget>[
|
||||
Icon(MaterialDesignIcons.getIconDataFromIconName(
|
||||
"mdi:television"), color: Colors.white,),
|
||||
Positioned(
|
||||
bottom: -4,
|
||||
right: -4,
|
||||
child: Container(
|
||||
height: 16,
|
||||
width: 16,
|
||||
decoration: new BoxDecoration(
|
||||
color: Colors.orange,
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: Center(
|
||||
child: Text("$playersCount", style: TextStyle(fontSize: 12)),
|
||||
),
|
||||
),
|
||||
)
|
||||
],
|
||||
);
|
||||
} else {
|
||||
mediaMenuIcon = Icon(MaterialDesignIcons.getIconDataFromIconName(
|
||||
"mdi:television"), color: Colors.white,);
|
||||
}
|
||||
Widget mainScrollBody;
|
||||
if (empty) {
|
||||
if (_showLoginButton) {
|
||||
emptyBody = [
|
||||
mainScrollBody = Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: <Widget>[
|
||||
FlatButton(
|
||||
child: Text("Login with Home Assistant", style: TextStyle(fontSize: 16.0, color: Colors.white)),
|
||||
color: Colors.blue,
|
||||
onPressed: () => _fullLoad(),
|
||||
)
|
||||
];
|
||||
]
|
||||
)
|
||||
);
|
||||
} else {
|
||||
mainScrollBody = Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: <Widget>[
|
||||
Text("...")
|
||||
]
|
||||
),
|
||||
);
|
||||
}
|
||||
} else {
|
||||
if (_entityToShow != null && MediaQuery.of(context).size.width >= Sizes.tabletMinWidth) {
|
||||
mainScrollBody = Flex(
|
||||
direction: Axis.horizontal,
|
||||
children: <Widget>[
|
||||
Expanded(
|
||||
child: HomeAssistant().ui.build(context, _viewsTabController),
|
||||
),
|
||||
Container(
|
||||
width: Sizes.mainPageScreenSeparatorWidth,
|
||||
color: Colors.blue,
|
||||
),
|
||||
ConstrainedBox(
|
||||
constraints: BoxConstraints.tightFor(width: Sizes.entityPageMaxWidth),
|
||||
child: EntityPageLayout(entity: _entityToShow, showClose: true,),
|
||||
)
|
||||
],
|
||||
);
|
||||
} else if (_entityToShow != null) {
|
||||
mainScrollBody = EntityPageLayout(entity: _entityToShow, showClose: true,);
|
||||
} else {
|
||||
mainScrollBody = HomeAssistant().ui.build(context, _viewsTabController);
|
||||
}
|
||||
}
|
||||
|
||||
return NestedScrollView(
|
||||
headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) {
|
||||
return <Widget>[
|
||||
@ -659,16 +749,34 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver, Ticker
|
||||
primary: true,
|
||||
title: Text(HomeAssistant().locationName ?? ""),
|
||||
actions: <Widget>[
|
||||
IconButton(
|
||||
icon: mediaMenuIcon,
|
||||
onPressed: () {
|
||||
showMenu(
|
||||
position: RelativeRect.fromLTRB(MediaQuery.of(context).size.width, 100.0, 50, 0.0),
|
||||
context: context,
|
||||
items: mediaMenuItems
|
||||
).then((String val) {
|
||||
if (val == "play_media") {
|
||||
Navigator.pushNamed(context, "/play-media", arguments: {"url": ""});
|
||||
} else {
|
||||
_showEntityPage(val);
|
||||
}
|
||||
});
|
||||
}
|
||||
),
|
||||
IconButton(
|
||||
icon: Icon(MaterialDesignIcons.getIconDataFromIconName(
|
||||
"mdi:dots-vertical"), color: Colors.white,),
|
||||
onPressed: () {
|
||||
showMenu(
|
||||
position: RelativeRect.fromLTRB(MediaQuery.of(context).size.width, 70.0, 0.0, 0.0),
|
||||
position: RelativeRect.fromLTRB(MediaQuery.of(context).size.width, 100, 0.0, 0.0),
|
||||
context: context,
|
||||
items: popupMenuItems
|
||||
items: serviceMenuItems
|
||||
).then((String val) {
|
||||
HomeAssistant().lovelaceDashboardUrl = HomeAssistant.DEFAULT_DASHBOARD;
|
||||
if (val == "reload") {
|
||||
|
||||
_quickLoad();
|
||||
} else if (val == "logout") {
|
||||
HomeAssistant().logout().then((_) {
|
||||
@ -685,7 +793,7 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver, Ticker
|
||||
_scaffoldKey.currentState.openDrawer();
|
||||
},
|
||||
),
|
||||
bottom: empty ? null : TabBar(
|
||||
bottom: (empty || _entityToShow != null) ? null : TabBar(
|
||||
controller: _viewsTabController,
|
||||
tabs: buildUIViewTabs(),
|
||||
isScrollable: true,
|
||||
@ -694,15 +802,8 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver, Ticker
|
||||
|
||||
];
|
||||
},
|
||||
body: empty ?
|
||||
Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: emptyBody
|
||||
),
|
||||
)
|
||||
:
|
||||
HomeAssistant().buildViews(context, _viewsTabController),
|
||||
body: mainScrollBody,
|
||||
controller: _mainScrollController,
|
||||
);
|
||||
}
|
||||
|
||||
@ -758,7 +859,6 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver, Ticker
|
||||
);
|
||||
}
|
||||
}
|
||||
// This method is rerun every time setState is called.
|
||||
if (HomeAssistant().isNoViews) {
|
||||
return Scaffold(
|
||||
key: _scaffoldKey,
|
||||
@ -768,23 +868,34 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver, Ticker
|
||||
body: _buildScaffoldBody(true)
|
||||
);
|
||||
} else {
|
||||
return Scaffold(
|
||||
return WillPopScope(
|
||||
child: Scaffold(
|
||||
key: _scaffoldKey,
|
||||
drawer: _buildAppDrawer(),
|
||||
primary: false,
|
||||
bottomNavigationBar: bottomBar,
|
||||
body: _buildScaffoldBody(false),
|
||||
body: _buildScaffoldBody(false)
|
||||
),
|
||||
onWillPop: () {
|
||||
if (_entityToShow != null) {
|
||||
eventBus.fire(ShowEntityPageEvent());
|
||||
return Future.value(false);
|
||||
} else {
|
||||
return Future.value(true);
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
final flutterWebviewPlugin = new FlutterWebviewPlugin();
|
||||
flutterWebviewPlugin.dispose();
|
||||
WidgetsBinding.instance.removeObserver(this);
|
||||
//final flutterWebviewPlugin = new FlutterWebviewPlugin();
|
||||
//flutterWebviewPlugin.dispose();
|
||||
_viewsTabController?.dispose();
|
||||
_stateSubscription?.cancel();
|
||||
_lovelaceSubscription?.cancel();
|
||||
_settingsSubscription?.cancel();
|
||||
_serviceCallSubscription?.cancel();
|
||||
_showPopupDialogSubscription?.cancel();
|
||||
@ -792,7 +903,6 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver, Ticker
|
||||
_showEntityPageSubscription?.cancel();
|
||||
_showErrorSubscription?.cancel();
|
||||
_startAuthSubscription?.cancel();
|
||||
_subscription?.cancel();
|
||||
_showPageSubscription?.cancel();
|
||||
_reloadUISubscription?.cancel();
|
||||
//TODO disconnect
|
249
lib/pages/play_media.page.dart
Normal file
249
lib/pages/play_media.page.dart
Normal file
@ -0,0 +1,249 @@
|
||||
part of '../main.dart';
|
||||
|
||||
class PlayMediaPage extends StatefulWidget {
|
||||
|
||||
final String mediaUrl;
|
||||
final String mediaType;
|
||||
|
||||
PlayMediaPage({Key key, this.mediaUrl, this.mediaType}) : super(key: key);
|
||||
|
||||
@override
|
||||
_PlayMediaPageState createState() => new _PlayMediaPageState();
|
||||
}
|
||||
|
||||
class _PlayMediaPageState extends State<PlayMediaPage> {
|
||||
|
||||
bool _loaded = false;
|
||||
String _error = "";
|
||||
String _validationMessage = "";
|
||||
List<Entity> _players;
|
||||
String _mediaUrl;
|
||||
String _contentType;
|
||||
bool _useMediaExtractor = false;
|
||||
bool _isMediaExtractorExist = false;
|
||||
StreamSubscription _stateSubscription;
|
||||
StreamSubscription _refreshDataSubscription;
|
||||
List<String> _contentTypes = ["movie", "video", "music", "image", "image/jpg", "playlist"];
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_mediaUrl = widget.mediaUrl;
|
||||
if (widget.mediaType.isNotEmpty) {
|
||||
if (!_contentTypes.contains(widget.mediaType)) {
|
||||
_contentTypes.insert(0, widget.mediaType);
|
||||
_contentType = _contentTypes[0];
|
||||
} else {
|
||||
_contentType = widget.mediaType;
|
||||
}
|
||||
} else {
|
||||
_contentType = _contentTypes[0];
|
||||
}
|
||||
_stateSubscription = eventBus.on<StateChangedEvent>().listen((event) {
|
||||
if (event.entityId.contains("media_player")) {
|
||||
Logger.d("State change event handled by play media page: ${event.entityId}");
|
||||
setState(() {});
|
||||
}
|
||||
});
|
||||
_refreshDataSubscription = eventBus.on<RefreshDataFinishedEvent>().listen((event) {
|
||||
_loadMediaEntities();
|
||||
});
|
||||
_loadMediaEntities();
|
||||
}
|
||||
|
||||
_loadMediaEntities() async {
|
||||
if (HomeAssistant().isNoEntities) {
|
||||
setState(() {
|
||||
_loaded = false;
|
||||
});
|
||||
} else {
|
||||
_isMediaExtractorExist = HomeAssistant().isServiceExist("media_extractor");
|
||||
//_useMediaExtractor = _isMediaExtractorExist;
|
||||
_players = HomeAssistant().entities.getByDomains(includeDomains: ["media_player"]);
|
||||
setState(() {
|
||||
if (_players.isNotEmpty) {
|
||||
_loaded = true;
|
||||
} else {
|
||||
_loaded = false;
|
||||
_error = "Looks like you don't have any media player";
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
void _playMedia(Entity entity) {
|
||||
if (_mediaUrl == null || _mediaUrl.isEmpty) {
|
||||
setState(() {
|
||||
_validationMessage = "Media url must be specified";
|
||||
});
|
||||
} else if (entity.state == EntityState.unavailable || entity.state == EntityState.unknown) {
|
||||
setState(() {
|
||||
_validationMessage = "${entity.displayName} is not available";
|
||||
});
|
||||
} else {
|
||||
String serviceDomain;
|
||||
if (_useMediaExtractor) {
|
||||
serviceDomain = "media_extractor";
|
||||
} else {
|
||||
serviceDomain = "media_player";
|
||||
}
|
||||
Navigator.pop(context);
|
||||
ConnectionManager().callService(
|
||||
domain: serviceDomain,
|
||||
service: "play_media",
|
||||
entityId: entity.entityId,
|
||||
data: {
|
||||
"media_content_id": _mediaUrl,
|
||||
"media_content_type": _contentType
|
||||
}
|
||||
);
|
||||
HomeAssistant().sendToPlayerId = entity.entityId;
|
||||
if (HomeAssistant().sendFromPlayerId != null && HomeAssistant().sendFromPlayerId != HomeAssistant().sendToPlayerId) {
|
||||
ConnectionManager().callService(
|
||||
domain: HomeAssistant().sendFromPlayerId.split(".")[0],
|
||||
service: "turn_off",
|
||||
entityId: HomeAssistant().sendFromPlayerId
|
||||
);
|
||||
HomeAssistant().sendFromPlayerId = null;
|
||||
}
|
||||
eventBus.fire(ShowEntityPageEvent(entity: entity));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
Widget body;
|
||||
if (!_loaded) {
|
||||
body = _error.isEmpty ? PageLoadingIndicator() : PageLoadingError(errorText: _error);
|
||||
} else {
|
||||
List<Widget> children = [];
|
||||
children.add(CardHeader(name: "Media:"));
|
||||
children.add(
|
||||
TextField(
|
||||
maxLines: 5,
|
||||
minLines: 1,
|
||||
decoration: InputDecoration(
|
||||
labelText: "Media url"
|
||||
),
|
||||
controller: TextEditingController.fromValue(TextEditingValue(text: _mediaUrl)),
|
||||
onChanged: (value) {
|
||||
_mediaUrl = value;
|
||||
}
|
||||
),
|
||||
);
|
||||
if (_validationMessage.isNotEmpty) {
|
||||
children.add(Text(
|
||||
"$_validationMessage",
|
||||
style: TextStyle(color: Colors.red)
|
||||
));
|
||||
}
|
||||
children.addAll(<Widget>[
|
||||
Container(height: Sizes.rowPadding,),
|
||||
DropdownButton<String>(
|
||||
value: _contentType,
|
||||
isExpanded: true,
|
||||
items: _contentTypes.map((String value) {
|
||||
return new DropdownMenuItem<String>(
|
||||
value: value,
|
||||
child: new Text(value),
|
||||
);
|
||||
}).toList(),
|
||||
onChanged: (value) {
|
||||
setState(() {
|
||||
_contentType = value;
|
||||
});
|
||||
},
|
||||
)
|
||||
]
|
||||
);
|
||||
if (_isMediaExtractorExist) {
|
||||
children.addAll(<Widget>[
|
||||
Row(
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
children: <Widget>[
|
||||
Expanded(
|
||||
child: Text("Use media extractor"),
|
||||
),
|
||||
Switch(
|
||||
value: _useMediaExtractor,
|
||||
onChanged: (value) => setState((){_useMediaExtractor = value;}),
|
||||
),
|
||||
],
|
||||
),
|
||||
Container(
|
||||
height: Sizes.rowPadding,
|
||||
)
|
||||
]
|
||||
);
|
||||
} else {
|
||||
children.addAll(<Widget>[
|
||||
Row(
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
children: <Widget>[
|
||||
Expanded(
|
||||
child: Text("You can use media extractor here"),
|
||||
),
|
||||
GestureDetector(
|
||||
onTap: () {
|
||||
Launcher.launchURLInCustomTab(
|
||||
context: context,
|
||||
url: "https://www.home-assistant.io/components/media_extractor/"
|
||||
);
|
||||
},
|
||||
child: Text(
|
||||
"How?",
|
||||
style: TextStyle(
|
||||
color: Colors.blue,
|
||||
decoration: TextDecoration.underline
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
Container(
|
||||
height: Sizes.doubleRowPadding,
|
||||
)
|
||||
]
|
||||
);
|
||||
}
|
||||
children.add(CardHeader(name: "Play on:"));
|
||||
children.addAll(
|
||||
_players.map((player) => InkWell(
|
||||
child: EntityModel(
|
||||
entityWrapper: EntityWrapper(entity: player),
|
||||
handleTap: false,
|
||||
child: Padding(
|
||||
padding: EdgeInsets.only(bottom: Sizes.doubleRowPadding),
|
||||
child: DefaultEntityContainer(state: player._buildStatePart(context)),
|
||||
)
|
||||
),
|
||||
onTap: () => _playMedia(player),
|
||||
))
|
||||
);
|
||||
body = ListView(
|
||||
padding: EdgeInsets.all(Sizes.leftWidgetPadding),
|
||||
scrollDirection: Axis.vertical,
|
||||
children: children
|
||||
);
|
||||
}
|
||||
return new Scaffold(
|
||||
appBar: new AppBar(
|
||||
leading: IconButton(icon: Icon(Icons.arrow_back), onPressed: (){
|
||||
Navigator.pop(context);
|
||||
}),
|
||||
title: new Text("Play media"),
|
||||
),
|
||||
body: body,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose(){
|
||||
HomeAssistant().sendFromPlayerId = null;
|
||||
_stateSubscription?.cancel();
|
||||
_refreshDataSubscription?.cancel();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
}
|
@ -75,10 +75,16 @@ class _ConnectionSettingsPageState extends State<ConnectionSettingsPage> {
|
||||
|
||||
_saveSettings() async {
|
||||
_newHassioDomain = _newHassioDomain.trim();
|
||||
if (_newHassioDomain.indexOf("http") == 0 && _newHassioDomain.indexOf("//") > 0) {
|
||||
if (_newHassioDomain.startsWith("http") && _newHassioDomain.indexOf("//") > 0) {
|
||||
_newHassioDomain.startsWith("https") ? _newSocketProtocol = "wss" : _newSocketProtocol = "ws";
|
||||
_newHassioDomain = _newHassioDomain.split("//")[1];
|
||||
}
|
||||
_newHassioDomain = _newHassioDomain.split("/")[0];
|
||||
if (_newHassioDomain.contains(":")) {
|
||||
List<String> domainAndPort = _newHassioDomain.split(":");
|
||||
_newHassioDomain = domainAndPort[0];
|
||||
_newHassioPort = domainAndPort[1];
|
||||
}
|
||||
SharedPreferences prefs = await SharedPreferences.getInstance();
|
||||
final storage = new FlutterSecureStorage();
|
||||
if (_newLongLivedToken.isNotEmpty) {
|
||||
|
69
lib/pages/whats_new.page.dart
Normal file
69
lib/pages/whats_new.page.dart
Normal file
@ -0,0 +1,69 @@
|
||||
part of '../main.dart';
|
||||
|
||||
class WhatsNewPage extends StatefulWidget {
|
||||
WhatsNewPage({Key key}) : super(key: key);
|
||||
|
||||
@override
|
||||
_WhatsNewPageState createState() => new _WhatsNewPageState();
|
||||
}
|
||||
|
||||
class _WhatsNewPageState extends State<WhatsNewPage> {
|
||||
|
||||
String data = "";
|
||||
String error = "";
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_loadData();
|
||||
}
|
||||
|
||||
_loadData() async {
|
||||
setState(() {
|
||||
data = "";
|
||||
error = "";
|
||||
});
|
||||
http.Response response;
|
||||
response = await http.get("http://ha-client.app/service/whats_new_0.8.md");
|
||||
if (response.statusCode == 200) {
|
||||
setState(() {
|
||||
data = response.body;
|
||||
});
|
||||
} else {
|
||||
setState(() {
|
||||
error = "Can't load changelog";
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
Widget body;
|
||||
if (error.isNotEmpty) {
|
||||
body = PageLoadingError(errorText: error,);
|
||||
} else if (data.isEmpty) {
|
||||
body = PageLoadingIndicator();
|
||||
} else {
|
||||
body = Markdown(
|
||||
data: data,
|
||||
);
|
||||
}
|
||||
return new Scaffold(
|
||||
appBar: new AppBar(
|
||||
leading: IconButton(icon: Icon(Icons.arrow_back), onPressed: (){
|
||||
Navigator.pop(context);
|
||||
}),
|
||||
actions: <Widget>[
|
||||
IconButton(
|
||||
icon: Icon(Icons.refresh),
|
||||
onPressed: () => _loadData(),
|
||||
)
|
||||
],
|
||||
// Here we take the value from the MyHomePage object that was created by
|
||||
// the App.build method, and use it to set our appbar title.
|
||||
title: new Text("What's new"),
|
||||
),
|
||||
body: body
|
||||
);
|
||||
}
|
||||
}
|
90
lib/pages/zha_page.dart
Normal file
90
lib/pages/zha_page.dart
Normal file
@ -0,0 +1,90 @@
|
||||
part of '../main.dart';
|
||||
|
||||
class ZhaPage extends StatefulWidget {
|
||||
ZhaPage({Key key}) : super(key: key);
|
||||
|
||||
@override
|
||||
_ZhaPageState createState() => new _ZhaPageState();
|
||||
}
|
||||
|
||||
class _ZhaPageState extends State<ZhaPage> {
|
||||
|
||||
List data = [];
|
||||
String error = "";
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_loadData();
|
||||
}
|
||||
|
||||
_loadData() async {
|
||||
setState(() {
|
||||
data.clear();
|
||||
error = "";
|
||||
});
|
||||
ConnectionManager().sendSocketMessage(
|
||||
type: 'zha_map/devices'
|
||||
).then((response){
|
||||
setState(() {
|
||||
data = response['devices'];
|
||||
});
|
||||
}).catchError((e){
|
||||
setState(() {
|
||||
error = '$e';
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
Widget body;
|
||||
if (error.isNotEmpty) {
|
||||
body = PageLoadingError(errorText: error,);
|
||||
} else if (data.isEmpty) {
|
||||
body = PageLoadingIndicator();
|
||||
} else {
|
||||
List<Widget> devicesListWindet = [];
|
||||
data.forEach((device) {
|
||||
devicesListWindet.add(
|
||||
Card(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: <Widget>[
|
||||
CardHeader(
|
||||
name: '${device['ieee']}',
|
||||
subtitle: Text('${device['manufacturer']}'),
|
||||
),
|
||||
Text('${device['device_type']}'),
|
||||
Text('model: ${device['model']}'),
|
||||
Text('offline: ${device['offline']}'),
|
||||
Text('neighbours: ${device['neighbours'].length}'),
|
||||
Text('raw: $device'),
|
||||
],
|
||||
),
|
||||
)
|
||||
);
|
||||
});
|
||||
body = ListView(
|
||||
children: devicesListWindet
|
||||
);
|
||||
}
|
||||
return new Scaffold(
|
||||
appBar: new AppBar(
|
||||
leading: IconButton(icon: Icon(Icons.arrow_back), onPressed: (){
|
||||
Navigator.pop(context);
|
||||
}),
|
||||
actions: <Widget>[
|
||||
IconButton(
|
||||
icon: Icon(Icons.refresh),
|
||||
onPressed: () => _loadData(),
|
||||
)
|
||||
],
|
||||
// Here we take the value from the MyHomePage object that was created by
|
||||
// the App.build method, and use it to set our appbar title.
|
||||
title: new Text('ZHA'),
|
||||
),
|
||||
body: body
|
||||
);
|
||||
}
|
||||
}
|
@ -14,101 +14,13 @@ class _ConfigPanelWidgetState extends State<ConfigPanelWidget> {
|
||||
super.initState();
|
||||
}
|
||||
|
||||
restart() {
|
||||
eventBus.fire(ShowPopupDialogEvent(
|
||||
title: "Are you sure you want to restart Home Assistant?",
|
||||
body: "This will restart your Home Assistant server.",
|
||||
positiveText: "Sure. Make it so",
|
||||
negativeText: "What?? No!",
|
||||
onPositive: () {
|
||||
ConnectionManager().callService(domain: "homeassistant", service: "restart", entityId: null);
|
||||
},
|
||||
));
|
||||
}
|
||||
|
||||
stop() {
|
||||
eventBus.fire(ShowPopupDialogEvent(
|
||||
title: "Are you sure you wanr to STOP Home Assistant?",
|
||||
body: "This will STOP your Home Assistant server. It means that your web interface as well as HA Client will not work untill you'll find a way to start your server using ssh or something.",
|
||||
positiveText: "Sure. Make it so",
|
||||
negativeText: "What?? No!",
|
||||
onPositive: () {
|
||||
ConnectionManager().callService(domain: "homeassistant", service: "stop", entityId: null);
|
||||
},
|
||||
));
|
||||
}
|
||||
|
||||
updateRegistration() {
|
||||
MobileAppIntegrationManager.checkAppRegistration(showOkDialog: true);
|
||||
}
|
||||
|
||||
resetRegistration() {
|
||||
eventBus.fire(ShowPopupDialogEvent(
|
||||
title: "Waaaait",
|
||||
body: "If you don't whant to have duplicate integrations and entities in your HA for your current device, first you need to remove MobileApp integration from Integration settings in HA and restart server.",
|
||||
positiveText: "Done it already",
|
||||
negativeText: "Ok, I will",
|
||||
onPositive: () {
|
||||
MobileAppIntegrationManager.checkAppRegistration(showOkDialog: true, forceRegister: true);
|
||||
},
|
||||
));
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ListView(
|
||||
children: [
|
||||
Card(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.only(left: Sizes.leftWidgetPadding, right: Sizes.rightWidgetPadding),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: <Widget>[
|
||||
ListTile(
|
||||
title: Text("Mobile app integration",
|
||||
textAlign: TextAlign.left,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: new TextStyle(fontWeight: FontWeight.bold, fontSize: Sizes.largeFontSize))
|
||||
),
|
||||
Text("Registration", style: TextStyle(fontSize: Sizes.largeFontSize-2)),
|
||||
Container(height: Sizes.rowPadding,),
|
||||
Text("${HomeAssistant().userName}'s ${DeviceInfoManager().model}, ${DeviceInfoManager().osName} ${DeviceInfoManager().osVersion}"),
|
||||
Container(height: 6.0,),
|
||||
Text("Here you can manually check if HA Client integration with your Home Assistant works fine. As mobileApp integration in Home Assistant is still in development, this is not 100% correct check."),
|
||||
//Divider(),
|
||||
Row(
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
children: <Widget>[
|
||||
RaisedButton(
|
||||
color: Colors.blue,
|
||||
onPressed: () => updateRegistration(),
|
||||
child: Text("Check registration", style: TextStyle(color: Colors.white))
|
||||
),
|
||||
Container(width: 10.0,),
|
||||
RaisedButton(
|
||||
color: Colors.redAccent,
|
||||
onPressed: () => resetRegistration(),
|
||||
child: Text("Reset registration", style: TextStyle(color: Colors.white))
|
||||
)
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
LinkToWebConfig(name: "Home Assistant Cloud", url: ConnectionManager().httpWebHost+"/config/cloud/account"),
|
||||
Container(height: 8.0,),
|
||||
LinkToWebConfig(name: "Integrations", url: ConnectionManager().httpWebHost+"/config/integrations/dashboard"),
|
||||
LinkToWebConfig(name: "Users", url: ConnectionManager().httpWebHost+"/config/users/picker"),
|
||||
Container(height: 8.0,),
|
||||
LinkToWebConfig(name: "General", url: ConnectionManager().httpWebHost+"/config/core"),
|
||||
LinkToWebConfig(name: "Server Control", url: ConnectionManager().httpWebHost+"/config/server_control"),
|
||||
LinkToWebConfig(name: "Persons", url: ConnectionManager().httpWebHost+"/config/person"),
|
||||
LinkToWebConfig(name: "Entity Registry", url: ConnectionManager().httpWebHost+"/config/entity_registry"),
|
||||
LinkToWebConfig(name: "Area Registry", url: ConnectionManager().httpWebHost+"/config/area_registry"),
|
||||
LinkToWebConfig(name: "Automation", url: ConnectionManager().httpWebHost+"/config/automation"),
|
||||
LinkToWebConfig(name: "Script", url: ConnectionManager().httpWebHost+"/config/script"),
|
||||
LinkToWebConfig(name: "Customization", url: ConnectionManager().httpWebHost+"/config/customize"),
|
||||
LinkToWebConfig(name: "Home Assistant configuration", url: ConnectionManager().httpWebHost+"/config"),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
81
lib/panels/panel_class.dart
Normal file
81
lib/panels/panel_class.dart
Normal file
@ -0,0 +1,81 @@
|
||||
part of '../main.dart';
|
||||
|
||||
class Panel {
|
||||
|
||||
static const iconsByComponent = {
|
||||
"config": "mdi:settings",
|
||||
"history": "mdi:poll-box",
|
||||
"map": "mdi:tooltip-account",
|
||||
"logbook": "mdi:format-list-bulleted-type",
|
||||
"custom": "mdi:home-assistant"
|
||||
};
|
||||
|
||||
final String id;
|
||||
final String componentName;
|
||||
final String title;
|
||||
final String urlPath;
|
||||
final Map config;
|
||||
String icon;
|
||||
bool isHidden = true;
|
||||
bool isWebView = false;
|
||||
|
||||
Panel({this.id, this.componentName, this.title, this.urlPath, this.icon, this.config}) {
|
||||
if (icon == null || !icon.startsWith("mdi:")) {
|
||||
icon = Panel.iconsByComponent[componentName];
|
||||
}
|
||||
isHidden = (componentName == 'kiosk' || componentName == 'states' || componentName == 'profile' || componentName == 'developer-tools');
|
||||
isWebView = (componentName != 'config' && componentName != 'lovelace' && !componentName.startsWith('haclient'));
|
||||
}
|
||||
|
||||
void handleOpen(BuildContext context) {
|
||||
if (componentName == "config") {
|
||||
Navigator.of(context).push(
|
||||
MaterialPageRoute(
|
||||
builder: (context) => PanelPage(title: "$title", panel: this),
|
||||
)
|
||||
);
|
||||
} else if (componentName.startsWith('haclient')) {
|
||||
Navigator.of(context).pushNamed(urlPath);
|
||||
} else if (componentName == 'lovelace') {
|
||||
HomeAssistant().lovelaceDashboardUrl = this.urlPath;
|
||||
HomeAssistant().autoUi = false;
|
||||
SharedPreferences.getInstance().then((prefs) {
|
||||
prefs.setString('lovelace_dashboard_url', this.urlPath);
|
||||
eventBus.fire(ReloadUIEvent());
|
||||
});
|
||||
} else {
|
||||
Launcher.launchAuthenticatedWebView(context: context, url: "${ConnectionManager().httpWebHost}/$urlPath", title: "${this.title}");
|
||||
}
|
||||
}
|
||||
|
||||
Widget getMenuItemWidget(BuildContext context) {
|
||||
return ListTile(
|
||||
leading: Icon(MaterialDesignIcons.getIconDataFromIconName(this.icon)),
|
||||
title: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: <Widget>[
|
||||
Text("${this.title}"),
|
||||
Container(width: 4.0,),
|
||||
isWebView ? Text("webview", style: TextStyle(fontSize: 8.0, color: Colors.black45),) : Container(width: 1.0,)
|
||||
],
|
||||
),
|
||||
onTap: () {
|
||||
Navigator.of(context).pop();
|
||||
this.handleOpen(context);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
Widget getWidget() {
|
||||
switch (componentName) {
|
||||
case "config": {
|
||||
return ConfigPanelWidget();
|
||||
}
|
||||
|
||||
default: {
|
||||
return Text("Unsupported panel component: $componentName");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -17,7 +17,7 @@ class LinkToWebConfig extends StatelessWidget {
|
||||
textAlign: TextAlign.left,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: new TextStyle(fontWeight: FontWeight.bold, fontSize: Sizes.largeFontSize)),
|
||||
subtitle: Text("Tap to opne web version"),
|
||||
subtitle: Text("Tap to open web version"),
|
||||
onTap: () {
|
||||
Launcher.launchAuthenticatedWebView(context: context, url: this.url, title: this.name);
|
||||
},
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user